AlexCTF 2017: Catalyst system - Reversing 150
For this challenge, I wanted to practice using a tool that has the potential to replace GDB in my toolset: radare2. I would also like to acknowledge a friend who helped me with the most difficult parts (figuring out constraint solving with angr) of this challenge: DigitalCold https://twitter.com/digital_cold
Recon
First thing I do is check out the file using file
:
-> % file catalyst
catalyst: 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.32, BuildID[sha1]=3c4646c45da147f57cfa3fe0b9f1022d84fbe85f, stripped
Next, since this was a binary, I just decided I'd run it.
-> % ./catalyst
▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄
▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀
▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌
▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄
▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀
▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌
▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌
▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌
▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀
Welcome to Catalyst systems
Loading....
Running it produced this colorful banner, but then proceeded to "load" for the next several minutes.
Being impatient, I decided to take a look at the binary in radare2 while it "loaded":
-> % radare2 catalyst
[0x00400780]> pd @ main
;-- main:
0x00400d93 55 push rbp
0x00400d94 4889e5 mov rbp, rsp
0x00400d97 4883ec20 sub rsp, 0x20
0x00400d9b bfe8030000 mov edi, 0x3e8
0x00400da0 e87bf9ffff call sym.imp.malloc
0x00400da5 488945f0 mov qword [rbp - 0x10], rax
0x00400da9 bfe8030000 mov edi, 0x3e8
0x00400dae e86df9ffff call sym.imp.malloc
0x00400db3 488945e8 mov qword [rbp - 0x18], rax
0x00400db7 bf00000000 mov edi, 0
0x00400dbc e84ff9ffff call sym.imp.time
0x00400dc1 89c7 mov edi, eax
0x00400dc3 e838f9ffff call sym.imp.srand
0x00400dc8 bf88104000 mov edi, 0x401088
0x00400dcd e8fef8ffff call sym.imp.puts
...
<SNIP>
...
0x00400e31 e89af8ffff call sym.imp.puts
0x00400e36 bf90184000 mov edi, str._e_0mWelcome_to_Catalyst_systems ; str._e_0mWelcome_to_Catalyst_systems
0x00400e3b e890f8ffff call sym.imp.puts
0x00400e40 bfb0184000 mov edi, str.Loading ; "Loading" @ 0x4018b0
0x00400e45 b800000000 mov eax, 0
0x00400e4a e8a1f8ffff call sym.imp.printf
0x00400e4f 488b05721220. mov rax, qword [obj.stdout]
0x00400e56 4889c7 mov rdi, rax
0x00400e59 e8d2f8ffff call sym.imp.fflush
...
<SNIP>
We can immediately see at addresses 0x400dcd
and afterward a series of puts
calls which are used to print the welcome banner to the screen, as well as references to the Welcome to Catalyst...
and Loading
strings.
Immediately following this code we see the following:
0x00400e5e c745fc000000. mov dword [rbp - 4], 0 #loop counter initialization
,=< 0x00400e65 eb3e jmp 0x400ea5 #jump to check
.--> 0x00400e67 e804f9ffff call sym.imp.rand
|| 0x00400e6c 89c6 mov esi, eax
|| 0x00400e6e 8b55fc mov edx, dword [rbp - 4]
|| 0x00400e71 89d0 mov eax, edx
|| 0x00400e73 01c0 add eax, eax
|| 0x00400e75 01d0 add eax, edx
|| 0x00400e77 8d4801 lea ecx, dword [rax + 1]
|| 0x00400e7a 89f0 mov eax, esi
|| 0x00400e7c 99 cdq
|| 0x00400e7d f7f9 idiv ecx
|| 0x00400e7f 89d0 mov eax, edx
|| 0x00400e81 89c7 mov edi, eax
|| 0x00400e83 e8d8f8ffff call sym.imp.sleep #useless waiting
|| 0x00400e88 bf2e000000 mov edi, 0x2e
|| 0x00400e8d e82ef8ffff call sym.imp.putchar
|| 0x00400e92 488b052f1220. mov rax, qword [obj.stdout]
|| 0x00400e99 4889c7 mov rdi, rax
|| 0x00400e9c e88ff8ffff call sym.imp.fflush
|| 0x00400ea1 8345fc01 add dword [rbp - 4], 1 #increment loop counter
|`-> 0x00400ea5 837dfc1d cmp dword [rbp - 4], 0x1d #see if we've drawn 29 dot (0x2e) characters
`==< 0x00400ea9 7ebc jle 0x400e67
We can see here that after printing the Loading
string, that at address 0x400e5e
, we have a counter initialized to 0, followed by an unconditional jump to address 0x00400ea5
which checks that counter's value against 0x1d
. Since it is less the first time (having initialized to 0
at 0x00400e5e
), we proceed to jump back up (following the jle 0x400e67
) to enter our "Loading" loop.
We see that within this loop, sym.imp.rand
is called first before sym.imp.sleep
is called (which explains the variable-duration waiting during the "Loading" phase). After each sleep
occurs, the loop counter is incremented at 0x400ea1
.
Since nobody likes needless waiting, I wanted to avoid this sleep loop. As an extra precaution, I scanned ahead in the binary and found a second "Loading" loop at 0x00400f1d
:
[0x00400780]> pd @ 0x00400f1d
0x00400f1d c745f8000000. mov dword [rbp - 8], 0
,=< 0x00400f24 eb38 jmp 0x400f5e
.--> 0x00400f26 e845f8ffff call sym.imp.rand
|| 0x00400f2b 89c2 mov edx, eax
|| 0x00400f2d 8b45f8 mov eax, dword [rbp - 8]
|| 0x00400f30 8d4801 lea ecx, dword [rax + 1]
|| 0x00400f33 89d0 mov eax, edx
|| 0x00400f35 99 cdq
|| 0x00400f36 f7f9 idiv ecx
|| 0x00400f38 89d0 mov eax, edx
|| 0x00400f3a 89c7 mov edi, eax
|| 0x00400f3c e81ff8ffff call sym.imp.sleep
|| 0x00400f41 bf2e000000 mov edi, 0x2e
|| 0x00400f46 e875f7ffff call sym.imp.putchar
|| 0x00400f4b 488b05761120. mov rax, qword [obj.stdout]
|| 0x00400f52 4889c7 mov rdi, rax
|| 0x00400f55 e8d6f7ffff call sym.imp.fflush
|| 0x00400f5a 8345f801 add dword [rbp - 8], 1
|`-> 0x00400f5e 837df81d cmp dword [rbp - 8], 0x1d
`==< 0x00400f62 7ec2 jle 0x400f26
Patching the Binary
In order to patch the binary we have to re-open it in -w
rite mode:
-> % radare2 -w catalyst
[0x00400780]> pd 1@ 0x00400ea9
`=< 0x00400ea9 7ebc jle 0x400e67 #instruction is 2 bytes
[0x00400780]> pd 1@ 0x00400f62
`=< 0x00400f62 7ec2 jle 0x400f26
[0x00400780]> wx 9090 @ 0x00400ea9 #so we write 2 'NOP' bytes
[0x00400780]> wx 9090 @ 0x00400f62
[0x00400780]> pd 2 @ 0x00400ea9 #and verify the jumps were overwritten
0x00400ea9 90 nop
0x00400eaa 90 nop
[0x00400780]> pd 2 @ 0x00400f62
0x00400f62 90 nop
0x00400f63 90 nop
In the highlighted lines you can see the commands used to print disassembly (pd
), write hex (wx
), and again print the disassembly for one of the two loading loops. Radare2 writes these bytes directly to disk, which means I was able to close and verify that I could run the program:
[0x00400780]> qy
tobaljackson@binarystudio [06:26:50 PM] [~/AlexCTF/RE3]
-> % ./catalyst
▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄
▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀
▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌
▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄
▐░░░░░░░░░░░▌▐░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌
▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀
▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌
▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌
▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌
▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀
Welcome to Catalyst systems
Loading
Username: HERPDERP
Password: hurrdurr
Logging in
invalid username or password
Relief! Upon quitting radare2 and re-running the binary, I was greeted with a Username and Password prompt, which accepted my input and failed gracefully. Now that I had a binary in hand that wouldn't waste my time, I proceeded to the next phase.
Finding What We Really Want In the Binary (or Life)
At this point, the binary is runnable with input I can control, but it's right around this time I asked myself, "How do I get the flag out of the binary?" Sometimes, the flag is just stored in plaintext in the binary as a string. These kinds of challenges are entry-level type, which this one probably is not, but it doesn't hurt to check.
-> % strings catalyst
...
<SNIP>
__gmon_start__
GLIBC_2.7
GLIBC_2.2.5
<z~(
<z~d
<Z~<
H=VKf\uEH
AWAVA
AUATL
[]A\A]A^A_
your flag is: ALEXCTF{
...
<SNIP>
...
invalid username or password
[0mWelcome to Catalyst systems
Loading
Username:
Password:
Logging in
;*3$"
'bA5k
{F<>g
YDr6
TYGCC: (GNU) 6.1.1 20160721 (Red Hat 6.1.1-4)
GCC: (GNU) 6.2.1 20160916 (Red Hat 6.2.1-2)
...
<SNIP>
We can see some strings stored in the binary, including the first part of the flag ALEXCTF{
, but nothing that looks like it could be the flag string itself. As to be expected. Next thing to check is whether the flag is stored in an obfuscated or enciphered form which might be easily decoded. To do this, we just need to look in the binary for the winning function, wherever the above highlighted 'victory' string is referenced.
Putting the Reverse
in Reverse Engineering
Reverse
in Reverse Engineering
Since the flag is what I'm after, why not try and see how the program makes use of that your flag is:
string and work my way backward?
Let's load the binary back up in radare and enter V
isual mode. After entering visual mode, pressing p
once takes you to a assembled byte view. I then used _
(underscore) to display a list of flags that radare2 had identified for me:
0> |
- 0x00401050 str.your_flag_is:_ALEXCTF_
0x00401069 str.invalid_username_or_password
...
<SNIP>
...
0x00401890 str._e_0mWelcome_to_Catalyst_systems
0x004018b0 str.Loading
0x004018b8 str.Username:
...
<SNIP>
Since the your_flag_is:_ALEXCTF_
string is listed first, pressing <enter>
will s
eek to that memory address:
[0x00401050 39% 912 catalyst]> pd $r @ str.your_flag_is:_ALEXCTF_
;-- str.your_flag_is:_ALEXCTF_:
0x00401050 .string "your flag is: ALEXCTF{" ; len=23
,=< 0x00401067 7d00 jge str.invalid_username_or_password ;[1]
`-> ;-- str.invalid_username_or_password:
`-> 0x00401069 .string "invalid username or password" ; len=29
0x00401086 0000 add byte [rax], al
Here we can see that there is a radare flag
defined here (the text above 0x401050
, starting with ;--
), but no XREF
defined. To find out where this string is referenced, run our trusty aaa
command with :aaa
from visual mode. (After executing a command from visual mode with :
, simply pressing <enter>
will bring you out of cmdline mode):
[0x00401050 39% 996 catalyst]> pd $r @ str.your_flag_is:_ALEXCTF_
;-- str.your_flag_is:_ALEXCTF_:
; DATA XREF from 0x00400887 (sub.printf_876)
0x00401050 .string "your flag is: ALEXCTF{" ; len=23
; JMP XREF from 0x00401064 (str.your_flag_is:_ALEXCTF_ + 20)
; DATA XREF from 0x004008e5 (sub.printf_876)
,=< 0x00401067 7d00 jge str.invalid_username_or_password ;[1]
`-> ;-- str.invalid_username_or_password:
| ; XREFS: JMP 0x00401067 DATA 0x00400d7c DATA 0x00400948 DATA 0x00400a1c DATA 0x00400c25 DATA 0x00400bf7 DATA 0x00400bc9
| ; XREFS: DATA 0x00400b9b DATA 0x00400b6d DATA 0x00400b3f DATA 0x00400b11 DATA 0x00400ae3 DATA 0x00400ab5 DATA 0x00400a87
`-> 0x00401069 .string "invalid username or password" ; len=29
Here the highlighted lines indicate all the XREF
's (cross-references) that were found for these data. Since we're still at our string address, pressing x
will bring up radare2's XREF
browser:
[GOTO XREF]> 0x00401050
0 [0] 0x00400887 DATA XREF (sub.printf_876) | 0x00400887 mov edi, str.your_flag_is:_ALEXCTF_ ; "your flag is: ALEXCTF{" @ 0x401050
Pressing 0
or <enter>
will take you to the first item on the XREF
list. Scrolling up a few lines shows us we're in function sub.printf_876
:
[0x00400876 20% 285 catalyst]> pd $r @ sub.printf_876
/ (fcn) sub.printf_876 129
| sub.printf_876 ();
| ; var int local_30h @ rbp-0x30
| ; var int local_28h @ rbp-0x28
| ; var int local_14h @ rbp-0x14
| ; CALL XREF from 0x00400fb3 (loc.00400f5e)
| 0x00400876 55 push rbp
| 0x00400877 4889e5 mov rbp, rsp
| 0x0040087a 53 push rbx
| 0x0040087b 4883ec28 sub rsp, 0x28; '('
| 0x0040087f 48897dd8 mov qword [rbp - local_28h], rdi #Something1 pushed onto stack
| 0x00400883 488975d0 mov qword [rbp - local_30h], rsi #Something2 pushed onto stack
| 0x00400887 bf50104000 mov edi, str.your_flag_is:_ALEXCTF_ ; "your flag is: ALEXCTF{" @ 0x401050
| 0x0040088c b800000000 mov eax, 0
| 0x00400891 e85afeffff call sym.imp.printf ;[1]; int printf(const char *format)
| 0x00400896 c745ec000000. mov dword [rbp - local_14h], 0 #Counter initialized
| ,=< 0x0040089d eb2f jmp 0x4008ce;[2]
| .--> 0x0040089f 8b45ec mov eax, dword [rbp - local_14h]
| || 0x004008a2 4898 cdqe
| || 0x004008a4 0fb680a02060. movzx eax, byte [rax + 0x6020a0] #load xor'd flag byte from memory
| || 0x004008ab 0fb6d0 movzx edx, al #and stick it in rdx
| || 0x004008ae 8b45ec mov eax, dword [rbp - local_14h]
| || 0x004008b1 4863c8 movsxd rcx, eax
| || 0x004008b4 488b45d0 mov rax, qword [rbp - local_30h] #Something2 loaded
| || 0x004008b8 4801c8 add rax, rcx; '&' #Counter offset into Something2
| || 0x004008bb 0fb600 movzx eax, byte [rax] #Get that byte
| || 0x004008be 0fbec0 movsx eax, al #Really get it
| || 0x004008c1 31d0 xor eax, edx #xor that byte with something else
| || 0x004008c3 89c7 mov edi, eax #get the byte into edi to print it out
| || 0x004008c5 e8f6fdffff call sym.imp.putchar ;[3]; sym.imp.malloc-0x60; void *malloc(size_t size)
| || 0x004008ca 8345ec01 add dword [rbp - local_14h], 1
| !| ; JMP XREF from 0x0040089d (sub.printf_876)
| |`-> 0x004008ce 8b45ec mov eax, dword [rbp - local_14h]
| | 0x004008d1 4863d8 movsxd rbx, eax
| | 0x004008d4 488b45d0 mov rax, qword [rbp - local_30h]
| | 0x004008d8 4889c7 mov rdi, rax
| | 0x004008db e800feffff call sym.imp.strlen ;[4]; size_t strlen(const char *s)
| | 0x004008e0 4839c3 cmp rbx, rax
| `==< 0x004008e3 72ba jb 0x40089f;[5]
| 0x004008e5 bf67104000 mov edi, 0x401067
| 0x004008ea e8e1fdffff call sym.imp.puts ;[6]; int puts(const char *s)
| 0x004008ef 90 nop
| 0x004008f0 4883c428 add rsp, 0x28; '('
| 0x004008f4 5b pop rbx
| 0x004008f5 5d pop rbp
\ 0x004008f6 c3 ret
We can see from the assembly of where the victory string is referenced that the flag is loaded from memory address 0x6020a0
(at instruction 0x004008a4
) and un-xor'd one byte at a time from what was passed into the function via the rsi
register. Refer to the highlighted lines above and my #
added comments to see how that works. At this point I have a sneaking suspicion that the XOR
key to decode the flag is some combination of the Username and Password that must be supplied to the program, rather than some hard-coded key.
But we can keep tracing a backward path through the program to determine if this is the case. Back(On)ward!
Back to the Future (Main)
Since I wanna know where this function is called, I can :s
eek to the first memory address (or use k
to go up) until I'm at 0x400876
, and use x
to show where this function is XREF
'd from, and <enter>
to jump to where it's called.
Once there, scrolling up reveals that we're in main
(just below where we patched the NOP
s for the second loading screen @ 0x00400f62
):
[0x00400f5e 37% 285 catalyst]> pd $r @ loc.00400f5e
|- loc.00400f5e 97
| loc.00400f5e ();
| ; var int local_18h @ rbp-0x18
| ; var int local_10h @ rbp-0x10
| ; var int local_8h @ rbp-0x8
| ; JMP XREF from 0x00400f24 (loc.00400ea5)
| 0x00400f5e 837df81d cmp dword [rbp - local_8h], 0x1d ; [0x1d:4]=0x40000000
| 0x00400f62 90 nop
| 0x00400f63 90 nop
| 0x00400f64 bf0a000000 mov edi, 0xa
| 0x00400f69 e852f7ffff call sym.imp.putchar ;[1]; sym.imp.malloc-0x60; void *malloc(size_t size)
| 0x00400f6e 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f72 4889c7 mov rdi, rax
| 0x00400f75 e820fdffff call fcn.00400c9a ;[2]
| 0x00400f7a 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f7e 4889c7 mov rdi, rax
| 0x00400f81 e857fdffff call sub.puts_cdd ;[3]; int puts(const char *s)
| 0x00400f86 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f8a 4889c7 mov rdi, rax
| 0x00400f8d e865f9ffff call sub.puts_8f7 ;[4]; int puts(const char *s)
| 0x00400f92 488b55e8 mov rdx, qword [rbp - local_18h]
| 0x00400f96 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f9a 4889d6 mov rsi, rdx
| 0x00400f9d 4889c7 mov rdi, rax
| 0x00400fa0 e8d2f9ffff call sub.puts_977 ;[5]; int puts(const char *s)
| 0x00400fa5 488b55e8 mov rdx, qword [rbp - local_18h]
| 0x00400fa9 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400fad 4889d6 mov rsi, rdx
| 0x00400fb0 4889c7 mov rdi, rax
| 0x00400fb3 e8bef8ffff call sub.printf_876 ;[6]; int printf(const char *format)
| 0x00400fb8 b800000000 mov eax, 0
| 0x00400fbd c9 leave
\ 0x00400fbe c3 ret
0x00400fbf 90 nop
Looking at the highlighted assembly above, we see that rsi
is set by loading from rbp-0x18
, and that this memory address as well as rbp-0x10
are used all throughout. Since I had a feeling that these two variables were going to be pointers to the user input strings (Username and Password), I figured it was time to fire up full-on debug mode.
Full-Frontal Debug
In order to have a controlled environment within which to run radare2 and have its STDIO
separated from the debugged binary's STDIO
, it's necessary to invoke the usage of rarun2
. The steps to get this working are somewhat non-intuitive, however once it is configured, it is well worth the effort.
Rarun2 Config File
In my working directory I created a rarun2
configuration file:
-> % cat catalyst.rr2
#!/usr/bin/env rarun2
program=catalyst
stdio=/dev/pts/2
This file's path is eventually passed as an argument to r2
with the -R
flag. The highlighted line is especially important; it specifies which terminal window I'd like to redirect all STDIO
to when debugging the program in radare.
Setting Up My Catalyst IO Terminal
To set up this terminal, I spawned a new terminal session and ran the tty
command, which output /dev/pts/2
. Then I ran clear; sleep 999999999999999999999;
in this window to prepare its use for rarun2.
tobaljackson@binarystudio [06:34:15 PM] [~/AlexCTF/RE3]
-> % tty
/dev/pts/2
tobaljackson@binarystudio [06:38:02 PM] [~/AlexCTF/RE3]
-> % clear; sleep 999999999999999999999999999999999999;
Doing this allows rarun2 access to a "clean" tty for STDIO
.
Starting the Debug Session
Next, in another terminal window (not the one that's sleeping), I simply ran r2
again, but specifying rarun2
as the debug target:
-> % r2 -d rarun2 -R ./catalyst.rr2
Process with PID 32043 started...
= attach 32043 32043
bin.baddr 0x00400000
USING 400000
Assuming filepath /home/tobaljackson/AlexCTF/RE3/catalyst
asm.bits 64
-- Too old to crash
[0x7f52047a4d70]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
TODO: esil-vm not initialized
[Cannot determine xref search boundariesr references (aar)
[x] Analyze len bytes of instructions for references (aar)
[Oops invalid rangen calls (aac)
[x] Analyze function calls (aac)
[ ] [*] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan))
= attach 32043 32043
[0x7f52047a4d70]> db main
[0x7f52047a4d70]> dc
Selecting and continuing: 32043
hit breakpoint at: 400d93
[0x00400d93]>
The highlighted lines show the 3 first commands I entered: aaa
which analyzes all functions and automatically names them, db main
which sets a breakpoint at main, and dc
which continues execution until it breaks at main. New users of radare will take note that all commands generally have subcommands which are mnemonics. To see a list of all commands (with descriptions) postfix any command with a ?
.
All d
- commands are debug commands, for instance.
Getting Visual
Up to this point I've presented some things with radare2
's command line interface, and others through the V
isual assembly (sans stack/registers) interface. However for debugging nothing beats its V
isual mode. Simply enter the V
command at the prompt to enter visual mode, and cycle through the display modes with p
until you arrive at the debugging mode:
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffee837a1b8 9162 4204 527f 0000 0000 0400 0000 0000 .bB.R...........
0x7ffee837a1c8 98a2 37e8 fe7f 0000 486c 5604 0100 0000 ..7.....HlV.....
0x7ffee837a1d8 930d 4000 0000 0000 0000 0000 0000 0000 ..@.............
0x7ffee837a1e8 5396 8348 c834 f1d8 8007 4000 0000 0000 S..H.4....@.....
rax 0x00400d93 rbx 0x00000000 rcx 0x00000000
rdx 0x7ffee837a2a8 r8 0x00401030 r9 0x7f52047b3900
r10 0x00000008 r11 0x00000001 r12 0x00400780
r13 0x7ffee837a290 r14 0x00000000 r15 0x00000000
rsi 0x7ffee837a298 rdi 0x00000001 rsp 0x7ffee837a1b8
rbp 0x00400fc0 rip 0x00400d93 rflags 1PZI
orax 0xffffffffffffffff
;-- rax:
;-- rip:
/ (fcn) main 341
| main ();
| ; var int local_18h @ rbp-0x18
| ; var int local_10h @ rbp-0x10
| ; var int local_4h @ rbp-0x4
| ; DATA XREF from 0x0040079d (entry0)
| 0x00400d93 b 55 push rbp
| 0x00400d94 4889e5 mov rbp, rsp
| 0x00400d97 4883ec20 sub rsp, 0x20
| 0x00400d9b bfe8030000 mov edi, 0x3e8 ; 1000
| 0x00400da0 e87bf9ffff call sym.imp.malloc ;[1]; void *malloc(size_t size)
| 0x00400da5 488945f0 mov qword [rbp - local_10h], rax
| 0x00400da9 bfe8030000 mov edi, 0x3e8 ; 1000
| 0x00400dae e86df9ffff call sym.imp.malloc ;[1]; void *malloc(size_t size)
| 0x00400db3 488945e8 mov qword [rbp - local_18h], rax
| 0x00400db7 bf00000000 mov edi, 0
| 0x00400dbc e84ff9ffff call sym.imp.time ;[2]; time_t time(time_t *timer)
| 0x00400dc1 89c7 mov edi, eax
| 0x00400dc3 e838f9ffff call sym.imp.srand ;[3]; void srand(int seed)
| 0x00400dc8 bf88104000 mov edi, 0x401088
From this mode, you can use vim
navigation keys jk
to move up and down the current instruction space. S
and s
allows stepping over and stepping into respectively, while :
allows entering of any normal (commandline) radare2 command. c
toggles cursor mode, while TAB
shifts focus (in cursor mode) between stack
, register
, and code
sections of the interface.
In the output above, the three highlighted lines indicate the boundaries of the three sections I just mentioned. Now that we're in visual mode, we can see what the program loads into the rbp-0x10
and rbp-0x18
qwords on the stack.
Using the j
and k
keys to look forward and backward at the program's assembly, I scanned down past the two loading loops which we've already NOP
d out, until I was at the section we've seen before which calls several more functions (including the winning function sub.printf_876
@ 0x00400fb3
).
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffee837a1b8 9162 4204 527f 0000 0000 0400 0000 0000 .bB.R...........
0x7ffee837a1c8 98a2 37e8 fe7f 0000 486c 5604 0100 0000 ..7.....HlV.....
0x7ffee837a1d8 930d 4000 0000 0000 0000 0000 0000 0000 ..@.............
0x7ffee837a1e8 5396 8348 c834 f1d8 8007 4000 0000 0000 S..H.4....@.....
rax 0x00400d93 rbx 0x00000000 rcx 0x00000000
rdx 0x7ffee837a2a8 r8 0x00401030 r9 0x7f52047b3900
r10 0x00000008 r11 0x00000001 r12 0x00400780
r13 0x7ffee837a290 r14 0x00000000 r15 0x00000000
rsi 0x7ffee837a298 rdi 0x00000001 rsp 0x7ffee837a1b8
rbp 0x00400fc0 rip 0x00400d93 rflags 1PZI
orax 0xffffffffffffffff
| 0x00400f62 90 nop
| 0x00400f63 90 nop
| 0x00400f64 bf0a000000 mov edi, 0xa
| 0x00400f69 e852f7ffff call sym.imp.putchar ;[1]; sym.imp.malloc-0x60; void *malloc(size_t size)
| 0x00400f6e 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f72 4889c7 mov rdi, rax
| 0x00400f75 e820fdffff call fcn.00400c9a ;[2]
| 0x00400f7a 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f7e 4889c7 mov rdi, rax
| 0x00400f81 e857fdffff call sub.puts_cdd ;[3]; int puts(const char *s)
| 0x00400f86 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f8a 4889c7 mov rdi, rax
| 0x00400f8d e865f9ffff call sub.puts_8f7 ;[4]; int puts(const char *s)
| 0x00400f92 488b55e8 mov rdx, qword [rbp - local_18h]
| 0x00400f96 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400f9a 4889d6 mov rsi, rdx
| 0x00400f9d 4889c7 mov rdi, rax
| 0x00400fa0 e8d2f9ffff call sub.puts_977 ;[5]; int puts(const char *s)
| 0x00400fa5 488b55e8 mov rdx, qword [rbp - local_18h]
| 0x00400fa9 488b45f0 mov rax, qword [rbp - local_10h]
| 0x00400fad 4889d6 mov rsi, rdx
| 0x00400fb0 4889c7 mov rdi, rax
| 0x00400fb3 e8bef8ffff call sub.printf_876 ;[6]; int printf(const char *format)
| 0x00400fb8 b800000000 mov eax, 0
| 0x00400fbd c9 leave
\ 0x00400fbe c3 ret
Here you can see at the top of the assembly our second "Loading" NOP
s that we overwrote the jle
instruction with (at 0x00400f63
). Also to note is that simply navigating the code view down hasn't advanced the program counter; our instruction pointer is still at the start of main, 0x400d93
shown above on the first highlighted line as rip
.
A quick note about memory addresses: you may notice above that sometimes my Stack memory addresses (and potentially instruction addresses) are different than yours, and this has to do with how different operating systems map memory and whether or not something called ASLR (Address Space Layout Randomization) is turned on. If you'd like to turn off ASLR (which can simplify exploit development and debugging/reversing), simply execute the command
echo 0 > /proc/sys/kernel/randomize_va_space
as root and you'll disable ASLR for all future-spawned processes. Don't worry, the setting will revert upon reboot.
Since I've been dying to know what is put onto the stack that each of these functions appear to rely on, lets put a breakpoint at the first function at 0x00400f75
. You can either enter c
ursor mode and navigate to the first byte of the call instruction and press b
to set a breakpoint, or enter the command :bp 0x00400f75
.
Another sidenote: the radare2 team has been working on some features which integrate the mouse pointer (for scrolling and some other potential things) a side effect is that clicking within the radare2 window can have some unintended consequences, such as changing settings or modifying data! To disable this, either enter the command
e scr.wheel=false
or (in visual mode) press e to enter the settings browser, findscr
and change thescr.wheel
setting to false with<enter>
.Last one: all configuration options set with
e
can be saved to~/.radare2rc
. Here is mine:e stack.bytes = false e scr.wheel = false e stack.size = 114
Now that we have a breakpoint set here, we can simply :dc
to continue execution to our breakpoint.
Detached Head
While it may look like radare2 has frozen, if you switch to the terminal prepared for the STDIO
interaction, you should see our familiar catalyst prompt asking for a username and password. Go ahead and type a username and password when prompted, following each with an <enter>
keystroke. Once you've entered the password, you'll see the radare2 window update and stop at the breakpoint we've set.
At this point, we can see that the rdi
register is loaded with one of the values we were interested in before. To inspect what this value is, use a variation of the p
rint command: ps @ rdi
:> ps @ rdi
HERPDERP
As you can see, the rdi
register holds a pointer to our Username string. Since we want to know what the other address points to (the rbp-0x18
one) we can also inspect it with the ps
command, however since rbp-0x18
is itself an address, we have to dereference it using the []
characters:
:> ps @ [rbp-0x18]
hurrdurr
Now armed with the knowledge of what was being put into the registers (and where from) I used the s
command to step-into the call fcn.00400c9a
function to see what it does:
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7fffffffdf58 7a0f 4000 0000 0000 c00f 4000 0000 0000 z.@.......@.....
0x7fffffffdf68 0034 6000 0000 0000 1030 6000 0000 0000 .4`......0`.....
0x7fffffffdf78 0000 0000 0000 0000 c00f 4000 0000 0000 ..........@.....
0x7fffffffdf88 91c2 a5f7 ff7f 0000 0000 0400 0000 0000 ................
rax 0x00603010 rbx 0x00000000 rcx 0x7ffff7b17530
rdx 0x0000000a r8 0x7ffff7dd6740 r9 0x7ffff7fba440
r10 0x0000000a r11 0x00000246 r12 0x00400780
r13 0x7fffffffe060 r14 0x00000000 r15 0x00000000
rsi 0x7ffff7dd6740 rdi 0x00603010 rsp 0x7fffffffdf58
rbp 0x7fffffffdf80 rip 0x00400c9a rflags 1PZI
orax 0xffffffffffffffff
;-- rip:
/ (fcn) fcn.00400c9a 67
| fcn.00400c9a ();
| ; var int local_18h @ rbp-0x18
| ; var int local_4h @ rbp-0x4
| ; CALL XREF from 0x00400f75 (loc.00400f5e)
| 0x00400c9a 55 push rbp
| 0x00400c9b 4889e5 mov rbp, rsp
| 0x00400c9e 4883ec20 sub rsp, 0x20
| 0x00400ca2 48897de8 mov qword [rbp - local_18h], rdi #put Username pointer on stack
| 0x00400ca6 c745fc000000. mov dword [rbp - local_4h], 0 #initialize counter
| ,=< 0x00400cad eb18 jmp 0x400cc7;[1] #unconditional jump
| | ; JMP XREF from 0x00400ccb (fcn.00400c9a)
| .--> 0x00400caf 8b45fc mov eax, dword [rbp - local_4h]
| || 0x00400cb2 4863d0 movsxd rdx, eax #put counter in rdx
| || 0x00400cb5 488b45e8 mov rax, qword [rbp - local_18h] #get username addr
| || 0x00400cb9 4801d0 add rax, rdx #offset [counter] bytes into it
| || 0x00400cbc 0fb600 movzx eax, byte [rax] #get that character
| || 0x00400cbf 84c0 test al, al #see if it's null (\x00)
| ,===< 0x00400cc1 740c je 0x400ccf;[2] #if it is, we have STRLEN
| ||| 0x00400cc3 8345fc01 add dword [rbp - local_4h], 1 #else, count+=1 and try again
| |!| ; JMP XREF from 0x00400cad (fcn.00400c9a)
| ||`-> 0x00400cc7 837dfc31 cmp dword [rbp - local_4h], 0x31 #max username length == 0x31
| |`==< 0x00400ccb 7ee2 jle 0x400caf;[3]
| | ,=< 0x00400ccd eb01 jmp 0x400cd0;[4]
| | | ; JMP XREF from 0x00400cc1 (fcn.00400c9a)
| `---> 0x00400ccf 90 nop
| | ; JMP XREF from 0x00400ccd (fcn.00400c9a)
| `-> 0x00400cd0 8b45fc mov eax, dword [rbp - local_4h] #put STRLEN in rax
| 0x00400cd3 89c7 mov edi, eax #and in edi
| 0x00400cd5 e867ffffff call sub.puts_c41 #now pass STRLEN to sub-function
| 0x00400cda 90 nop
| 0x00400cdb c9 leave
\ 0x00400cdc c3 ret
This function is counting the string length of the Username, up to a maximum of 0x31 characters. It does this by initializing a loop counter to 0 (at 0x400ca6
) on the stack, as well as placing the username string pointer on the stack (at 0x400ca2
). After this occurs, an unconditional jump takes us to 0x400cc7
, which checks the loop counter against the value 0x31
.
Since the loop counter starts at 0, the jle
(jump if less than or equal to) instruction takes rip back up to 0x400caf
, which loads the loop counter value into rax (which ends up in rdx) and then doubly-serves as an index into the string at 0x400cb9
. Adding the loop counter to the string address allows the instruction at 0x400cbc
to load the indexed character of the string into the rax
register and use test al,al
on it.
A Side Note About Registers: in the three highlighted lines above,
rax
,eax
, andal
each refer to the same register, albeit different parts of it. Here is a simple ascii graphic representing the different "parts" of the x86_64rax
register (taken from this stackoverflow answer):0x1122334455667788 ================ rax (64 bits) ======== eax (32 bits) ==== ax (16 bits) == ah (8 bits) == al (8 bits)The same "slicing" scheme is used for the other general-purpose registers,rbx
,rcx
,rdx
The test
instruction will set the ZF
(zero flag) of the processor if the value being tested is zero, and the following je
(jump if equal) instruction at 0x400cc1
is also referred to as jz
or jump if zero; it checks if the zero flag of the processor is set, and if so, follows the jump and unsets the flag.
Despite whether the above explanation for the assembly still seems fuzzy or not (if you're new to RE), I would highly recommend using the s
command to single-step through this loop, watching the value of the stack and registers change as you do so. At any point in time (like after the instruction at 0x400cbc
) you can use the ps @ rax
command to inspect the value of the rax register and see the username characters being loaded and checked. It's rather cool to see and even more rewarding to understand!
Username Validation
After determining that this function was just a custom strlen()
, I set a breakpoint on the call sub.puts_c41
with :bp 0x400cd5
, and used :dc
to continue to it. s
tepping into the function yielded the following:
| 0x00400c41 55 push rbp
| 0x00400c42 4889e5 mov rbp, rsp
| 0x00400c45 4883ec20 sub rsp, 0x20
| 0x00400c49 897dec mov dword [rbp - local_14h], edi #put strlen on stack
| 0x00400c4c 8b45ec mov eax, dword [rbp - local_14h] #put strlen in eax
| 0x00400c4f c1f802 sar eax, 2 #divide it by 4
| 0x00400c52 8945fc mov dword [rbp - local_4h], eax #store result on stack
| 0x00400c55 8b45fc mov eax, dword [rbp - local_4h]
| 0x00400c58 c1e002 shl eax, 2 #multiply it by 4
| 0x00400c5b 3b45ec cmp eax, dword [rbp - local_14h] #see if number was evenly divisible by 4
| ,=< 0x00400c5e 7523 jne 0x400c83;[1] #jump to fail if not
| | 0x00400c60 8b45fc mov eax, dword [rbp - local_4h] #load previously divided value
| | 0x00400c63 c1f802 sar eax, 2 #div by 4 again
| | 0x00400c66 8945f8 mov dword [rbp - local_8h], eax #store on stack
| | 0x00400c69 8b45f8 mov eax, dword [rbp - local_8h]
| | 0x00400c6c c1e002 shl eax, 2 #multiply by 4
| | 0x00400c6f 3b45fc cmp eax, dword [rbp - local_4h] #check if equal
| ,==< 0x00400c72 740f je 0x400c83;[1] #jump to fail if they are
| || 0x00400c74 8b45fc mov eax, dword [rbp - local_4h] #load previously divided value
| || 0x00400c77 d1f8 sar eax, 1 #shift right by 1
| || 0x00400c79 85c0 test eax, eax #check if its zero
| ,===< 0x00400c7b 7406 je 0x400c83;[1] #fail if it is zero
| ||| 0x00400c7d 837df800 cmp dword [rbp - local_8h], 0
| ,====< 0x00400c81 7414 je 0x400c97;[2]
| |```-> 0x00400c83 bf69104000 mov edi, str.invalid_username_or_password ; "invalid username or password" @ 0x401069
| | 0x00400c88 e843faffff call sym.imp.puts
| | 0x00400c8d bf00000000 mov edi, 0
| | 0x00400c92 e8b9faffff call sym.imp.exit
| `----> 0x00400c97 90 nop
| 0x00400c98 c9 leave
\ 0x00400c99 c3 ret
THIS POST STILL UNDER CONSTRUCTION
The All-In-One Solution
Here is the python script which solves this challenge. Required dependencies are python2
and the angr
package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | #!/usr/bin/env python2
import os
import stat
import sys
import angr
import claripy
import logging
from binascii import hexlify
from subprocess import Popen, PIPE
def analyzePaths(start, target, avoid, length, user=None):
print("Let's do this; beginning analysis")
fname = 'catalyst2'
print("Loading the binary")
b = angr.Project(fname)
print("Reticulating Splines")
length += 1
#setting the addresses that will guide angr
START = start
TARGET = target
AVOID = avoid
print("\tStart: {:#x}\n\tWant: {:#x}\n\tAvoid: {}".format(START, TARGET, ', '.join([hex(x) for x in AVOID])))
#create a blank angr state
state = b.factory.blank_state(addr=START)
if (user == None):
#declare symbolic user object, 12 characters in length
username = claripy.BVS('username', 0x60)
#constrain what the username bytes can be (printable and non-null '\x00' and non-space)
for byte in username.chop(8):
state.add_constraints(byte != '\x00')
state.add_constraints(byte > ' ')
state.add_constraints(byte <= '~')
else:
print("\tThis might take a while...")
#set a concrete value using supplied username
username = claripy.BVV(int(hexlify(user),16), 0x60)
#this time password is symbolic
password = claripy.BVS('password', length * 8)
for bits in range(0, len(password.chop(8))):
#unlike username, password needs to have it's last byte set to null
byte = password.chop(8)[bits]
if (bits == len(password.chop(8)) - 1):
state.add_constraints(byte = '\x00')
else:
state.add_constraints(byte != '\x00')
state.add_constraints(byte >= ' ')
state.add_constraints(byte <= '~')
#store password in memory
state.memory.store(0x00603400, password)
#set rsi to password
state.regs.rsi = 0x00603400
#store username in memory
state.memory.store(0x00603010, username)
#set rdi to username
state.regs.rdi = 0x00603010
#initialize path
path = b.factory.path(state)
#set up path group
pg = b.factory.path_group(path, threads=32)
#Start Exploring!
ex = pg.explore(find=TARGET, avoid=AVOID)
print("FOUND A THING!!\n")
if (user == None):
#retrieve username from memory
userStr = ex.found[0].state.se.any_str(username)
print("username: {}".format(userStr))
return userStr
else:
#retrieve password from memory
passwordStr = ex.found[0].state.se.any_str(password)
print("password: {}".format(passwordStr))
return passwordStr
def patch_binary():
print("Creating patched binary...")
#open catalyst
f = open("catalyst", 'rb')
fo = list(f.read())
f.close()
print("\tskipping loading routines...")
#write nops over loading loop jumps
#first: 0x00400ea9
fo[0xea9] = '\x90'
fo[0xeaa] = '\x90'
#second: 0x00400f62
fo[0xf62] = '\x90'
fo[0xf63] = '\x90'
#nop password explosion check
#seek to 0x40099f
#write 'jmp 0x400a4c'
print("\tskipping branch-exploder...")
fo[0x99f] = '\xe9'
fo[0x9a0] = '\xa8'
#write patched binary
print("\twriting patched binary 'catalyst2'")
f = open("catalyst2", 'wb')
f.write(''.join(fo))
f.close()
#make it executable
print("making it executable")
st = os.stat("catalyst2")
os.chmod("catalyst2", st.st_mode | stat.S_IEXEC)
if __name__ == "__main__":
#we need to make the binary usable, removing long sleep()s
patch_binary()
#username length of 12 discovered through science
user = analyzePaths(0x400cdd, 0x400d92, (0x400d7c,), 12)
#password length of 40 discovered through guessing ;)
pw = analyzePaths(0x00400977, 0x00400c40, (0x400a1c, 0x400a87, 0x400ab5, 0x400ae3, 0x400b11, 0x400b3f, 0x400b6d, 0x400b9b,
0x400bc9, 0x400bf7, 0x400c25), 40, user)
#after discovering username/pass, feed them into the program
p = Popen(['./catalyst2'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
#GET THE FLAG
flag = p.communicate(input='{}\n{}\n'.format(user, pw))
#PROFIT
print(flag[0])
|
THIS POST STILL UNDER CONSTRUCTION