Tags: pwn radare2 

Rating:

For this challenge we are given an executable which asks for some input on startup. Let's decompile it with r2ghidra-dec -- a Ghidra decompiler integration into radare.

undefined8 main(void)
{
    char *s;
    uint32_t var_4h;

    var_4h = 0;
    sym.imp.setbuf(_reloc.stdout, 0);
    sym.imp.setbuf(_reloc.stdin, 0);
    sym.imp.setbuf(_reloc.stderr, 0);
    sym.imp.puts("Please pour me some coffee:");
    sym.imp.gets(&s);
    sym.imp.puts("\nThanks!\n");
    if (var_4h != 0) {
        sym.imp.puts("Oh no, you spilled some coffee on the floor! Use the flag to clean it.");
        sym.imp.system("cat flag.txt");
    }
    return 0;
}

Something to note is that if we were to use Ghidra, it would show something like char s [44];, but let's find this out using radare and see how these two variables are placed one after the other on the stack.

158: int main (int argc, char **argv, char **envp);
│           ; var char *s @ rbp-0x30
│           ; var uint32_t var_4h @ rbp-0x40x00401156      55             push rbp
│           0x00401157      4889e5         mov rbp, rsp
│           0x0040115a      4883ec30       sub rsp, 0x300x0040115e      c745fc000000.  mov dword [var_4h], 0
│           ; <redacted>, Sets up stdio buffers and prompts for input 
│           0x004011ad      488d45d0       lea rax, [s]
│           0x004011b1      4889c7         mov rdi, rax        ; char *s
│           0x004011b4      b800000000     mov eax, 00x004011b9      e8a2feffff     call sym.imp.gets   ;[3] ; char *gets(char *s)
│           0x004011be      488d3d5f0e00.  lea rdi, str.Thanks ; 0x402024 ; "\nThanks!\n" ; const char *s
│           0x004011c5      e866feffff     call sym.imp.puts   ;[2] ; int puts(const char *s)
│           0x004011ca      837dfc00       cmp dword [var_4h], 0
│       ┌─< 0x004011ce      741d           je 0x4011ed
│       │   ; <redacted>, Flag is printed here
│       └─> 0x004011ed      b800000000     mov eax, 00x004011f2      c9             leave
└           0x004011f3      c3             ret

At the very start we see the two variables, s and var_4h defined by radare with offsets off rbp and then assigned to in assembly below:

; var char *s @ rbp-0x30
; var uint32_t var_4h @ rbp-0x4
0x0040115e      c745fc000000.  mov dword [var_4h], 0
0x004011ad      488d45d0       lea rax, [s]

It's clear that the size of them together is 0x30:

0x0040115a      4883ec30       sub rsp, 0x30

s is placed at the start with offset rbp - 0x30 and then var_4h is placed at rbp - 0x4. This means that the s buffer which we control is of size 0x30 - 0x4 = 0x2c or 44 in decimal. Thus, we can overflow this buffer with the vulnerable gets() call and overwrite var_4h by sending 44 bytes followed by our new value for var_4h.

The flag is printed if var_4h is no longer 0, so overwriting it with anything but null bytes should do the trick. The comparison can be seen here:

0x004011ca      837dfc00       cmp dword [var_4h], 0

The flag we get is: csictf{y0u_ov3rfl0w3d_th@t_c0ff33l1ke@_buff3r}

Original writeup (https://fluix.dev/blog/csictf-2020-pwn-intended/).