pwnable.kr (Collision)

Tutorial Notes

What you’ll learn:

  • Basic command-line operations (ssh, ls, gcc, etc.)
  • Unix based file permissions (owner, group, suid, etc.)
  • Variable representation in memory (the difference between char arrays, integers, etc.)

Directions:

Download the Assignment 2 virtual machine from OWL Brightspace. Refer to the VM setup instructions to install and setup and access the assignment virtual machine on your host device.

Using username col and password col, use ssh to log into the Assignment 2 virtual machine. Once logged in, use ls -l to print the directory:

ssh col@pwnable.kr -p2222
col@pwnable.kr's password:
██     ██ ███████ ██       ██████  ██████  ███    ███ ███████     ████████  ██████
██     ██ ██      ██      ██      ██    ██ ████  ████ ██             ██    ██    ██
██  █  ██ █████   ██      ██      ██    ██ ██ ████ ██ █████          ██    ██    ██
██ ███ ██ ██      ██      ██      ██    ██ ██  ██  ██ ██             ██    ██    ██
 ███ ███  ███████ ███████  ██████  ██████  ██      ██ ███████        ██     ██████


██████  ██     ██ ███    ██  █████  ██████  ██      ███████    ██   ██ ██████
██   ██ ██     ██ ████   ██ ██   ██ ██   ██ ██      ██         ██  ██  ██   ██
██████  ██  █  ██ ██ ██  ██ ███████ ██████  ██      █████      █████   ██████
██      ██ ███ ██ ██  ██ ██ ██   ██ ██   ██ ██      ██         ██  ██  ██   ██
██       ███ ███  ██   ████ ██   ██ ██████  ███████ ███████ ██ ██   ██ ██   ██


Admin: daehee (daehee87@khu.ac.kr)
Please note that server is under renewal/update.
Please don't brute-force the resource, be kind to other users.
Installed Tools: pwndbg, qemu, python2, python3
(let admin know if some essential tool is missing)
**IMPORTANT: stuff under /tmp can be erased. "/usr/local/bin/cleanup_tmp.sh" runs every 24H **
col@ubuntu:~$

First let’s find out about ourselves:

col@ubuntu:~$ id
uid=1005(col) gid=1005(col) groups=1005(col)

Ok so we have username col and we belong to group col. Next let’s see what’s contained in our home directory:

col@ubuntu:~$ ls -la
total 44
drwxr-x---   5 root col      4096 Apr  2  2025 .
drwxr-xr-x 118 root root     4096 Jun  1  2025 ..
d---------   2 root root     4096 Jun 12  2014 .bash_history
-r-xr-sr-x   1 root col_pwn 15164 Mar 26  2025 col
-rw-r--r--   1 root root      589 Mar 26  2025 col.c
-r--r-----   1 root col_pwn    26 Apr  2  2025 flag
dr-xr-xr-x   2 root root     4096 Aug 20  2014 .irssi
drwxr-xr-x   2 root root     4096 Oct 23  2016 .pwntools-cache

First we observe there’s an executable file named col that we can run, as well as a source file col.c that presumably contains the code to the executable. We can also see the flag file flag, but we don’t have permission to read from it. The root user, and the group col_pwn can read from it, and interestingly the executable col has the guid bit set as shown by the “s” in:

-r--r-----   1 root col_pwn    26 Apr  2  2025 flag

guid stands for “set owner group ID,” meaning if we run the program, it will execute at the privilege level of its group (i.e., col_pwn). So if (for whatever reason) col tried to read from flag, would have the necessary permission, because col would be running with the group ID col_pwn and thus would be allowed to access flag.

Ok, so let’s take a look at the code in 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] )){
		setregid(getegid(), getegid());
		system("/bin/cat flag");
		return 0;
	}
	else
		printf("wrong passcode.\n");
	return 0;
}

In the main function we see that if the check_password function returns the same value as hashcode, then the program will print the contents of the flag.

Awesome. So we somehow need to get check_password to return the value 0x21DD09EC. But how?

Maybe we could make a copy col.c and add in some printf statements to help us see what the program is doing. Let’s try to make a copy of col.c:

col@box:~$ cp col.c mycol.c
cp: can't create 'mycol.c': Permission denied

Ah, yes. The permission of the directory is:

drwxr-x---    3 root     col            120 Feb  4 14:59 .

That means we are not allowed to create files here. But recall the login message made a mention to “make your directory under /tmp”. Let’s try that:

col@box:~$ mkdir /tmp/some-random-name/
col@box:~$ cp col.c /tmp/some-random-name/mycol.c

