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@<IP of virtual machine>
col@<IP of virtual machine>'s password:
NOTE: This is a *simulation* of a pwnable.kr CTF challenge
____ __ __ ____ ____ ____ _ ___ __ _ ____
| \| |__| || \ / || \ | | / _] | |/ ]| \
| o ) | | || _ || o || o )| | / [_ | ' / | D )
| _/| | | || | || || || |___ | _] | \ | /
| | | ` ' || | || _ || O || || [_ __ | \| \
| | \ / | | || | || || || || || . || . \
|__| \_/\_/ |__|__||__|__||_____||_____||_____||__||__|\_||__|\_|
- Site admin : daehee87.kr@gmail.com
- IRC : irc.netgarage.org:6667 / #pwnable.kr
- Simply type "irssi" command to join IRC now
- files under /tmp can be erased anytime. make your directory under /tmp
- to use peda, issue `source /usr/share/peda/peda.py` in gdb terminal
col@box:~$
First let’s find out about ourselves:
col@box:~$ id
uid=1000(col) gid=1000(col) groups=1000(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@box:~$ ls -la
total 24
drwxr-x--- 3 root col 120 Feb 4 14:59 .
drwxr-xr-x 4 root root 80 Feb 4 14:59 ..
d--------- 2 root root 40 Feb 4 03:41 .ash_history
-r-sr-x--- 1 col_pwn col 15448 Feb 4 04:42 col
-rw-r--r-- 1 root root 555 Feb 4 04:41 col.c
-r--r----- 1 col_pwn col_pwn 27 Feb 4 00:44 flag
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. User col_pwn
can read from it, and interestingly the executable col
has the SUID bit set as shown by the “s
” in -r-sr-x---
.
suid stands for “Set owner User ID up on execution,” meaning if we run the program, it will execute at the privilege level of its owner (i.e., col_pwn
). So if (for whatever reason) col
tried to read from flag
, it could, because col
would be running with the user 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 = 0xC3EA9FF2;
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;
}
In the main
function we see that if the check_password
function returns the same values 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/mytemp
col@box:~$ cp col.c /tmp/mytemp/mycol.c
Let’s have a look:
col@box:~$ cd /tmp/mytemp
col@box:/tmp/mytemp$ ls -la
total 4
drwxr-xr-x 2 col col 60 Feb 4 15:54 .
drwxrwxrwt 5 root staff 200 Feb 4 15:54 ..
-rw-r--r-- 1 col col 555 Feb 4 15:54 mycol.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@box:/tmp/mytemp$ vim /tmp/mytemp/mycol.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@box:/tmp/mytemp$ gcc -Wno-implicit mycol.c -o mycol
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@box:/tmp/mytemp$ ls -la
total 20
drwxr-xr-x 2 col col 80 Feb 4 15:56 .
drwxrwxrwt 5 root staff 200 Feb 4 15:56 ..
-rwxr-xr-x 1 col col 15452 Feb 4 15:56 mycol
-rw-r--r-- 1 col col 555 Feb 4 15:54 mycol.c
Here we see an executable file mycol
was created. Ok, let’s run it:
col@boc:/tmp/mytemp$ ./mycol
usage : ./mycol [passcode]
Let’s give it some input
col@box:/tmp/mytemp$ ./mycol 00001111
passcode length should be 20 bytes
col@box:/tmp/mytemp$ ./mycol 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@box:/tmp/mytemp$ python
Python 2.7.17 (default, Dec 11 2019, 16:49:43)
[GCC 9.2.0] on linux2
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 0xC3EA9FF2
. 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 “!” which has a hex value of 31
. It would be nice if we could input, say, a 01
. There’s no character we can type out directly, but we can tell the program to input a byte value of 01
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@box:/tmp/mytemp$ ./mycol $'\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.
Ok, now we have a little freedom to play around with these values to get them to add up to 0xC3EA9FF2
.
So at this point you should have all the background knowledge you need. Time to go capture that flag!