Attention all UTCTF players, asper is in great danger, and he needs YOUR help to reverse engineer this binary and figure out the password. To do this, he needs IDA Pro and a couple of breakpoints. To help him, all he needs is your credit card number, the three numbers on the back, and the expiration month and date. But you gotta be quick so that asper can secure the flag, and achieve the epic victory R O Y A L.
This was a 1200 point (tied for highest) reversing challenge. I'm guessing they over-estimated the difficulty as there were 21 separate solves. Also, given the challenge description, it was meant to be solved via breakpoints. However, I mostly used angr.
crackme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
It's not stripped and dynamically linked, so that's pretty helpful already. Running it:
./crackme
Please enter the correct password.
>blerg
Incorrect password.
utflag{wrong_password_btw_this_is_not_the_flag_and_if_you_submit_this_i_will_judge_you}
So it's your standard, prompt for input and check for correct password. Attempting to ltrace it we get the following:
[pid 125078] [0x400a78] ptrace(0, 0, 1, 0 <unfinished ...>
[pid 125078] [0x7f07d2d4393f] SYS_ptrace(0, 0, 1, 0) = -1
[pid 125078] [0x400a78] <... ptrace resumed> ) = -1
[pid 125078] [0x400a8c] exit(1 <unfinished ...>
[pid 125078] [0x7f07d2d10e06] SYS_exit_group(1 <no return ...>
[pid 125078] [0xffffffffffffffff] +++ exited (status 1) +++
So there's anti-debugging here. Using radare2 to look back at that address we find:
╭ (fcn) entry.init1 46
│ entry.init1 ();
│ 0x00400a60 55 push rbp
│ 0x00400a61 4889e5 mov rbp, rsp
│ 0x00400a64 31c0 xor eax, eax
│ 0x00400a66 ba01000000 mov edx, 1 ; void*addr
│ 0x00400a6b 89c7 mov edi, eax ; __ptrace_request request
│ 0x00400a6d 89c6 mov esi, eax ; pid_t pid
│ 0x00400a6f 89c1 mov ecx, eax ; void*data
│ 0x00400a71 b000 mov al, 0
│ 0x00400a73 e8b8feffff call sym.imp.ptrace ;[1] ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data)
│ 0x00400a78 4883f8ff cmp rax, 0xffffffffffffffff
│ ╭─< 0x00400a7c 0f850a000000 jne 0x400a8c ;[2]
│ │ 0x00400a82 bf01000000 mov edi, 1 ; int status
│ │ 0x00400a87 e8d4feffff call sym.imp.exit ;[3] ; void exit(int status)
│ │ ; CODE XREF from entry.init1 (0x400a7c)
│ ╰─> 0x00400a8c 5d pop rbp
╰ 0x00400a8d c3 ret
0x00400a8e 6690 nop
At this point, I simply NOP'd out that ptrace call so that it wouldn't keep bothering me. Re-running ltrace:
[pid 125199] [0x400b26] setbuf(0x7f175bd3aa00, 0) = <void>
[pid 125199] [0x400b39] setbuf(0x7f175bd3b760, 0) = <void>
[pid 125199] [0x400b4f] printf("Please enter the correct password.\n>" <unfinished ...>
[pid 125199] [0x7f175ba5f154] SYS_write(1, "Please enter the correct password.\n>", 36Please enter the correct password.
>) = 36
[pid 125199] [0x400b4f] <... printf resumed> ) = 36
[pid 125199] [0x400b78] fgets( <unfinished ...>
[pid 125199] [0x7f175ba5f081] SYS_read(0blerg
, "b", 1) = 1
[pid 125199] [0x7f175ba5f081] SYS_read(0, "l", 1) = 1
[pid 125199] [0x7f175ba5f081] SYS_read(0, "e", 1) = 1
[pid 125199] [0x7f175ba5f081] SYS_read(0, "r", 1) = 1
[pid 125199] [0x7f175ba5f081] SYS_read(0, "g", 1) = 1
[pid 125199] [0x7f175ba5f081] SYS_read(0, "\n", 1) = 1
[pid 125199] [0x400b78] <... fgets resumed> "blerg\n", 64, 0x7f175bd3aa00) = 0x7ffcf616e520
[pid 125199] [0x400b8b] strlen("blerg\n") = 6
[pid 125199] [0x400ab4] __cxa_allocate_exception(4, 0, 0x7ffcf616e520, 32 <unfinished ...>
[pid 125199] [0x7f175ba654b9] SYS_brk(0) = 0x1251000
[pid 125199] [0x7f175ba654b9] SYS_brk(0x1272000) = 0x1272000
[pid 125199] [0x400ab4] <... __cxa_allocate_exception resumed> ) = 0x12512e0
[pid 125199] [0x400ad8] __cxa_throw(0x12512e0, 0x601df0, 0, 0 <unfinished ...>
[pid 125199] [0x7f175b73f84e] SYS_futex(0x7f175bf6f238, 129, 0x7fffffff, 0) = 0
[pid 125199] [0x7f175b73f84e] SYS_futex(0x7f175b72f1a0, 129, 0x7fffffff, 0) = 0
[pid 125199] [0x7f175b52894b] __gxx_personality_v0(1, 1, 0x434c4e47432b2b00, 0x12512c0) = 6
[pid 125199] [0x7f175b528553] __gxx_personality_v0(1, 6, 0x434c4e47432b2b00, 0x12512c0) = 7
[pid 125199] [0x400c4c] __cxa_begin_catch(0x12512c0, 0x400c35, 1, 1) = 0x12512e0
[pid 125199] [0x400c96] __cxa_end_catch(0, 68, 68, 68) = 0
[pid 125199] [0x400da3] memcmp(0x602230, 0x7ffcf616e520, 64, 64) = 0xffffffaf
[pid 125199] [0x400dd9] printf("Incorrect password.\n" <unfinished ...>
[pid 125199] [0x7f175ba5f154] SYS_write(1, "Incorrect password.\n", 20Incorrect password.
) = 20
[pid 125199] [0x400dd9] <... printf resumed> ) = 20
[pid 125199] [0x400df0] printf("utflag{wrong_password_btw_this_is_not_the_flag_and_if_you_submit_this_i_will_judge_you}\n" <unfinished ...>
[pid 125199] [0x7f175ba5f154] SYS_write(1, "utflag{wrong_password_btw_this_is_not_the_flag_and_if_you_submit_this_i_will_judge_you}\n", 88utflag{wrong_password_btw_this_is_not_the_flag_and_if_you_submit_this_i_will_judge_you}
) = 88
[pid 125199] [0x400df0] <... printf resumed> ) = 88
[pid 125199] [0x7f175ba33e06] SYS_exit_group(0 <no return ...>
[pid 125199] [0xffffffffffffffff] +++ exited (status 0) +++
So this time around, we can see that it calls fgets, strlen, then runs into an exception and, importantly, catches it. Looking for some more information, it's time to use the shiny new Ghidra decompiler:
undefined8 main(undefined4 argc,undefined8 argv)
{
int iVar1;
size_t iMyInputLen;
double dVar2;
int local_98;
int local_88;
byte bufMyInput [45];
setbuf(stdin,(char *)0x0);
setbuf(stdout,(char *)0x0);
printf("Please enter the correct password.\n>");
fgets((char *)bufMyInput,0x40,stdin);
iMyInputLen = strlen((char *)bufMyInput);
/* try { // try from 00400bc1 to 00400bce has its CatchHandler @ 00400c35 */
dVar2 = (double)divide(0x20,0);
bufMyInput[(long)(int)dVar2] = bufMyInput[(long)(int)dVar2] ^ 8;
local_88 = 0;
while ((ulong)(long)local_88 < iMyInputLen) {
bufMyInput[(long)local_88] = bufMyInput[(long)local_88] ^ 0x27;
local_88 = local_88 + 1;
}
local_98 = 0;
while (local_98 < 0xcb) {
stuff[(long)local_98] = stuff[(long)local_98] - 1 ^ stuff2[(long)(0xca - local_98)];
local_98 = local_98 + 1;
}
(*(code *)stuff)(bufMyInput,iMyInputLen);
iVar1 = memcmp(test,bufMyInput,0x40);
if (iVar1 == 0) {
printf("Correct Password!");
}
else {
printf("Incorrect password.\n");
printf(
"utflag{wrong_password_btw_this_is_not_the_flag_and_if_you_submit_this_i_will_judge_you}\n"
);
}
return 0;
}
The code is pretty readable. One thing that jumps out is that static call to the function "divide". It's getting called with two static arguments, 0x20 and 0. Let's look at the divide code:
/* divide(int, int) */
double divide(int param_1,int param_2)
{
undefined4 *puVar1;
if (param_2 == 0) {
puVar1 = (undefined4 *)__cxa_allocate_exception(4);
*puVar1 = 8;
/* WARNING: Subroutine does not return */
__cxa_throw(puVar1,typeinfo,0,0);
}
return (double)(param_1 / param_2);
}
So given that there is always a static call to divide with the second argument of 0, divide will always throw an exception. Ghidra was also nice enough to tell us in it's C comments that the exception catcher is at 0x400c35. This is obviously intended to throw people off since you generally don't have your exception handler jump into the middle of your function, but that's what is going on here.
At this point I attempted to manually reverse the crypt, starting from the end going forward. It does a bunch of xor's but some point during my reverse process I clearly got off the rails and my algorithm didn't work properly. After deciding to stop attempting to manually reverse it, I wanted to give angr a go.
While it's possible angr would be able to execute through from the beginning, that seemed like a tricky endeavor. Instead, I wanted to use angr Symbion but ran into a bug in it which the angr devs are looking into. Then, I tried r2angrdb, which similarly ran into bugs (with their own implementation). Finally, I decided to give angrgdb a shot and it worked as follows:
First off, looking back at the decompiled code we can see that a function (named 'stuff') is decrypted in memory and then called. For angr to be able to handle this, it needs to specifically enable support for self modifying code. To do this, we need to open the constructor python source for angrgdb. Open up "angrdbg/context.py" and edit line 30 to add the option "support_selfmodifying_code=True".
With that completed, using angr to solve the challenge becomes strait forward and fairly fast.
# Break AFTER all that setup and exception catching is done
break *0x400C96
# Start it up
r
# Give it a buffer of 63 "A"s
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# Tell angr to find the solution and avoid the failures
angrgdb find 0x400DAC
angrgdb avoid 0x400DC8
# Tell angr to mark our 64 bytes as symbolic
angrgdb sim $rbp-0x50 0x40
# Run!
angrgdb run
The flag pops out quickly, though 2 bytes specifically are wrong. Easy enough to correct and you get the flag:
utflag{1_hav3_1nf0rmat10n_that_w1ll_lead_t0_th3_arr3st_0f_c0pp3rstick6}