Skip to main content

DG'hAck 2020: The 'dharma.exe' challenge

Introduction

dharma is a binary. It is required to reverse it, to understand it & retrieve the flag from it. No information is given outside the binary itself:

$ sha256sum dharma
99eebee06f1145bbc16e94afdf523c95035a1ca979a75f48d9d3f19a5cac1645  dharma

Walkthrough

Launching it doesn’t really help:

$ ./dharma 
Password: 
blah
Nope, try again...

No need to say the binary implements some anti-debugging techniques:

$ gdb ./dharma 
... snap ...
Thread 2 "dharma" received signal SIGTRAP, Trace/breakpoint trap.
[Switching to Thread 0x7ffff7d97700 (LWP 1308614)]
0x0000555555555894 in ?? ()
(gdb) bt
#0  0x0000555555555894 in ?? ()
#1  0x00007ffff7f6e432 in start_thread () from /lib64/libpthread.so.0
#2  0x00007ffff7e9c913 in clone () from /lib64/libc.so.6

The most common breakpoint trap is the int3/0xCC opcode trick. With objdump(1), it is quite easy to locate it. We’ll replace it with a NOP:

$ objdump -d dharma|grep -1 int3
    188f:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    1893:       cc                      int3   
    1894:       bf e8 03 00 00          mov    $0x3e8,%edi

$ cp dharma dharma.bak; printf '\x90' | dd of=dharma bs=1 seek=$((0x1893)) conv=notrunc
1+0 records in
1+0 records out
1 byte copied, 3.0021e-05 s, 33.3 kB/s

$ xxd dharma|grep 00001890
00001890: 897d f890 bfe8 0300 00e8 72fa ffff ebf3  .}........r.....

Next issue: Running the new binary will trigger another problem. The binary now receives signal SIG32, Real-time event 32:

(gdb) r
[New Thread 0x7ffff7d97700 (LWP 1310410)]
[New Thread 0x7ffff7596700 (LWP 1310411)]
Password: 

Thread 2 "dharma" received signal SIG32, Real-time event 32.
[Switching to Thread 0x7ffff7d97700 (LWP 1310410)]
0x00007ffff7e63801 in clock_nanosleep@GLIBC_2.2.5 () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff7e63801 in clock_nanosleep@GLIBC_2.2.5 () from /lib64/libc.so.6
#1  0x00007ffff7e69157 in nanosleep () from /lib64/libc.so.6
#2  0x00007ffff7e94869 in usleep () from /lib64/libc.so.6
#3  0x000055555555589e in ?? ()
#4  0x00007ffff7f6e432 in start_thread () from /lib64/libpthread.so.0
#5  0x00007ffff7e9c913 in clone () from /lib64/libc.so.6
(gdb) info threads
  Id   Target Id                                    Frame 
  1    Thread 0x7ffff7d98740 (LWP 1310409) "dharma" 0x00007ffff7e8d4cc in read () from /lib64/libc.so.6
* 2    Thread 0x7ffff7d97700 (LWP 1310410) "dharma" 0x00007ffff7e63801 in clock_nanosleep@GLIBC_2.2.5 () from /lib64/libc.so.6

Okay, that’s it. Checking with radare2:

            ; DATA XREF from main @ 0x18ad
            0x00001883      f30f1efa       endbr64
            0x00001887      55             push rbp
            0x00001888      4889e5         mov rbp, rsp
            0x0000188b      4883ec10       sub rsp, 0x10
            0x0000188f      48897df8       mov qword [rbp - 8], rdi
            ; CODE XREF from rip @ +0x29
        ┌─> 0x00001893      cc             int3
           0x00001894      bfe8030000     mov edi, 0x3e8
           0x00001899      e872faffff     call sym.imp.usleep         ; int usleep(int s)
        └─< 0x0000189e      ebf3           jmp 0x1893
            ; DATA XREF from entry0 @ 0x1341
 86: int main (int argc, char **argv, char **envp);
           0x000018a0      f30f1efa       endbr64
           0x000018a4      55             push rbp
           0x000018a5      4889e5         mov rbp, rsp
           0x000018a8      b900000000     mov ecx, 0
           0x000018ad      488d15cfffff.  lea rdx, [0x00001883]
           0x000018b4      be00000000     mov esi, 0
           0x000018b9      488d3db86900.  lea rdi, [0x00008278]
           0x000018c0      e8ebf8ffff     call sym.imp.pthread_create

