feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-17 23:30:50 +08:00
parent 83fefff66b
commit e3fde7a1df
15 changed files with 380 additions and 38 deletions

View File

@@ -270,7 +270,7 @@ int hooked_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *
## sysctl 安全版本
## sysctl 安全性改进
修改思路参考上面的 ptrace知道 fishhook 的原理,绕开懒加载符号表,绕开 dyld 修正符号和填充地址这个过程。
@@ -284,10 +284,75 @@ int hooked_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *
## syscall 简易版本
`int syscall(int, ...) ``syscall`函数是一种用于调用系统调用的方法。系统调用是用户空间程序请求操作系统内核服务的一种机制。
在用户空间和内核空间之间,有一个叫做 Syscall (系统调用, system call )的中间层是连接用户态和内核态的桥梁。这样即提高了内核的安全型也便于移植只需实现同一套接口即可。Linux系统用户空间通过向内核空间发出 syscall产生软中断从而让程序陷入内核态执行相应的操作。对于每个系统调用都会有一个对应的系统调用号比很多操作系统要少很多。
引入头文件 `#import <sys/syscall.h>`
```c++
syscall(26, 31, 0, 0);
// 等价于 syscall(SYS_ptrace, PT_DENY_ATTACH, 0, 0);
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SyscallParam.png" style="zoom:30%" />
syscall 调用的时候第一个参数是调用函数的名称,后面的参数是调用函数的参数。
## syscall 安全吗
syscall 本质也是系统函数,最后还是躲不开 fishhook 的追杀。所以是不安全的。有没有什么办法可以解决?
这里就不再去写一遍 fishhook 的代码了,很重复...
## syscall 安全性改进
- 隐藏符号,还原符号
- 使用 dlopen、dlsym 的方式,找到 syscall 符号的地址
- syscall 发起系统调用,调用 sysctl 能力
- GCD 定时器检测,判断是否处于调试模式
- 如果处于调试模式,调用汇编 quit_process 结束进程
可以看到:即使 fishhook hook 了 ptrace、sysctl 绕过 hook但是这种方式还是可以对非法调试 App 的行为进行了保护,立马会结束进程。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HiddenSyscallSymbol.png" style="zoom:30%" />
## svc 调用
SVC指令在ARM体系中被归于异常处理类指令该指令能允许用户程序调用内核其格式如下
```c++
SVC{cond} #imm // Supervisor call, allows application program to call the kernel (EL1)
```
传统 arm中使用 svc 0 表示中断,在 xnu 中使用的是 svc 0x80。具体的看下面的例子
## 更安全的版本
### 隐藏符号名称
iOS 中常量字符串可以在 Mach-O 文件的 `__TEXT` 段中找到。如果加密的 salt、一些支付的 key、地图的 key直接明文存储很不安全一个可能的方案是采用 c 字符脱符号,比如通过下面的方式获取字符串
```objective-c
char name[] = {'s', 'y', 's', 'c', 'a', 'l', 'l', '\0'};
NSString *funcName = [NSString stringWithUTF8String:name];
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StringValueVisableInMachO.png" style="zoom:30%" />
更安全的是不让分析者在 MachO 中显示的看到 ptrace、sysctl 符号名称。所以采用异或运算一个固定的 key再根据指针指向字符串初始值再次异或得到原始字符串。
隐藏 ptrace 符号名称的方法,如下所示
@@ -376,8 +441,8 @@ asm volatile(
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n" //这里就是syscall的函数编号
"svc #0x80\n" //这条指令就是触发中断(系统级别的跳转)
"mov x16,#0\n" // 这里就是syscall的函数编号
"svc #0x80\n" // 这条指令就是触发中断(系统级别的跳转)
);
```
@@ -385,12 +450,12 @@ asm volatile(
```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执行(系统级别的跳转!)
"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 执行
);
```
@@ -401,7 +466,7 @@ static __attribute__((always_inline)) void quit_process () {
#ifdef __arm64__
asm(
"mov x0,#0\n"
"mov x16,#1\n" //这里相当于 Sys_exit,调用exit函数
"mov x16,#1\n" // 这里相当于 Sys_exit,调用exit函数
"svc #0x80\n"
);
return;
@@ -409,7 +474,7 @@ static __attribute__((always_inline)) void quit_process () {
#ifdef __arm__
asm(
"mov r0,#0\n"
"mov r16,#1\n" //这里相当于 Sys_exit
"mov r16,#1\n" // 这里相当于 Sys_exit
"svc #80\n"
);
return;
@@ -424,6 +489,72 @@ static __attribute__((always_inline)) void quit_process () {
## 一些其他的思路
### 符号混淆
做 iOS 开发的同学,在类名、方法名等命名上,都会做到见名知意,这点在开发阶段、日常维护阶段是好事情。但是站在黑客和攻击者角度来看的话,这对他们来说也是一件好事,但对 App 的安全来讲就是一件坏事。所以需要做**符号混淆**。
在静态分析应用的时候,常常会使用 `class-dump` 导出应用的头文件,通过头文件中的函数名或变量名 猜测这些函数的功能,然后进行 `Hook` 动态分析窥探大概逻辑。如果让这些方法名、变量名、类名从名称上看没有任何意义,那么就能从一定程度干扰攻击者猜测,这叫做代码混淆
如果一个登陆注册如下所示(伪代码):
```objective-c
@interface LoginViewController: UIViewController
- (void)handleLoginAction;
@end
```
这样的代码上传到 App Store 后,攻击者利用 class-dump 还原后,还是很清晰的,见名知意,一下子就可以判断这是登陆事件的处理函数。如果对符号进行混淆,如下所示
```objective-c
@interface $38wiewh81_Controller: UIViewController
- (void)0jjd1;
@end
```
攻击者看到这样的符号,无疑会增大破解难度,至少不会像以前的一样,代码做到裸奔。
### 动态库白名单检测
除了保护应用中的关键代码,还可以通过代码检测应用中动态库是否是合法的。无论是越狱环境还是非越狱环境,如果要入侵除了修改二进制就是注入动态库,所以可以写段逻辑判断 App 中除了我们自己项目中的动态库,是否还存在入侵的动态库。
通过 dyld API 函数获取应用中的动态库名称,把这些字符串名称合并作为一个白名单,如果发现动态库不在白名单中,则结束进程。
```objective-c
const char *whitstr= "";
void checkWhiteStr(){
    uint32_t count= _dyld_image_count();
    for(int i=1;i<count;i++){
      const char* dyname=_dyld_get_image_name(i);
        //printf(dyname);
        if(!strstr(whitstr, dyname)){ // 不在白名单中
quit_process();
        }
    }
}
```
当然,更严谨的做法就是 App 启动完成,主页加载显示完毕后,拉取接口,服务端告诉当前版本的 App 有哪些库App 内存保存这些数据GCD 定时器检测,发现有库不在该白名单内存中,则结束进程。
服务端白名单数据怎么来App 打出 release 模式的包,然后运行上述代码,包库的名称数组,上传到服务端。
### 逻辑混淆
一些核心逻辑还是有必要混淆的。编译工程时使用这个自定义的 `OLLVM` 工具可以编写 `Pass` 来混淆 `IR`。
具体查看 [obfuscator](https://github.com/obfuscator-llvm/obfuscator) 这个 Repo。具体使用这里不展开。
完整代码可以这里:
- [AppHook](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHook)