LNC 4.0: Welcome2Pwn - Mon, Mar 11, 2024
What better way to learn pwn than to go on a trip to the beach? (Because there are shells!)
Challenge files: https://github.com/KaligulaArmblessed/CTF-Challenges/tree/main/Welcome2Pwn
Welcome2Pwn
Welcome to the land of pwn! Come follow me to the beach; however, make sure to ignore Sally standing sadly at the side selling shells. Who needs to buy shells when we can pop our own? :D
But first… What is pwn?
The fancy name for pwn is binary exploitation. Pwn is the art of crafting a custom exploit to hack a vulnerable program (often called a binary), be it one that is running on a remote server, or the operating system kernel itself! This will be an (almost) step-by-step guide to pwning your very first binary.
If you look at the challenge files, you will see that you have been provided with the following:
- welcome: The vulnerable binary that you will exploit
- welcome.c: The source code for the binary
Tools of the Trade
In this case, we are exploiting a Linux binary, so having access to a Linux command line is crucial. The following tools would be of great help, so do make sure to install them before proceeding:
- Python
- The pwntools python package – this is a specialized set of tools and wrappers designed to make exploit writing a lot easier
- gdb: The GNU debugger
- pwndbg: A plugin for gdb that adds many useful functions and pretty colors
- A gdb cheat sheet if you are unfamiliar with using gdb – just google one!
Source Code Analysis and Vulnerability
We are provided with the source code of the binary, so let’s look for anything that seems like it could go wrong:
int main(void) {
char buf[0x100]; <-- [1]
...
printf("Welcome to pwn!\n");
printf("Show me a cool exploit technique!\n");
printf(">> ");
fgets(stuff, 0x100, stdin);
printf("Now tell me where to go: \n");
printf(">> ");
fgets(buf, 0x200, stdin); <-- [2]
return 0;
}
This is the main function. Note that at the start, we define a character buffer of size 0x100 bytes (1). However, later on in the function, we call fgets (2). The function prologue of fgets is as such:
char *fgets(char *str, int n, FILE *stream)
So in (2), we are trying to read 0x200 bytes from the file stream stdin (which is where the user would enter text in a terminal), and write these 0x200 bytes into the buffer “buf”. However, remember that we defined the buffer “buf” to be of size 0x100. If we attempt to write more than 0x100 bytes into buf, we will exceed its capacity, leading to a vulnerability known as a buffer overflow.
When a local variable is defined in a function, it will be allocated in a location of memory known as the stack. When a function is running, all the local variables defined during its runtime would be inside a stack frame. In this case, when buf was defined, 0x100 bytes of memory would be allocated to it on the stack. Two CPU registers define the stack frame: rsp points to the top of the stack, and rbp points to its base. A typical stack layout would look like this:
Assuming that we only key in 20 “A"s and hit enter, fgets will only write 20 characters to buf, which is less that its capacity of 0x100. Nothing is written outside of the buf region, so once fgets has finished, and the main function returns, it will pop the return address (which is stored on the stack frame) into RIP (the instruction pointer, which tells the program what instruction to run next), and allow main to return normally.
Now, imagine you are a person who really likes shells on remote servers you shouldn’t have access to malicious actor who wants to control program flow to run whatever code you want. Knowing that you have the ability to write more than 0x100 bytes into buf, and that the return address would be popped into the instruction pointer once main returns, what would you do?
If your idea was to overflow “buf” by writing more than 0x100 bytes and overwriting the return address with whatever you want, you are on the right track! The question now would be how many bytes must I spam into the buffer in order to overwrite the return address, and calculating this is very simple. We know that the buffer is 0x100 bytes, and that the rbp of the previous stack frame (which would be 8 bytes in a x64 system) is stored before the return address, which means that we need to have a padding of 0x108 to reach the return address. If we write 0x108 As and 8 Bs, we will be able to overwrite the return address with 8 Bs. When the program returns from main, it will attempt to jump to memory address 0x4242424242424242 (“BBBBBBBB”), which does not exist, causing the program to crash with a segmentation fault.
If you don’t like math (like me), there is an even simpler way to do this. Enter pwntools, which has this wonderful utility known as “cyclic”. What cyclic does is that it generates a de Brujin sequence, which can be used to easily identify the offset to the return address for you.
To use cyclic, simply type cyclic <number>
, where the number is the length of the sequence you want to generate.
┌──(kaligula㉿cat)-[~/Downloads/CTF/LNC4/welcome2pwn]
└─$ cyclic 500
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae
Now, copy the sequence generated. We are going to start gdb with gdb <binary>
, where binary would be the location of the executable file (in this case probably ./welcome). If you have downloaded pwndbg it will look like this:
Hit r
and enter to start the program, enter some random crap when it asks you about cool exploit techniques (we will talk about this later) and hit enter, then paste in the cyclic payload you previously copied, and hit enter. You will see that the program has segfaulted, and looks something like this:
Notice that the program is trying to return to address 0x6361617263616171, which is “qaacraac”. Now open up a text editor, and start writing a new python script:
from pwn import *
proc = process("./welcome") ## Start the binary as a local process
elf = ELF("./welcome") ## To get the symbols in the binary
padding = cyclic(cyclic_find("qaac"))
rip = b"BBBBBBBB"
payload = padding + rip
print(payload)
If you run the script and copy the payload printed out, and then run gdb again, but pasting in the newly generated payload instead of the previously generated cyclic sequence when the program asks you “where to go”, you will note that RIP is now 0x4242424242424242 (BBBBBBBB), and that you have successfully controlled the instruction pointer.
Return Target
Now that we can overwrite the return address with whatever we want, it is time to decide where to return to. Remember when we told the program some rubbish when it asked us about cool exploit techniques? Let’s look back at what is going on with that in the source code.
We know that from main, 0x100 bytes are read into a buffer called stuff. Looking at the source code would tell us that stuff is defined as a global variable (not allocated on stack), and is a character array of size 0x100. As fgets will read a maximum of (count - 1) characters, and will terminate with a null byte afterwards, there is no buffer overflow in “stuff”. However, there is this rather interesting function present:
void shellz(void) {
printf("Here's your shell!\n");
memcpy(code, stuff, 0x100);
typedef void (*func_t)(void);
((func_t)code)();
}
What this function does is that it copies memory from the “stuff” buffer into an executable memory region (previously set up) called “code”, and then executes whatever machine instructions present in that memory region. This means that if we can call the shellz function, we can run whatever machine code we want! (I wonder who put this super convenient “run any random shellcode” function there… 🤔)
For instance, say I want to make the CPU execute 3 nops (no instruction, i.e. do nothing 3 times). The opcode (which is usually a bunch of hexadecimal numbers) corresponding to nop is \x90, hence 3 nops would be \x90\x90\x90. If I fill the “stuff” buffer with 3 nops, then overflow the “buf” buffer and overwrite the return address with the address of shellz, the code flow would be such after the program returns from main:
-> return from main
-> call the shellz function
-> copy whatever is inside stuff to code
-> execute the code region as a series of machine instructions
When the code region is executed, the 3 nops would be run!
A possible exploitation plan would hence be:
- Fill the “stuff” buffer with whatever machine instructions we want to execute
- Overflow the buffer
- Overwrite the return address with the address of shellz
- Profit!!! When main returns, it will return to shellz, which will then run the machine code that you have provided when the program asked you about cool exploits :D
PWN TIME!
It is time to exploit the target! This is when pwntools becomes extremely convenient, because it helps to set up pipes so that you can send and receive data from the target binary.
Your exploit would be something like this:
from pwn import *
proc = process("./welcome")
elf = ELF("./welcome")
shellcode = ## your own shellcode! Maybe something that pops a shell? :D
proc.sendline(shellcode) ## proc.sendline lets you send whatever you want to the binary
padding = cyclic(cyclic_find("qaac"))
rip = p64(<address of shellz>) ## figure out how to get the address of the shellz function!
## Note that the p64 function converts the order of your bytes to little endian
payload = padding + rip
print(payload)
proc.sendline(payload)
proc.interactive() ## Open an interactive session
proc.close()
If you aren’t familiar with shellcode: shellcode is a small piece of machine code written in assembly (and translated to hex opcodes) that when run, does whatever you want it to do (e.g. pop a shell, open and read a file, etc.).
If all goes well, you should have your shell!
In order to connect to the remote instance, swap out the proc = process("./welcome")
line in your exploit with proc = remote("<ip>", port)
and run it; it should give you a shell on the remote server!
If you like pwn, there are many great pwn resources out there, such as Pwn Nightmare, pwn.college, and many more. And of course, you can try the Cheminventory challenge ;)
Hope you’ve enjoyed this challenge!