lldb finding exit point of app


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
->  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
===========  =====  =====  ======
SIGHUP       false  true   true 
... [removed for brevity]
(lldb) c
Process 17849 resuming
Process 17849 exited with status = 45 (0x0000002d) 

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?

asked on Stack Overflow Mar 26, 2016 by Zimm3r

1 Answer


For those interested, a different take on this answer can be found here.

What happens 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 */

How to reproduce this?

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
    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_NUMBER_MASK & (syscall_number)))

Doing the math the result is 0x200001A

Why does 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

Possible solutions

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
answered on Stack Overflow Dec 11, 2017 by cYrus • edited Oct 18, 2018 by valiano

User contributions licensed under CC BY-SA 3.0