picoCTF 2017 - Shells writeup & more
as a part of my new year resolution (maybe a little late), i decided to improve my knowledge in binary exploitation. and since i’m pretty new at this, i ended up taking a shot at one of the binexploit problem from picoctf as it’s aimed at “entry-level” players
How much can a couple bytes do
1 |
|
from the source code provided, we can see that mmap()
will allocate a small executable virtual memory space (10 bytes) which then will hold our input passed using read()
. the buffer is then executed using a function pointer. the goal here is to call the win()
function in just 10 bytes, which will cat the flag back to us. therefore a simple push <win() address>; ret
will suffice
the challenge was fun and all, and i learnt a ton about shellcodes. but since it’s a fairly simple question, i decided to test myself and try to acquire a full shell access. the only problem here is that both virtual memory allocated and the read()
input was limited to 10 bytes
chaining shellcodes
as mentioned above, we can execute any arbitrary 10 bytes shellcode. so the idea here is to chain multiple 10 bytes shellcodes by jumping back to the read()
function at the end of each shellcode, while keeping enough space for other instructions. with this, we will have ample space to manipulate the stack and pass arbitrary arguments to the functions. also note that the system()
function is called in the win()
function. therefore, we can easily obtain its address by disassembling win()
function. now putting it all together:
1 | "\x90\x6A\x12\xBB\xC8\x85\x04\x08\xFF\xE3" \ |
let’s analyze the shellcode
the shellcode consist of 3 parts:
- increase buffer limit to 18 and jump back to
read()
- jump to
read()
and overflow the allocated space with “/bin//sh” - call
system()
with “/bin//sh” as the argument
notice that short shellcodes is padded with nops to fill out 10 bytes so that they will not be truncated by read()
. all we need to do now is pass the shellcode to the binary. but bear in mind that we need to keep the shell open for access so we can run other commands. this can be achieved by using cat -
to keep stdin open
no system for you
now the above method worked because we have acces to system()
address, but can we obtain full shell access without having access to it? remember how we can manipulate the stack and pass arbitrary arguments to the functions? we can apply the same technique and call mmap()
with larger buffer space. we then can call read()
with bigger count limit and pass a longer shellcode to be executed
1 | "\x53\x53\x53\xBB\xC6\x85\x04\x08\xFF\xE3" \ |
piping the shellcode to the binary will spawn us a shell:
conclusion
all of the stuff here is written from the perspective of a binexploit rookie. since it’s my very first attempt in binary exploitation, i had a lot of fun and saw it as an opportunity to learn more about elf debugging and radare2. hence, any tips or feedbacks are very much welcomed!