Okay, in fact, the function called by pthread_create is basically useless. Let’s just kill it. To do this, I could edit the binary again, but for some reason, I used an old shared library trick to overwrite pthread_create & pthread_cancel functions.

#include <pthread.h>
#include <signal.h>
#define __USE_GNU
#include <dlfcn.h>
#include <stdio.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg)
{
    printf("pthread_create: %p %p %p %p\n", thread, attr, start_routine, arg);
    return 0;
}

int pthread_cancel(pthread_t thread)
{
    return 0;
}

Build & use with:

$ gcc -shared -o fuck.so fuck.c -fPIC
$ export LD_PRELOAD=$(pwd)/fuck.so

Then, I got curious. What’s happening when calling strace(1) ?

... snip ...
memfd_create("2O3naSbh", MFD_CLOEXEC)   = 3
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \22\0\0\0\0\0\0"..., 16936) = 16936
... snip ...
openat(AT_FDCWD, "/proc/1314097/fd/3", O_RDONLY|O_CLOEXEC) = 4
read(4, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \22\0\0\0\0\0\0"..., 832) = 832
fstat(4, {st_mode=S_IFREG|0777, st_size=16936, ...}) = 0
mmap(NULL, 16544, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 4, 0) = 0x7f1b4803a000
mmap(0x7f1b4803b000, 4096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x1000) = 0x7f1b4803b000
mmap(0x7f1b4803c000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x2000) = 0x7f1b4803c000
mmap(0x7f1b4803d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x2000) = 0x7f1b4803d000
close(4)                                = 0
mprotect(0x7f1b4803d000, 4096, PROT_READ) = 0
close(3)                                = 0
write(1, "pthread_create: 0x561c5e63d278 ("..., 58pthread_create: 0x561c5e63d278 (nil) 0x7f1b4803bd64 (nil)
) = 58
write(1, "Password: \n", 11Password: 
)            = 11
fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x4), ...}) = 0

It is even better with ltrace(1):

$ ltrace ./dharma
pthread_create(0x55f1b1ac6278, 0, 0x55f1b1abf883, 0pthread_create: 0x55f1b1ac6278 (nil) 0x55f1b1abf883 (nil)
)                                             = 0
signal(SIGTRAP, 0x55f1b1abf875)                                                                  = nil
memfd_create(0x55f1b1ac6250, 1, 0x7ffcd1f76bb0, 0)                                               = 3
strlen("2O3naSbh")                                                                               = 8
write(3, "\177ELF\002\001\001", 16936)                                                           = 16936
uname(0x7ffcd1f767a0)                                                                            = 0
strtok("5.9.9-100.fc32.x86_64", ".")                                                             = "5"
atoi(0x7ffcd1f76822, 2, 2, 1)                                                                    = 5
atoi(0x7ffcd1f76822, 5, 0, 0x7ffcd1f76823)                                                       = 5
getpid()                                                                                         = 1314298
snprintf("/proc/1314298/fd/3", 1024, "/proc/%d/fd/%d", 1314298, 3)                               = 18
dlopen("/proc/1314298/fd/3", 1)                                                                  = 0x55f1b295a700
close(3)                                                                                         = 0
dlsym(0x55f1b295a700, "init")                                                                    = 0x7f5af81a5c80
dlsym(0x55f1b295a700, "_libc_start_main")                                                        = 0x7f5af81a5d64
pthread_cancel(0, 1, 1, 0)                                                                       = 0
pthread_create(0x55f1b1ac6278, 0, 0x7f5af81a5d64, 0pthread_create: 0x55f1b1ac6278 (nil) 0x7f5af81a5d64 (nil)
)                                             = 0
Password: 

The binary will create a new binary with memfd_create, and load it with dlopen! I didn’t even know this function at the time. Well, let’s “overwrite” it to use open instead in fuck.c:

