Open Table of contents
写这篇博客的起因应该是为了一个即将到来的比赛,而我还有好多 high level tricks 没学过,万一在比赛上碰到了再临场学肯定是很浪费时间的,而且为不同 tricks 都单独写一篇博客显然不是很好,我一般喜欢围绕一个大的系列来写博客,这样才能显得不那么水,是吧?
好吧,上面说的只是一个最次要的原因罢了。这就不得不提到我 23 年刚推开 Pwn 之门的一条缝后的一个小梦想了……众所不周知 Pwn 方面优秀的系统教程应该可以说是少得可怜,相当于没有。所以我当时的这个小梦想就是写一份有关 Pwn 的详细教程,让有志之士从入门到入坟,少走弯路,不那么痛苦。伟大吗?
唉,你还能在我的原博客看到我以前写的系列文章。现在看看写的什么 trash,叫人从哪开始看都不知道,而且当时只是边学边翻译了 ir0nstone 的笔记,说白了就是搬运,没多少自己的成分在里面……所以这第二次做同样的事嘛,我一定会比第一次做的好 ∞ 倍。有关这方面,我的字典里面没有,也不允许出现「不行」这个词。
其实我本来想用 GitBook 或者建一个类似 wiki 的平台来写这个的,不过最终还是决定放在这里,为啥?我不道啊……
正如我在 Description 中写的:Keep updating as the mood strikes. 不论你现在看到的这篇文章有多简陋……给我点时间,未来它一定会成为一本不错的手册!
莫欺少年穷,咱顶峰相见。
——以上,书于 02/01/2025
我在 ROP Emporium - Challenge 8 写的已经很详细了,最近比赛赶时间,等我打完之后再来慢慢完善吧,只能暂且劳请各位老爷们先凑合着看了(如果有人看的话。/真叫人伤心)。
Okay, the first thing, what is SROP
?
SROP (Signal Return Oriented Programming)
,思想是利用操作系统信号处理机制的漏洞来实现对程序控制流的劫持。
首先得知道,信号是操作系统发送给程序的中断信息,通常用于指示发生了一些必须立即处理的异常情况。
而 sigreturn
则是一个特殊的 syscall
,负责在信号处理函数执行完后根据栈上保存的 sigcontext
的内容进行清理工作(还原到程序在进行信号处理之前的运行状态,就好像什么都没有发生一样,以便于接着运行因中断而暂停的程序)。它帮助程序从 Signal Handler(泛指信号处理函数)中返回,并通过清理信号处理函数使用的栈帧来恢复程序的运行状态。
我们知道,在传统的 ROP 中,攻击者可以利用程序中已存在的 gadgets 构造一个指令序列来执行恶意代码。SROP 则在此基础上利用了信号处理机制的漏洞,在信号处理函数的上下文切换中执行攻击,实现一次控制所有寄存器的效果。
攻击步骤大致如下:
- 攻击者通过在栈上伪造
sigcontext
结构来为控制寄存器的值做准备。一般会在这一步设置好后续 ROP Chain 中恶意代码要用到的参数,需要注意的是没有设置的寄存器在执行 sigreturn
后会被 zero out.
- 想办法令目标程序执行
sigreturn
,进行信号处理。
- 调用
sigreturn
后,系统会暂停当前程序的执行,通过 Signal Handler 处理信号,处理完后根据 sigcontext
的内容恢复运行环境。
- 这时候我们已经达成了设置参数的目的,可以选择返回到 ROP Chain 继续执行恶意代码。
正常情况下当遇到需要处理信号的时候,kernel
会在栈上创建一个新的栈帧,并生成一个 sigcontext
结构保存在新的栈帧中,sigcontext
本质上就是一个对执行信号处理函数前的运行环境的快照,以便之后恢复到执行信号处理函数之前的环境接着运行;接着切换上下文到 Signal Handler 中进行信号处理工作;当信号不再被阻塞时,sigreturn
会根据栈中保存的 sigcontext
弹出所有寄存器的值,有效地将寄存器还原为执行信号处理之前的状态。
那对于我们主动诱导程序去执行 sigreturn
的这种非正常情况,栈上肯定是不会有 kernel
生成的 sigcontext
结构的,我就问你你能不能在栈上伪造一个 sigcontext
出来?嘿嘿。
敏锐的你现在是不是想跳起来惊呼,这是不是好比核武器?没错,通过伪造 sigcontext
你可以一次控制所有寄存器的值,SO FUCKING POWERFUL!
不幸的是这也是它的缺点所在……如果你不能泄漏栈值,就无法为 RSP 等寄存器设置一个有效的值,这或许是个棘手的问题。但无论如何,这都是一个强大的 trick, 尤其是在可用的 gadgets 有限的情况下。
用于恢复状态的 sigcontext
的结构如下 (基于 x86_64
):
有关 SROP 的演示,这里还有一个视频讲的也很好,强推给你!
好了,知道了这些基础概念后,下面就通过 Backdoor CTF 2017 的 Fun Signals 这道题来实战一下吧~
可以看到这是一个非常简单的程序,单纯的就是为了考 SROP,以至于出题人都直接手撸汇编了。
注意到 flag 被硬编码在 0x10000023
这个位置了,所以我们的目标就是输出这个地址处的内容。由于这个程序没开 ASLR 什么的保护,拿下它还是非常轻松的。
简单分析一下这个程序,我们知道它在第一个 syscall
处调用了 read
,从 stdin
读取了 0x400
bytes 到栈上。紧接着第二个 syscall
直接帮我们调用了 rt_sigreturn
,那就不用我们自己动手了,我们只要伪造并发送 sigcontext
栈帧即可。思路是伪造一个 SYS_write
的调用,将 flag 输出到 stdout
。
总结:有的时候你的 ROP Chain 可能会缺少一些必要的 gadgets,导致无法设定某些后续攻击代码需要用到的参数,这时候就可以考虑使用 SROP 来控制寄存器。当然,使用 SROP 也是有条件的,比如你起码得有 syscall
这个 gadget,并且能控制 rax
的值为 sigreturn
的系统调用号。
TIP
要知道 rax
是一个特殊的寄存器,通常用于保存函数的返回值。所以当我说控制 rax
的值时,你不一定非得通过某些 gadgets 来实现这一点,有时候程序本身就可以为你设置好它,比如 read
函数会返回读到的字节数。
对于 dynamically linked
的程序,当它第一次调用共享库中的函数时,动态链接器(如 ld-linux-x86-64.so.2
)会通过 _dl_runtime_resolve
函数动态解析共享库中符号的地址,并将解析出来的实际地址保存到 GOT (Global Offset Table)
中,这样下次调用这个函数就不需要再次解析,可以直接通过全局偏移表进行跳转。
以上流程我们称之为重定位 (Relocation)。
具体重定位流程以及为什么需要重定位,不同 RELRO
保护级别之间的区别之类的,我之后再单独开一个小标题来写,这里先占个坑。
_dl_runtime_resolve
函数从栈中获取对一些它需要的结构的引用,以便解析指定的符号。因此攻击者通过伪造这个结构就可以劫持 _dl_runtime_resolve
让它去解析任意符号地址。
我们就以 pwntools
官方文档里面提供的示例程序来学习 how to ret2dlresolve. 其实就是学一下如何使用它提供的自动化工具……有关如何手动伪造 _dl_runtime_resolve
所需的结构体,以及一些更深入的话题,我之后应该还会回来填这个坑,先立 flag 了哈哈哈。
示例程序来源于 pwnlib.rop.ret2dlresolve — Return to dl_resolve,通过下面这条指令来编译:
pwntools
官方文档里给我们的程序源码是这样的:
但是编译出来后发现真 TM 坑,没有控制前三个参数的 gadgets,导致我们不能写入伪造的结构体……所以为了实验的顺利进行,我只得手动插入几个 gadgets 了:
程序很简单,从 stdin
读取了 200 字节数据到 buf
,由于 buf
只有可怜的 64 字节空间,故存在 136 字节溢出空间。空间如此之充裕,让我们有足够的余地来编排一曲邪恶的代码交响乐,演绎攻击者的狂想曲。
我们的目标是通过 ret2dlresolve
技术来 getshell. 思路大致应该是:将伪造的用于解析 system
符号的结构体放到一个 rw
空间,并提前设置好 system
函数要用到的参数,也就是将 rdi
设为 /bin/sh
字符串的地址。接着在栈上布置我们伪造的结构的地址,以便 _dl_runtime_resolve
引用我们伪造的这个结构体来解析符号。现在我们调用 _dl_runtime_resolve
就会解析出 system
的地址,根据我们先前设置好的参数,程序会乖乖的 spawn a shell.
当没有可用的 syscall
gadgets 来实现 ret2syscall
或 SROP
,并且没有办法泄漏 libc
地址时,可以考虑 ret2dlresolve
。
vDSO (Virtual Dynamic Shared Object)
是 Linux 内核为用户态程序提供的一个特殊共享库,注意它是虚拟的,本身并不存在,它做的只是将一些常用的内核态调用映射到用户地址空间。这么做的目的是为了加速系统调用,避免频繁地从用户态切换到内核态,有效的减少了切换带来的巨大开销。
在 vDSO 区域 可能存在一些 gadgets,用于从用户态切换到内核态。我们关注的就是这块区域里有没有什么可供我们利用的 gadgets,通常需要手动把 vDSO dump 出来分析。
崩溃了兄弟,我自己出了一道题然后折腾了两天做不出来……我好菜啊……受到致命心理打击。例题等我缓过来再说吧,估计一年都不想碰这个了……反正只要知道 vDSO 区域里面存在一些可用的 gadgets 就好了,剩下的和普通 ROP 没啥区别。
示范一下怎么通过 gdb dump 出 vDSO:
用 ROPgadget
分析 dump 出来的文件,大概那么一看有将近 500 个 gadgets,不过好像并不是很实用呢?感觉用这个 trick 性价比不高,不过也是一个值得尝试的方法。
嗯……再来介绍个好东西,叫 ELF Auxiliary Vectors (AUXV)
,ELF 辅助向量。它是内核在加载 ELF 可执行文件时传递给用户态程序的一组键值对。包含了与程序运行环境相关的底层信息,例如系统调用接口位置、内存布局、硬件能力等。
当一个程序被加载时,Linux 内核将参数数量 (argc)、参数 (argv)、环境变量 (envp) 以及 AUXV 结构传递给程序的入口函数。程序可以通过系统提供的 getauxval
访问这些辅助向量,以获取系统信息。
看看当前最新的 v6.14-rc1
内核中有关它的定义(旧版本中有关它的定义在 elf.h
中):
对于特定的架构可能还有一些特别的宏定义:
以上所有内容的参考链接我都放在末尾的 References 了,感兴趣的自行查阅。
我们可以通过指定 LD_SHOW_AUXV=1
来查看程序的 AUXV 信息:
注意到这个变量是以 LD_
开头的,说明动态链接器会负责解析这个变量,因此,如果程序是静态链接的,那使用这个变量将不会得到任何输出。
但是得不到输出不代表它没有,用 pwndbg
的 i auxv
或 auxv
也可以查看程序的 AUXV 信息(或者你手动 telescope stack):
这之中我们最关心的应该是 AT_SYSINFO_EHDR
,它与 vDSO
的起始地址相同。因此,只要能把它泄漏出来,我们就可以掌握 vDSO 的 gadgets 了。
其中 AT_RANDOM
好像也是一个很实用的东西,等我有空了再好好研究研究这些,话说这是我立的第几个 flag 了……
一般程序的返回地址之后紧接着的就是 argc
,然后是 argv
,再之后就是 envp
,最后还有一堆信息,它们就是 AUXV
了,这些都在栈上保存,自己研究去吧,我心好累……
反正我感觉这是一个性价比不怎么高的 trick,不过要是实在没办法搞到可用的 gadgets 的话还是可以考虑一下的。