11.Locks IsometricPattern

In our previous Hacker's Corner article, we covered some simple anti-debugging. Here, we will see some better techniques.

Self-Modifying Code

Last time we tried to attach to own process, and used that as detection/prevention mechanism for debugging by someone else. Here, we will improve it by making it much harder to run code under debugger. This technique relies on using incorrect code, and patching it dynamically to make it correct. The patching part is done by same ptrace mechanism.

Here, we have to ensure that our incorrect code should not kill the code. Instead we should be able to intercept the error, and then patch the code on the fly before re-attempting the execution. There are many ways to achieve this:

1. Raising a signal like SIGCONT
2. Forcibly triggering CPU fault (like illegal instruction fault)
3. Segmentation Fault
4. and many more...

For sake of example, we will use an illegal syscall (that does not even exist) for our incorrect code. To ensure that we get a signal right before said code is called, we will raise SIGCONT, and capture that. The scheme goes something like this:

Parent Process

1. Fork
2. Run protected function in child process
3. Wait for child process to change its status
4. Continue the process until it hits a syscall.
5. Check if syscall number matches to illegal syscall number used.
6. Change the syscall to correct one.
7. Let the process run. Go back to step 3.

Child Process

1. Attach to self
2. Raise SIGCONT
3. Run the code which is using illegal syscall

The syscall we are going to use is syscall number 10000, which is slightly modified version of **write** syscall:

Original Syscall

- Syscall number: 1
- RAX: 1
- RDI: File descriptor
- RSI: Pointer to buffer
- RDX: Number of bytes

Our Syscall

- Syscall number: 10000
- RAX: 10000
- RDI: Pointer to buffer
- RSI: File descriptor
- RDX: Number of bytes

This means, patching the code is as simple as:

1. Put 1 in RAX
2. Swap RDI and RSI

The complete code for this will look something like this:


#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/user.h>

#define SYS_CUSTOM_write 10000

void print_custom(char *str)
{
    syscall(SYS_CUSTOM_write, str, 1, strlen(str));
}

void tracee()
{
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    raise(SIGCONT);
    
    std::cout << "Try attaching debugger to child process :P" << std::endl;

    for (int i = 0; i < 10; i++)
    {
        print_custom("Try debugging me please.\n");
    }
}

void tracer(pid_t child_pid)
{
    int status;
    waitpid(child_pid, &status, 0);
    
    if (!WIFSTOPPED(status))
    {
        std::cerr<< "Incorrect state." << std::endl;
        return;
    }
    
    ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_EXITKILL);
    struct user_regs_struct regs;
    
    while (WIFSTOPPED(status))
    {
        ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
        waitpid(child_pid, &status, 0);
        ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
        
        if (regs.orig_rax == SYS_CUSTOM_write)
        {
            regs.orig_rax = SYS_write;
            //swap arg1 & arg2
            unsigned long long int orig_rdi = regs.rdi;
            regs.rdi = regs.rsi;
            regs.rsi = orig_rdi;

            ptrace(PTRACE_SETREGS, child_pid, 0, &regs);
        }
        ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
        waitpid(child_pid, &status, 0);
    }
}

int main() 
{
    std::cout << "Linux Anti-debugging Demo" << std::endl;
    pid_t child_pid = fork();

    if (child_pid < 0)
    {
        printf("Fork failed.\n");
        return 1;
    }
    if (child_pid == 0)
    {
        tracee();
    } else
    {
        tracer(child_pid);
    }
    return 0;
}

Now what happens when we try to run this code? When we run without debugger, it works as expected. If you run it under debugger, it will still seem to run:


$ gdb -q ./ptrace3                                                                                                                                                                                             
Reading symbols from ./ptrace3...
(gdb) run
Starting program: /home/adhokshajmishra/ptrace3 
Linux Anti-debugging Demo
[Detaching after fork from child process 66719]
Try attaching debugger to child process :P
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
Try debugging me please.
[Inferior 1 (process 66715) exited normally]
(gdb) 

What went wrong? Did you notice that debugger did not attach to child process after fork: **[Detaching after fork from child process 66719]**. To actually debug the child process, we have to use:

**set follow-fork-mode child**

When the debugger attaches to child, the loop with illegal syscall does not get fixed:


$ gdb -q ./ptrace3                                                                                                                                                                                             
Reading symbols from ./ptrace3... 
(gdb) set follow-fork-mode child
(gdb) run
Starting program: /home/adhokshajmishra/course/antireverse/cmake-build-debug/ptrace3 
Linux Anti-debugging Demo
[Attaching after process 67295 fork to child process 67299]
[New inferior 2 (process 67299)]
[Detaching after fork from parent process 67295]
[Inferior 1 (process 67295) detached]

Thread 2.1 "ptrace3" received signal SIGCONT, Continued.
[Switching to process 67299]
0x00007ffff7ace615 in raise () from /usr/lib/libc.so.6
──── assembly ────
   0x7ffff7ace609 $<raise+313$>   mov    edi, 0x2
   0x7ffff7ace60e $<raise+318>    mov    eax, 0xe
   0x7ffff7ace613 $<raise+323>    syscall 
 → 0x7ffff7ace615 $<raise+325>    mov    rax, QWORD PTR [rsp+0x108]
   0x7ffff7ace61d $<raise+333>    sub    rax, QWORD PTR fs:0x28
   0x7ffff7ace626 $<raise+342>    jne    0x7ffff7ace64c <raise+380>
   0x7ffff7ace628 $<raise+344>    mov    eax, r8d
   0x7ffff7ace62b $<raise+347>    add    rsp, 0x118
   0x7ffff7ace632 $<raise+354>    ret    
──── threads ────
[#0] Id 1, Name: "ptrace3", stopped 0x7ffff7ace615 in raise (), reason: SIGCONT
──── trace ────
[#0] 0x7ffff7ace615 → raise()
[#1] 0x555555555255 → tracee()
[#2] 0x5555555554b8 → main()
(gdb) continue 
Continuing.
Try attaching debugger to child process :P
[Inferior 2 (process 67299) exited normally]
Incorrect state.
(gdb)

Here, instead of parent process getting the chance to handle the signal, debugger got it, as it attached to child process right after fork (before ptrace to self could happen). Even though we continued execution, our child process did not behave as expected. By attaching a debugger, we managed to change the behaviour of the process.

Exercise for You

Can you find a way to get out of debugger's grip? In other words, is there a way to force debugger to detach from your process irrespective of fork mode being used?

About the Author

Adhokshaj Mishra works as a security researcher (malware - Linux) at Uptycs. His interest lies in offensive and defensive side of Linux malware research. He has been working on attacks related to containers, kubernetes; and various techniques to write better malware targeting Linux platform. In his free time, he loves to dabble into applied cryptography, and present his work in various security meetups and conferences.