前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过实例学习ROP技术

通过实例学习ROP技术

作者头像
信安之路
发布2018-08-08 15:13:14
7580
发布2018-08-08 15:13:14
举报
文章被收录于专栏:信安之路

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 还搜索出了两个 .m3u8M3U8 也是一种 M3U,只是它的编码格式是 UTF-8 格式。M3ULatin-1 字符集编码),全部下断,接着点击软件左上角打开一个 m3u 文件,程序断在了 004217f7

接着单步,你会进入到这样的一个死循环,emmmm……

对 windows 消息机制熟悉的人,一眼就能看出来,这是一个消息循环,消息循环的大致流程是这样的,先获取消息( GetMessage ),如果有消息到达就调用回调处理函数( DisPatchMessage ),如果消息是 WM_QUIT,则退出消息循环。

很明显这是一条死胡同,出师不利……只能换第二种方法了:

接着我们对 ReadFile() 下断( bp ReadFile ),重新载入软件,软件直接断在了 7c801812 处………???

我还没载入 m3u 文件呢,怎么就直接断了?看了一眼堆栈。

WTFQQpinyin?看了一眼屏幕的右下角,emmmm……

大概知道原因了。但是我在任务管理器里面始终找不到 QQpinyin 的进程,可能是程序加载了 QQPinYin 的模块,而这个模块一直在循环 ReadFile (这难道是键盘记录嘛,不懂不懂,求大佬赐教)。还懒得卸载,折腾了一会,放弃了。

哎,不溯源了,直接异常跟踪吧!换 OD!先用 Python 脚本生成一个由 5000 个 \x41 组成的 m3u 文件,

代码语言:javascript
复制
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,反汇编如下:

代码语言:javascript
复制
add esp,17cc
pop ebx
pop esi
pop edi
pop ebp
retn

第二个位于 64988c54,反汇编如下:

代码语言:javascript
复制
add esp 940
pop ebx
pop esi
pop edi
retn

我选择第一个,因为它有 pop ebp,详细原因构造 ROP 链的时候再说。

先了解一下什么是 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 所在的内存区域变为可执行。

ROP 链的设计

SEH 处理函数覆盖为 6497AB0COD 重新载入,并在 6497AB0C 处下断,执行 4 个 pop 之后,观察堆栈,可以看到 esp 变为了 0022FD54

也就是说从 0022FD54 位置开始就是 ROP 链了,然后在 ROP 链下面存放 shellcode 就行了

先介绍一下 virtualProtect 函数吧:

代码语言:javascript
复制
BOOL VirtualProtect(

LPVOID lpAddress, // 目标地址起始位置

DWORD dwSize, // 大小

DWORD flNewProtect, // 请求的保护方式

PDWORD lpflOldProtect // 保存老的保护方式

);

我们只需要 lpAddress 处在低址,而 shellcode 处在高址,并且 size 的大小不超过 DWORD 就行。

代码语言:javascript
复制
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 sp3VirtualProtect 的值为 6d7cbbe4),重新调试。如下图:

相信大家都已经看明白了。

如果选 EDIVirtualProtect 函数地址的话:

ESI 就是函数执行完的返回地址

EBP 就是第一个参数 lpAddress

ESP 就是第二个参数 dwSize

EBX 就是第三个参数 flNewProtect

EDX 就是第四个参数 lpflOldProtect

但是这样做可行吗?

很明显不可行,因为 ESP 的值也就是 dwSize 的值超出 DOWRD 的范围了,函数肯定返回失败,那么就剩下两种方法了

代码语言:javascript
复制
eax                             
ecx //lpflOldProtect        
edx //flNewProtect              
ebx //dwSize                
esp //lpAddress             
ebp //返回地址                  
esi //VirtualProtect地址
edi

这里我们要巧妙的利用 esp,由于 esp 的值不可改变,所以我们直接把 esp 的值当成返回地址,而且这个返回地址的内容正好被我们的 shellcode 覆盖了。

那么 ebp 的值就应该是 VirtualProtect 的地址了。

问题来了,怎么把 ebp 的值变成 VirtualProtect 地址呢?

emmmm…… 还记得之前选择 SEH 处理函数的时候,我选择了:

代码语言:javascript
复制
add esp,17cc
pop ebx
pop esi
pop edi
pop ebp
retn

看到 pop ebp 没?

只需要计算一下 pop ebp 时栈的内容,我们将其覆盖成 VirtualProtect 的地址就行了。

接下来就是对四个参数进行运算了,

先计算第一个参数 lpAddressEBX

这个参数只要求小于 shellcode 的地址即可,由于 shellcode 本来就位于高址,而且 ESP 是函数的返回地址,也是 shellcode 的起始地址,所以我们干脆把 EBX 的值等于 ESP 就行了,接着我在 649abc7b 处找到了这样的指令:

代码语言:javascript
复制
push esp
pop ebx
pop esi
retn

关于 pop esi,我们可以随便给 esi 弹一个值就行了

计算第二个参数 dwSizeEDX

我的想法是让 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,在这个参数上我思考了好久,一直没有找到解决方法,本来我是这么打算的

代码语言:javascript
复制
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 不就行了吗?然而,我内存空间中找不到这样的指令……

这就很麻烦了,难道还要用 esiedi

其实我们不妨跳出计算的思维,直接 pop ebx 不就行了吗?请看这一段指令

代码语言:javascript
复制
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 的操作一定要最后再处理,具体原因看下面的代码就懂了。

下面是组装后的汇编指令

代码语言:javascript
复制
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 里面进行构造,下面只是 POCROP 的片段

进行调试如图

这里有一个严重的问题,就是 pushad 之后,会先执行 EDI 的内容,而 EDI 我们把它覆盖成了 41414141……

emmmm…… 并且我们还想跳过 esi 执行 ebp 的内容,有没有一个指令能帮我们呢?

聪明的你已经想到了!对!那就是 pop retn,我们只需要把 edi 的值改成 pop retn 的地址就行了

修改后的 POC(部分)如下:

重新运行

完美!接着按f9运行就可以看到我们的计算器弹了出来

结束!

小结

这篇文章主要向大家展示了 ROP 链的构造。如果大家没有看太懂,不妨通过我写好的 POC,一步一步进行调试,体会 ROP 的精髓。

可能大家已经发现了,我写的文章会把自己当时出现的错误,以及思考的过程全部向大家展示出来,这样大家看的时候会有一种代入感,而且最重要的是对我来讲这是一种令人难忘的回忆。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 软件介绍及环境搭建
  • 开始调试并找到溢出位置
  • ROP 技术简介
  • ROP 链的设计
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档