Flash CTF – Do Not Strike The Clouds

Summary

This is a pwn challenge where you have to host (and yes, there are free services for this!) a subdomain in DNS with so many records that it overflows a fixed-size buffer in the program’s memory. The contents of the DNS records will overflow onto the return address, and if one of your records contains the address of the win() function, the program will jump there.

Walkthrough

Let’s start by running checksec on the binary to see if it has the standard memory corruption mitigations enabled:

$ checksec chal
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/home/nikola/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

If you decompiled the program, you might see the presence of a win function:

void win(void) {
  char *pcVar1 = flag;
  if (flag == (char *)0x0) {
    pcVar1 = "FLAG NOT LOADED";
  }
  printf("Hi! Here\'s the flag: %s\n",pcVar1);
  return;
}

The lack of mitigations and the presence of a win() function might hint us towards this being a pretty straightforward challenge, binary-exploitation-wise. We’re probably not going to have to write shellcode or ret2libc or any of that, or even get a shell on the box. There’s literally a win function that prints the flag if we manage to jump to it.

Let’s try running the program.

$ ./chal 
Enter hostname: google.com
Enter port number: 80
Will try opening port 80 on google.com...
Checking if 142.251.167.139:80 is open... Port is open!
Checking if 142.251.167.113:80 is open... Port is open!
Checking if 142.251.167.138:80 is open... Port is open!
Checking if 142.251.167.101:80 is open... Port is open!
Checking if 142.251.167.100:80 is open... Port is open!
Checking if 142.251.167.102:80 is open... Port is open!
Checking if [2607:f8b0:4004:c19::8b]:80 is open... Port is open!
Checking if [2607:f8b0:4004:c19::8a]:80 is open... Port is open!
Checking if [2607:f8b0:4004:c19::66]:80 is open... Port is open!
Checking if [2607:f8b0:4004:c19::71]:80 is open... Port is open!

As promised in the description, it looks to be a port-scanner of some sort.

Looking through the decompiled code, it looks like it gets our hostname and port number (securely, with fgets and scanf – no obvious exploit there) then enters into test_host_port(host, port);. In there, it calls get_addrs(host, ???);, a function which seems to be doing DNS resolution on the hostnames we enter and storing the resulting IP addresses (v4 and v6) into an array, which is the second parameter to the function. Then in order, each IP address in that array is printed out and test_addr_port is called, although oddly, Ghidra doesn’t catch the arguments being passed to that function (it’s just a sockaddr*).

What is there to exploit here? The hostname and port number are the only input we supply, and those are read in securely. The test_addr_port function seems complicated, but there’s no obvious memory corruption issues – the server is never actually read from, the program just tests whether connect()ing the socket succeeds or not.

It does seem like there’s a whole lot of complexity in how it does DNS resolution, with that whole separate get_addrs function and that weird custom-formatted array. Let’s take a closer look at that.

We might notice that get_addrs seems to be passed a fixed-size array, while the function itself…

for (addrinfo = initial_addrinfo; addrinfo != (addrinfo *)0x0; addrinfo = addrinfo->ai_next) {

… just loops through the linked list results of getaddrinfo. Interesting. What would happen if we made a hostname with a whole bunch of DNS records, and pointed the program at that? Let’s use a tool like https://messwithdns.net/ or https://dynv6.com/ to do this for free. Let’s use IPv6 records so they take up as much space as possible.

I made a domain with nine AAA records, and tried it against the binary locally:

./chal 
Enter hostname: test.sweater232.messwithdns.com
Enter port number: 22
Will try opening port 22 on test.sweater232.messwithdns.com...
Checking if [::1]:22 is open... Port is closed.
Checking if [::7]:22 is open... Port is closed.
Checking if [::6]:22 is open... Port is closed.
Checking if [::5]:22 is open... Port is closed.
Checking if [::4]:22 is open... Port is closed.
Checking if [::2]:22 is open... Port is closed.
Checking if [::9]:22 is open... Port is closed.
Checking if [600:0:900:0:4e:3dee:ff7f:0]:22 is open... Port is closed.
Checking if [0:89::]:22 is open... Port is closed.
Segmentation fault

Hey!

From here, you’ll want to try and figure out the right mix of IPv4 and IPv6 addresses to supply to get full overflow coverage of the return address on the stack.

You can generate a cyclic pattern using pwntools, or just sort of make one up yourself (0102:0304:0506:0708:090a:0b0c:0d0e:0f10), and run the program under GDB and see where it tries to jump to. Then slot in the actual address of the win function (objdump -d chal | grep win to find 00000000004013b6) and offset it within the IPv6 address such that it’s positioned properly. Note that the program appends 06 or 04 to the beginning of each address within the array, and you don’t want those bytes to get in your way, so you’ll want at least some IPv4 addresses on your domain to make sure the offset of the IPv6es is correct.

Craft an IPv6 address that contains the address of the win function at the proper offset. Our solve used:

            vvvvvvvvv WIN ADDR HERE INCL. ZEROES REPRESENTED BY THE "::", other bytes are irrelevant
8888:9999:aab6:1340::ee:ff06

You may need multiple copies of this record, with the non-address bytes changed because DNS needs unique record values.

You can dig x.testmeta12345.dynv6.net or nslookup test.knight206.messwithdns.com to see how our solve domains did it. Note the two A records as well, for proper offset of the AAAAs.

Just supply one of these domains as the hostname, and any port at all as the port, and it should get the flag.