6.1.15 pwn 34C3CTF2017 SimpleGC
题目复现
$ file sgc
sgc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f7ef90bc896e72ba0c3191a2ce6acb732bf3b172, stripped
$ checksec -f sgc
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 4 sgc
$ strings libc-2.26.so | grep "GNU C"
GNU C Library (Ubuntu GLIBC 2.26-0ubuntu2) stable release version 2.26, by Roland McGrath et al.
Compiled by GNU CC version 6.4.0 20171010.一看 libc-2.26,请参考章节 4.14,tcache 了解一下。然后程序开启了 Canary 和 NX。
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1 # 假设两个 user 的 group 相同
Enter group name: A
User:
Name: a
Group: A
Age: 1
User:
Name: b
Group: A
Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 3 # 修改 group,输入 y
Enter index: 0
Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): y
Enter new group name: B
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1 # 两个 user 的 group 都被修改
Enter group name: B
User:
Name: a
Group: B
Age: 1
User:
Name: b
Group: B
Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 3 # 修改 group,输入 n
Enter index: 0
Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): n
Enter new group name: A
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1 # 仅当前 user 的 group 被修改
Enter group name: A
User:
Name: a
Group: A
Age: 1
0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 1
Enter group name: B
User:
Name: b
Group: B
Age: 1玩一下,程序似乎有两个结构分别放置 user 和 group。而且 Edit 功能很有趣,根据选择 y 还是 n 有不同的操作,应该重点看看。
题目解析
GC
main 函数开始会启动一个新的线程,用于垃圾回收,然后才让我们输入菜单的选项。刚开始 r2 并不能识别这个线程函数,用命令 af 给它重新分析一下。函数如下:
从这段代码中我们看出一个结构体 group:
然后是 0x60 个 group 类型指针构成的数组 groups,其起始地址为 0x6023e0。仔细看的话可以发现,这段代码在取 ref_count 值的时候,只取出了一个字节。所以 ref_count 的类型可以推断地更精细一点,为 uint8_t。
该垃圾回收函数会遍历 groups,当 groups[i]->count 为 0 时,表示该 group 没有 user 在使用,于是对 groups[i]->group_name 和 groups[i] 分别进行 free 操作,最后把 groups[i] 设置为 0。
最后需要注意的是垃圾回收的周期,在写 exp 的时候要考虑。
add a user
从这个函数中能看出第二个结构体 user:
同样的,0x60 个 user 类型指针构成了数组 users,其起始地址为 0x6020e0。
我们看到输入的 group 作为参数调用了 sub.strcmp_be0():
所以这个函数的作用是检查 groups 中是否已经存在同名的 group,如果是,那么将该 group 的 ref_count 加 1,并返回这个 group。否则返回 0。
当返回值为 0 的时候,会调用函数 fcn.00400cdd(),参数为 group:
该函数在第一个 groups[i] 为 0 的地方创建一个新的 group,将其放入 groups,并返回这个 groups[i]。
总的来说,当添加一个 user 时,首先检查输入的 group 是否存在,如果存在,那么将这个 group->ref_count 加 1,设置 user->group 指向这个 group->group_name,否则新建一个 group,并将新 group->ref_count 设置为 1,同样设置 user->group 指向它。
display
其中 display-a-user 用于打印出指定 index 的 user,即 users[i]。display-a-group 遍历 users,并打印出指定 group 与 users[i]->group 相同的 users[i]。根据经验,这个功能就是为了泄漏 heap 和 libc 地址的。
edit a group
我们比较感兴趣的修改 group 操作:
该函数有两种操作:
输入 "y" 时:修改 users[i]->group,于是所有具有相同 group 的 user->group 都被修改了。这样的问题是会造成有两个同名 group 的存在。
输入 "n" 时:如果 group 已经存在,则将 group->ref_count 加 1,并设置 users[i]->group 赋值为 group->group_name。否则新建一个 new_group,将 group_ref_count 设置为 1,同样将 users[i]->group 赋值为 new_group->group_name。这里同样存在问题,当修改了一个 user 的 group 之后,原 group->ref_count 并没有减 1,可能会造成溢出。
delete a user
最后是删除 user 的操作:
其中调用了函数 sub.strcmp_139(),如下:
该函数的作用是遍历 groups 寻找与传入 group 相同的 groups[i],然后将 groups[i]->ref_count 减 1。这里有个问题,正如我们在 edit-a-group 分析的,通过修改 group,可能使 groups 中存在两个同名的 group,那么根据这里的逻辑,这两个同名的 group 的 ref_count 都会被减去 1,可能导致 UAF 漏洞。
然后是删除 user 的过程中,只释放了 user 本身和 user->group,而 user->name 没有被释放。可能导致信息泄漏。
漏洞利用
逆向分析完成,来简单地总结一下。
两个结构体和两个由结构体指针构成的数组:
添加 user 时将创建 user 结构体,name 字符串两个 chunk
新建 group 时将创建 group 结构体,group_name 字符串两个 chunk
group 本身和 group->group_name 由 GC 线程来释放
user 在删除时释放了 user 本身,group->ref_count 减 1,而 user->name 将导致信息泄漏
ref_count 类型为 uint8_t 且在修改组是不会减 1,将导致溢出(例如:0x100 和 0x0),使 GC 进行释放 group 的操作
如果有两个同名的 group,两个 user 分别指向这两个 group,那么释放其中一个 user 时,另一个也会被释放,造成 UAF
然后是关于 tcache 的问题。在这个程序中有两个线程,thread-1 为主线程,thread-2 为 GC 线程,它们都有自己的 tcache。程序中所有 chunk 的分配工作都由 thread-1 执行,thread-2 只释放(group和group_name)不分配,所以在它的 tcache bins 被装满以后所有该线程释放的 fast chunk 都被放进 fastbins 中。而 fastbins 是进程公用的,所以会被主线程在分配时使用。
第一种方法,我们利用 ref_count 溢出的 UAF。
overflow
首先我们来溢出 ref_count:
首先说一下 for 循环,前几次当 thread-2 的 tcache 还未装满时,它的操作和下面类似(顺序可能不同):
当 thread-2 tcache 装满时,它释放的 chunk 都会被放进 fastbins,于是就可以被 thread-1 取出,下面是第 4 和 第 5 次循环:
此时的 thread-1 tcache 和 fastbin 如下所示:
于是第 6 次循环,在第一次从 fastbin 中取出 chunk 后,剩余的 chunk 会被放入 thread-1 tcache(逆序),然后再从 tcache 里取(FILO):
再往后,其实都是重复这个过程。循环结束时的状态为:
紧接着我们再添加一个 user,导致 ref_count 溢出为 0x100 后,程序只有只有将低位的 0x00 放回 ref_count,于是 GC 会将 group_name 和 group struct 依次释放,放进 fastbin。
最终结果为:
最后将 groups[0] 赋值为 0,表现为 groups[] 为空。但 users[0] 依然存在,users[0]->group 依然指向 group_name(0x603380),悬指针产生。
uaf and leak
接下来利用悬指针泄漏 libc 的地址:
在执行该函数前的 tcache 如下:
当我们添加一个 user 时,因为 group "BBBB" 不存在,所以首先创建一个 group,然后再创建 user,这个 user struct 将从 thread-1 tcache 中取出。接下来我们修改 user[0]->group 就是修改 user[1]。我们将 strlen@got 写进去,在延迟绑定之后,它将指向 strlen 函数的地址,如下所示:
接下来只要 display users[1],就可以将 strlen 的地址打印出来,然而:
strlen@got 指向的并不是 strlen 函数,而是它里面的 __strlen_sse2,这就很奇怪了。原因出在这次 commit。libc-2.26 中使用了 AVX2 对 strlen 系列函数进行优化。
那我们修改一下,反正计算偏移的方法是相同的:
然而就得到了 system 的地址。
get shell
最后只需要修改 strlen@got 为 system@got 就可以了:
exploit
完整的 exp 如下:
虽然这一切看起来都没有问题,但我在运行的时候 system('/bin/sh') 却执行失败了,应该是我的 /bin/sh 不能使用这个 libc 的原因:
应该换成 Ubuntu-17.10 试试。(本机Arch)
第二种方法,我们利用两个具有同名 group 的 user 释放时的 UAF。这种方法似乎与 tcache 的关系更大一点。
参考资料
Last updated
Was this helpful?