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.

Anti Debug Message

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.

IDA Debugger Location

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.

Patch program

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.

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.

Breakpoint address

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.

Flag

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.