RTIS CTF 战队初建,需要大量对 CTF 感兴趣且有大量学习时间的小伙伴加入,急缺 pwn、逆向相关的小伙伴,欢迎有兴趣的同学将自己的个人简介发送到
1293348082@qq.com
,如果能让我看到你的学习热情,我将拉你进我们的RTIS CTF 雏鹰进阶之路
交流群,跟一群志同道合的小伙伴一起学习,一起打比赛,欢迎你的加入。
这两天闲着无聊就在 exploit-db
上找个小软件练练手,但是 exploit-db
上面给的 poc 并能正常运行,无奈只好自己写了一个,顺便写篇文章把自己写 shellcode
的思路分享给大家。
漏洞软件:MPlayer
虚拟机:win xp sp3
实验工具:OD
软件下载地址及POC:
https://pan.baidu.com/s/1kWdnKkZ
这款软件有点年份了,并且开启了 DEP
保护机制。我们的目的是通过使用 ROP
技术绕过 DEP
并执行 shellcode
,我会把文章重点着重放在 ROP
链的构造上。
漏洞的成因及利用:该播放器在打开 .m3u
文件时会调用 msvcrt.dll
的某个函数并发生栈溢出,我们通过构造超长字符串溢出覆盖 SEH
指针和 SEH
处理函数并触发异常执行我们的 ROP
链,通过 ROP
链使 shellcode
所在的内存区域变为可执行紧接着去调用 shellcode
。
什么是.m3u文件?
.m3u
文件是音频文件的列表文件,是纯文本文件。播放器会根据它的记录找到网络地址进行在线播放。
开始调试程序……
分析这种文件型漏洞,一般有两种方法:
1、对相应的文件后缀下断
2、对 ReadFile()
函数下断
先选择第一种
OD
载入软件,F9
运行软件,搜索 ASCII
可以找到两个 .m3u
,这里注意一下,OD
还搜索出了两个 .m3u8
( M3U8
也是一种 M3U
,只是它的编码格式是 UTF-8
格式。M3U
用 Latin-1
字符集编码),全部下断,接着点击软件左上角打开一个 m3u
文件,程序断在了 004217f7
处
接着单步,你会进入到这样的一个死循环,emmmm……
对 windows 消息机制熟悉的人,一眼就能看出来,这是一个消息循环,消息循环的大致流程是这样的,先获取消息( GetMessage
),如果有消息到达就调用回调处理函数( DisPatchMessage
),如果消息是 WM_QUIT
,则退出消息循环。
很明显这是一条死胡同,出师不利……只能换第二种方法了:
接着我们对 ReadFile()
下断( bp ReadFile
),重新载入软件,软件直接断在了 7c801812
处………???
我还没载入 m3u
文件呢,怎么就直接断了?看了一眼堆栈。
WTF
?QQpinyin
?看了一眼屏幕的右下角,emmmm……
大概知道原因了。但是我在任务管理器里面始终找不到 QQpinyin
的进程,可能是程序加载了 QQPinYin
的模块,而这个模块一直在循环 ReadFile
(这难道是键盘记录嘛,不懂不懂,求大佬赐教)。还懒得卸载,折腾了一会,放弃了。
哎,不溯源了,直接异常跟踪吧!换 OD
!先用 Python
脚本生成一个由 5000 个 \x41
组成的 m3u
文件,
file = "myMplayer.m3u"
buf = "\x41" * 5000
fobj = open(file,"w")
fobj.write(buf)
fobj.close()
重启软件,OD
附加,用软件打开该文件,触发异常,紧接着查看堆栈调用
可以看到 mplayer.00561813
,可以在这里下个断点,然后单步跟,在 00561758
处,程序把 m3u
文件所在路径和 5000 个 \x41
复制到 0022EBB8
的栈中。如果字符串够长,淹没了整个栈,那么就会触发异常,执行 SEH
异常处理函数。(如果你不想单步跟,直接在 00561758
处下断即可)
这里要强调一点我的 m3u
文件是放在 c 盘根目录的,想放到其他位置也可以,但是相应的缓冲区会发生变化,大家可以自行调试,接着我们把字符串增加到 6000 个,可以看到最后一个 SEH
被覆盖,整个栈笼罩在 41
的阴影之下。
\x41
的范围是 0022EBE4~0022FFFC
。
通过 OD
我们可以了解到程序发生异常时 esp
的值为 0022E578
,并不在 0022EBE4~0022FFFC
范围内,也就是说,esp
不在我们可控的范围之内。所以我们要覆盖的 SEH
处理函数所执行的操作应该是使 esp
变大,使 esp
跳到我们的构造好的缓冲区内。
在程序中寻找这些代码片段的地址,运气还行,找到了两个
第一个位于 6497AB0C
,反汇编如下:
add esp,17cc
pop ebx
pop esi
pop edi
pop ebp
retn
第二个位于 64988c54
,反汇编如下:
add esp 940
pop ebx
pop esi
pop edi
retn
我选择第一个,因为它有 pop ebp
,详细原因构造 ROP
链的时候再说。
先了解一下什么是 ROP
?
ROP(Return Oriented Programming)
:
连续调用程序代码本身的内存地址,以逐步地创建一连串欲执行的指令序列。ROP
技术主要是对抗微软的 DEP
保护机制的。
什么是 DEP
?
DEP
的运行机制是,Windows 利用 DEP
标记只包含数据的内存位置为非可执行( NX
),当应用程序试图从标记为 NX
的内存位置执行代码时,Windows 的 DEP
逻辑将阻止应用程序这样做,从而达到保护系统防止溢出。换句话说微软通过 DEP
技术把数据和代码彻底的分离了。我们在栈中放置的 shellcode
在没有特殊处理的情况下是无法被执行的。
怎么绕过呢?
一般使用 Ret2Libc
技术,常用的 Ret2Libc
技术通过调用系统自带的 API 如 ZwSetInformationProcess()
函数(关闭 DEP
)、virtualProtect()
函数(将指定的内存空间改为可执行)、virtualAlloc()
函数(创建一段可执行的内存)来达到绕过 DEP
的效果。之后微软引入了 ALSR
技术来对抗 ROP
不过这都是后话了……
这里我选择 virtualProtect()
构造 ROP
链使 shellcode
所在的内存区域变为可执行。
把 SEH
处理函数覆盖为 6497AB0C
,OD
重新载入,并在 6497AB0C
处下断,执行 4 个 pop
之后,观察堆栈,可以看到 esp
变为了 0022FD54
。
也就是说从 0022FD54
位置开始就是 ROP
链了,然后在 ROP
链下面存放 shellcode
就行了
先介绍一下 virtualProtect
函数吧:
BOOL VirtualProtect(
LPVOID lpAddress, // 目标地址起始位置
DWORD dwSize, // 大小
DWORD flNewProtect, // 请求的保护方式
PDWORD lpflOldProtect // 保存老的保护方式
);
我们只需要 lpAddress
处在低址,而 shellcode
处在高址,并且 size 的大小不超过 DWORD
就行。
BOOL VirtualProtect(
LPVOID lpAddress, // 小于shellcode的起始位置
DWORD dwSize, // 大小不越界
DWORD flNewProtect, // 这个值设为0x40表示可读可写可执行
PDWORD lpflOldProtect // 随便一个可写低址就行
);
怎么构造ROP链呢?上面这些参数存到哪里呢?
以我的经验,参数可以存到两个地方,一个是寄存器,一个是栈。
当然 virtualProtect
的地址也要相应的存在寄存器或栈中。
这里我选择寄存器,因为如果存在栈中的话你需要不断的改变 esp
mov [esp],某个寄存器
并且不断调用这样的语句对栈的内容进行赋值,操作简单但是过于繁琐。最后会发现 ROP
链会非常长,影响观看(不过有兴趣的可以试试,也是可以达到效果的)
既然选择了寄存器,那么即使四个参数和函数地址都已经存入寄存器,我们该怎么执行呢?调用这个语句
push ad retn
push ad
的含义是把 EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX
依次入栈,这里我们不妨调试一下,首先我们找到 push ad retn
的地址,通过 OD
插件进行搜索,找到了一组,地址为 649b11ec
。
把该地址放在 ROP
链的首端,也就是把 0022FD54
的值覆盖为 649b11ec
(具体怎么覆盖,大家可以自行计算),顺便手动把 OD
中这几个寄存器的值都改变一下(esp
的值不能动)如图:
寄存器的值依次为 1,2,3,4,0022FD54,5,6,7
。接着单步跳到 649b11ec
处执行 push ad
,查看堆栈情况:
可以看到栈接下来会执行 00000007
的内容,也就是 EDI
的内容。那么我们把 EDI
的值改成 VirtualProtect
的地址(xp sp3
下 VirtualProtect
的值为 6d7cbbe4
),重新调试。如下图:
相信大家都已经看明白了。
如果选 EDI
为 VirtualProtect
函数地址的话:
ESI
就是函数执行完的返回地址
EBP
就是第一个参数 lpAddress
ESP
就是第二个参数 dwSize
EBX
就是第三个参数 flNewProtect
EDX
就是第四个参数 lpflOldProtect
但是这样做可行吗?
很明显不可行,因为 ESP
的值也就是 dwSize
的值超出 DOWRD
的范围了,函数肯定返回失败,那么就剩下两种方法了
eax
ecx //lpflOldProtect
edx //flNewProtect
ebx //dwSize
esp //lpAddress
ebp //返回地址
esi //VirtualProtect地址
edi
这里我们要巧妙的利用 esp
,由于 esp
的值不可改变,所以我们直接把 esp
的值当成返回地址,而且这个返回地址的内容正好被我们的 shellcode
覆盖了。
那么 ebp
的值就应该是 VirtualProtect
的地址了。
问题来了,怎么把 ebp
的值变成 VirtualProtect
地址呢?
emmmm…… 还记得之前选择 SEH
处理函数的时候,我选择了:
add esp,17cc
pop ebx
pop esi
pop edi
pop ebp
retn
看到 pop ebp
没?
只需要计算一下 pop ebp
时栈的内容,我们将其覆盖成 VirtualProtect
的地址就行了。
接下来就是对四个参数进行运算了,
先计算第一个参数 lpAddress
(EBX
)
这个参数只要求小于 shellcode
的地址即可,由于 shellcode
本来就位于高址,而且 ESP
是函数的返回地址,也是 shellcode
的起始地址,所以我们干脆把 EBX
的值等于 ESP
就行了,接着我在 649abc7b
处找到了这样的指令:
push esp
pop ebx
pop esi
retn
关于 pop esi
,我们可以随便给 esi
弹一个值就行了
计算第二个参数 dwSize
(EDX
)
我的想法是让 eax
做运算,最后把 eax
的值赋给 edx
。
我在 649a3d6c
处找到给 eax
清零的指令
xor eax,eax
接着在 6ad5c728
处找到
add eax,69 retn
我们连续执行这条指令三次把 eax
的值变为 13b
(十进制 315
)。
又在 6B0B7A46
处找到了给 edx
赋值的指令
mov edx,eax mov eax,edx retn
ok!
计算第三个参数(ECX
)
我们需要把 ecx
的值变为 00000040
,在这个参数上我思考了好久,一直没有找到解决方法,本来我是这么打算的
xor eax,eax
add eax,8
add eax,8
add eax,8
add eax,8
add eax,8
add eax,8
add eax,8
add eax,8
mov cl,al
发现在执行 mov cl,al
的时候产生异常,莫名其妙的异常而且还不知道怎么解决,折腾了半天,就换了种思路……
能不能利用 ebx
?
在 60e00bf0
处找到了
mov cl,bl
也就是说我们只需要把 bl
的值等于 40
即可
如何把 bl
的值变为 40
呢?
一开始我满脑子只有计算,既然上面的 add eax,8
使得 eax
等于 40
了,那么我直接 mov ebx,eax
或者 mov bl,al
不就行了吗?然而,我内存空间中找不到这样的指令……
这就很麻烦了,难道还要用 esi
和 edi
?
其实我们不妨跳出计算的思维,直接 pop ebx
不就行了吗?请看这一段指令
add esp,17cc
pop ebx
pop esi
pop edi
pop ebp
retn
emmmmm…… 熟不熟悉?说实话之前选择 SEH
处理函数的时候,并没有想到这段指令会帮我完成这么多复杂的操作。
只需要 pop ebx
的时候把栈的值覆盖为 40404040
即可,接着让 ecx
清零,执行 mov cl,bl
就行了。
计算第四个参数(EAX
)
这个就很简单了,直接 pop eax retn
,把 eax
的值变为一个可写地址就行,我选择的可执行地址是 10028024
好了!接下来就开始组装了,这里要注意一点,lpAddress
也就是 ebx
的操作一定要最后再处理,具体原因看下面的代码就懂了。
下面是组装后的汇编指令
xor eax,eax//计算第二个参数
add eax,69
retn
add eax,69
retn
add eax,69
retn
mov edx,eax
mov eax,edx
retn
pop ecx //计算第三个参数,pop ecx使ecx等于FFFFFFFF,当然你也可以直接xor ecx,ecx
retn
inc ecx
retn
mov cl,bl
pop eax retn //计算第四个参数
push esp //计算第一个参数
pop ebx
pop esi
retn
push ad //最后push ad
retn
ok,按照这样的思想,我们在 POC
里面进行构造,下面只是 POC
中 ROP
的片段
进行调试如图
这里有一个严重的问题,就是 pushad
之后,会先执行 EDI
的内容,而 EDI
我们把它覆盖成了 41414141
……
emmmm…… 并且我们还想跳过 esi
执行 ebp
的内容,有没有一个指令能帮我们呢?
聪明的你已经想到了!对!那就是 pop retn
,我们只需要把 edi
的值改成 pop retn
的地址就行了
修改后的 POC(部分)如下:
重新运行
完美!接着按f9运行就可以看到我们的计算器弹了出来
结束!
这篇文章主要向大家展示了 ROP
链的构造。如果大家没有看太懂,不妨通过我写好的 POC
,一步一步进行调试,体会 ROP
的精髓。
可能大家已经发现了,我写的文章会把自己当时出现的错误,以及思考的过程全部向大家展示出来,这样大家看的时候会有一种代入感,而且最重要的是对我来讲这是一种令人难忘的回忆。