经过一场面试没想到自己的基础如此 vulnerable,在此复盘一下面试并回顾一下几个用户态 PWN 常见的难点。
多线程与 TLS
问题:1)TLS 在什么位置 2)主线程与子线程的堆分配有什么不同
TLS 所在的空间由 mmap 分配,主线程的 TLS 位置通常比较随机,而子线程的 TLS 通常作为线程栈的一部分被分配,其好处是避免额外的内存分配。
其中带来的问题就是子线程中可能可以通过不太大的栈溢出就能覆写 TLS 的 stack_guard
:
在 glibc 中,线程有自己的 arena,但是 arena 的个数是有限的,一般跟处理器核心个数有关,假如线程个数超过 arena 总个数,并且执行线程都在使用,那么该怎么办呢。Glibc 会遍历所有的 arena,首先是从主线程的 main_arena 开始,尝试 lock 该 arena,如果成功 lock,那么就把这个 arena 给线程使用。
Arena 负责堆块内存的管理,但 Thread 与 Arena 也不是一一对应的:
- 32 位系统 arena 数量为:
2 * core + 1
- 64 位系统 arena 数量为:
8 * core + 1
在多线程程序中就出现了堆块重用:
-
从 main_arena
开始遍历所有 arena,并尝试对其上锁
-
若成功上锁,则返回给用户
-
若没有可用的 arena,则阻塞这次调用
ret2dlresolve
问题:简述 ret2dlresolve 的使用场景与限制条件
ret2dlresolve 通常用来应对栈题不给泄漏的情况,要求已经能控制程序控制流(ROP)、已知 elf 基址、没有开启 FULL_RELRO
。
这里有几个思路类似的利用手法:
原理
ret2dlresolve 的利用技巧与动态链接 elf 的 lazybinding 机制有关:
- 首先在调用
libc_func@plt
的时候,动态链接的程序并不能直接跳转到 libc 里执行,而是先 jmp 到其 got 表对应的表项上继续运行,这也是 got-hijack 利用手法的由来。
► 0x555555400710 <puts@plt> jmp qword ptr [rip + 0x200902] <puts@got[plt]>
0x555555400716 <puts@plt+6> push 0
0x55555540071b <puts@plt+11> jmp 0x555555400700 <0x555555400700>
↓
0x555555400700 push qword ptr [rip + 0x200902] <_GLOBAL_OFFSET_TABLE_+8>
0x555555400706 jmp qword ptr [rip + 0x200904] <_dl_runtime_resolve_xsavec>
- 在非
FULL_RELRO
的情况下,程序会先将目标 got 表项序号(如此处 puts 为 0,write 为 1)推入栈中作为第二个参数,link_map
推入栈中作为第一个参数调用 _dl_runtime_resolve
。源码见 /sysdeps/x86_64/dl-trampoline.h
。
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 6
[0x555555601018] puts@GLIBC_2.2.5 -> 0x555555400716 (puts@plt+6) ◂— push 0 /* 'h' */
[0x555555601020] write@GLIBC_2.2.5 -> 0x555555400726 (write@plt+6) ◂— push 1
[0x555555601028] strlen@GLIBC_2.2.5 -> 0x555555400736 (strlen@plt+6) ◂— push 2
[0x555555601030] __stack_chk_fail@GLIBC_2.4 -> 0x555555400746 (__stack_chk_fail@plt+6) ◂— push 3
[0x555555601038] read@GLIBC_2.2.5 -> 0x555555400756 (read@plt+6) ◂— push 4
[0x555555601040] setvbuf@GLIBC_2.2.5 -> 0x555555400766 (setvbuf@plt+6) ◂— push 5
- 最后
_dl_runtime_resolve
函数内部调用 _dl_fixup
实现动态库中函数的查找,实则又是通过字符串定位的目标函数。源码见 /elf/dl-runtime.c#L61
。
其中有如下关键定义:
NO_RELRO
直接劫持 strtab
NO_RELRO
的场景下可以直接写 _DYNAMIC
区域内存,在另一个地方伪造新的 strtab 并存入 _DYNAMIC.DT_STRTAB
:
无需泄漏 libc 地址,最终可以把 victim 函数@plt + offset 的地址当作指定 libc 函数在 ROP 链中进行调用(这里是 puts@plt+6 被解析成 system 函数实现利用):
Partial_RELRO
伪造 link_map
在 Partial_RELRO
的情况下,GOT 表以外的数据段就变成了只读(包括上面的 _DYNAMIC
),但是 GOT 表仍然是可写的(为了支持 lazy binding 加速程序启动速度)。在这种情况下上述针对 NO_RELRO
修改 DT_STRTAB
的利用方法就失效了,但 link_map
还是可以伪造的。
利用的关键在 _dl_fixup
函数里,函数本身比较复杂。但是假如把关注点放在伪造 link_map
任意调用 libc 函数上,我们就只需要使 l->l_addr
为自定义偏移值,sym->st_value
为已解析函数,最后手动模拟 plt 表中调用 _dl_runtime_resolve
函数就能在无泄漏的情况下任意调用 libc 函数。
继续结合调试分析 link_map
伪造格式:
这里值得思考的是 fake_symtab
的构造,其它都可以根据调试逆推得到:
-
首先最终利用要借助 l->l_addr + sym->st_value
,前者已经能够由伪造 link_map
任意控制,后者最好是一个已解析的 got 表项的值。
-
还要绕过 sym->st_other != 0
,调试可以得到 ( *(sym + 5) ) & 0x03 != 0
的条件。
故 got 表项中的第一项是最合适的 sym->st_value
值,同时也符合条件 2。
在这里(victim 程序见附录)即为 elf.got.puts - 8
,同时手动调用 _dl_runtime_resolve
前需要额外布置栈上的索引值(0):
FULL_RELRO
不再适用
在开启 FULL_RELRO
的情况下 ret2dlresolve 的利用方法不再合适,若题目仍然无法产生任何泄漏,ROP 可以低位覆盖栈中残留数据来触发 syscall(比赛中成功过)。
补充
2024/03/18
今天突然想看一下控制流相关的保护,一查才发现 linux 竟然早在 5.13 版本就为 x86_64
架构提供了用户空间影子栈的支持。用户空间程序需要使用支持影子栈的 glibc 版本,并通过 prctl 系统调用显式启用影子栈。
2024/03/13
面试时还多次问到了「你知不知道有什么保护可以阻止栈溢出」类似的问题,我只回答了一个 canary。但结束后仔细一想 fortify 其实也是,作为 GCC 提供的源码级别的保护,可以通过编译选项 -D_FORTIFY_SOURCE={0,1,2} -O1
选择开启级别,会将 printf、read、memcpy 等函数编译成 __read_chk
等函数,若 nbytes > buflen
,则会直接 SIGABRT
结束程序运行。
作为源码级别的保护,我一直下意识地把它当做 消除漏洞 而非保护,就没回答上来,不知道还有没有其它的答案。此外,如果在编写题目时受到了这个东西的干扰,可以直接用 -O0
来禁用。
此外面试时似乎会很在意技术博客里的内容,以后还是要好好写((
「所以说你现在 V8 也看得不多,kernel 也是刚起步」,不过我之前也一直不太确定做什么。之前看 browser pwn 感觉学习资料太少就转内核了,这次面试的时候我也把自己的方向往 kernel 上去说,但是对方好像更想要浏览器的新人(。总归固定下自己的方向比较好,精力是有限的,如果不出意外的话我会继续研究内核,感觉这里面有很多东西可以学习。
References
[1] 线程PWN之gkctf2020 GirlFriendSimulator . ha1vk
[2] V8 沙箱绕过 . jayl1n
[3] ret2dl-runtime-resolve详细分析(32位&64位) . ha1vk
[4] ctf-wiki ret2dlresolve . ctf-wiki
[5] Understanding glibc malloc . sploitfun
附录
malloc 测试程序
ret2dlresolve victim 程序