The r2con CTF is the CTF for the r2con 2019 held during the weekend before the conference which consisted mainly of reversing challenges. I managed to solve all but one challenge (technicaly, at least...) and it was so much fun! I'd like to thank the organizers a lot for making the event happen :-)
There may be errors and inclompete sections. I tried to make a write-up for every challenge, just contact me if anything is unclear or missing!
Time to remember the best games ever!
Would you able to find pancake? He is keeping a secret for you!
We download a gameboy rom. I played around with it in an emulator, then looked at it in r2. There flag is in strings:
> r2 -qqc 'izz~punk4ke' r2boy1.gb
909 0x000180be 0x000540be 18 19 (rombank06) ascii yay_punk4ke_f0wnd\n
Have you attended r2con before? Show me then
We download an ARM aarch64
ELF binary. Havin qemu-aarch64-static
installed I can run the binary on my notebook:
> qemu-aarch64-static babyr2
wr0ng length?
Ok cool. Lets do r2 -A babyr2
. No more flags in strings, so lets seek to the main
method. Right at the beginning is the following code:
│ 0x00400408 a02f00b9 str w0, [arg_2ch] ; argc |
│ 0x0040040c a11300f9 str x1, [arg_20h] ; argv |
│ 0x00400410 a02f40b9 ldr w0, [arg_2ch] ; [0x2c:4]=-1 ; 44 ; tmp=0xffffffffffffffec ; w0=0xffffffff |
│ 0x00400414 1f100071 cmp w0, 4 ; zf=0x0 ; nf=0x1 ; cf=0x1 ; vf=0x0 |
│ ╭─< 0x00400418 c0000054 b.eq 0x400430 ; unlikely |
│ │ 0x0040041c 600200f0 adrp x0, 0x44f000 ; x0=0x44f000 -> 0x97ff1324 |
│ │ 0x00400420 00600891 add x0, x0, 0x218 ; 0x44f218 ; "wr0ng length?"
This tells us that there are 3 arguments and my assumption was you will get the flag once you got them all right.
Right after the check against argc
, the first argument is parsed to an integer and then checked against the static value of 0xcafe
:
│ 0x00400434 00200091 add x0, x0, 8 ; x0=0x7 │ 0x00400434 00200091 add x0, x0, 8 ; x0=0x7 |
│ 0x00400438 000040f9 ldr x0, [x0] ; tmp=0x7 ; x0=0xffffffffffffffff |
│ 0x0040043c 05140094 bl loc._x_36 ;[3] ; lr=0x400440 -> 0xb90037a0 ; pc=0x405450 -> 0xa9bf7bfd sym.atoi |
│ 0x00400440 a03700b9 str w0, [arg_34h] |
│ 0x00400444 a13740b9 ldr w1, [arg_34h] ; [0x34:4]=-1 ; 52 ; tmp=0x34 ; w1=0xffffffff |
│ 0x00400448 c05f9952 movz w0, 0xcafe ; w0=0xcafe |
│ 0x0040044c 3f00006b cmp w1, w0 ; zf=0x0 ; nf=0x1 ; cf=0x1 ; vf=0x0 |
│ ╭─< 0x00400450 c0000054 b.eq 0x400468 ; unlikely |
│ 0x00400438 000040f9 ldr x0, [x0] ; tmp=0x7 ; x0=0xffffffffffffffff |
│ 0x0040043c 05140094 bl loc._x_36 ;[3] ; lr=0x400440 -> 0xb90037a0 ; pc=0x405450 -> 0xa9bf7bfd sym.atoi |
│ 0x00400440 a03700b9 str w0, [arg_34h] |
│ 0x00400444 a13740b9 ldr w1, [arg_34h] ; [0x34:4]=-1 ; 52 ; tmp=0x34 ; w1=0xffffffff |
│ 0x00400448 c05f9952 movz w0, 0xcafe ; w0=0xcafe |
│ 0x0040044c 3f00006b cmp w1, w0 ; zf=0x0 ; nf=0x1 ; cf=0x1 ; vf=0x0 |
│ ╭─< 0x00400450 c0000054 b.eq 0x400468 ; unlikely
0xcafe
is 51966 in decimal.
Second arg looks more complicated, but the third arg is just a static strcmp
against radare2c0n
.
Knowing the first and third arg and being a lazy person, I just decided to brute force the second arg using a simple shell script.
Turns out that the first hit for the second arg is 25
:
> for i in {0..30}; do ./babyr2 51966 $i radare2c0n; done
[...]
ok,ok! you are not a baby r2 reverser!
The key to be a g00d reverser is: r2con{c0ffee}
[...]
We don’t have much time. Let’s get started.
We download ctf.file
which is a statically linked ELF64 binary.
First, by checking the strings we find the following:
[0x0047cb70]> iz~r2con
2191 0x000c3185 0x004c3185 2035 2037 (.rodata) utf8 [...] r2con2019{all_your_answers_in_order_without_spaces} [...]
Let's just run run:
> ./ctf.file
> ./ctf.file
Welcome To The Greatest Adventure Of Your Life
---------------------
In this game, I will ask you questions and you will have to answer. Some valid answers are y or n, but these are not the only ones. You will need to figure out the rest by yourself :)
---------------------
Do you understand the rules?
y
Great. First question: Have you ever cried during sex?
wtf
Your poor decisions led you to your death. Sorry.
So the goal is to make it to last question :-) Let's analyze the binary. My workflow is usally to check the strings first and the functions:
:> afl~sym.go.main
0x0048a260 15 503 sym.go.main.encode
0x0048a460 8 68 sym.go.main.check
0x0048a4b0 8 2766 sym.go.main.main
0x0048af80 7 107 sym.go.main.init
After a quick analysis of the main function (sym.go.main.main
) you can conclude that inside the main function there is a loop that scans some input, calls the encode
function with your input and then check
your input.
By having a glance at the disassembly inside encode
one particular opcode sticks out:
:> pd1@0x0048a32f
│ 0x0048a32f 4131d2 xor r10d, edx
This looks very interesting. Basically, in a crackme, whenever you see an xor
instruction xoring 2 different value, you should take note. So let's add breakpoint there.
On to the check
function. It's a really simple function that loops through something and compares this something against another something:
:> pd3@0x0048a485
│ 0x0048a485 0fb6341a movzx esi, byte [rdx + rbx]
│ 0x0048a489 0fb63c19 movzx edi, byte [rcx + rbx]
│ 0x0048a48d 4038fe cmp sil, dil
Time for another breakpoint @ 0x0048a48d.
Let's start debugging. Once our breakpoint inside encode
is hit, we note that our input (y
for the first question) is xored against the first value in a byte array, currently pointed to by the rsi
register. We keep that in mind and continue execution (this is probably the key).
As we hit the next breakpoint we see that, indeed, our xored input is compared against some other value. So now we have everything, we can xor
the first cipher byte (0x17
) against the first key byte (n
= 0x6e) to get the correct first answer:
[0x00000000]> ? 0x6e^0x17
[...]
string "y"
[...]]
Now we can step through manually or use a script to extract the all answers (I was too lazy to write a script but given the length of the flag it would've been worth it I guess):
echo -e "y\nnever\nboth\nuserious\nwhat\nlightthetorch\niguessiopenit\nturntheknob\nWITHAKEYIUSEAKEY" | ./ctf.file
Welcome To The Greatest Adventure Of Your Life
---------------------
In this game, I will ask you questions and you will have to answer. Some valid answers are y or n, but these are not the only ones. You will need to figure out the rest by yourself :)
---------------------
Do you understand the rules?
Great. First question: Have you ever cried during sex?
Hmmm. Are you red or purple?
Are you a giraffe or a seagull?
Is the key in the room?
You are standing in a dark room and can’t see anything. There is a torch and a match. What do you do?
The torch fills the dark room with light. You see a door in front of you. What do you do?
How do you open it?
The knob doesn't turn. How do you open it?
Thank you.
---------------------
The flag is r2con2019{all_your_answers_in_order_without_spaces}
An Android developer was playing around with some cryptography mechanisms to hide secrets, but maybe the method is not secure at all.
We download a file re.rada.con.ctf.r2xor.apk
which is an android app.
I extraczed it's content using jadx
.
The interesting Java source files are to be found inside sources/re/rada/con/ctf/r2xor/
:
> l sources/re/rada/con/ctf/r2xor/
.rw-r--r-- cyrill users 15.6 KB Sat Aug 31 15:55:29 2019 MainActivity.java
.rw-r--r-- cyrill users 89.4 KB Sat Aug 31 15:55:28 2019 R.java
.rw-r--r-- cyrill users 2 KB Sat Aug 31 15:55:28 2019 RootDetection.java
Let's quickly have a look at MainActivity
class, which is more or less like a main function for the App.
First thing we see: It implements a callback when something is clicked. This calls some validate
method and logs Congratulations, your flag is correct :)
if validate
returns 1.
The next function jadx failed to decompile. I checked the disassembly; it's some antidebug/antiroot functionality, but nothing directly related to the flag (I think).
So let's proceed with the validate
function. Where is it? On the very bottom of MainActivity.java
it is declared:
public native boolean validate(String str);
But I was unable to find it's implementation in the Java source code. After a quick internet research I learned that native
is used to call native function. A native function is a function that is not implemented in Java, so there must be a library containing it's implementation.
Found some promising shared object files:
find . -name *.so
./resources/lib/armeabi-v7a/libtool-checker.so
./resources/lib/armeabi-v7a/libnative-lib.so
./resources/lib/x86_64/libtool-checker.so
./resources/lib/x86_64/libnative-lib.so
./resources/lib/x86/libtool-checker.so
./resources/lib/x86/libnative-lib.so
./resources/lib/arm64-v8a/libtool-checker.so
./resources/lib/arm64-v8a/libnative-lib.so
Two libraries for each supported architecture. libtool-checker.so
is smaller and contains anti debugging stuff, so lets focus on libnative-lib.so
.
As usual check strings first. One sticks out and is really interesting:
:> iz~super
006 0x00025d35 0x00025d35 11 12 (.rodata) ascii supersecure
:> axt str.supersecure
(nofunc) 0xd533 [DATA] lea r10, str.supersecure
(nofunc) 0xd5b9 [DATA] lea rax, str.supersecure
sym.decrypt_char_const___char 0xd64f [DATA] lea r13, str.supersecure
Let's seek to this function decrypt_char
and analyze it. In a nutshell it loads our supersecret
as a key and XORs this key against some other bytes.
│ ╎││ 0x0000d674 4132041f xor al, byte [r15 + rbx]
The pointer to the encrypted flag is an argument to that function, so not really easy to get with static analysis. But just when I was working on it, an easier version of this challenge was release. I downloaded it and checked what has changed. Among the changes I found it interesting that the very beginning of the .rodata
section changed (.rodata
is where the strings are stored). In the easier challenge file, the .rodata
section looks like this:
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF comment [0x00025cc0]
0x00025cc0 0147 130a 1c08 1d53 072d 5446 2a1e 5545 .G.....S.-TF*.UE ; section..rodata ; [14] -r-- section size 11616 named .rodata ; str.S___TF ; str.UEre_rada_con_ctf_r2xor_MainActivity |
0x00025cd0 7265 2f72 6164 612f 636f 6e2f 6374 662f re/rada/con/ctf/
That first string is really suspicious ;-) I guessed it was the encrypted flag and I was right.
import sys
key = "supersecure"
cipher=[0x01, 0x47, 0x13, 0x0a, 0x1c, 0x08, 0x1d, 0x53, 0x07, 0x2d, 0x54, 0x46, 0x2a, 0x1e, 0x55, 0x45, 0x2c, 0x50, 0x53, 0x2a, 0x47, 0x56, 0x10, 0x20, 0x45, 0x56, 0xf]
for i in range(0, len(cipher)):
sys.stdout.write(chr(cipher[i]^ord(key[i%len(key)])))
print()
# r2con{x0r_15_n07_50_53cU53}
New r2 plugin to remotely reverse binaries with your folks. First of all, you need to login....
We download a binary r2darling
which is an ELF64 executable.
It looks like the following if we execute it:
> ./r2darling
RadareCON Secure collaborative-REshell Login
Password:
> Checking Password ... Fail!
So, a classic crackme :-) Let's open r2, seek to the main
function and get an overview. The following command shows all calls from inside the main function:
:> pdr@main~call
│ 0x00401d23 e898ffffff call sym.my_print
│ 0x00401d35 e886ffffff call sym.my_print
│ 0x00401d49 e8a2360100 call sym.malloc ; sym.malloc_hook_ini-0x310
│ 0x00401d5c e85ffeffff call sym.my_getpass
│ 0x00401d86 e8c5fdffff call sym.scramble
│ 0x00401d95 e826ffffff call sym.my_print
│ 0x00401dba e801ffffff call sym.my_print
│ 0x00401dc7 e834aa0300 call sym.sleep ; int sleep(int s)
│ 0x00401df3 e878f2ffff call 0x401070
│ 0x00401e0b e8b0feffff call sym.my_print
│ 0x00401e1d e89efeffff call sym.my_print
│ 0x00401e34 e887feffff call sym.my_print
Ok, get_pass
followed by scramble
look already quite interesting. Lets break there:
:> dcu sym.scramble
Continue until 0x00401b50 using 1 bpsize
RadareCON Secure collaborative-REshell Login
Password:hit breakpoint at: 401b50
The function does not look complicated and at address 0x00401b9d
there's an XOR, and before that at address 0x00401b8c
the key byte is loaded. So we can break there and extract it:
:> db 0x00401b88; dc; x@rdi
hit breakpoint at: 401b9d
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x004a5100 1003 2e1f 101a 6e04 1852 5833 5f01 6b21 ......n..RX3_.k!
0x004a5110 0716 535d 0000 0000 0000 0000 0000 0000 ..S]............
The compare function is called at address 0x00401df3
. If we break there the encrypted flag is in the rsi
register (an argument to the strncmp_avx2
) function:
:> x@rsi
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x004a5120 7333 6372 6574 5f70 616e 6b41 6b65 5f53 s3cret_pankAke_S
0x004a5130 3475 6333 2100 0000 0000 0000 0000 0000 4uc3!...........
This is all we need :-)
import sys
key = [0x10, 0x03, 0x2e, 0x1f, 0x10, 0x1a, 0x6e, 0x04, 0x18, 0x52, 0x58, 0x33, 0x5f, 0x01, 0x6b, 0x21, 0x07, 0x16, 0x53, 0x5d, 0x0]
cipher = [0x73, 0x33, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x6e, 0x6b, 0x41, 0x6b, 0x65, 0x5f, 0x53, 0x34, 0x75, 0x63, 0x33, 0x21]
for i in range(0, len(cipher)):
sys.stdout.write(chr(cipher[i]^key[i]))
print()
# c0Mmun1ty<3r4d4r3c0n!
Find Ole into the labyrinth and ask him for the flag.
Insist but don't be rude!
We download a gameboy rom r2boy.gb
.
I did not use r2 to solve this challenge. I started the ROM using the mGBA
emulator and had a look at the tileset. The flag is in the Tileset from the first moment you start the game: HIDR3NM4P
I failed to type it correctly into the validator and so did not get the points. But after that I thought this is too easy and started looking into the roms code. Time was wasted xD
Find the secret flag in the upside down.
We download 2 files, vermu_elf
which is a ELF64 binary and another file called level1
.
level1
is a small file:
> r2 -qqc 'x304' level1
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00000000 1000 0010 0810 0000 0020 1000 0000 0050 ......... .....P
0x00000010 1000 0000 5641 bce6 b80a baad 1bab c5dd ....VA..........
0x00000020 1214 b2c1 1b74 7fc5 2197 6555 1298 9315 .....t..!.eU....
0x00000030 df80 1529 7c7d 312b 249a 2b29 d567 0d7d ...)|}1+$.+).g.}
0x00000040 80d8 124e 91be e865 a758 efd8 b252 18de ...N...e.X...R..
0x00000050 4314 cbed aaff 1000 0000 0110 0000 1004 C...............
0x00000060 2010 0000 1008 1000 0010 0021 1000 0000 ..........!....
0x00000070 0433 3021 1000 0000 1610 0000 1000 2110 .30!..........!.
0x00000080 0000 0004 3330 2138 3110 0000 0036 1000 ....30!81....6..
0x00000090 0010 0021 1000 0000 0433 3021 3210 0000 ...!.....30!2...
0x000000a0 00b4 4210 0000 0000 1000 0010 0420 1000 ..B.......... ..
0x000000b0 0000 c641 1000 0000 0110 0000 1004 2134 ...A..........!4
0x000000c0 1000 0010 0420 1000 0010 0021 1000 0000 ..... .....!....
0x000000d0 0130 1000 0010 0020 1000 0000 0810 0000 .0..... ........
0x000000e0 1000 2132 1000 0000 6145 1000 0010 0421 ..!2....aE.....!
0x000000f0 1000 0000 0132 1000 0001 0d42 1000 0001 .....2.....B....
0x00000100 2410 0000 0008 1000 0000 0150 6010 0000 $..........P`...
0x00000110 011e 1000 0000 0610 0000 0001 5060 7965 ............P`ye
0x00000120 6168 210a 6e6f 7065 203a 280a ffff ffff ah!.nope :(.....
We can now assume that this is a VM crackme because of the name of the crackme and also the level1 file looks like bytecode followed by some string data.
Reversing an entire VM can be a tedious and very time consuming task, so I always try to avoid it and find some "smart" way to solve the task. To get a first impression and overview I played around a bit, by that I mean I just stepped a bit through it using r2 debugger. It's not really obfuscated or otherwise fucked up task, so I was able to find the find the VMs handler that perform xor
and cmp
rather quickly.
xor
is located here:
│││ 0x55fbbec02876 31c2 xor edx, eax
cmp
is located here:
││ │ 0x55fbbec02926 39c2 cmp edx, eax
As usual, breaking at the xor
handle we will see the key, and breaking at the cmp
handle we will see the encrypted flag. Let's see what we get:
import sys
key = [0x43,0x19,0x47,0xf5,0x45,0x52,0xe4,0x54,0x3a,0x22,0xed,0xeb,0x4d,0x3e,0xe4,0x8b,0x80, 0x3a, 0xde, 0x68, 0x9a, 0xaa, 0xed, 0x67, 0x6c, 0xea, 0x20, 0x7f, 0xea, 0xd6, 0x83, 0x82]
cipher = [0x31, 0x2b, 0x24, 0x9a, 0x2b, 0x29, 0xd5, 0x67, 0x0d, 0x7d, 0x80, 0x8d, 0x12, 0x4e, 0x91, 0xbe, 0xe8, 0x65, 0xa7, 0x58, 0xef, 0xd8, 0xb2, 0x52, 0x18, 0xde, 0x43, 0x14, 0xcb, 0xed, 0xaa, 0xff]
for i in range(0, len(cipher)):
sys.stdout.write(chr(cipher[i]^key[i]))
print()
# r2con{137_m3_pu5h_y0ur_5t4ck!;)}
Hack the Gibson through this secret gateway.
Connect here: nc 206.189.109.0 1337
We download a file cerberus
which is a ELF64 binary.
The binary is a server that binds to port 1337. First I connected it and played with it for a while...
> nc localhost 1337
/===========================================================================\
/===========================================================================\
| Cerberus Gate Kepper |
+===========================================================================+
/===========================================================================\
$help
Available commands:
* cat echo env exit help ls set unset login
$ls
[!] A logged user is required
$login
cyrill@0x00 ~/ctf/r2con/cerberus
> nc localhost 1337
/===========================================================================\
/===========================================================================\
| Cerberus Gate Kepper |
+===========================================================================+
/===========================================================================\
$login help
usage: login <username> <password>
So the goal seems to login somehow and the login command expects a username and a password as arguments. We're going do that later inside a debugger.
First I looked at the main function. It's a bit lengthy and has a lot of calls to other functions. None of those function calls look interesting (most of the are just the server setup) except one:
:> pd1@0x00403cdf
│ 0x00403cdf e8ecf6ffff call 0x4033d0
It sticks out because it's the only call to something that was not imported and r2 did not automatically define that function. When you seek there you'll notice that the greeting banner string is referenced at the very beginning, so looks like this is the servers mainloop
.
We define that function because breaking there in dynamic analysis seems like a good starting point.
af mainloop@0x004033d0
At the end of our mainloop
function there is another interesting line of code:
:> pd1@0x004031ec
0x004031ec ffd0 call rax
We can not easily tell right now where this call will end up, so let's take note of this address for dynamic anylsis.
Whenever a user connect to the server it forks, so for convenience you want to connect first and then attach to the process.
So, first terminal:
> ./cerberus
Second terminal:
> nc localhost 1337
Third terminal:
> pgrep cerberus
18972 <-- Server
19021 <-- Client
cyrill@0x00 ~/ctf/r2con/cerberus
> sudo r2 -Ad 19021
This is the base setup for solving this challenge, which actually consists of 4 stages.
The attached client process should now be stuck inside a syscall (expecting some user input), so in the client we type login AAAAAA AAAAAA <enter>
and then start stepping in r2.
We break at our call rax
instruction and continue execution:
db 0x004031ec; dc
Now rax
points to Address 0x00402600
which is actually the login function. Let's define it:
af login@0x00402600
Now, by studying the strings we can confirm that this is the login function:
:> :> axt str.ACCESS_GRANTED
login 0x4028ae [DATA] movabs rdi, str.ACCESS_GRANTED
Inside this function r2 identifies 2 calls to strncmp
. Because this may be the password check, we break there, and:
:> x32@rdi
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7fff81daf6e6 4141 4141 4141 4141 0041 4141 4141 4141 AAAAAAAA.AAAAAAA
0x7fff81daf6f6 4100 0002 0000 0000 0000 2300 0000 0000 A.........#.....
:> x32@rsi
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x004082f1 616e 306e 796d 3075 7300 496e 7472 7564 an0nym0us.Intrud
0x00408301 3372 0000 0000 0000 0000 0000 0000 0045 3r.............E
Lets try:
> nc localhost 1337
/===========================================================================\
/===========================================================================\
| Cerberus Gate Kepper |
+===========================================================================+
/===========================================================================\
$login an0nym0us Intrud3r
~> Verifying......ACCESS GRANTED
Welcome Neo...
$ls
cerberus
..
notes.txt
.
During stepping trough the binary I noticed that there is actually another command su
, that does not show up when you type the help
command. Inside fcn.00402f90
(called in the mainloop) there is a strcmp
at address 0x004031aa
that checks your command. If you break there you can see all the other possible commands:
:> x@rsi
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x004081a0 6578 6974 0063 6174 0065 6368 6f00 656e exit.cat.echo.en
0x004081b0 7600 6865 6c70 006c 7300 7365 7400 756e v.help.ls.set.un
0x004081c0 7365 7400 6c6f 6769 6e00 7375 0025 7300 set.login.su.%s.
Let's confirm that it works:
> nc 206.189.109.0 1337
/===========================================================================\
/===========================================================================\
| Cerberus Gate Kepper |
+===========================================================================+
/===========================================================================\
$login an0nym0us Intrud3r
~> Verifying......ACCESS GRANTED
Welcome Neo...
$su
~> Getting privileges......
If we try to cat flag.txt
we still fail with ERROR: permission denied
. This step I did not solve by analyzing the binary further but by some lucky guesses.
I noted that if you type env
there is the ADMIN
variable, but by default it does not look like it's set:
$env
ADMIN:
$
I figured that I have to set this var the value true
to gain real admin priviledges before su
:
> nc 206.189.109.0 1337
/===========================================================================\
/===========================================================================\
| Cerberus Gate Kepper |
+===========================================================================+
/===========================================================================\
$login an0nym0us Intrud3r
~> Verifying......ACCESS GRANTED
Welcome Neo...
$set ADMIN true
$su
~> Getting privileges......
(admin) ~cat flag.txt
This is not the flag :( but you almost got it! Remember that not all elements might be shown...
Stage 4 (hidden file)
The last stage is easy once you realize it ;-) The real flag is not in flag.txt
that is shown when you type ls
but in a hidden .flag.txt
:
(admin) ~cat .flag.txt
Congratulations! Here you have the flag: r2con{the_gu4rd_0f_the_g4t3s_s3rv3_y0u}
Register your Radare2 copy!
We download a binary r2licensemgr.exe
which is PE32 binary.
I don't have my Windows machine with me at the moment so this is a stripped writeup.
I patched the bianry to get it working at all and in debugger:
radiff2 -D r2licensemgr_patched.exe r2licensemgr.exe
--- 0x000ac542 e97d000000
- jmp 0x82
+++ 0x000ac542 737c488d05
+ jae 0x7e
+ invalid
+ invalid
+ invalid
The strings (well, the important ones) are obfuscated using an XOR with the key aSSf4%23#$!"
. I decrypted some of them first:
'2023230a5d4653474a4b4f411621145b573552713678800500929355621337890625' # Application Error
'33323707464000136f4d42043d20031468535d42434413' # Radare2 License Manager
'383c261414465d435a044e077301075044405611044812733d0940054056444d521536210350043f392e2e710d3632155105445a504d5541273b0314570013' # not registered
'383c26141477535742564453733009445c125a5004420e21210357515e4a03484802363d15514113' # correctly licensed
'1420361407171c574f4837252902984619140625' # user32.dll
'22690f36464a5541424901273a3f034779405247455304610f14060b59565a' # C:\Program Files\radare2\r2.key
'ba7c47cf1a98bb78b15cf90dba') # kernel.dll
'383c261414465d435a044e077301075044405611044812733d0940054056444d521536210350043f392e2e710d3632155105445a504d5541273b03145700134c424708303a075805415a574101153c73164157515b4257444132730a5d46575d504100010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899') # not registered
'222132155c056056534b53155e596b3e765d5e465049083d3446404a46524f485841263d034c5557505741454139261540055a525354440f363748'# Crash Report
'2023230a5d4653474a4b4f411621145b573552713678800500929355621337890625' # Crash Report
This helped a lot, because now I was able to work with string XRefs. So it expects the r2 license to be in C:\Program Files\radare2\r2.key
. The binary reads that file and checks it using some simple algorithm. I made up the following script to solve it:
arr = bytearray.fromhex('a495d2dde9df9495d8d2d1a491cdd3cac9c3c0c0cb9d9496a1e1d8a4bc')
flag = "r"
for c in range(0, len(arr)):
flag += chr(arr[c]-ord(flag[c]))
print(flag)
# r2con{d0es_r2_need_a_l1c3nse?}
Find the secret melody!
flag format: r2con{flag}
We download a game boy rom r2tune.gb
.
For this challenge I also did not use r2 :-(
I started the rom in mGBA and the flag appeared in RAM dump. The rest is left as an exercise for the reader.
A hospital has been hacked, and records relating to patients have been leaked.
After initial investigations by the hospital's Blue Team, and after speaking with some hospital employees, it has been inferred that an employee's computer has been infected by malicious email and then swung to a hospital server.
The email the employee received was as follows:
Hello, Doctor,
After the last analysis I decided to go to a private hospital and have some x-rays taken.
They told me that I might have cancer and I wanted to consult with you the next day.
I leave you attached the evidence.
**Attached document: x_rays.zip The Blue team is analyzing attached file at the moment.
Would you like to help them?
We download a file cancer.dcm
.
In this write-up I'll only need some open source tools to solve the challenge :-)
I've never seen or heard about the .dcm
file extension before. Well, r2 identifies it as PE32 executable, and you can actually run it:
cyrill@0x00 ~/ctf/r2con/doctor
> r2 -qqci~class cancer.dcm
class PE32
cyrill@0x00 ~/ctf/r2con/doctor
> wine cancer.dcm
[!] Who am I? When you know it, you can try to execute me from the cmd. Just 4 fun
That was good enough for me as a start but we will come back to it later.
As usual lets open the binary in r2 and have a look at the main function. Probably the first thing I noticed is that there are many rather large blocks of code that are like this one:
│ 0x0040157b c685f0feffff. mov byte [s], 0x52 ; 'R' ; 82
│ 0x00401582 c685f1feffff. mov byte [var_10fh], 9
│ 0x00401589 c685f2feffff. mov byte [var_10eh], 0x73 ; 's' ; 115
│ [...]
│ 0x004017b2 c68541ffffff. mov byte [var_bfh], 0x27 ; ''' ; 39
│ 0x004017b9 c68542ffffff. mov byte [var_beh], 0x3c ; '<' ; 60
│ 0x004017c0 c68543ffffff. mov byte [var_bdh], 0
│ 0x004017c7 c745e4010000. mov dword [var_1ch], 1
Followed by a small loop:
│ ╭──> 0x004017d0 0fb695f0feff. movzx edx, byte [s]
│ ╎│ 0x004017d7 8d8df0feffff lea ecx, [s]
│ ╎│ 0x004017dd 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x004017e0 01c8 add eax, ecx
│ ╎│ 0x004017e2 0fb600 movzx eax, byte [eax]
│ ╎│ 0x004017e5 89d1 mov ecx, edx
│ ╎│ 0x004017e7 31c1 xor ecx, eax
│ ╎│ 0x004017e9 8d95f0feffff lea edx, [s]
│ ╎│ 0x004017ef 8b45e4 mov eax, dword [var_1ch]
│ ╎│ 0x004017f2 01d0 add eax, edx
│ ╎│ 0x004017f4 8808 mov byte [eax], cl
│ ╎│ 0x004017f6 8345e401 add dword [var_1ch], 1
│ ╎│ ; CODE XREF from main @ 0x4017ce
│ ╎╰─> 0x004017fa 0fb685f0feff. movzx eax, byte [s]
│ ╎ 0x00401801 0fbec0 movsx eax, al
│ ╎ 0x00401804 83c001 add eax, 1
│ ╎ 0x00401807 3b45e4 cmp eax, dword [var_1ch]
│ ╰──< 0x0040180a 7fc4 jg 0x4017d0
│ 0x0040180c 8d85f0feffff lea eax, [s]
│ 0x00401812 83c001 add eax, 1
│ 0x00401815 890424 mov dword [esp], eax ; const char *s
│ ;-- eip:
│ 0x00401818 e86b390000 call sub.msvcrt.dll_puts ;[1] ; int puts(const char *s)
What this does is it simply loads all these bytes into memory and XORs them using a static key byte (0x52
in this case, see the s
variable at the beginning). It's basically obfuscating the strings a bit inside the binary. If we have all these strings decrypted we can get a way better understanding of what's going on. But we don't do that manually. One easy way is to decrypt them using ESIL emulation.
We achieve that using the following r2 commands:
:> aei # initialize ESIL VM state
:> aeim # initialize ESIL VM stack
:> s 0x0040157b # seek to the start of our block
:> aeip # initialize ESIL program counter to current seek
:> aecc # continue until call (note the call to puts after the loop=
call at 0x00401818
:> ps@eax # print the decrypted string
[!] Who am I? When you know it, you can try to execute me from the cmd. Just 4 fun
Yay, that worked really well! Let's hack a small script together to quickly extract all the strings:
#!/bin/sh
for i in 0x0040157b 0x00401827 0x00401a18 0x00401bf7 0x00401ea3 0x0040207a 0x004021a9 0x00402367 0x00402599 0x00402817 0x00402917 0x00402ac9
do
echo $i `r2 -qqc "aei;aeim;s $i;aeip;aecc;ps@eax" cancer.dcm 2>&1 | grep -v "call at"`
done
> bash strings.sh
0x0040157b [!] Who am I? When you know it, you can try to exe
0x00401827 [+] Well, now you have the file, let's see what is
0x00401a18 [!] My fault :S
0x00401bf7 [+] Well done! now the flag is almost in your hand
0x00401ea3 [+] Well done! now the flag is in your hands...!
0x0040207a [!] Error writing file
0x004021a9 [!] Did you look for the prefix I'm looking for?
0x00402367 [+] You are a lucky guy, now you know more about D
0x00402599 [!] Are you sure you are executing from the correc
0x00402817 [!] My fault :S
0x00402917 [+] Well Done, Good luck with the next stage...
0x00402ac9 [!] Can't handle this error, please delete al file
Based on that it looks like our goal is to reach Address 0x00401ea3
at some point. We could maybe use an SMT solver here but I decided to go for some more analysis first.
When we currently execute the binary now the string at address 0x0040157b
is printed to the terminal, which means we don't take the branch there. Before we land there we see another branch which depends on the return value of the function fcn.00401500
. It is a short function:
│ 0x00401500 55 push ebp
│ 0x00401501 89e5 mov ebp, esp
│ 0x00401503 83ec18 sub esp, 0x18
│ 0x00401506 8b4508 mov eax, dword [arg_8h]
│ 0x00401509 890424 mov dword [esp], eax
│ 0x0040150c a16c924000 mov eax, dword [sym.imp.SHLWAPI.DLL_PathFileExistsA] ; [0x40926c:4]=0x9622 reloc.SHLWAPI.DLL_PathFileExistsA ; "\"\x96"
│ 0x00401511 ffd0 call eax
│ 0x00401513 83ec04 sub esp, 4
│ 0x00401516 c9 leave
╰ 0x00401517 c3 ret
r2 helps us here because it understands that the call instruction at address 0x00401511 will be a DLL call to PathFileExistsA
. The file that's checked will be first argument, lets check what that will be.
:> ps@[0x406008]
stage2.dcm
If the setup was more complex you could also call out ESIL to do it for you:
> r2 -Aqqc "s 0x0040154a;aei;aeim;aeip;4aes;ps@[esp]" cancer.dcm
stage2.dcm
What this does it seeks to the function call "setup" and emulates it. The first argument is now on the stack so we can just print it.
For convenience we rename the function to something useful.
afn DLL_PathFileExistsA @ fcn.00401500
Let's juts prepend the file stage2.dcm
exists and we are at address 0x402187
:
:> pd3@0x402187
│ ; CODE XREF from main @ 0x40155f
│ 0x00402187 a108604000 mov eax, dword [0x406008] ; [0x406008:4]=0x40717c eax
│ 0x0040218c 890424 mov dword [esp], eax
│ 0x0040218f e86cf3ffff call DLL_PathFileExistsA
It agains checks for the existance (according to Microsoft Docs: "Determines whether a path to a file system object such as a file or folder is valid."). So assuming we don't take the branch at 0x0040155f
the next checks happens inside fcn.00403b18
. This function first calls GetModuleFileNameA
with the first parameter set to 0 which "retrieves the path of the executable file of the current process". The next call to PathFindFileNameA
checks that this file exists and after that the file name is compared against the first arg, which is stage2.dcm
. In short it checks whether the file name of the executed binary is stage2.dcm
.
So let's just see what will happen if we name it correctly:
> cp cancer.dcm stage2.dcm
cyrill@0x00 ~/ctf/r2con/doctor/writeup master
> wine stage2.dcm
[+] Well, now you have the file, let's see what is it
[+] Well done! now the flag is almost in your hands...!
If you pay attention you will notice that if you run the binary like that it will actually open the cancer.dcm
file and write some stuff into it. I just noticed that the file changes on disk everytime I run stage2.dcm
, but let's find proof for that in the code.
We seek to where the last string that is printed get constructed and see what happens there. Since we deobfuscated the strings already we know it's at address 0x00401ea3
. When we have a look at what happens just before that address we see some loops and function calls that probably take a while to understand, so let's reverse a bit more and see how we got to this block first hand. It actually starts at address 0x00401de8
, where r2 found a XRef from address 0x401b9b
:
│ 0x00401b7d 8b45ac mov eax, dword [var_54h]
│ 0x00401b80 83c005 add eax, 5
│ 0x00401b83 0fb600 movzx eax, byte [eax]
│ 0x00401b86 0fb6c0 movzx eax, al
│ 0x00401b89 6689857effff. mov word [var_82h], ax
│ 0x00401b90 0fb7857effff. movzx eax, word [var_82h]
│ 0x00401b97 6683f863 cmp ax, 0x63 ; 99
│ ╭─< 0x00401b9b 0f8747020000 ja 0x401de8
So it checks some value at address var_54h + 5
to be 0x64 or greater. The easiest way to find out how this local variable get's populated is to just grep for it inside the main function:
:> pdf@main~var_54h
│ ; var uint32_t var_54h @ ebp-0x54
│ │ │ 0x00401a0b 8945ac mov dword [var_54h], eax
│ │ │ 0x00401a0e 837dac00 cmp dword [var_54h], 0
│ │││ │ 0x00401b5d 8b45ac mov eax, dword [var_54h]
│ │││ │ 0x00401b6e 8b45ac mov eax, dword [var_54h]
Interesting is obviously the first hit at address 0x00401a0b
where It becomes the return value of fcn.00403a36
. This function returns some local variable ptr
:
│ │││ 0x00403b13 8b45ec mov eax, dword [ptr]
│ │││ ; CODE XREFS from fcn.00403a36 @ 0x403a5d, 0x403ac2, 0x403ae5
│ ╰╰╰──> 0x00403b16 c9 leave
╰ 0x00403b17 c3 ret
Using the same grepping as we did just before we can learn that it is a pointer to some allocated memory:
│ │ 0x00403aaf e81c170000 call sub.msvcrt.dll_calloc ; void *calloc(size_t nmeb, size_t size)
│ │ 0x00403ab4 8945ec mov dword [ptr], eax
When you reverse the rest of this function you will learn that it more or less reads a file into this buffer. The filename is passed as an argument, and how to get that we already know:
:> ps@[0x406004]
cancer.dcm
In conclusion the byte at offset 5 in the cancer.dcm
file needs to be greater than 0x63 to pass this stage (remember we are executing stage2.dcm
currently). But we can not just monkey patch it, because there's more to it than just that.
In fact what happens is that some data inside cancer.dcm
is incrementally decrypted every time you run stage2.dcm
! To keep track of how many iterations we have done already the byte at offset 5 gets increment by one. For the sake of trying to keep this write up somewhat compact I'll skip dissecting this part.
We can use a small script to fully decrypt the flag file:
> cat execute.sh
#!/bin/bash
# get a clean cancer.dcm
cp stage2.dcm cancer.dcm
for i in {0..100}
do
wine stage2.dcm
done
Output:
cyrill@0x00 ~/ctf/r2con/doctor
> bash execute.sh
[+] Well, now you have the file, let's see what is it
[+] Well done! now the flag is almost in your hands...!
[...]
[+] Well, now you have the file, let's see what is it
[!] I need you to run me inside a debugger please =)
cyrill@0x00 ~/ctf/r2con/doctor
This is easy:
> r2 -d wine stage2.dcm
Process with PID 16186 started...
= attach 16186 16186
bin.baddr 0x7c000000
Using 0x7c000000
asm.bits 32
glibc.fc_offset = 0x00148
-- Press 'C' in visual mode to toggle colors
[0xf7fc6120]> dc
hit hardware breakpoint 1 at: 7c400000
[0x7c400000]> dc
child stopped with signal 17
[+] SIGNAL 17 errno=0 addr=0x3e800003f44 code=1 ret=0
[+] signal 17 aka SIGCHLD received 0
[0xf7fac929]> dc
child stopped with signal 17
[+] SIGNAL 17 errno=0 addr=0x3e800003f48 code=1 ret=0
[+] signal 17 aka SIGCHLD received 0
[0xf7fac929]> dc
[+] Well, now you have the file, let's see what is it
hit hardware breakpoint 1 at: 403dc9
[0x00403dc9]> dc
hit hardware breakpoint 1 at: 403dc9
Wtf where are all these breakpoints coming from?
[0x00403dc9]> pd1@eip-1
0x00403dc8 cc int3
Ok debug traps or whatever. Note that this is in a loop that will be execute 0xff
times. I guess I can either patch out the interrupt or just continue at least 256 times. The latter makes a nice oneliner:
> r2 -qqdc260dc wine stage2.dcm
Process with PID 17041 started...
[...]
hit hardware breakpoint 1 at: 403dc9
hit hardware breakpoint 1 at: 403dc9
[+] Well done! now the flag is in your hands...!
[+] signal 17 aka SIGCHLD received 0
It sais Well done! now the flag is in your hands...!
which is nice but what does it mean? Remember the challenge description and the .dcm
file extension? cancer.dcm
is actually a DICOM
file! There are many open DICOM viewers for Linux, I installed some of them but all failed to open the cancer.dcm
. I did not know this file format before so I did quick internet research. There are 4 magic bytes at offset 0x80
that must be DICM
. But it is not like that with cancer.dcm
:
> r2 -nqqcx4@0x80 cancer.dcm
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00000080 4b45 5921 KEY!
This is why it cannot not be viewed. What you see is what you fix:
r2 -wnqqc "w DICM@0x80" cancer.dcm
Finally we can get the flag using our preferred DICOM image viwer \o/
Here is full script to get the flag in one shot using aliza
DICOM viewer:
#!/bin/bash
cp cancer.dcm stage2.dcm
for i in {0..100}
do
wine stage2.dcm
done
r2 -qqdc260dc wine stage2.dcm
r2 -wnqqc "w DICM@0x80" cancer.dcm
aliza cancer.dcm
# r2con{s0m3t1m3s_th1ngs_4r3_n0t_wh4t_th3y_s33m}
You have all you need, but this time do it harder!
Please see the Solution for vermu 1
, you just need to be more patient until the key and cipher appears :-)
import sys
key=[0xcc,0xe1,0xa2,0x0a,0xba,0xda,0xb1,0xba,0x7c,0xdd,0x12,0x0a,0xb2,0x1c,0x1b,0x57,0x7f,0xc5,0x21,0x97,0x65,0x55,0x02,0x48,0x93,0x15,0xdf,0x80,0x15,0x29,0x7c,0x7d]
cipher=[0xbe,0xd3,0xc1,0x65,0xd4,0xa1,0xd2,0xd6,0x48,0xbc,0x7c,0x55,0xd6,0x7d,0x44,0x24,0x0b,0xf1,0x42,0xfc,0x3a,0x37,0x31,0x2e,0xfc,0x67,0xba,0xdf,0x7f,0x44,0x0c,0x00]
for i in range(0, len(cipher)):
sys.stdout.write(chr(cipher[i]^key[i]))
print()
# r2con{cl4an_da_st4ck_b3fore_jmp}
Be careful with moths, can make small holes in your socks.
r2 has new socks, and they are in kernel land! Pwn the kernel in this challenge and read the flag off /flag.txt.
$ nc 206.189.109.0 31337
I think this is a kernel crackme which nobody managed to solve :-)