feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-15 20:03:01 +08:00
parent 13f7457be9
commit 83fefff66b
109 changed files with 2549 additions and 672 deletions

View File

@@ -1,25 +1,441 @@
# 守护你的App安全
App Crash 会严重影响用户体验Crash 率和客户端工程师的个人评级和绩效考核挂钩。因此写出的代码必须安全可靠。崩溃率要保持在一个什么样的水平以下
App 在不断业务迭代,经过多人开发维护后可能因为开发者的水平层次不齐,造成代码质量较低,所以要实现的效果就是保证 App 稳健运行,常见的问题捕获处理,将造成奔溃或者 Crash 的因素处理掉,让 App 正常运行。
## 功能设想
对业务代码零侵入性地将原本会导致 App 奔溃的 crash 信息处理掉,保证 App 正常稳健运行,再将 Crash 信息提取出来呈现给开发者。开发者可以根据相应的 Crash 信息去处理解决对应的代码。
> 从 Web 安全一样,所有的攻防离不开一句话“在合理范围内保证 App 安全,让攻击者增加破解成本,让一部分人三思而后行战术性放弃”
## ptrace 简易版本
在iOS系统中`ptrace` 被用于防止应用程序被调试。`ptrace` 函数提供了一种机制允许一个进程监听和控制另一个进程并且可以检测被控制进程的内存和寄存器中的数据。在iOS开发中`ptrace` 可以用于实现断点调试和系统调用跟踪,但它也常被用于反调试措施
通过传递 `PT_DENY_ATTACH` 标志,它允许应用程序设置一个标志,以防止其他调试器附加。如果其他调试器尝试附加,则进程将终止。
## KVO
可以使用类似下面的代码来防止别人破解、逆向。
```objective-c
#import <dlfcn.h>
__BEGIN_DECLS
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
__END_DECLS
void disable_gdb(void) {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
#if DEBUG
// 非 DEBUG 模式下禁止调试
disable_gdb();
#endif
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
```
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <LHMainHomePageNavigationBar 0x1035a05a0> for the key path "name" from <LHMainHomePageNavigationBar 0x1035a05a0> because it is not registered as an observer.'
*** First throw call stack:
(0x2045ac518 0x2037879f8 0x2044b6c70 0x204f52470 0x204f5222c 0x101d8ed68 0x2309ad230 0x230456af8 0x100f0edb0 0x230456e18 0x230455e84 0x2309e429c 0x2309e54c4 0x2309c5534 0x230a8b7c0 0x230a8deec 0x230a8711c 0x20453e2bc 0x20453e23c 0x20453db24 0x204538a60 0x204538354 0x20673879c 0x2309abb68 0x101c7086c 0x203ffe8e0)
libc++abi.dylib: terminating with uncaught exception of type NSException
```
## ptrace 安全吗
但上述方式安全吗?一个简单的 fishhook 都可以破解掉。
第一步:创建一个 AppHook 的动态库,和一个 AppHookProtoctor 的 iOS App
第二步AppHook 里面在 `+load` 方法里使用 fishhook 对 ptrace 进行 hook判断 `PT_DENY_ATTACH` 则绕过
```c++
int hooked_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data) {
if (_request == PT_DENY_ATTACH) {
return 0;
}
return ptrace_pointer(_request, _pid, _addr, _data);
}
```
第三步:在 App 的 main.m 中调用 disable_gdb 来禁止调试。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookCrackedPtrace.png" style="zoom:30%" />
结果:可以看到对 ptrace 使用 fishhook hook 之后ptrace 并没有让 App 进程结束掉。也就是 ptrace 失效了,并不安全。
## ptrace 安全性改进
我们知道 fishhook 的原理是根据符号表进行 rebind 的,那是不是可以通过该原理绕开?
`ptrace `是系统函数dyld 会在启动阶段进行 rebase、rebind遍历 Mach-O 文件的 `__DATA` 段中的 `__nl_symbol_ptr` 和 `__la_symbol_ptr` 两个 section。通过 Indirect Symbol Table、Symbol Table 和 String Table 的配合Fishhook 能够找到需要替换的函数,并修改其地址。想了解 fishhook 详细工作原理可以查看这篇文章:[fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md)
我们禁止 fishhook对 Xcode 添加一个符号断点 `ptrace`,如下所示。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ptraceSymbolBreakpoint.png" style="zoom:30%" />
我们可以看到 ptrace 位于 `libsystem_kernel.dylib` 动态库中。lldb 模式下通过 `image list` 查看所有的 image 信息。
可以看到当前电脑模拟器运行情况下libsystem_kernel.dylib 位于 `/usr/lib/system/libsystem_kernel.dylib` 路径。这个路径是我电脑调试环境下的路径。真机路径不一样。
通过 dlopen、dlsym 的方式来找到 ptrace 符号地址,再去执行,这种方式的本质是没有走符号表的流程。
Demo 如下:
```objective-c
#ifndef DEBUG
// 非 DEBUG 模式下禁止调试
char *ptraceLibPath = "/usr/lib/system/libsystem_kernel.dylib";
void *handler = dlopen(ptraceLibPath, RTLD_LAZY);
int (*ptrace_pointer)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_pointer = dlsym(handler, "ptrace");
if (ptrace_pointer) {
ptrace_pointer(PT_DENY_ATTACH, 0, 0, 0);
}
#endif
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DLOpenPtrace.png" style="zoom:30%" />
工程运行后会发现App 启动后立马 crash 结束进程。说明这种(通过 dlsym 找到符号地址) ptrace 的防护是有效的
对代码进行修改,整洁一些,如下所示:
```c++
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
void disable_gdb(void) {
// 简易版:容易被 FishHook 进行符号表的修改,从而破解 ptrace 的拦截
// ptrace(PT_DENY_ATTACH, 0, 0, 0);
// 安全版本
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PtraceSafeMethod.png" style="zoom:30%" />
该方式通过 dlopen、dlsym 的方式延迟、动态的找到 ptrace 的符号地址,没有走符号表的逻辑,避开了 fishhook 的工作流程,从而更安全一些。
## sysctl 简易版本
`sysctl` 函数是一个系统调用,用于获取或设置系统相关的信息。这个函数提供了一种机制来查询或修改系统的状态信息,比如系统配置参数、统计数据等。
在 Linux 和类 Unix 系统中用于查看和修改内核参数。然而,在 iOS 逆向工程中,`sysctl` 也常被用于检测应用程序是否正在被调试
```c++
int sysctl(int *, u_int, void *, size_t *, void *, size_t);
```
参数解释:
- `name`: 一个指向整数数组的指针数组中的每个元素代表一个级别的OID对象标识符用于指定要查询或设置的系统信息。
- `namelen`: `name` 数组的长度即OID的级别数。
- `oldp`: 一个指向缓冲区的指针用于接收查询到的现有值。如果设置值这个参数可以是NULL。
- `oldlenp`: 一个指向 `size_t` 的指针,用于指定 `oldp` 缓冲区的大小,并在调用后返回实际读取的数据大小。
- `newp`: 一个指向新值的指针用于设置系统信息。如果只是查询这个参数可以是NULL。
- `newlen`: 新值的大小
返回值:
- 如果成功返回0
- 如果失败,返回 -1
其中传递的结构体引用,`info.kp_proc.p_flag` 字段用于判断进程是否处于调试状态。是二进制的0、1。第12位为1代表处于调试状态。反之不是。
思考如何正确二进制判断某一位是0还是1用特定位置填充1其他位填充0来处理。按位与之后特定位置为1说明之前是1否则就是0.
```c++
#define P_TRACED 0x00000800 /* Debugged process being traced */
```
`info.kp_proc.p_flag` 判断系统提供了一个 `P_TRACED`,按位与用来判断是否是调试模式。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PTRACEDFlag.png" style="zoom:30%" >
使用
```objective-c
bool isInDebugMode(void) {
int name[4];
name[0] = CTL_KERN; // 内核
name[1] = KERN_PROC; // 查询进程
name[2] = KERN_PROC_PID; // 通过进程 id 来查找
name[3] = getpid();
struct kinfo_proc info; // 接收查询信息,利用结构体传递引用
size_t infoSize = sizeof(info);
int resultCode = sysctl(name, sizeof(name)/sizeof(*name), &info, &infoSize, 0, 0);
assert(resultCode == 0);
return info.kp_proc.p_flag & P_TRACED;
}
```
结合定时器每间隔1秒进行检查一下运行起来发现处于 debug 模式,则调用 `exit(0)` 结束进程。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SYSCTLKillInDebug.png" style="zoom:30%" >
为什么调用 `exit`而不是 `abort`
- exit(0)函数是C标准库中的一个函数用于正常退出程序。当调用时程序会执行清理操作比如关闭打开的文件、释放分配的资源等。它允许程序在退出前执行一些清理工作比如调用 `atexit` 注册的函数。`exit(0)` 表示程序正常退出返回状态码为0。
- abort 函数也是C标准库中的一个函数但它用于异常或紧急情况下的退出。当调用时程序会立即终止不会进行任何清理工作比如关闭文件或释放资源。会导致程序发送SIGABRT信号给自身这通常用于调试目的以便在发生严重错误时立即停止程序。表示程序是非正常退出的。
`exit(0)` 更适合在程序正常结束时使用,而 `abort` 更适合在发生不可恢复的错误时使用。使用 `abort` 可以快速停止程序,但可能会导致资源泄漏等问题,因为它不会执行任何清理操作。
不过我们的逻辑是,在非 debug 模式才进行这样的检测。所以用 `#ifndef DEBUG` 包装
```objective-c
#ifndef DEBUG
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
if (isInDebugMode()) {
NSLog(@"在调试模式");
exit(0);
} else {
NSLog(@"不在调试模式");
}
});
dispatch_resume(timer);
#endif
```
## sysctl 安全吗
`sysctl ` 是系统函数,存在间接符号表,所以可以用 fishhook 进行 hook。
继续用动态库 + App 的形式验证能否 hook 成功。
第一步:注册一个函数指针,用来保存 sysctl 的函数地址
```c++
// sysctl 函数指针,保存原始 sysctl 函数地址
int (*sysctl_pointer)(int *, u_int, void *, size_t *, void *, size_t);
```
第二步:写替换后的 sysctl 函数实现
```c++
int hooked_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *newInfo, size_t newInfoSize) {
int resultCode = sysctl_pointer(name, namelen, info, infosize, newInfo, newInfoSize);
if (namelen == 4 && name[0] == CTL_KERN && name[1] == KERN_PROC && name[2] == KERN_PROC_PID && info) {
struct kinfo_proc *myInfo = (struct kinfo_proc *)info;
if (myInfo->kp_proc.p_flag & P_TRACED) {
// 异或取反。设置调试判断位为0.
myInfo->kp_proc.p_flag ^= P_TRACED;
}
return resultCode;
}
return resultCode;
}
```
第三步:调用 fishhook rebind_symbols 完成系统符号 `sysctl` 的 hook
第四步:验证 hook 是否成功。如果成功,则 App 运行起来,处于 debug 模式下,还是会输出 `不在调试模式`
结果如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookSysctlSymbol.png" style="zoom:30%" >
可以看到 fishhook 也可以 hook sysctl。所以不安全。
## sysctl 安全版本
修改思路参考上面的 ptrace知道 fishhook 的原理,绕开懒加载符号表,绕开 dyld 修正符号和填充地址这个过程。
不再赘述,核心代码如下图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SysctlAntiFishHook.png" style="zoom:30%" >
效果就是在 fishhook hook 的情况下App 检测到处于 debug 模式下,调用 `exit(0)` 自动结束进程。
## 更安全的版本
### 隐藏符号名称
更安全的是不让分析者在 MachO 中显示的看到 ptrace、sysctl 符号名称。所以采用异或运算一个固定的 key再根据指针指向字符串初始值再次异或得到原始字符串。
隐藏 ptrace 符号名称的方法,如下所示
```c++
void disable_gdb_via_hidden_ptrace(void) {
// 使用一个 char 数组拼接一个 ptrace 字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串)
unsigned char funcName[] = {
(KEY ^ 'p'),
(KEY ^ 't'),
(KEY ^ 'r'),
(KEY ^ 'a'),
(KEY ^ 'c'),
(KEY ^ 'e'),
(KEY ^ '\0'),
};
unsigned char * p = funcName;
// 再次异或之后恢复原本的值
while (((*p) ^= KEY) != '\0') p++;
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, (const char *)funcName);
if (ptrace_ptr) {
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
}
dlclose(handle);
}
```
隐藏 sysctl 符号的方法如下
```c++
bool isInDebugModeViaHiddenSysctl(void) {
// 使用一个 char 数组拼接一个 sysctl 字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串)
unsigned char funcName[] = {
(KEY ^ 's'),
(KEY ^ 'y'),
(KEY ^ 's'),
(KEY ^ 'c'),
(KEY ^ 't'),
(KEY ^ 'l'),
(KEY ^ '\0'),
};
unsigned char * p = funcName;
//再次异或之后恢复原本的值
while (((*p) ^= KEY) != '\0') p++;
int name[4];
name[0] = CTL_KERN; // 内核
name[1] = KERN_PROC; // 查询进程
name[2] = KERN_PROC_PID; // 通过进程 ID 来查找
name[3] = getpid(); // 当前进程 ID
struct kinfo_proc info; // 接收查询信息,利用结构体传递引用
size_t infoSize = sizeof(info);
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
sysctl_ptr_t sysctl_ptr = dlsym(handle, (const char *)funcName);
sysctl_ptr(name, sizeof(name)/sizeof(*name), &info, &infoSize, 0, 0);
dlclose(handle);
return info.kp_proc.p_flag & P_TRACED;
}
```
会发现隐藏符号后,可以实现防止 hook 效果的。骚操作来还原符号,保证安全。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HidePtraceAndSysctlSymbol.png" style="zoom:30%" >
### 利用汇编调用系统函数
函数调用都可以利用 `syscall`的方式调用。
```c++
syscall(SYS_ptrace,PT_DENY_ATTACH,0,0);
// 等价于
syscall(26,31,0,0,0);
```
`volatile` 代表不优化此汇编代码
```assembly
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n" //这里就是syscall的函数编号
"svc #0x80\n" //这条指令就是触发中断(系统级别的跳转)
);
```
`ptrace(PT_DENY_ATTACH, 0, 0, 0);` 等价于
```assembly
asm volatile(
"mov x0,#31\n" //参数1
"mov x1,#0\n" //参数2
"mov x2,#0\n" //参数3
"mov x3,#0\n" //参数4
"mov x16,#26\n"//中断根据x16 里面的值跳转ptrace
"svc #0x80\n" //这条指令就是触发中断去找x16执行系统级别的跳转
);
```
还可以对 `exit(0)` 进行汇编混合,自定义符号 `quit_process`
```assembly
static __attribute__((always_inline)) void quit_process () {
#ifdef __arm64__
asm(
"mov x0,#0\n"
"mov x16,#1\n" //这里相当于 Sys_exit,调用exit函数
"svc #0x80\n"
);
return;
#endif
#ifdef __arm__
asm(
"mov r0,#0\n"
"mov r16,#1\n" //这里相当于 Sys_exit
"svc #80\n"
);
return;
#endif
exit(0);
}
```
最后的效果:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AntiDebugViaAssemblyAndPtrace.png" style="zoom:30%" >
完整代码可以这里:
- [AppHook](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHook)
- [AppHookProtector](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHookProtector)