LLDB查找应用程序退出点

4

我正在调试一个应用程序,这个应用程序很可能设置了反调试措施。设置断点和信号以停止应用程序退出,但应用程序仍然会退出。

$ 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)

这个应用是如何在不触发任何信号、退出(exit)、_exit或_Exit的情况下退出的呢?

在lldb中有没有一种方法可以运行进程,在退出后'回溯(backtrack)'以查看它退出的位置?

在lldb中是否有一种方法可以记录每条汇编指令等(例如当它被打断时),以便在退出时追踪它?


1
它可以直接调用退出系统调用。 - Jester
@Jester说得好; 而且似乎lldb(不像gdb)无法捕获系统调用。 (请参见https://dev59.com/NITba4cB1Zd3GeqP1BQk) - Zimm3r
至少对于查看系统调用,我在另一个终端中运行了sudo dtruss -p [pid],在lldb中运行process launch -stop-at-entry后查看系统调用,但遗憾的是没有进行退出系统调用。 - Zimm3r
lldb和dtruss如果同时使用,很可能会发生冲突。 - Jester
@Jester,好的,我会尝试单独运行dtruss并查看是否有所改变。感谢您提供的所有新信息!学到了很多。 - Zimm3r
1个回答

13

对于那些有兴趣的人,可以在 这里 找到一个不同的解决方案。


这里发生了什么?

很可能你正在处理像这样的反调试技术:

ptrace(PT_DENY_ATTACH, 0, NULL, 0);

基本思想是同时只有一个进程可以ptrace另一个进程,特别是PT_DENY_ATTACH选项确保被跟踪的进程以ENOTSUP(45)状态退出。关于PT_DENY_ATTACH,请参阅man ptrace

该请求是被跟踪进程使用的另一种操作;它允许当前未被跟踪的进程拒绝其父进程对其进行跟踪。所有其他参数都将被忽略。如果进程当前正在被跟踪,则它将以ENOTSUP的退出状态退出;否则,它将设置一个拒绝未来跟踪的标志。父进程试图跟踪已设置此标志的进程将导致父进程发生分段违例。

至于45,可以查看/System/Library/Frameworks/Kernel.framework/Versions/A/Headers/sys/errno.h

#define ENOTSUP     45      /* Operation not supported */

如何复现这个问题?

编写一个程序以展示相同的行为是微不足道的:

#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;
}

使用以下方式进行编译:

clang -Wall -pedantic ptrace.c -o ptrace

只是运行它会顺利退出,但尝试调试它将产生以下结果:

(lldb) r
Process 4188 launched: './ptrace' (x86_64)
--- before ptrace()
Process 4188 exited with status = 45 (0x0000002d)

由于此示例非常小,因此可以一直运行到syscall指令:

(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)

因此,正是内核代码在没有信号或适当的退出系统调用的情况下“杀死”进程。(我今天学到了这个知识点,仍然感到惊奇。)

执行的系统调用由EAX寄存器的值确定,在本例中为0x200001A,这似乎很奇怪,因为ptrace系统调用号只有26(0x1a),请参见syscalls.master

26  AUE_PTRACE  ALL { int ptrace(int req, pid_t pid, caddr_t addr, int data); }

经过一番搜索,我找到了syscall_sw.h

#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
            ((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))

做完数学运算后,结果为0x200001A

dtruss为什么不能跟踪ptrace系统调用?

使用dtruss似乎是个好主意,但它不会报告ptrace系统调用(我的理解是因为在这种情况下ptrace系统调用不会返回)。

幸运的是,您可以编写一个DTrace脚本来记录一次系统调用进入时的信息(即不是在其返回后)。要触发此行为,必须从lldb启动程序:

$ lldb ./ptrace
(lldb) process launch --stop-at-entry

注意PID:

sudo dtrace -q -n 'syscall:::entry /pid == $target/ { printf("syscall> %s\n", probefunc); }' -p $PID

最后,在lldb中使用continue,结果应该是:

[...]
syscall> sysctl
syscall> csops
syscall> getrlimit
syscall> fstat64
syscall> ioctl
syscall> write_nocancel
syscall> ptrace

可能的解决方案

现在最好在ptrace系统调用之前中断,并找到调用它的程序代码,或者只是跳过当前调试会话(LLDB:thread jump -a ADDRESS)。

当然,可以尝试在ptrace库调用上中断,但如果这真的是一种反调试尝试,那么实际调用可能是在一个asm块中执行,因此上述断点永远不会被触发。

一个可能的解决方案是使用DTrace在系统调用之前放置一个断点,但这需要禁用系统完整性保护,所以我没有尝试。

或者,您可以使用ustack()函数打印用户空间堆栈跟踪:

sudo dtrace -q -n 'syscall:::entry /pid == $target && probefunc == "ptrace"/ { ustack(); }' -p $PID

1
哇,写得非常详细。我一定会看看这是否是我所经历的,并希望能回报告。 - Zimm3r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接