二进制漏洞利用部分的题目难度会稍微高一些,逆向和 PWN 也一直是信安实践「压轴题」的部分。PWN 的主要学习目标是理解程序运行过程中的内存结构、了解调试器的使用等。
去年 PWN 的三道题完成人数分别是 40+、30+、10+,由于有同学反映学期末时间紧张、环境配置麻烦等问题,今年将题目数量缩减到了两题、每题分值降低到了 25 分、并且降低了交互的难度。
经过实验报告的审核,今年压轴题有 16 个提交是有效的,两道拓展题也有 2 个有效提交。
PWN 部分在总分中占比很少,无论是否完成了题目都不用担心这道题给最终得分带来的影响。
isadbg 专业版
这道题和去年出给 NebuCTF 的 DebugMyself 很像,但去年比赛时完全没有人看我的题,于是信安实践上重新拿来用了。
从设定上来说,这道题不需要选手编写利用脚本来解题,只需要通过 stack
指令查看栈布局,通过 gift
指令获取 pop rdi ; pop rsi ; pop rdx ; pop rax ; ret
等语句的地址,最后通过 set
指令完成栈上数据的布置就能 getshell:
这道题需要思考的点在于 SYS_execve
的第一个参数需要传入指向「目标可执行文件绝对地址」字符串的指针,例如 /bin/sh\x00
或者 /readflag\x00
,但是直接输在缓冲区中会让换行符 \n
也一起跟进去,导致利用失败。
解决方法有两种:
- 把字符串用 set 也设置到可控地址上,这也是上面利用脚本中的做法;
- 把输入字符串填充到 fgets 的最大读入长度(题目中是 0x30),在这种情况下 fgets 不会读入最后的换行符:
b"a"*0x29 + b"/bin/sh"
,同时栈底 canary 最低字节的 \x00
可以保证地址字符串的截断。
isadbg 试用版
为了实现「所有必做题都可以不写脚本」的目标,这道题沿用了 isadbg 的题目框架,但是增加了一些限制:
- 需要泄漏 ELF 与 libc 的基地址
- 写入的地址范围受限,只能改
.bss
段的数据
题目的难点在于泄漏地址与偏移的计算,其中要求 GOT 表劫持、格式化字符串泄漏等知识。关于劫持 atoi 函数 got 表到 printf 函数 plt 表的利用是本题难点,不过已经在讲义 / 课堂上进行了充分的提示,希望做题人完成后有「眼前一亮」的感觉。
分析
主要漏洞函数位于 update_to_pro
:
- 残留数据:这里更安全的写法是使用 buf 前先进行清零。为了使泄漏的漏洞更明显,出题时先用
.bss
段上的全局变量地址对 buf 进行了赋值。read 是非常底层的库函数,即它不会处理换行符,也不会在末尾添加 \x00
截断。因此这里如果输入一个大于 2 的十进制数就会把 command 地址泄漏出来,进而得到了 elf 的基地址。GOT 表和 elf 中的其它函数调用都可以根据 elf 基地址 + 固定偏移量得到,因此就攻破了 PIE 保护。
- 任意地址写:在
vuln
函数的 set
功能中存在一个受限的任意地址写,通过课程 / 讲义讲解可以意识到 Partial RELRO ELF 的 GOT 表也是位于这部分的,这样就可以劫持 GOT 表完成利用。
这明明不任意啊,地址不是被限制在 .bss
段上了吗?
实际上像这种能够指定地址进行写入的能力统称为「任意地址写」,有别于普通的溢出,任意地址写可以实现分段写入数据,提供了更强大的内存控制能力。我所熟知的 V8 等 RealWorld 等漏洞利用中就是在构造「任意读」和「任意写」的原语,在此之后就有通用的模板写入 shellcode 完成完整利用。
解法1 - ret2dlresolve(非预期)
这个解法门槛较高,需要对 lazy binding 机制有更深入的理解,我把它放在前面是为了更好地解释 GOT 表和 plt 表的调用机制。
ret2dlresolve 通常用来应对栈题不给泄漏的情况,要求已经能控制程序控制流(ROP)、已知 elf 基址、没有开启 FULL_RELRO
。
这里有几个思路类似的利用手法:
对于本题,不妨来考虑原 elf 中根本没有调用过 printf 的情况,失去格式化字符串泄漏地址的能力后我们难以拿到 libc 的基地址,这时候就可以通过 ret2dlresolve
的方法完成利用。
原理
- 首先在调用
libc_func@plt
的时候,动态链接的程序并不能直接跳转到 libc 里执行,而是先 jmp 到其 got 表对应的表项上继续运行,这也是 got-hijack 利用手法的由来。
- 在非
FULL_RELRO
的情况下,程序会先将目标 got 表项序号(如此处 puts 为 0,write 为 1)推入栈中作为第二个参数,link_map
推入栈中作为第一个参数调用 _dl_runtime_resolve
。源码见 /sysdeps/x86_64/dl-trampoline.h
。
- 最后
_dl_runtime_resolve
函数内部调用 _dl_fixup
实现动态库中函数的查找,实则又是通过字符串定位的目标函数。源码见 /elf/dl-runtime.c#L61
。
其中有如下关键定义:
NO_RELRO
的场景下可以直接写 _DYNAMIC
区域内存,在另一个地方伪造新的 strtab 并存入 _DYNAMIC.DT_STRTAB
,在 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。
解题
回到题目中,泄漏 elf 基地址的方法是一样的,在此之后可以按照上述思路构造出如下 fake_link_map
结构:
接下来要完成利用,还要构造出 [my_buf, 0]
的栈布局并且调用到 plt0 + 6
的位置,这对于常规 ret2dlresolve 栈溢出的例题而言并不困难,但是本题只能篡改 .bss
段上的内存,没有提供栈上 ROP 的能力,通常会想到栈迁移之类的做法,不过这里还有更简单的解法
我用 ropper 看了一下,题目中并没有能够直接操作栈的 gaegets,但是 plt 头部本身就提供了先 push 0x0
再 jump 到 plt0 的位置 push [_GLOBAL_OFFSET_TABLE_+8]
的调用链,got 表又是可写的,因此就免去了栈迁移的麻烦。
因此可以篡改 puts 以外的某个函数的 got 表项到上面 0x555555555034
的位置,再布置好 GOT 表上 [_GLOBAL_OFFSET_TABLE_+8] = my_buf
实现利用,最终 exp 如下:
解法2 - GOT Hijacking
回到标准解法,思路其实已经呈现在第二次课讲义中了,这里再引用一下:
本地运行程序
若下载附件后本地运行报错,多半是 libc 版本的问题,可以 ldd --version
查看本地的 libc 版本,用 patchelf 工具来改变题目可执行文件的动态链接库路径:
此外如果想要 glibc 的调试信息,可以直接借助 glibc-all-in-one 下载打包好的 libc。
got 表与 plt 表
课上演示了篡改 got 表后会发生什么,即设置好参数后执行篡改后的地址。
例如程序原本写了 puts("hello")
,编译出来就是 mov rdi, hello_string_address; call puts@plt
,而 puts@plt
内代码最终会执行 jmp [puts@got]
,即通过篡改 got 表可以实现控制流的劫持。
计算偏移
因为开启了 ASLR 和 PIE,所以每次运行程序所有地址都会发生变化,包括 got 表、静态变量区、栈、libc 函数等,因此要在利用过程中完成如下过程:
graph TB
A[漏洞1] --> B[泄漏 ELF 范围内地址]
B --> C[计算 got 表相关地址]
C --> E[泄漏 libc 范围内地址]
D[漏洞2] --> E
E --> F
D --> F[调用 system 函数 getshell]
计算偏移的方法很多,包括 gdb 或 python。
解题
至于标准解法是不需要编写脚本的,这里演示直接在命令行中解题的办法:
-
连接远程题目,同时本地开启一个计算偏移的工具(ipython / gdb 等都可以)。
-
泄漏 ELF 基地址,这需要借助 subscribe
功能:
- 注意输入数字后不能传入换行符,而需要用
ctrl + D
传入 EOF:
- 计算偏移,这里选用
python + pwntools
:
-
得到 elf 基地址后可以篡改 atoi
的 got 表项为 printf
以泄漏 libc 地址:
-
用格式化字符串来泄漏地址,这里提供一种无需调试的办法:
- 先在本地运行起来,用
cat /proc/<pid>/maps
确定 libc 地址范围:
- 再依次试格式化字符串泄漏,直到得到一个符合范围的地址,因此可以确定格式化字符串参数位置:
- 可喜可贺的是,第三个地址就符合要求,因此泄漏 libc 的 payload 确定为
%3$p
,计算出远程 libc 地址如下:
-
最后一步就是劫持 atoi 的 got 表到 libc 中的 system 函数并且执行 system("sh;)
,此处分号是因为原 atoi 的参数是用 read 读入的,并且没有 \x00
截断,为了防止栈上脏数据影响利用故用分号隔开。
- 先计算出 system 函数在 libc 中的地址:
完整利用截图如下:
isadbg 免费版
这道题有两个考查点:
- IO leak
- 在新版本 libc 下拥有了全局任意读写能力后怎么 getshell
第一步的 IO leak 需要爆破 161 的概率,第二步则非常自由,可以再次使用 IO leak 去泄漏 libc 中 environ 变量的内容得到栈地址,回到栈上进行 ROP。或者可以用 IO 相关的攻击,下面演示了 House of Apple2 的利用:
测测你的 uri
这是一道整数溢出导致堆溢出的题目,灵感来源于 CVE-2024-34402、CVE-2024-34403 两个 CVE,难点在于虽然溢出长度是无限的,但是难以写入不可见字符。
因此要完成利用需要进行一些堆风水,篡改堆块的大小导致堆块重叠,进而进行 Tcache bin 的利用。