gdb vs. ptrace ...... fight!
ptrace
is very commonly used in software protections schemes, because it can be very effective. I see 3 levels of ptrace
based protection, 2 of which can be addressed with gdb
in some way.
1 - a single ptrace call.
This is an almost trivial anti-debugging method. There is a single ptrace
() call in the executable. Consider this detect_ptrace.c
#include <stdio.h>
#include <sys/ptrace.h>
int main() {
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) {
printf("Debugger\n");
} else {
printf("Normal\n");
}
return 0;
}
If the application is being debugged, ptrace
returns -1 and is detected in that way by the above code.
$ gcc detect_ptrace.c
$ chmod +x a.out
$ ./a.out
Normal
$ gdb a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
[ ... ]
(gdb) r
Starting program: /home/jan/Downloads/a.out
Debugger
[Inferior 1 (process 16137) exited normally]
(gdb) quit
$
This is easy to circumvent in gdb:
$ gdb a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
[ ... ]
(gdb) catch syscall ptrace
Catchpoint 1 (syscall 'ptrace' [101])
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set $eax = 0
>continue
>end
(gdb) r
Starting program: /home/jan/Downloads/a.out
Catchpoint 1 (call to syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 ../sysdeps/unix/sysv/linux/ptrace.c: No such file or directory.
Catchpoint 1 (returned from syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 in ../sysdeps/unix/sysv/linux/ptrace.c
Normal
[Inferior 1 (process 16262) exited normally]
(gdb)
What happens here is that catch syscall ptrace
sets a breakpoint on ptrace
. gdb
allows for execution of a list of commands upon entering a breakpoint. Since the catch
command created the first catchpoint, we can enter these commands for catchpoint 1 with commands 1
. set $eax = 0
sets the return value. The ptrace
function call would set $eax
to 0 if successful and -1 if it fails. continue
means just that - continue program execution. end
tells gdb
that we are done entering commands.
Note that we actually break twice, once on entering the ptrace
syscall and once on exit.
2 - multiple ptrace calls in single thread
This is tougher, especially if the code is obfuscated. Some binaries execute ptrace
more than once, checking that the first time the return code is 0 and that future calls are -1. Often this check is obfuscated to various degrees. Consider this detect_ptrace.c
#include <stdio.h>
#include <sys/ptrace.h>
int main()
{
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) // first call
{
printf("Debugger (first check)\n");
}
else
{
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) // second call
{
printf("Normal\n");
}
else
{
printf("Debugger (second check)\n");
}
}
return 0;
}
$ gcc detect_ptrace2.c
jan@jan-HP-ENVY-17-Notebook-PC:~/Downloads$ ./a.out
Normal
jan@jan-HP-ENVY-17-Notebook-PC:~/Downloads$ gdb a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
[ ... ]
(gdb) r
Starting program: /home/jan/Downloads/a.out
Debugger (first check)
[Inferior 1 (process 16818) exited normally]
(gdb) quit
$ gdb a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
[ ... ]
(gdb) catch syscall ptrace
Catchpoint 1 (syscall 'ptrace' [101])
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set $eax = 0
>continue
>end
(gdb) r
Starting program: /home/jan/Downloads/a.out
Catchpoint 1 (call to syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 ../sysdeps/unix/sysv/linux/ptrace.c: No such file or directory.
Catchpoint 1 (returned from syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 in ../sysdeps/unix/sysv/linux/ptrace.c
Catchpoint 1 (call to syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 in ../sysdeps/unix/sysv/linux/ptrace.c
Catchpoint 1 (returned from syscall ptrace), 0x00007ffff7afb93f in ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:45
45 in ../sysdeps/unix/sysv/linux/ptrace.c
Debugger (second check)
[Inferior 1 (process 16856) exited normally]
(gdb)
This can be addressed by first determining the address of the first ptrace call, and then writing a script that checks for that address. Here an example for an executable that has the first ptrace
call at 0x4d55dc
which then proceeds to print out the addresses of all the other ptrace
calls:
catch syscall ptrace
commands 1
if ($rip) == 0x4d55dc
print "yep"
print $rip
set $rax = 0
continue
else
print "nope"
print $rip
set $rax = -1
continue
end
end
In that particular example, I was able to take a list of the addresses where ptrace
was called and tell ghidra
that that was code:
3 - multi-threaded ptrace - self-debugging & nanomites.
Some advanced protection systems fork a thread which then tries to attach to the main thread as a debugger. This will fail if gdb
is already attached as a debugger. And gdb
will not be able to attach to a thread that has done this already. nanomites protection goes one step further, replacing some of the code with instructions that trip the debugger. This debugger then performs some work before returning control to the child. This type of protection needs a more advanced approach, manual unpacking, PANDA
tracing or some such thing. But that is a topic for another time.