How to fly with radare2 - The Wall writup
Instead of writing an usual writeup on how I solved The Wall (rev200) during HackTheVote2016, I'd rather put more emphasis on how I solved this chall by using radare2 only. No need to introduce radare2 if you are reading this post. I still have to learn most of the commands r2 comes with, yet a couple of them are enough to approach the r2 world and start doing some cool stuff.
I played this CTF along with the fuffateam.
We were given the following problem:
The Trump campaign is running a trial of The Wall plan. They want to prove that no illegal immigrants could get past it. If that goes as planned, us here at the DNC will have a hard time swinging some votes in the southern boarder states. We need you to hack system and get past the wall. I heard they have put extra protections into place, but we think you can still do it. If you do get into America, there should be a flag somewhere in the midwest that you can have. You will be US “citizen” after all.
author's irc nick: itszn
The challenge link is named minetest and gives us a tarball with quite a lot of files and dirs. The real point is inside ./bin/
:
$ file minetest
minetest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=a44ee695ed9835af8e752c665f7ef5e373648323, stripped
By having a quick look at the structure of the tarball and its files, we can assume we got something that is also real and not completely crafted from scratch for the purpose of the challenge only. Let's try to google “minetest”, open the first result and … A free, open source voxel game engine and game. Fully extendable. You are in control. Ok, now it's clear that the guys at RPISEC wants us to play instead of reversing :D
If we launch the game and connect to the server of the challenge, we spawn in the middle of nowhere. Hey, I'm not joking! Have a look at the picture below.
At this point you should stop thinking about the CTF, like I did. I literally spent 10 minutes walking around, jumping now and then. New people join the server? We go to say hi with some jumps and pirouettes. Ordinary life on multiplayer games. We continue like this until we find a huge wall on our way. It should be the wall built by Trump's goons… that's sad.
It's time to move on. We have to get past this wall (remember the problem statement) and score +200 points for the fuffateam.
The very first thing that comes to my mind is: why don't we use some sort of cheats to fly and reach the other side? Yeah, it isn't the ethical way but extreme situations call for extreme measures after all.
Indeed google helps us again returning back a forum where people discuss a hypothetical routine Client::checkLocalPrivilege()
. Legend has it that the aforementioned function performs client-side checks on user's privileges e.g. to establish if you can use fly mode, fast mode, no-clip mode, etc. Ok, we want to grab them all!
It's time to finally switch to r2 now. We open the binary with the -A
flag to let r2 analyze all referenced code:
$ r2 -A ./minetest
We should now check if this version of minetest relies on Client::checkLocalPrivilege
. Let's ask r2 if it's there and at which offset:
[0x004b8d13]> is ~Client::checkLocalPrivilege
vaddr=0x005f27c0 paddr=0x001f27c0 ord=2241 fwd=NONE sz=198 bind=UNKNOWN type=FUNC name=Client::checkLocalPrivilege
vaddr=0x005f2890 paddr=0x001f2890 ord=2488 fwd=NONE sz=9 bind=UNKNOWN type=FUNC name=non-virtualthunktoClient::checkLocalPrivilege(std::stringconst&)
[0x004b8d13]>
The is ~word
command stands for:
is
: get all symbols from the opened file;~word
: grep for lines matching word.
So we have what we want and it's at virtual address 0x005f27c0
. To seek to the juicy function use s 0x005f27c0
or s sym.Client::checkLocalPrivilege
(since the binary has symbols). To inspect the asm code there are more choices instead. We can use the set of pd
commands or more handy views like visual mode (V
) or graph mode (VV
). The graph mode seems to best fit our needs.
Basically checkLocalPrivilege()
accepts a string, the name of the privilege, and returns a boolean. True if the privilege is granted or false otherwise. Wait… what happens if we patch it to always return a true value?
First of all we reopen the binary in read-write mode. Two options here:
- command
oo+
reopens the file in read-write mode (from the current session of r2); - close r2 and launch it again but with
-w
flag (r2 -w ./minetest
).
There are of course multiple strategies to patch the function. One way could be to force RAX
register to be true and return immediately.
or eax,1 ; or with 1 and store in eax
ret ; return
In order to assemble the asm code we use…r2 again (boooring), more precisely rasm2 configured to assemble x86 over 64 bits.
$ echo 'or eax,1; ret;' | rasm2 -a x86 -b 64 -f -
83c801c3
Going back to the r2 shell we finally have a new command to execute. We use wx
to write the opcodes at the magical offset we have talked about just before.
[0x004b8d13]> wx 83c801c3 @ 0x005f27c0
If you have done everything correctly you should obtain a final result as the one showed in the picture.
Well, this is the end of our adventure. Let's open minetest to get these 200 points!
Access denied. Reason: Failed anti-cheat check! Oh damn :( looks like there is an anti-cheat engine in place. It's still not over. Back to reversing again.
Actually that string is referenced in our binary and we can find it at 0x008bae4c
by using iz ~anti-cheat
. One xref to the string (axt 0x008bae4c
) and we spot a suspicious routine at 0x4f4afc
called Server::handleCommand_CheatResponse
. Definitively confirmed, they don't like cheats. Even more suspicious is Client::handleCommand_CheatChallange
at 0x4d0090
.
This is the algorithm:
- open /proc/self/maps;
- read one line with “%lx-%lx %4s %lx %5s %ld %s” (the code in memory);
- compute the SHA1 of the client and send the final digest to the server.
We know why the authentication wasn't successful. We can't patch the client or we'll get detected. However, the is the only anti-cheat check between client and server, so if we pass this, it's done.
My approach was to put a breakpoint immediately after the SHA1 computation and as soon as the breakpoint is hit, we live-patch the client and continue its execution. Will it work?
r2pipe was the way to go for me. The script below will try to defeat the anti-cheat check.
import r2pipe
# Open minetest in debug mode and read-write mode
r2 = r2pipe.open("./minetest/bin/minetest", ["-w","-d"])
# Hardware breakpoint since a software breakpoint will force an incorrect hash
r2.cmd("drx 0 0x4d01f0 1 x")
# Continue execution
r2.cmd("dc")
print "[+] Patching Client::checkLocalPrivilege"
# If we are here it means we passed the SHA1 challenge
# Patch checkLocalPrivilege to return always true
r2.cmd("wx 83c801c3 @ 0x005f27c0")
print "[+] Oups! Cheats enabled"
# Enjoy!
r2.cmd("dc")
A picture is worth a thousand words.
Do you even see the flag?
The flag is: flag{pl34s3_d0N7_v4c_b4N_M3_n0tch!}