Pwn with File结构体之利用 vtable 进行 ROP
前言
本文以 0x00 CTF 2017 的 babyheap 为例介绍下通过修改 vtable 进行 rop 的操作 (:-_-
漏洞分析
首先查看一下程序开启的安全措施
|
|
没开 PIE 。
接下来看看程序

对程序功能做个介绍
- 程序一开始需要用户往
bss段的name缓冲区输入内容 add函数: 增加一个user, 其实就是根据需要的大小使用malloc分配内存, 然后读入username.edit函数: 根据输入的index, 取出指针, 往里面写入内容。index没有校验。ban函数:free掉一个user.changename函数: 修改bss段的name
- 输入为
5时, 会打印read函数的地址, 这样就可以拿到libc的基地址了。
来看看 edit 函数

直接使用我们输入的数字作为数组索引,在 users 数组中取到 obj 指针,然后使用 strlen 获取输入的长度,最后调用 read 往 obj 里面写内容。
如果我们输入的数字大于 users 数组的长度就可以读取 users 数组 外面的数据作为 read 读取数据的指针了。
下面来看看 bss 段的布局

我们可以看到 users 后面就是 name 缓冲区, name 的内容我们可控, 于是利用 edit 函数里面的 越界读 漏洞,我们就可以伪造 obj 指针, 然后在 通过 read 读取数据时 就可以往 obj 指针处写东西, 任意地址写
漏洞利用
控制rip
整理一下现在拥有的能力。
- 通过 选项5 可以
leak出libc的地址 - 通过
edit和changename可以实现任意地址写
题目给的 libc 是 2.23,没有虚表保护,于是选择改 stdout 的虚表指针,这样我们就可以伪造 stdout 的虚表,然后在调用虚表的时候,就可以控制 rip 了。
我们知道 stdout 是 _IO_FILE_plus 类型,大小为 0xe0 , 最后 8 个字节是 vtable (即 stdout+0xd8 处), 类型是struct _IO_jump_t 。
|
|
我们不能 leak 堆的地址,伪造虚表只能在 name 缓冲区内伪造,name 缓冲区的大小为 0x28 , 而 虚表(struct _IO_jump_t)的大小 为 0xa8 , 所以是不能伪造整个虚表的, 不过我们只需要把虚表中接下来会被调用的项的指针改了就行了 。有点绕,直接调试看。
首先使用选项5的函数, leak 出 libc 的基地址
|
|
然后我们在 name 缓冲区内布置好内容,让 越界读 使用
|
|
数据布置好了以后,利用 edit 里面的越界读漏洞,进行任意地址写, 修改 IO_2_1_stdout->vtable 为 name 缓冲区的地址
|
|
使用 ida 可以看到 users 数组的起始地址为 0x0602040 , name 缓冲区的地址 为 0x006020a0。 所以
|
|
这样一来就会把 name 缓冲区开始 的 8 个字节作为 user 指针对其进行内容修改。而在之前我们已经布局好 name ,使得 name 缓冲区开始 的 8 个字节 为 IO_2_1_stdout->vtable 的地址,这样在后面设置 new username 时 就可以修改 IO_2_1_stdout->vtable 了。
然后输入 new username 为 p64(bss_name) 前 6 字节 , 就可以修改 IO_2_1_stdout->vtable 为 name 缓冲区的地址。
只发送前 6 个字节的原因是
12 > len = strlen(obj);>
>
长度是用
strlen获取的,IO_2_1_stdout->vtable原来的值是libc的地址开始的6个字节是非\x00, 所以strlen会返回6。
接下来使用到 stdout 时,就会用到伪造的 虚表 (name 缓冲区)
调试看看, 会发现 crash 了

这里没有破坏栈的数据,所以栈回溯应该是正确的,所以看看栈回溯

可以看到 call [$rax + 0x38] , 然后 $rax 是 name 缓冲区的地址
所以现在 $rax 的值我们可控, 只需要使得 rax + 0x38 也可控即可
|
|
这样一来就可以控制 rip 了。
getshell
思路分析
可以控制 rip 后, 同时还有 libc 的地址 one_gadget 可以试一试,不过这东西看运气,在这个题就不能用。这里我们使用 rop 来搞。
要进行 rop 首先得控制栈的数据,现在 rax 是我们可控的,一般的思路就是利用 xchg rax,rsp 之类的 gadget 来迁移栈到我们可控的地方,这里采取另外一种方式, 利用 libc 的代码片段 ,直接往栈里面写数据, 布置 rop 链。
首先来分析下要用到的 gadget
位于 authnone_create-0x35 处
|
|
可以看到 首先
|
|
这样只要然后 rax+0x20 为 gets 函数的地址,就可以往 栈里面写数据了。
开始以为 gets 函数会读到 \x00 终止,后来发现不是, 函数定义
12 > gets 函数从流中读取字符串,直到出现换行符或读到EOF为止,最后加上NULL作为字符串结束>
>
EOF貌似是-1,所以我们可以读入\x00,而且输入数据的长度还是我们可控的 (通过控制\n)
此时已经可以覆盖返回地址了,下面就是让 上面的代码块 执行完 gets 后进入 loc_12B84A , 分支。
执行完 call qword ptr [rax+20h] 后,会从 esp+8 处取出 8 字节放到 rax ,然后判断 rax+0x38 处存放的值是不是 0 , 如果为 0, 就可以进入 loc_12B84A 进行 rop 了 .
|
|
exp分析
整理一下,分析分析最终的 exp
首先 leak 处 libc 的地址,获取到后面需要的一些 gadget 的地址
然后往 name 缓冲区布置数据
|
|
然后往触发漏洞,修改 _IO_2_1_stdout_->vtable 为 bss_name - 0x18
|
|
这就使得 接下来 call [eax + 0x38] 会变成 call [name+0x20] , 也就是 进入 gadget 。
会调用 call qword ptr [rax+20h] ,其实就是 call [name+0x8] , 之前已经设置为 gets 函数的地址,所以会调用 gets
|
|
然后通过 gets 往栈里面布置数据, 把 rsp+8 设置为 zero_addr (该位置的值为 p64(0)),然后 rop 调用 system("sh") 即可

总结
authnone_create-0x35 处的这个 gadget 还是比较有趣的,以后能控制 rax 处的内容 时可以选择用这种方式, 比如可以修改 虚表指针时。
参考
https://github.com/SPRITZ-Research-Group/ctf-writeups/tree/master/0x00ctf-2017/pwn/babyheap-200
本站文章均原创, 转载注明来源
本文链接:http://blog.hac425.top/2018/04/30/pwn_with_file_rop_by_vtable.html