int memfd_create(const char *name, unsigned int flags)
{
    return open("/tmp/blah", flags | O_CREAT, 0755);
}

This will allow to dump this new binary of 16936 bytes, in /tmp/blah. A quick look with radare2 didn’t help much, so this time I used ghidra:

  undefined8 in_RSI;
  long in_FS_OFFSET;
  ulong local_20;
  int *local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_18 = (int *)(*(code *)ctx)(ctx,in_RSI,ctx);
  puts("Password: ");
  __isoc99_scanf(&DAT_00102105,&local_20);
  if (((long)((long)(int)(uint)in_RSI * (local_20 ^ (uint)in_RSI & 0x42)) / (long)local_18[1] ==
       (long)*local_18) && ((local_20 & 0xffff) == 0xfb4c)) {
    printf("Yes !\nHere is your flag: ");
    print_flag(local_20);
  }
  else {
    puts("Nope, try again...");
  }

We’re near it. Unfortunately, breaking this code won’t help you getting the flag, as it will be computed with the given input. The only way is to get the expected value. We only need to find out the local_18 & in_RSI values, as local_20 is the input (which must be an integer), and make sure it matches the condition. Let’s use gdb & find out the values. ghidra already told us where those values were in memory, so it will be mostly easy:

(gdb) set env LD_PRELOAD=/home/mycroft/dev/dghack/dharma/fuck.so
(gdb) b init
Function "init" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
.. snip ...
0x00007ffff7fbfc80 in init () from /proc/1316873/fd/3
(gdb) disas
Dump of assembler code for function init:
=> 0x00007ffff7fbfc80 <+0>:     endbr64
   0x00007ffff7fbfc84 <+4>:     push   rbp
   0x00007ffff7fbfc85 <+5>:     mov    rbp,rsp
   0x00007ffff7fbfc88 <+8>:     sub    rsp,0x30
   0x00007ffff7fbfc8c <+12>:    mov    QWORD PTR [rbp-0x28],rdi
   0x00007ffff7fbfc90 <+16>:    mov    DWORD PTR [rbp-0x2c],esi
   0x00007ffff7fbfc93 <+19>:    mov    rax,QWORD PTR fs:0x28
   0x00007ffff7fbfc9c <+28>:    mov    QWORD PTR [rbp-0x8],rax
   0x00007ffff7fbfca0 <+32>:    xor    eax,eax
   0x00007ffff7fbfca2 <+34>:    mov    rdx,QWORD PTR [rbp-0x28]
   0x00007ffff7fbfca6 <+38>:    mov    eax,0x0
   0x00007ffff7fbfcab <+43>:    call   rdx
   0x00007ffff7fbfcad <+45>:    mov    QWORD PTR [rbp-0x10],rax
   0x00007ffff7fbfcb1 <+49>:    lea    rdi,[rip+0x451]        # 0x7ffff7fc0109
   0x00007ffff7fbfcb8 <+56>:    call   0x7ffff7fbf140 <puts@plt>
(gdb) b puts
Breakpoint 2 at 0x7ffff7e0e4d0
(gdb) c
Continuing.

Breakpoint 2, 0x00007ffff7e0e4d0 in puts () from /lib64/libc.so.6

(gdb) x/2wx *(long *)($rbp-0x10)
0x55555555c270: 0x6e334f32      0x68625361

(gdb) x/wx $rbp-0x2c
0x7fffffffcd24: 0x00000c80

We have the values. Still need to find the local_20 value. It is just a simple equation: local_18[0] * local_18[1] / in_RS1 = local_20 ^ (uint)in_RSI & 0x42, and local_20 must finished by 0xfb4c:

>>> l = int((0x68625361 * 0x6e334f32) / 0x0c80)
>>> l + (0xfb4c - l & 0xffff)
1011829597993804

and, finally, in the shell:

$ ./dharma
Password: 
1011829597993804
Yes !
Here is your flag: b2d4837830f4853656e993e4561670d0e461471acbb0e2f7183b514b04440d69

What did I learn?

I already knew the 0xCC & the binary encryption technics. I didn’t know about ghidra or radare2 tools. Very impressive what you can do with those today.