经过1.1和1.2节的讲述,我们已经知道了如何更改EIP的值。程序执行函数之后将跳转到我们设定的位置开始执行,因此,我们需要准备一个自己的程序,接手后面的工作。这是一个什么样的程序?是一个C语言编写的代码?是一个可直接调用的exe?肯定不是,因为EIP所指的地址保存的内容为指令的操作码,CPU读取该操作码执行相应的操作。所以我们要准备的程序也应该是一段“操作码”。
继续写1.1中的Hello World,这次我们要把一个C语言编写的MessageBox换成一个只有“操作码”的程序。要获取操作码很容易,Immunity Debugger中显示出了每句汇编语句对应的操作码:
图19
上图就是调用MessageBoxA函数对应的汇编指令及操作码。
再正式写Shellcode之前,我们先编写一个汇编形式的MessageBox。我们当然不是用MASM,而是使用VC++的内联汇编__asm,这样就需要解决几个问题。
(1)内联汇编中无法定义字符串常量“example_1”和“HelloWorld”,我们如何定义它,并获取其地址?
我们无法用汇编语句中的db在内联汇编中定义字符串常量,但有一个东西在我们的掌控之下——栈,因此,可以将字符串硬编码压入栈上,同时可以获取其地址。
(2)内联汇编中没有API函数MessageBoxA,如何调用它?
没有了c语言之间调用的MessageBoxA,但通过example_1在Immunity Debugger中的调试,我们知道了MessageBoxA的地址(见图5),为0x77d507ea(未启用地址随机化,且不同机器不相同),这个地址在我的Windows XP SP3下是固定的。因此,我可以直接在汇编中调用该地址即可。
似乎没什么问题了,写出来如下代码:
/****************************************************************/
int main()
{
__asm
{
push ebp
mov ebp, esp
push 0x0000646c
push 0x726f576f
push 0x6c6c6548
push 0x00000031
push 0x5f656c70
push 0x6d617865
push 0
lea ebx, [ebp-18h]
push ebx
lea ebx, [ebp-0ch]
push ebx
push 0
mov ebx, 0x77d507ea
call ebx
add esp, 18h
pop ebp
}
return 0;
}
/********************************************************* */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
是的,不用头文件,也没有C函数(main除外)。编译运行它,然后炸了。。。
图20
0x77d507ea访问错误?这不是MessageBoxA函数函数吗?难道是我的地址找错了?用Immunity Debugger看看。
图21
这是main函数,可以看到中间我们用内联汇编写的代码基本保持原样。我们在CALL EBX上设断点,可以看到两个字符串及MessageBoxA的参数都已经被成功放入栈中:
图22
接着单步执行F7,发现CALL EBX跳转后没有任何指令,接着调试器给出了如下错误:
图23
查看以下内存空间,大概就可以发现问题了:
图24
是的,并没有0x77d507ea这个地址范围,也没有MessageBoxA所在的user32.dll。也就是说,程序根本没有加载user32.dll,这就是直接使用地址和通过API调用的差别,编译器不会检查0x77d507ea这个函数是否存在。
我们用简单的方法来验证这个猜想,用LoadLibrary加载user32.dll:
/****************************************************************/
int main()
{
LoadLibraryA("user32.dll");
...
/****************************************************************/
好了,运行成功:
图25
但这不是我们想要的,我们不想要头文件,也不想要API调用。所以我们要再改一下,把LoadLibraryA也写入汇编中,LoadLibraryA位于kernel32.dll中,这个动态库是肯定会加载的。因此,找到LoadLibraryA的地址就行了,在我的机器上为0x7c801d7b。程序改为这样:
/*****************************************************************************/
int main()
{
__asm
{
push ebp
mov ebp, esp
push 0x0000646c
push 0x726f576f
push 0x6c6c6548
push 0x00000031
push 0x5f656c70
push 0x6d617865
push 0x00006c6c
push 0x642e3233
push 0x72657375
lea ebx, [ebp-24h]
push ebx
mov ebx, 0x7c801d7b
call ebx
push 0
lea ebx, [ebp-18h]
push ebx
lea ebx, [ebp-0ch]
push ebx
push 0
mov ebx, 0x77d507ea
call ebx
add esp, 24h
pop ebp
}
return 0;
}
/*****************************************************************************/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
编译后运行成功。这样,我们把这段汇编代码的操作码抠出来运行,也应该可以达到同样的效果。先把操作码抠出来吧:
/*****************************************************************************/
55
8BEC
68 6C640000
68 6F576F72
68 48656C6C
6A 31
68 706C655F
68 6578616D
68 6C6C0000
68 33322E64
68 75736572
8D5D DC
53
BB 7B1D807C
FFD3
6A 00
8D5D E8
53
8D5D F4
53
6A 00
BB EA07D577
FFD3
/*****************************************************************************/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
去掉了尾部几句恢复EBP,ESP的语句,我们不再需要它,后面你将知道(但是首部的不能去掉,因为要用它寻址字符串),整理一下操作码:
/*****************************************************************************/
char opcode[] = "\x55\x8B\xEC\x68\x6C\x64\x00\x00\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F"
"\x68\x65\x78\x61\x6D\x68\x6C\x6C\x00\x00\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B"
"\x1D\x80\x7C\xFF\xD3\x6A\x00\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x6A\x00\xBB\xEA\x07\xD5\x77\xFF\xD3"; /*****************************************************************************/
下面是测试程序:
/*****************************************************************************/
char opcode[] = "\x55\x8B\xEC\x68\x6C\x64\x00\x00\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F"
"\x68\x65\x78\x61\x6D\x68\x6C\x6C\x00\x00\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B"
"\x1D\x80\x7C\xFF\xD3\x6A\x00\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x6A\x00\xBB\xEA\x07\xD5\x77\xFF\xD3";
int main()
{
int* ret;
ret = (int*)&ret + 2;
(*ret) = (int)opcode;
}
/*****************************************************************************/
测试程序中直接用opcode的地址覆盖了main函数的返回地址,程序执行效果如下:
图26
MessageBoxA成功执行,但是随后程序崩溃,这是正确的。因为我们覆盖了main的返回地址,opcode接管程序后无法再返回main中,程序跑飞了,无法正常结束。我们需要在opcode中结束它。需要再加一个ExitProcess调用,同样,我找到了它在我机器上的地址:0x7c81cafa。
将example_3中的最后两句:add esp, 24h, pop ebp 换为如下:
/*****************************************************************************/
xor eax, eax
push eax
mov ebx, 0x7c81cafa // ExitProcess
call ebx
/*****************************************************************************/
最后得到下面这个正常工作并退出的Shellcode:
/*****************************************************************************/
// example_4:MessageBox的Shellcode版本
char opcode[] = "\x55\x8B\xEC\x68\x6C\x64\x00\x00\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F"
"\x68\x65\x78\x61\x6D\x68\x6C\x6C\x00\x00\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B"
"\x1D\x80\x7C\xFF\xD3\x6A\x00\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x6A\x00\xBB\xEA\x07\xD5\x77\xFF\xD3\x33\xC0"
"\x50\xBB\xFA\xCA\x81\x7C\xFF\xD3";
int main()
{
int* ret;
ret = (int*)&ret + 2;
(*ret) = (int)opcode;
}
/*****************************************************************************/