这样的Hello World好玩么
春节之前我写过一个 Happy New Year 的文章,文章地址如下:用代码送上 Happy New Year,可能这样的代码对于很多程序员来说不多见,但是对于做安全的人来说可能就比较多见了。我来简单的介绍一下,我这段代码是如何实现的。再顺表说一下,前面文章的代码中是有 Bug 的,在这篇文章中会修复这个 Bug。
写一个简单的 HelloWorld
先来写一个简单的 Hello World 的程序,代码如下:
int main()
{
MessageBox(NULL, "Hello World", "Hello World", MB_OK);
return 0;
}
该代码的运行效果就是一开始提到的文章中的运行效果。这样的代码很容易看得懂,基本上不用过多的介绍就可以明白。然后我们接着改造一下。
把 C 语言代码改为对应的内联汇编
对于上面的代码而言,是使用 C 语言调用了系统的 MessageBox 函数,我们改写为内联汇编来看看效果。代码如下:
#include
int main()
{
char *p = "Hello World";
__asm
{
push MB_OK
push p
push p
push 0
call MessageBox
}
return 0;
}
改成内联汇编,也可以比较容易的搞明白,然后我们接着修改。
修改字符串为ASCII码
我们在修改代码之前,先来获取 Hello World 字符串的 ASCII 码,代码如下:
int main()
{
char *p = "Hello World";
int len = lstrlen(p);
for (int i = 0; i < len; i ++)
{
printf("\\x%02x", *(p + i));
}
__asm
{
push MB_OK
push p
push p
push 0
call MessageBox
}
return 0;
}
上面的 for 循环就是帮我们提去字符串的 ASCII 码,我们获取到的 ASCII 码如下:
\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64
使用 ASCII 码来替换掉字符串,代码如下:
#include
#include
int main()
{
__asm
{
push 0x00646c72
push 0x6f57206f
push 0x6c6c6548
mov eax, esp
push MB_OK
push eax
push eax
push 0
call MessageBox
}
return 0;
}
这样是不是就看不出字符串了,但是我们可以达到同样的效果。在这一步弹出对话框后,点击确定按钮,对话框关闭,但是会有一个报错出现。但是,这个报错不重要。
替换 MessageBox 和 MB_OK
MB_OK 是一个常量,其值为 0,直接替换即可。而 MessageBox 是一个 Windows 的 API 函数,该函数是 user32.dll 文件中导出的函数。我们来获取该函数的地址,然后替换即可。先来获取该函数的地址。代码如下:
#include
#include
int main()
{
DWORD dwAddr = (DWORD)GetProcAddress(LoadLibrary("user32.dll"), "MessageBoxA");
printf("%08x\r\n", dwAddr);
__asm
{
push 0x00646c72
push 0x6f57206f
push 0x6c6c6548
mov eax, esp
push MB_OK
push eax
push eax
push 0
call MessageBox
}
return 0;
}
在上面 printf 中输出了 MessageBoxA 函数的地址,该地址为 0x74bd1930,我们使用该地址进行替换,替换代码如下:
#include
#include
int main()
{
__asm
{
push 0x00646c72
push 0x6f57206f
push 0x6c6c6548
mov eax, esp
push 0
push eax
push eax
push 0
mov eax, 0x74bd1930
call eax
}
return 0;
}
这样基本上已经看不出代码是干什么了。然后,我们把前面的报错解决了。
解决报错的问题
产生报错的原因很简单,因为有三个 push 操作,而在最后退出时,debug 版本编译会检查栈是否平衡,因此只要手动让 esp 寄存器平衡即可。代码如下:
#include
#include
int main()
{
__asm
{
push 0x00646c72
push 0x6f57206f
push 0x6c6c6548
mov eax, esp
push 0
push eax
push eax
push 0
mov eax, 0x74bd1930
call eax
add esp, 0x0c
}
return 0;
}
在代码中使用了 add esp, 0x0c 使得栈平衡了,当然也可以使用三条 pop 指令来让栈进行平衡。
提取二进制代码
这一步就是把上面对应的机器码拿出来就可以了,直接使用 VC 的调试模式,并且显示机器码即可。我这里直接贴代码,代码如下:
#include
#include
int main()
{
/************************************************************************
push 0x00646c72
004116AE 68 72 6C 64 00 push 646C72h
push 0x6f57206f
004116B3 68 6F 20 57 6F push 6F57206Fh
push 0x6c6c6548
004116B8 68 48 65 6C 6C push 6C6C6548h
mov eax, esp
004116BD 8B C4 mov eax,esp
push 0
004116BF 6A 00 push 0
push eax
004116C1 50 push eax
push eax
004116C2 50 push eax
push 0
004116C3 6A 00 push 0
mov eax, 0x74bd1930
004116C5 B8 30 19 BD 74 mov eax,74BD1930h
call eax
004116CA FF D0 call eax
add esp, 0x0c
004116CC 83 C4 0C add esp,0Ch
************************************************************************/
__asm
{
push 0x00646c72
push 0x6f57206f
push 0x6c6c6548
mov eax, esp
push 0
push eax
push eax
push 0
mov eax, 0x74bd1930
call eax
add esp, 0x0c
}
return 0;
}
在 __asm 上面的就是我直接在 VS 中复制出来的反汇编代码和机器码。其实反汇编代码和我们这里的内联汇编代码一模一样,我们更重要的是拿它的机器码。
将机器码存入数组
将对应的机器码存入一个数组即可,数组如下:
char ShellCode[] =
"\x68\x72\x6C\x64\x00"
"\x68\x6F\x20\x57\x6F"
"\x68\x48\x65\x6C\x6C"
"\x8B\xC4"
"\x6A\x00"
"\x50"
"\x50"
"\x6A\x00"
"\xB8\x30\x19\xBD\x74"
"\xFF\xD0"
"\x83\xC4\x0C";
调用数组
将数组内的数据当作代码来执行,其实数组内本身就是机器码,代码如下:
char ShellCode[] =
"\x68\x72\x6C\x64\x00"
"\x68\x6F\x20\x57\x6F"
"\x68\x48\x65\x6C\x6C"
"\x8B\xC4"
"\x6A\x00"
"\x50"
"\x50"
"\x6A\x00"
"\xB8\x30\x19\xBD\x74"
"\xFF\xD0"
"\x83\xC4\x0C";
int main()
{
__asm
{
lea eax, ShellCode
push eax
ret
}
return 0;
}
至此,其实代码已经和上篇文章的代码一致了。但是这个代码是有 Bug 的。因为这个代码竟然没有弹出我们要的 MessageBox。
解决 Bug
该 Bug 的原因是该代码编译连接生成 EXE 文件后,并没有导入 user32.dll 文件,而 MessageBoxA 函数在该 DLL 文件中。因此,我们需要在手动的动态加载它就可以了。方法和上面类似,就是调用 LoadLibraryA 即可。这里我们就省略详细步骤了。直接上代码。
char ShellCode[] =
"\x68\x6C\x6C\x00\x00"
"\x68\x33\x32\x2E\x64"
"\x68\x75\x73\x65\x72"
"\x8B\xC4"
"\x50"
"\xB8\x40\x2A\x39\x75"
"\xFF\xD0"
"\x83\xC4\x0C"
"\x68\x72\x6C\x64\x00"
"\x68\x6F\x20\x57\x6F"
"\x68\x48\x65\x6C\x6C"
"\x8B\xC4"
"\x6A\x00"
"\x50"
"\x50"
"\x6A\x00"
"\xB8\x30\x19\xBD\x74"
"\xFF\xD0"
"\x83\xC4\x0C";
int main()
{
__asm
{
lea eax, ShellCode
push eax
ret
}
return 0;
}
总结
这样的代码对于做软件安全的、网络安全的是比较常见的。这种代码有它比较专业的名词,叫做 ShellCode。这种 ShellCode 多被用在二进制软件的保护、缓冲区溢出攻击等方面。当然了,真正的 ShellCode 需要更多的技巧和更复杂的技术。
不同版本、补丁的系统对于 LoadLibraryA 和 MessageBoxA 函数的地址是不同的,不能直接使用我这里的地址。
还有,编译运行该代码,需要修改一些编译、连接参数,关闭 /GS(这个其实应该可以不关)、关闭 随即基础地址 和 关闭 DEP。否则可能由于安全机制而导致代码运行失败!