A walk outside the sandbox

Home Blog Cheat Sheets MacOS Tips Area 51 About

[CTF] Untraceable (PTRACE_TRACEME anti-anti-debug)


Understanding the challenge

This is a simple challenge but has interesting lessons to teach us. It starts with a a binary that is asking for a password:

$ ./untraceable 
What is the password to the archive? HelloWorld
Intruder detected!

If we look at the Ghidra’s decompiled code, we note a few interesting things:

undefined8 main(void)
  int iVar1;
  long lVar2;
  size_t sVar3;
  undefined8 uStack_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined4 local_20;
  undefined2 local_1c;
  undefined local_1a;
  uint local_c;
  local_38 = 0x6365537265707553;
  local_30 = 0x7773736150746572;
  local_28 = 0x6f4e6f442d64726f;
  local_20 = 0x61655274;
  local_1c = 0x2164;
  local_1a = 0;
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  uStack_60 = 0x101217;
  lVar2 = ptrace(PTRACE_TRACEME);
  if (lVar2 == -1) {
    uStack_60 = 0x101229;
    puts("Tampering detected!");
    uStack_60 = 0x101233;
  uStack_60 = 0x101244;
  printf("What is the password to the archive? ");
  uStack_60 = 0x10125c;
  fgets((char *)&local_58,0x20,stdin);
  if ((char)local_58 != '\0') {
    uStack_60 = 0x101273;
    sVar3 = strlen((char *)&local_58);
    *(undefined *)((long)&uStack_60 + sVar3 + 7) = 0;
  uStack_60 = 0x101294;
  iVar1 = strncmp((char *)&local_38,(char *)&local_58,0x1f);
  if (iVar1 == 0) {
  for (local_c = 0; local_c < 0x1d; local_c = local_c + 1) {
      uStack_60 = 0x1012ca;
      putchar((int)*(char *)((long)&local_38 + (long)(int)local_c) ^
    uStack_60 = 0x1012e2;
  else {
    uStack_60 = 0x1012f0;
    puts("Intruder detected!");
  return 0;

Observations at first sight:

  • There are a few interesting variables on the stack that seem to contain ASCII values.
  • The binary is using the ptrace(PTRACE_TRACEME) anti-debugging technique.
  • Towards the end of the program there is a XOR (possibly decryption) loop.

Each of these observation will lead to a separete method to solve the challenge.


Method 1 - Hardcoded password

The variables on the stack represent the password required to “unlock the archive”. After converting from hex to ASCII, we get the following very well hidden password: SuperSecretPassword-DoNotRead! With this, it’s very easy to get the flag:

What is the password to the archive? SuperSecretPassword-DoNotRead!

Method 2 - Bypass PTRACE anti-debugging

The idea behind the PTRACE anti-debugging trick is that a process can only be traced by one other process. If we call ptrace ourselves, other programs cannot debug through ptrace or inject code into our program. If the program is currently being debugged by gdb, the ptrace function will return an error, which indicates that the debugger is detected.

A quick way to workaround this trick is to create a library with a custom ptrace function, then use the LD_PRELOAD environment variable to point the executable to our own custom ptrace function. The C code looks like this:

// gcc -shared ptrace.c -o

#include <stdio.h>

int ptrace(int i, int j, int k, int l) {
    printf(" PTRACE CALLED!\n");

Compile, set the environment variable in GDB and happy debugging!

$ gcc -shared ptrace.c -o

$ gdb ./untraceable

gef➤  set environment LD_PRELOAD ./

gef➤  r
Starting program: /mnt/hgfs/CTF-apr-23/rev_untraceable/untraceable 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/".
What is the password to the archive?  

Now that we’re able to debug our program, we can simply set a breakpoint before the comparison of the provided password with the hardcoded value (the strncmp call): GDB

Method 3 - XOR decryption

The third method to solve this challenge is based on the decryption loop, which we can easily reproduce in a separate Python script, after extracting the encrypted flag from DAT_00104070 location:

enc = [
    0x1b, 0x21, 0x32, 0x1e, 0x42, 0x3f, 0x01, 0x50,
    0x01, 0x11, 0x2b, 0x24, 0x13, 0x42, 0x10, 0x1c,
    0x30, 0x43, 0x0a, 0x72, 0x30, 0x07, 0x7d, 0x30, 
    0x16, 0x62, 0x55, 0x0a, 0x19

password = "SuperSecretPassword-DoNotRead!"

print("[*] Encrypted data length: %d (0x%02x)" % (len(enc), len(enc)))

dec = "".join([chr(enc[i] ^ ord(password[i])) for i in range(len(enc))])
print (dec)

And get the flag:

$ python 
[*] Encrypted data length: 29 (0x1d)