Peterpan0927

how to reach the steins gate?

反动态调试保护的实现和破解

  |  
 阅读次数

之前在一章博客中提到了关于反动态调试保护的一些基础知识,也就是ptrace系统调用,那么我们就用实力来看看是如何实现还有如何去绕开的。

反调试

反调试从逻辑上分大概分为, 一种是直接屏蔽调试器挂载, 另一种就是根据特征手动检测调试器挂载. 当然也分为使用函数实现 和 直接使用内联 asm 实现.

  1. ptrace

我们知道ptrace是一个可以对运行中的进程进行跟踪和控制的手段,但是还有一个参数大部分人都没有注意,那就是PT_DENY_ATTACH,这个参数的意义就是告诉系统阻止调试器的调试,所以最常见的方法就是这种,这次我们的绕开示例也是从这个角度来下手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import<mach-o/dyld.h>
#import<dlfcn.h>
#ifndef PT_DENY_ATTACH
true#define PT_DENY_ATTACH 31
#endif
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid,caddr_t _addr, int _data);
void disable_gdb(){
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
trueptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(handle, "ptrace");
trueptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
int main(){
@autoreleasepool{
//DEBUG代表是测试版而不是发行版,所以不需要开启反调试
#ifdef DEBUG
//do nothing
#else
disable_gdb();
#endif
}
}

我们可以将这个写在main函数中,用来防止我们的程序被调试,相当于向ptrace它的父进程发送消息说不要再跟踪我了,如果对这里有什么不明白的可以参考它的前置章节ptrace基础

屏幕快照 2018-04-17 下午3.32.23.png

从man手册中我们看到,使用PT_DENY_ATTACH的时候,其他的参数都会被忽略,因为这个是由被追踪进程发送的信号

还有一个我们会发现就是我们没有直接使用ptrace,而是通过另一种方式,这是因为在iPhone的真实运行环境中是没有sys/ptrace抛出的,所以我们可以使用dlopen去拿到他,然后再操作

在我们做了反调试之后再使用gdb的时候就会发生段错误了:

屏幕快照 2018-04-19 上午11.52.57.png

说明我的操作已经成功。

  1. sysctl

这本来是一个在内核运行时动态修改内核的运行参数的命令,我们可以用它来查看进程的信息,也就是说我们可以发现这个进程是否处于调试状态。当进程处于调试状态的时候info.kp_proc.p_flag就会变成1,所以就可以通过sysctl查询进程的kinfo_proc信息就可以判断了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>
static bool AmIBeingDebugged(void)
// Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto).
{
int junk;
int mib[4];
struct kinfo_proc info;
size_t size;
// Initialize the flags so that, if sysctl fails for some bizarre
// reason, we get a predictable result.
info.kp_proc.p_flag = 0;
// Initialize mib, which tells sysctl the info we want, in this case
// we're looking for information about a specific process ID.
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
// Call sysctl.
size = sizeof(info);
junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
assert(junk == 0);
// We're being debugged if the P_TRACED flag is set.
true
return ( (info.kp_proc.p_flag & P_TRACED) != 0 );
}
  1. syscall

这个是利用一个内核调用表找到ptrace所对应的编号,然后直接将编号作为参数使用系统调用,效果等同于ptrace:

1
syscall(26,31,0,0)
  1. arm

直接使用汇编来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifdef __arm__
trueasm volatile(
"mov r0,#31\n"
"mov r1,#0\n"
"mov r2,#26\n"
"mov r12,#26\n"
"svc #80\n"
true);
#endif
#ifdef __arm64__
trueasm volatile(
truetrue"mov x0,#26\n"
truetrue"mov x1,#31\n"
truetrue"mov x2,#0\n"
truetrue"mov x3,#0\n"
truetrue"mov x16,#0\n"
truetrue"svc #128\n"
true);
  1. 内联svc+ptrace实现
1
2
3
4
5
6
7
8
9
10
11
static __attribute__((always_inline)) void AntiDebug_003() {
#ifdef __arm64__
__asm__("mov X0, #31\n"
"mov X1, #0\n"
"mov X2, #0\n"
"mov X3, #0\n"
"mov w16, #26\n"
"svc #0x80");
#endif
}

反调试检测

除了屏蔽调试器挂载之外,我们也可以主动去检测调试器并退出程序

这里主要是调试器的检测手段, 很多检测到调试器后使用 exit(-1) 退出程序. 这里很容易让 cracker 断点到 exit 函数上. 其实有一个 trick 就是利用利用系统异常造成 crash. 比如: 覆盖/重写 __TEXT 内容(debugmode 模式下可以对 rx- 内存进行操作).

或者利用内联汇编实现退出, 并清除堆栈(防止暴力 svc patch with nop).

1
2
3
4
5
6
7
8
9
10
11
12
13
static __attribute__((always_inline)) void asm_exit() {
#ifdef __arm64__
__asm__("mov X0, #0\n"
"mov w16, #1\n"
"svc #0x80\n"
"mov x1, #0\n"
"mov sp, x1\n"
"mov x29, x1\n"
"mov x30, x1\n"
"ret");
#endif
}

使用 sysctl 检测

这里在检测时也可以通过 svc 实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static int DetectDebug_sysctl() __attribute__((always_inline));
int DetectDebug_sysctl() {
size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc info;
int ret, name[4];
memset(&info, 0, sizeof(struct kinfo_proc));
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();
#if 0
if ((ret = (sysctl(name, 4, &info, &size, NULL, 0)))) {
return ret; // sysctl() failed for some reason
}
#else
// or change as `AntiDebug_003` and `AntiDebug_004`
// https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
__asm__ volatile("mov x0, %[name_ptr]\n"
"mov x1, #4\n"
"mov x2, %[info_ptr]\n"
"mov x3, %[size_ptr]\n"
"mov x4, #0\n"
"mov x5, #0\n"
"mov w16, #202\n"
"svc #0x80"
:
: [name_ptr] "r"(name), [info_ptr] "r"(&info),
[size_ptr] "r"(&size));
#endif
return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}
void AntiDebug_006() {
if (DetectDebug_sysctl()) {
asm_exit();
}
}

使用 isatty 检测

1
2
3
4
5
6
7
#include <unistd.h>
void AntiDebug_isatty() {
if (isatty(1)) {
exit(1);
} else {
}
}

使用 ioctl 检测

1
2
3
4
5
6
7
#include <sys/ioctl.h>
void AntiDebug_ioctl() {
if (!ioctl(1, TIOCGWINSZ)) {
exit(1);
} else {
}
}

svc 完整性检测

上述的 svc 反调试手段, 可以通过 patch svc #0x80 with nop 轻松绕过. 所以需要校验 svc #0x80 是否被 patch, 一个想当然的方法是在正常的代码中使用 svc 进行 coding, 仔细想想并不合适.

所以另一个想法就是, 使用 svc 实现一个小功能, 之后检测 x0 返回值. 这里使用的是 getpid().

tips: longjmp 本来是用在异常时恢复状态, 这里由于未保存状态. 所以可以让攻击者不能对退出进行断点.

这里使用, 下面一小段内联汇编可以达到相同的目的.

1
2
3
4
5
"mov x1, #0\n"
"mov sp, x1\n"
"mov x29, x1\n"
"mov x30, x1\n"
"ret\n"

整体的 svc 完整检测原型如下, 仅做抛砖引玉.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static __attribute__((always_inline)) void check_svc_integrity() {
int pid;
static jmp_buf protectionJMP;
#ifdef __arm64__
__asm__("mov x0, #0\n"
"mov w16, #20\n"
"svc #0x80\n"
"cmp x0, #0\n"
"b.ne #24\n"
"mov x1, #0\n"
"mov sp, x1\n"
"mov x29, x1\n"
"mov x30, x1\n"
"ret\n"
"mov %[result], x0\n"
: [result] "=r" (pid)
:
:
);
if(pid == 0) {
longjmp(protectionJMP, 1);
}
#endif
}

打破反调试

除了下面的手段也可以使用HookZz框架,参见第二个链接,在此就不废话了

只要对对应的方法进行hook就好了,在这里我们使用MSHookFunction而不采用封装好的上层Logos语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#import <substrate.h>
#import <sys/sysctl.h>
static int (*orig_ptrace)(int request, pid_t pid, caddr_t addr, int data);
static int my_ptrace(int request, pid_t pid, caddr_t addr, int data){
if(request == 31){
NSLog(@"[AntiAntiDebug] - ptrace is PT_DENY_ATTACH");
return 0;
}
return orig_ptrace(request,pid,addr,data);
}
static void* (*orig_dlsym)(void* handle, const char* symbol);
static void* my_dlsym(void* handle, const char* symbol){
trueif(strcmp(symbol, "ptrace") == 0){
truetrueNSLog(@"[AntiAntiDebug] - dlsym get ptrace symbol");
truetruereturn (void*)my_ptrace;
}
return orig_dlsym(handle, symbol);
}
static int (*orig_sysctl)(int * name, u_int namelen, void * info, size_t * infosize, void * newinfo, size_t newinfosize);
static int my_sysctl(int * name, u_int namelen, void * info, size_t * infosize, void * newinfo, size_t newinfosize){
trueint ret = orig_sysctl(name,namelen,info,infosize,newinfo,newinfosize);
trueif(namelen == 4 && name[0] == 1 && name[1] == 14 && name[2] == 1){
truetruestruct kinfo_proc *info_ptr = (struct kinfo_proc *)info;
if(info_ptr && (info_ptr->kp_proc.p_flag & P_TRACED) != 0){
NSLog(@"[AntiAntiDebug] - sysctl query trace status.");
info_ptr->kp_proc.p_flag ^= P_TRACED;
if((info_ptr->kp_proc.p_flag & P_TRACED) == 0){
NSLog(@"[AntiAntiDebug] trace status reomve success!");
}
}
true}
truereturn ret;
}
static void* (*orig_syscall)(int code, va_list args);
static void* my_syscall(int code, va_list args){
trueint request;
va_list newArgs;
va_copy(newArgs, args);
if(code == 26){
request = (long)args;
if(request == 31){
NSLog(@"[AntiAntiDebug] - syscall call ptrace, and request is PT_DENY_ATTACH");
return nil;
}
}
return (void*)orig_syscall(code, newArgs);
}
%ctor{
trueMSHookFunction((void *)MSFindSymbol(NULL,"_ptrace"),(void*)my_ptrace,(void**)&orig_ptrace);
trueMSHookFunction((void *)dlsym,(void*)my_dlsym,(void**)&orig_dlsym);
trueMSHookFunction((void *)sysctl,(void*)my_sysctl,(void**)&orig_sysctl);
trueMSHookFunction((void *)syscall,(void*)my_syscall,(void**)&orig_syscall);
trueNSLog(@"[AntiAntiDebug] Module loaded!!!");
}

然后我们去看日志就知道到底是什么保护了。

除了编写tweak之外我们还可以通过lldb下断点的情况来解决这个问题,我们可以通过debugserver连接然后找到ptrace(或者是其他的方法)调用的位置,然后在汇编代码中找到对应的位置,然后直接返回或者修改都可以,但是每次都这样子太麻烦了,所以我们可以用python的自动化脚本来封装一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
反反调试脚本,过了反调试后记得:
aadebug -d
否则会很卡,如果有定时器定时检测,建议写tweak
"""
import lldb
import fblldbbase as fb
import fblldbobjcruntimehelpers as objc
def lldbcommands():
return [
AMAntiAntiDebug()
]
class AMAntiAntiDebug(fb.FBCommand):
def name(self):
return 'aadebug'
def description(self):
return "anti anti debug ptrace syscall sysctl"
def options(self):
return [
fb.FBCommandArgument(short='-d', long='--disable', arg='disable', boolean=True, default=False, help='disable anti anti debug.')
]
def run(self, arguments, options):
if options.disable:
target = lldb.debugger.GetSelectedTarget()
target.BreakpointDelete(self.ptrace.id)
target.BreakpointDelete(self.syscall.id)
target.BreakpointDelete(self.sysctl.id)
print "anti anti debug is disabled!!!"
else:
self.antiPtrace()
self.antiSyscall()
self.antiSysctl()
print "anti anti debug finished!!!"
def antiPtrace(self):
ptrace = lldb.debugger.GetSelectedTarget().BreakpointCreateByName("ptrace")
if is64Bit():
ptrace.SetCondition('$x0==31')
else:
ptrace.SetCondition('$r0==31')
ptrace.SetScriptCallbackFunction('sys.modules[\'' + __name__ + '\'].ptrace_callback')
self.ptrace = ptrace
def antiSyscall(self):
syscall = lldb.debugger.GetSelectedTarget().BreakpointCreateByName("syscall")
if is64Bit():
syscall.SetCondition('$x0==26 && *(int *)$sp==31')
else:
syscall.SetCondition('$r0==26 && $r1==31')
syscall.SetScriptCallbackFunction('sys.modules[\'' + __name__ + '\'].syscall_callback')
self.syscall = syscall
def antiSysctl(self):
sysctl = lldb.debugger.GetSelectedTarget().BreakpointCreateByName("sysctl")
if is64Bit():
sysctl.SetCondition('$x1==4 && *(int *)$x0==1 && *(int *)($x0+4)==14 && *(int *)($x0+8)==1')
else:
sysctl.SetCondition('$r1==4 && *(int *)$r0==1 && *(int *)($r0+4)==14 && *(int *)($r0+8)==1')
sysctl.SetScriptCallbackFunction('sys.modules[\'' + __name__ + '\'].sysctl_callback')
self.sysctl = sysctl
def antiExit(self):
self.exit = lldb.debugger.GetSelectedTarget().BreakpointCreateByName("exit")
exit.SetScriptCallbackFunction('sys.modules[\'' + __name__ + '\'].exit_callback')
#暂时只考虑armv7和arm64
def is64Bit():
arch = objc.currentArch()
if arch == "arm64":
return True
return False
def ptrace_callback(frame, bp_loc, internal_dict):
print "find ptrace"
register = "x0"
if not is64Bit():
register = "r0"
frame.FindRegister(register).value = "0"
lldb.debugger.HandleCommand('continue')
def syscall_callback(frame, bp_loc, internal_dict):
print "find syscall"
#不知道怎么用api修改sp指向的内容QAQ
lldb.debugger.GetSelectedTarget().GetProcess().SetSelectedThread(frame.GetThread())
if is64Bit():
lldb.debugger.HandleCommand('memory write "$sp" 0')
else:
lldb.debugger.HandleCommand('register write $r1 0')
lldb.debugger.HandleCommand('continue')
def sysctl_callback(frame, bp_loc, internal_dict):
module = frame.GetThread().GetFrameAtIndex(1).GetModule()
currentModule = lldb.debugger.GetSelectedTarget().GetModuleAtIndex(0)
if module == currentModule:
print "find sysctl"
register = "x2"
if not is64Bit():
register = "r2"
frame.FindRegister(register).value = "0"
lldb.debugger.HandleCommand('continue')
def exit_callback(frame, bp_loc, internal_dict):
print "find exit"
lldb.debugger.GetSelectedTarget().GetProcess().SetSelectedThread(frame.GetThread())
lldb.debugger.HandleCommand('thread return')
lldb.debugger.HandleCommand('continue')

参考博客

关于反调试那些事

反调试及其绕过