I am debugging an app that presumably has anti debugging measures, setting up breakpoints and signals stops for quitting the app doesn't stop the app from exiting,
$ lldb App
(lldb) target create "App"
error: Invalid fde/cie next entry offset of 0x43029a18 found in cie/fde at 0x1404
Current executable set to 'App' (x86_64).
(lldb) br s -n exit
Breakpoint 1: 3 locations.
(lldb) br s -n _exit
Breakpoint 2: where = libsystem_kernel.dylib`__exit, address = 0x00000000000167a8
(lldb) br s -n _Exit
Breakpoint 3: where = libsystem_c.dylib`_Exit, address = 0x000000000005ed8b
(lldb) process launch -stop-at-entry
Process 17849 stopped
* thread #1: tid = 0xb9ebc, 0x00007fff5fc01000 dyld`_dyld_start, stop reason = signal SIGSTOP
frame #0: 0x00007fff5fc01000 dyld`_dyld_start
dyld`_dyld_start:
-> 0x7fff5fc01000 <+0>: popq %rdi
0x7fff5fc01001 <+1>: pushq $0x0
0x7fff5fc01003 <+3>: movq %rsp, %rbp
0x7fff5fc01006 <+6>: andq $-0x10, %rsp
Process 17849 launched: '/Users/admin/Downloads/App.app/Contents/MacOS/App' (x86_64)
(lldb) process handle -p false -s true
Do you really want to update all the signals?: [y/N] y
NAME PASS STOP NOTIFY
=========== ===== ===== ======
SIGHUP false true true
... [removed for brevity]
(lldb) c
Process 17849 resuming
Process 17849 exited with status = 45 (0x0000002d)
(lldb)
How is the app able to exit without triggering any signal, exit, _exit, or _Exit?
Is there a way in lldb to run the process, and upon exit then 'backtrack' to see where it exited?
Is there a way for lldb to log each assembly instruction etc (like when it breaks) so you can trace it back upon exit?
For those interested, a different take on this answer can be found here.
Most likely you are dealing with an anti-debug technique like this one:
ptrace(PT_DENY_ATTACH, 0, NULL, 0);
The basic idea is that only one process can ptrace
another at the same time, in particular the PT_DENY_ATTACH
option makes sure that the tracee exits with the ENOTSUP
(45) status. See man ptrace
about PT_DENY_ATTACH
:
This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.
For what concerns the 45, take a look at /System/Library/Frameworks/Kernel.framework/Versions/A/Headers/sys/errno.h
:
#define ENOTSUP 45 /* Operation not supported */
It is trivial to write a program that exhibits the same behavior:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ptrace.h>
int main() {
printf("--- before ptrace()\n");
ptrace(PT_DENY_ATTACH, 0, NULL, 0);
perror("--- ptrace()");
printf("--- after ptrace()\n");
return 0;
}
Compile with:
clang -Wall -pedantic ptrace.c -o ptrace
Simply running it will exit successfully, but trying to debug it will yield the following result:
(lldb) r
Process 4188 launched: './ptrace' (x86_64)
--- before ptrace()
Process 4188 exited with status = 45 (0x0000002d)
Since this example is pretty small it is possible to step until the syscall
instruction:
(lldb) disassemble
libsystem_kernel.dylib`__ptrace:
0x7fff6ea1900c <+0>: xorq %rax, %rax
0x7fff6ea1900f <+3>: leaq 0x394f12f2(%rip), %r11 ; errno
0x7fff6ea19016 <+10>: movl %eax, (%r11)
0x7fff6ea19019 <+13>: movl $0x200001a, %eax ; imm = 0x200001A
0x7fff6ea1901e <+18>: movq %rcx, %r10
-> 0x7fff6ea19021 <+21>: syscall
0x7fff6ea19023 <+23>: jae 0x7fff6ea1902d ; <+33>
0x7fff6ea19025 <+25>: movq %rax, %rdi
0x7fff6ea19028 <+28>: jmp 0x7fff6ea10791 ; cerror
0x7fff6ea1902d <+33>: retq
0x7fff6ea1902e <+34>: nop
0x7fff6ea1902f <+35>: nop
(lldb) s
Process 3170 exited with status = 45 (0x0000002d)
So it is the kernel code that kills the process, but without a signal or a proper exit
syscall. (TIL this and it still blows my mind.)
Which syscall is executed is determined by the value of the EAX
register, in this case 0x200001A
which it may seem strange because the ptrace
syscall number is just 26 (0x1a
), see syscalls.master
:
26 AUE_PTRACE ALL { int ptrace(int req, pid_t pid, caddr_t addr, int data); }
After some digging I come up with syscall_sw.h
:
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
Doing the math the result is 0x200001A
dtruss
not trace the ptrace
syscall?Using dtruss
seems like a good idea, unfortunately it does not report the ptrace
syscall (my understanding is that it fails to do that since the ptrace
syscall does not returns in this case).
Fortunately you can write a DTrace script to log a syscall once it is entered (i.e., not after it returns). To trigger the behavior, the program must be started from lldb
:
$ lldb ./ptrace
(lldb) process launch --stop-at-entry
Note the PID then:
sudo dtrace -q -n 'syscall:::entry /pid == $target/ { printf("syscall> %s\n", probefunc); }' -p $PID
Finally continue
in lldb
, the result should be:
[...]
syscall> sysctl
syscall> csops
syscall> getrlimit
syscall> fstat64
syscall> ioctl
syscall> write_nocancel
syscall> ptrace
Now it would be nice to break just before the ptrace
syscall and find the program code that calls it or just skip it for the current debugging session (LLDB: thread jump -a ADDRESS
).
Of course one could attempt to break on the ptrace
library call, but if this is really and anti-debug attempt chances are that the actual call is performed in an asm
block, thus the above breakpoint would never trigger.
A possible solution could be to use DTrace to place a breakpoint before the syscall but this requires to have the System Integrity Protection disabled so I didn't try.
Alternatively one could print the userland stacktrace with the ustack()
function:
sudo dtrace -q -n 'syscall:::entry /pid == $target && probefunc == "ptrace"/ { ustack(); }' -p $PID
User contributions licensed under CC BY-SA 3.0