Intro
This is my first RE write up and I am going to try and make it as transparent as possible in terms of how long it takes and any wrong turns I have taken along the way. I have read a lot of write ups and conference talks over the years and all you ever see is the end result. You don’t often see the path to the answers.
Target - XORcist
I have chosen a binary from https://crackmes.one. The file can be downloaded here
The binary is called xorcist
and described as follows.
easy crackme with Control Flow Obfuscation, XOR “encryption” and anti-debugging Can you get the password or patch the validator function?
Compile Time
This crackme is distributed as source code and you need to build it before the fun begins.
$ gcc -s -o xorcist xorcist.c
It is useful to have the source once it is solved so the disassembly can be compared with the original source.
Running before we walk
Before we launch into IDA lets see what the binary does when we run it.
$ ./xorcist
What's the password?
password
Nuh Uh Nuh Uh.
In order to solve it we need to enter the correct password.
Into the IDA verse
Loading the binary into IDA drops us at the entry point and we can open main().
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned int v4; // eax
unsigned int v5; // [rsp+8h] [rbp-68h]
int v6; // [rsp+10h] [rbp-60h]
char v7[20]; // [rsp+14h] [rbp-5Ch] BYREF
__int64 (__fastcall *v8)(); // [rsp+28h] [rbp-48h]
char v9; // [rsp+3Eh] [rbp-32h]
char s[40]; // [rsp+40h] [rbp-30h] BYREF
unsigned __int64 v11; // [rsp+68h] [rbp-8h]
v11 = __readfsqword(0x28u);
if ( (unsigned int)sub_1258(a1, a2, a3) )
{
puts("Nooooooo. La Policiaaa.");
return 1LL;
}
else
{
v6 = rand() % 100 + 1; // 1 - 100
strcpy(v7, "root"); //
v8 = sub_1349;
puts("What's the password?");
fgets(s, 32, stdin);
s[strcspn(s, "\n")] = 0; // remove the carriage return
v9 = rand() % 95 + 32;
v4 = time(0LL);
srand(v4);
v5 = rand() % 6969 + 1;
sub_13BF(v5);
if ( ((unsigned int (__fastcall *)(char *))v8)(s) && v6 )
printf("Logged in as %s.\n", v7);
else
puts("Nuh Uh Nuh Uh.");
return 0LL;
}
}
Anti-Debug Code
The first code that we need to look at is:
if ( (unsigned int)sub_1258(a1, a2, a3) )
{
puts("Nooooooo. La Policiaaa.");
return 1LL;
}
This code calls a sub-routine and if it returns true it outputs a failure message and exits the binary.
Examining the code for sub_1258()
we see:
_BOOL8 sub_1258()
{
int v1; // [rsp+4h] [rbp-11Ch]
FILE *stream; // [rsp+8h] [rbp-118h]
char s1[10]; // [rsp+10h] [rbp-110h] BYREF
char v4[254]; // [rsp+1Ah] [rbp-106h] BYREF
unsigned __int64 v5; // [rsp+118h] [rbp-8h]
v5 = __readfsqword(0x28u);
stream = fopen("/proc/self/status", "r");
if ( !stream )
return 0LL;
while ( fgets(s1, 256, stream) )
{
if ( !strncmp(s1, "TracerPid:", 0xAuLL) ) //check if the TracerPid entry is not == 0 ie it is being debugged
{
v1 = atoi(v4);
fclose(stream);
return v1 != 0;
}
}
fclose(stream);
return 0LL;
This code is reading the contents of /proc/self/status
The man page is here and has this information regarding the TracerPid entry.
TracerPid PID of process tracing this process (0 if not being traced).
This code is therefore checking if the code is binary is being traced in the debugger. This is an anti-debug check that will need to be bypassed if you are going to solve it using gdb
.
The Secret
If the debug check is false
the else() clause runs and this is where our input password is checked.
{
v6 = rand() % 100 + 1; // 1 - 100
strcpy(v7, "root"); // v7 == "root"
v8 = sub_1349; //v8 = sub_1349
puts("What's the password?");
fgets(s, 32, stdin); // get our password guess
s[strcspn(s, "\n")] = 0; // remove the carriage return
v9 = rand() % 95 + 32;
v4 = time(0LL);
srand(v4);
v5 = rand() % 6969 + 1;
sub_13BF(v5);
if ( ((unsigned int (__fastcall *)(char *))v8)(s) && v6 ) //This is where our password is checked with the secret, our password is checked in sub_1349
printf("Logged in as %s.\n", v7);
else
puts("Nuh Uh Nuh Uh.");
return 0LL;
}
Password check
BOOL8 __fastcall sub_1349(const char *a1) //a1 == input string
{
char dest[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
strcpy(dest, byte_2023);
sub_1209(dest);
return strcmp(a1, dest) == 0; //compare is done here, input string a1 == dest for the password check to pass
}
We can see our input string is compared to the dest
in the return clause. The data from byte2023
are copied into dest and then dest is passed into sub_1209
.
In order to view the data at byte2023
I used readelf
.
readelf -x .rodata ./xorcist
Hex dump of section '.rodata':
0x00002000 01000200 72002f70 726f632f 73656c66 ....r./proc/self
0x00002010 2f737461 74757300 54726163 65725069 /status.TracerPi
0x00002020 643a00dd daf6d9c4 c6f6cec7 cef6c0ca d:..............
0x00002030 c5004e6f 6f6f6f6f 6f6f2e20 4c612050 ..Nooooooo. La P
0x00002040 6f6c6963 69616161 2e005768 61742773 oliciaaa..What's
0x00002050 20746865 20706173 73776f72 643f000a the password?..
0x00002060 004c6f67 67656420 696e2061 73202573 .Logged in as %s
0x00002070 2e0a004e 75682055 68204e75 68205568 ...Nuh Uh Nuh Uh
0x00002080 2e00 ..
The bytes that we are interested are in the highlighted section.
0xdddaf6d9c4c6f6cec7cef6c0cac5
The transformation that is done on these bytes is in sub_1209
.
__int64 __fastcall sub_1209(__int64 a1) // input bytes
{
__int64 result; // rax
int i; // [rsp+14h] [rbp-4h]
for ( i = 0; ; ++i )
{
result = *(unsigned __int8 *)(i + a1);
if ( !(_BYTE)result )
break;
*(_BYTE *)(i + a1) ^= 0xA9u; // loop through the bytes and xor with fixed value 0xA9
}
return result;
}
The loop in this subroutine performs xor on each byte with the value 0xA9
.
s = input string
input string passed into sub_1349()
there is an array of bytes that is xored with 0xa9
to produce the secret.
I wrote a very short python script to output the secret.
byte2023 = [0xdd,0xda,0xf6,0xd9,0xc4,0xc6,0xf6,0xce,0xc7, 0xce,0xf6,0xc0,0xca,0xc5]
s = ""
for b in byte2023:
s += chr(b^0xa9)
print(f"The secret password is: {s}")
The output from the script is:
$ python solution.py
The secret password is: ts_pmo_gng_icl
The Flag
Passing this value as the password shows us logged in as root :-)
$ ./xorcist
What's the password?
ts_pmo_gng_icl
Logged in as root.
Dynamic Analysis - GDB
If we attach gdb
to the binary and run it we see the output we saw during the static analysis.
We will patch the instruction using IDA so we can set a breakpoint where the flag is compared to our input string and solve the challenge.
In the screenshot we can see that the program will only continue if the zero flag is set after testing the contents of eax
. This will only happen if a debugger is not detected in sub_133C
. We will use IDA to patch this instruction to be jnz
that way execution will only continue if there is a debugger connected.
Go to Edit > Patch program > Assemble
the following dialog is displayed. Change the instruction to jnz
and click ok. Then go to Edit > Patch program > Apply patches to input file
.
In order to set the breakpoint in gdb
to capture the flag we need to check the security settings of the binary. We can do this with checksec
in gdb
.
$ gdb xorcist
gef> checksec
[+] checksec for '/home/neil/crackmes/XORcist/xorcist'
Canary : ✓ (value: 0x3a74831cd91f6e00)
NX : ✓
PIE : ✓ // Position Independent EXE
Fortify : ✘
RelRO : Partial
checksec
shows that the binary has PIE
enabled so we will have find the load address in gdb
once the application has started.
Once the binary is loaded if we enter start
and then info proc mappings
we will see the runtime addresses.
We can see the base address is 0x555555554000
we will use this address to rebase the binary in IDA so we can find the correct breakpoint address for the flag.
In IDA select Edit > Segments > Rebase Program
then enter 0x555555554000
as the image base.
The address in IDA for the instruction of interest is now 0x55555555547a
.
Back in gdb
we can set the breakpoint with break *0x55555555547a
then continue the program with c
.
You will be asked to enter the password, you can enter anything here we just want to get to the breakpoint.
The program will stop on the breakpoint, just before the string is loaded into $rdx
. Step to the next instruction with ni
and then enter x/s $rdx
.
You will see the flag printed out in the terminal.
Final Thoughts
This challenge had some interesting little quirks.
-
The subroutine that checks the input string was assigned to a variable so in the the assembly when it was called with the input string it was called with
call $rdx
-
There was quite a lot of pointless code that were redundant and were not important to the finding the flag
-
The anti-debug check was also interesting checking
/proc/self/status
was a different approach to what I have seen previously.
Overall I spent a couple of evenings on this challenge, the part that took me the longest was figuring out how to accurately get the correct breakpoint address. Some reading on PIE
and gdb
helped to come up with the solution.