craftwa.re

A walk outside the sandbox

Home Blog Cheat Sheets MacOS Tips Area 51 About

[CTF] Erasure (XOR decryption)

|

Understanding the challenge

The challenge comes with a single ELF64 bit binary, which doesn’t seem to do anything:

$ ./erasure 
                                                                                                             
$ file erasure 
erasure: 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

We need to look under the hood to understand what’s happening. The following is the decompiled main function, using Ghidra:

undefined8 main(EVP_PKEY_CTX *param_1,uchar *param_2,size_t *param_3,uchar *param_4,size_t param_5)

{
  int iVar1;
  undefined4 extraout_var;
  FILE *__s;
  size_t sVar2;
  int local_c;
  
 iVar1 = decrypt(param_1,param_2,param_3,param_4,param_5);
 __s = fopen("flag.txt","w");
  if (__s == (FILE *)0x0) {
    perror("Couldn\'t open file");
    exit(-1);
  }
  fwrite((char *)CONCAT44(extraout_var,iVar1), 0x1e, 1, __s);
  fclose(__s);
  sVar2 = strlen((char *)CONCAT44(extraout_var,iVar1));
  local_c = (int)sVar2;
  while (local_c != 0) {
    truncate("flag.txt",(long)(local_c + -1));
    local_c = local_c + -1;
  }
  return 0;
}

Basically the main function will decrypt something, and write it to the flag.txt file. The decrypt function is short and straightforward too:

int decrypt(EVP_PKEY_CTX *ctx,uchar *out,size_t *outlen,uchar *in,size_t inlen)

{
  char *pcVar1;
  uint local_c;
  
 pcVar1 = strdup(secret);
  for (local_c = 0; local_c < 0x1d; local_c = local_c + 1) {
    pcVar1[(int)local_c] = pcVar1[(int)local_c] ^ 0x41;
  }
  return (int)pcVar1;
}

It takes a string of bytes - secret, and applies a simple XOR-in routine. The secret looks like this:

secret    
    00402010 09              undefined109h                     [0]                              
    00402011 15              undefined115h                     [1]
    00402012 03              undefined103h                     [2]
    00402013 3a              undefined13Ah                     [3]
    00402014 72              undefined172h                     [4]
    00402015 33              undefined133h                     [5]
    00402016 75              undefined175h                     [6]
    00402017 32              undefined132h                     [7]
    00402018 72              undefined172h                     [8]
    00402019 25              undefined125h                     [9]
    0040201a 1e              undefined11Eh                     [10]
...

Solution

The simplest way to solve this is using a debugger. Set up a breakpoint after the call to decrypt function, and examine the result. The function will allocate a string for the decrypted text, and return that in RAX: Decrypt

We can solve this programatically as well, using a simple Python script:

secret = [0x09, 0x15, 0x03, 0x3a, 0x72, 0x33, 0x75, 0x32, 0x72, 0x25, 0x1e, 0x27, 0x33, 0x71, 0x2c, 0x1e, 0x72, 0x39, 0x70, 0x32, 0x35, 0x72, 0x2f, 0x22, 0x72, 0x6f, 0x6f, 0x7e, 0x3c]
print("[*] Secret length: %d (0x%x)" % (len(secret), len(secret)))

decr = "".join([chr(n^0x41) for n in secret])
print("[*] Decrypted text: %s" % decr)

And we get the result straight away:

$ python decr.py
[*] Secret length: 29 (0x1d)
[*] Decrypted text: HTB{3r4s3d_fr0m_3x1st3nc3..?}