Let’s have a look:

col@ubuntu:~$ cd /tmp/some-random-name
col@ubuntu:/tmp/some-random-name$ ls -la
total 75968
drwxrwxr-x 2 col  col      4096 Jan 26 22:11 .
drwxrwx-wt 1 root root 77692928 Jan 26 22:11 ..
-rw-r--r-- 1 col  col       589 Jan 26 22:11 col.c

It worked. Ok, now let’s edit our file and stick in a couple of printf statements so we can see what’s happening:

col@ubuntu:/tmp/some-random-name$ vim col.c

I’m using Vim, a command-line based text editor. If you’ve never used Vim, check out this interactive tutorial. Ok, let’s add those printf statements:

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];
                printf("%x\n",ip[i]); // <---- print statement we added
        }
        printf("Result: %x\n",res); // <---- print statement we added
        return res;
}

Here the statement printf("%x\n",ip[i]); causes the program to print out the integer stored in ip[i], and the %x means format it as a hexidecimal value. We also will print out the sum stored in res to see how close we come to the target value.

Now let’s compile our modified program:

col@ubuntu:/tmp/some-random-name$ gcc -Wno-implicit col.c -o col

All the -Wno-implicit does is suppress a compiler warning about the use of the system() call, which is not particularly relevant to this challenge. Next, let’s see what we have:

col@ubuntu:/tmp/some-random-name$ ls -la
total 75984
drwxrwxr-x 2 col  col      4096 Jan 26 22:15 .
drwxrwx-wt 1 root root 77692928 Jan 26 22:16 ..
-rwxrwxr-x 1 col  col     16256 Jan 26 22:15 col
-rw-r--r-- 1 col  col       642 Jan 26 22:15 col.c

Here we see an executable file col was created. Ok, let’s run it:

col@ubuntu:/tmp/some-random-name$ ./col
usage : ./col [passcode]

Let’s give it some input

col@ubuntu:/tmp/some-random-name$ ./col 00001111
passcode length should be 20 bytes

col@ubuntu:/tmp/some-random-name$ ./col 00001111222233334444
30303030
31313131
32323232
33333333
34343434
Result: fafafafa
wrong passcode.

Interesting. If we look up the ASCII code for the character “0”, we see that it’s 30. Similarly “1” is 31, “2” 32, etc. And somehow these numbers are adding up to fa.

Let’s just check this in Python:

col@ubuntu:/tmp/some-random-name$ python
Python 3.10.12 (main, Feb  4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(int("30",16) + int("31",16) + int("32",16) + int("33",16) + int("34",16))
'0xfa'

So it looks like the program is consuming single-byte char types from standard in, and casting them into 4-byte integers types with int* ip = (int*)p;. So a 20 byte char array bytes becomes an integer array of 5 elements, and the program adds these 5 integers together and compares them to the target value of 0x21DD09EC. So we need to somehow manufacture the sum of these 5 integers to equal the target value.

But inputting characters makes things tricky for us. The lowest ASCII value we can manage is “ “ (space) which has a hex value of 0x20. It would be nice if we could input, say, a 0x01. There’s no character we can type out directly, but we can tell the program to input a byte value of 0x01 using the following notation:

$ ./program $'\x01'

There’s one caveat: you can’t input \x00. This is a null byte and denotes the end of the string. The program will read until it hits a null byte and stop. But we can still input a byte value between 01 and FF in this matter, so let’s try running our program again:

col@ubuntu:/tmp/some-random-name$ ./col $'\x01\x01\x01\x01\x02\x02\x02\x02\x03\x03\x03\x03\x04\x04\x04\x04\x05\x05\x05\x05'
1010101
2020202
3030303
4040404
5050505
Result: f0f0f0f
wrong passcode.

Take note: The leading zeros are missing. Ok, now we have a little freedom to play around with these values to get them to add up to 0x21DD09EC.

But first, let’s just try one last example:

col@ubuntu:/tmp/some-random-name$ ./col $'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x03\x04\x05'
1010101
1010101
1010101
1010101
5040302
Result: 9080706
wrong passcode.

Interesting. Notice that the last 4 bytes was \x02\x03\x04\x05, i.e., lowest to highest. But the result was 9080706, i.e., highest to lowest. That’s because we’re using a little-endian architecture. That means the little of the integer comes first. In the case, the 0x02 comes first, so it’s the little end. But human-readable digits are printed with the little end last. The order is backwards. Take that into consideration as you try to solve this challenge.

At this point you should have all the background knowledge you need. Time to go capture that flag!