windows初级ROP
工具
VC++6.0
链接:https://pan.baidu.com/s/1jfAzvMvi27zPeiSGNTouQA 提取码:dust
老古董了
当编译链接报错的时候试试禁用预编译头,十有八九是他导致的
windows XP操作系统
去MSDN, 我告诉你 - 做一个安静的工具站 (itellyou.cn)这里找
实际在windows11上用VC++6.0也可以做实验,并且比古董XP更方便
缓冲区溢出修改临近变量
书上给出的例程长这样
1 |
|
VC++6.0的语法,关于main函数为啥没有返回值类型,入乡随俗吧
主函数里面一个1K字节的password庞然大物,其基地址传递给verify_password函数
该函数用了一个authenticated变量承载strcmp的返回值,如果输入的密码正确,即password=1234567,则strcmp(buffer,password)返回0,authenticated=0.否则strcmp根据两个参数的字典序返回正数或者负数
为了故意制造漏洞,书上在strcmp函数调用结束之后用strcmp将password拷贝到buffer上了strcpy(buffer, password);
下面动态调试verify_password函数,观察其栈帧的分布
用ollydbg,VC++6.0,ida等等调试均可
开端
1 | 5: int verify_password(char *password) |
声明两个局部变量
1 | 7: int authenticated; |
可以看到并没有对应的汇编指令,这是因为编译原理中,声明语句不生成可执行代码,只是起到规定符号类型的作用
调用strcmp计算authenticated
1 | 9: authenticated = strcmp(password, PASSWORD); |
这个strcmp起到了验证输入密码是否正确的作用
stdcall调用约定,使用栈传递参数,最先push入栈的"1234567"的地址作为第二个参数,后来连push入栈的ebp+8作为第一个参数
局部变量都是在本函数栈帧内部的,他们在栈中的地址总是ebp减去某个数,而现在ebp+8这个地址显然不是当前函数栈内的局部变量,是调用者压栈传递的参数password
strcmp调用返回之后立刻抬栈8个字节,这就消去了为了调用strcmp压入的两个参数,调用strcmp前后堆栈平衡
stdcall中,strcmp返回值放在eax寄存器中,拷贝到栈上ebp-4位置,这意味着ebp-4是authenticated的位置,它紧挨着栈底
画蛇添足造成漏洞
到了故意写上的strcpy了
1 | 10: strcpy(buffer, password); // over flowed here! |
ebp+8指向调用者压入的password字符串地址
ebp-0xC看来就是buffer的基地址了
然后调用strcpy从ebp+8拷贝字符串到ebp-0xC,啥时候碰到'\0'字符,啥时候拷贝结束
调用完成后抬栈8个字节,实现strcpy前后堆栈平衡
到此可以画出verify_password的栈帧分布了
buffer的长度为8,如果输入8个字符,正好填满buffer,还多出一个'\0'结束字符,不得不放到authenticated上了,这就溢出覆盖了
然后authenticated被返回
而main函数中就是凭verify_password的返回值决定是否输入了正确的密码
这里需要让valid_flag即verify_password的返回值为0才可以跳过if,进入else
1 | if (valid_flag){ |
这样看来我们只需要输入00000000,八个0,最后多一个'\0'正好覆盖了authenticated,让他等于0.
然而这只是覆盖了它的最低一个字节,authenticated有4个字节.
"那么可以输入11个0,然后自带一个'\0',一共12个0,这样authenticated被溢出成全0了",这样想更不对,因为0的ASCII码是0x30,
这样输入之后相当于authenticated=0x00303030=3158064
实际上调试的时候正是如此
'\0'是真的0,NULL,字符'0'是假的0,那应该再咋办呢
1 | authenticated = strcmp(password, PASSWORD) |
当字典序password<PASSWORD时,返回的是负数,那么authenticated的高位全都是1,(不管高几位是1了,反正最高位那个符号位是1)
当字典序password>PASSWROD时,返回的是正数,那么authenticated的高位全都是0了,这就有一个问题,高几位是0,如果高3个字节都是0则正好'\0'将最低字节也溢出成0,如果第二个字节非0就寄了
怎么验证这个事呢?要么直接调试看看,要么看strcmp的源代码
显然前者来的快,调试观察,结果是1,竟然不是一个乱七八糟的正数
逆向分析strcmp,其函数出口只有两个
要么是doneeq处作为出口,这里返回eax=0
要么是donene处作为出口,这里干了啥呢?
1
2
3
4
5 .text:00401294 donene: ; done not equals
.text:00401294 sbb eax, eax
.text:00401296 shl eax, 1
.text:00401298 inc eax
.text:00401299 retnsbb,带借位减法,sbb a,b意思是a=a-b-CF
那么sbb eax,eax的意思就是eax=eax-eax-CF=-CF
而CF的值来自于两个字符串逐个字节做差,strcmp(a,b)=a-b,如果a<b则CF=1否则即a>=b则CF=0
然后shl带进位左移,左移的时候最低位用CF补上
如果CF=0则eax=-0=0然后左移的时候低位补CF=0,还是0,最后inc eax导致变成1
因此如果a的字典序更大,则strcmp只会返回1
那么只需要输入2222222,七个2再加上'\0',就可以保证字典序大于1234567,eax返回1放到authenticated最低位,高三位都是0,然后'\0'溢出修改authenticated最低字节,得到全0的authenticated,这就跳过了if的检查
溢出返回地址
在verify_password函数的栈帧视角下,返回地址在ebp+4处
从ebp-0xC到ebp+0x4共16字节,输入15个字节之后加上'\0',正好到达返回地址的家门口
单反再多输入一个字节,就要修改返回地址了
输入19个'A'再加上一个'\0'则返回地址被修改为0x00414141(0x41是'A'的ASCII编码)
Buffer[0x19FACC,0x19FAD4)=0x4141414141414141
authenticated[0x19FAD4,0x19FAD8)=0x41414141
调用者ebp[0x19FAD8,0x19FADC)=0x41414141
返回地址[0x19FADC,0x19FAE0)=0x00414141
溢出之后再继续单步调试会变地很艰难,每一步都要等五六秒,最后retn指令之后eip将指向0x00414141处,跑飞了
在windowsXP上尝试运行然后输入19个A,windows会报错
在eip=0x414141处发生了0x80000003号错误
STATUS_BREAKPOINT,值为0x80000003,称为中断指令异常,表示在系统未附加内核调试器时遇到断点或断言。
确实如此,0x00414141处全都是0xCC
而int 3中断指令的机器码恰好就是0xCC,因此CPU认为碰到了没有定义的断点,报错了
ret2text
上一个实验中,由于终端上只能输入可打印ASCII字符,这就限制了我们可以输入的地址
为了表演如何劫持控制,书上将输入改成了文件,这就允许输入不可打印ASCII字符了
1 |
|
确定栈帧分布
main函数给password预留了1024个字节,足够了,只需要考虑verify_password函数的栈帧分布
在项目根目录下面新建一个password.txt文档然后随便输入几个字符,开始调试
按理说应该是exe同目录,即Debug目录,但是在哪里建立之后程序找不到password.txt文件
exe同目录或者项目根目录都建立一个,以防万一
1 | 10: strcpy(buffer, password); // over flowed here! |
ebp+8是调用者传过来的参数,password的指针
ebp-0xC是buffer缓冲区地址
可以画出栈帧了,和刚才的实验中是一样的
溢出修改返回地址
现在企图溢出修改返回地址,直接跳转到main中打印通过的信息处
1 | 30: printf("Congratulation! You have passed the verification!\n"); |
即直接将返回地址溢出修改成0x0040111F就可以了,咋溢出呢?
前16个字节溢出成任意字符,然后输入三个字节,0x1F,0x11,0x40,总共19个字节了,然后最高位'\0'填充
问题是0x1F,0x11都是不可打印ASCII字符,怎么输入呢?
二进制 | 十进制 | 十六进制 | 字符 | 意义 |
---|---|---|---|---|
00011111 | 31 | 1F | US (Unit Separator) | 单元分隔符 |
00010001 | 17 | 11 | DC1/XON (Device Control 1/Transmission On) | 设备控制1/传输开始 |
01000000 | 64 | 40 | @ |
首先在password.txt中编辑1234567812341234abc这么19个字符,其中前16个阿拉伯数字全是填充,后面abc是需要修改的地方
保存好后用010editor打开password.txt
View->Edit As->Hex
将最后三个字节,改成0x1F,0x11,0x40
然后保存,再运行程序
1 | PS C:\Users\86135\Desktop\StackOverFlow\controlflow\Debug> ./controlflow |
已经可以"通过检查"了,下面调试观察发生了什么
调试观察
首先,verify_password中strcpy执行之后,栈帧的状态
EBP=0x19FAD4,这意味着返回地址在0x19FAD8,内存视图中可以看出,0x19FAD8开始的四个字节已经被溢出成0x0040111F了
继续执行直到retn指令,返回之后
EIP顺势被修改为0x0040111F,达到了目的
此时EBP帧指针指向0x34333231,显然已经飞了,不是main函数的帧底,但是ESP指向栈顶是没错的,
编译器会计算好函数返回时esp要抬多少,但是ebp是纯靠程序压栈记忆的
这个过程画到图上就是书上给的
ret2shellcode
上一个实验中将控制篡改到main函数中,还是属于代码段的,
这个实验要向栈中写入代码然后篡改返回地址,执行栈中代码.
要在栈中写一段创建MessageBoxA的shellcode,让程序弹窗
需要pwn的程序长这样
1 |
|
还是从password.txt中写东西,溢出verify_password.txt的buffer缓冲区
首先要调试verify_password观察栈帧分布,项目根目录下新建password.txt,随便输入些字符,然后在main上下断点,开始调试
调试观察栈帧分布
authenticated
1 | 12: authenticated = strcmp(password, PASSWORD); |
ebp+8指向参数password
ebp-4是authenticated的地址
buffer
1 | 13: strcpy(buffer, password); // over flowed here! |
ebp-30h是buffer的基地址
30h=48,即buffer是紧接着authenticated的
观察寄存器视图,ebp=0x0012FB20
那么"返回地址"在栈中的地址0x12FB24,buffer的基地址0x12FAF0
那么往buffer写44+4+4=52个字符,这52个字符里就包括了shellcode,就到返回地址的家门口了,然后将0x19FF04即buffer基地址写到返回地址上,如此函数返回时,控制将转移到0x12FAF0,
由于strcpy函数执行后,不会再有其他行为改变buffer缓冲区,然后函数返回之前退栈只是抬高esp,buffer成为非法区域,但是其内容不会被扬了,然后返回地址退栈交给eip,指向执行shellcode并执行
下面的任务就是如何编写shellcode
编写shellcode
首先需要找到MessageBox函数的地址
windows为了减少基址重定位的开销,给每个系统动态库都分配了各自的基地址,保证他们相互不冲突.
但是在不同的操作系统上,系统dll的基地址也是不一样的
在windows xp professional系统上,user32.dll的映像基址是77D10000h
而书上写的是windows xp SP3系统中user32.dll的映像基址是77D40000h
程序员的自我修养上写的是7E410000h
然后用VC++自带的DEPENDS.EXE程序观察user32.dll中MessageBoxA函数的偏移量
VA=RVA+ImageBase=0x2ADD7+0x77D10000=0x77D3ADD7
这就找到了MessageBoxA函数的虚拟地址
shellcode是这样写的:
1 | xor ebx,ebx |
编译成机器码为
1 | 33DB536877657374686661696C8BC453505053B8D7ADD377FFD0 |
这是26个字节了,用0x90(啥也不干nop)再填充26个字节到返回地址的家门口,
1 | 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 |
然后四个字节用buffer的基地址0x0019FF04,溢出返回地址,只写低3字节就可以,高一个字节'\0'自动填上了
1 | F0 FA 12 |
1 | 33 DB 53 68 77 65 73 74 68 66 61 69 6C 8B C4 53 |
用010editor写到password.txt中然后保存
在windows XP虚拟机上执行成功
在windows 11上白搭,一是没找到32位的user32.dll,二是ebp值和windows XP上不同
调试
在windows XP上调试本程序
在strcpy执行之后从0x12FAF0开始的44+4+4+4个字节都被溢出修改
retn返回之后,控制已经到达了0x0012FAF0处,开始执行shellcode
此时堆栈顶ESP指向0x12FB28,这是main函数call verify_password之前的栈顶位置,
堆栈从大到小生长的话,0x0012FAF0是在其生长道路上的,shellcode有没有被再次生长的堆栈覆盖的可能呢?
shellcode中有7个压栈,也就是28=0x1A个字节,0x12FB28-0x1A=0x12FB0E>0x12FAF0,不会覆盖shellcode,可以放心执行
当MessageBoxA函数返回时,下面就没有有意义的指令了,迟早要寄掉
在0x12fb0a处就寄了,错误代码0xc0000005