更新第11章

This commit is contained in:
feiocng 2023-04-24 12:17:19 +08:00
parent 9f6d849cc3
commit a250b8cf1c
2 changed files with 85 additions and 153 deletions

View File

@ -1,17 +1,21 @@
# 第十一章 反调试 # 第十一章 反调试
反调试是指在软件运行时,防止恶意用户或者黑客使用调试器来分析和修改程序的行为。在安卓系统中,反调试技术是应用程序开发中非常重要的一部分,因为它可以提高应用程序的安全性,防止黑客攻击和数据泄露。另一方面,恶意软件也通过反调试技术,防止安全系统与软件对其进行分析,因此,安全从业有必要了解常见的反调试对抗技术。
在本章中,我们将介绍反调试的概念、原理和常见的反调试方案,以及如何对这些方案进行相应的安全对抗。
## 11.1 反调试常见手段 ## 11.1 反调试常见手段
`Android`逆向分析中,最常见的情况就是攻防的对抗,攻击者通过对样本进行静态分析,以及动态调试等手段,获取想要的信息。而保护方则通过对混淆,以及多种加固方式,来对自己的重要信息进行保护。例如使用加固的手段来干扰攻击者的静态分析,通过检测环境来对抗攻击者注入`hook`函数,添加各种检测调试来阻止攻击者动态分析。 `Android`逆向分析中,最常见的情况就是攻防的对抗,攻击者通过对样本进行静态分析,以及动态调试等手段,获取想要的信息。而保护方则通过对混淆,以及多种加固方式,来对自己的重要信息进行保护。例如使用加固的手段来干扰攻击者的静态分析,通过检测环境来对抗攻击者注入`hook`函数,添加各种检测调试来阻止攻击者动态分析。
`ptrace``Linux`操作系统提供的一个系统调用,它允许一个进程监控另一个进程的执行,并能够在运行时修改它的寄存器和内存等资源。`ptrace`通常被用于调试应用程序、分析破解软件以及实现进程间沙盒隔离等场景。 ### 11.1.1 检测调试标志
调试软件离不开调试器,而调试器又高度依赖`ptrace``ptrace``Linux`操作系统提供的一个系统调用,它允许一个进程监控另一个进程的执行,并能够在运行时修改它的寄存器和内存等资源。`ptrace`通常被用于调试应用程序、分析破解软件以及实现进程间沙盒隔离等场景。
使用`ptrace`来监控目标进程时,需要以`tracer`(追踪者)的身份启动一个新的进程,然后通过`ptrace`函数来请求操作系统将目标进程挂起并转交给`tracer`进程。一旦目标进程被挂起,`tracer`进程就可以读写其虚拟地址空间中的数据、修改寄存器值、单步执行指令等操作。当`tracer`完成了对目标进程的调试操作后,可以通过`ptrace`函数将控制权还原到目标进程上,使其继续执行。 使用`ptrace`来监控目标进程时,需要以`tracer`(追踪者)的身份启动一个新的进程,然后通过`ptrace`函数来请求操作系统将目标进程挂起并转交给`tracer`进程。一旦目标进程被挂起,`tracer`进程就可以读写其虚拟地址空间中的数据、修改寄存器值、单步执行指令等操作。当`tracer`完成了对目标进程的调试操作后,可以通过`ptrace`函数将控制权还原到目标进程上,使其继续执行。
由于`ptrace`功能的强大,它也被广泛应用于破解软件、恶意攻击等场景。因此,在一些安全敏感的场合,为了防止恶意攻击者使用`ptrace`来监控和修改进程的行为,需要采取一些反调试的手段来加强保护。 由于`ptrace`功能的强大,它也被广泛应用于破解软件、恶意攻击等场景。因此,在一些安全敏感的场合,为了防止恶意攻击者使用`ptrace`来监控和修改进程的行为,需要采取一些反调试的手段来加强保护。
### 11.1.1 根据文件检测
通过在被保护程序中定期检测其父进程是否为指定的`tracer`进程,可以避免恶意攻击者使用`ptrace`跟踪程序的执行流程。 通过在被保护程序中定期检测其父进程是否为指定的`tracer`进程,可以避免恶意攻击者使用`ptrace`跟踪程序的执行流程。
接下来写一个简单的实例来进行测试。`Android Studio`创建`native c++`的项目。修改函数如下。 接下来写一个简单的实例来进行测试。`Android Studio`创建`native c++`的项目。修改函数如下。
@ -75,20 +79,13 @@ root 1053 1 14644500 115568 0 0 S zygote64
发现该进程是`zygote`进程,说明没有被调试。接下来使用`ida`调试该进程。找到`ida`下的`dbgsrv`目录,将其中的`android_server64`拷贝到`Android`系统中,将调试的端口`23946`转发到本地。并且将该服务启动起来,操作如下。 发现该进程是`zygote`进程,说明没有被调试。接下来使用`ida`调试该进程。找到`ida`下的`dbgsrv`目录,将其中的`android_server64`拷贝到`Android`系统中,将调试的端口`23946`转发到本地。并且将该服务启动起来,操作如下。
``` ```
adb push "D:\tools\IDA Pro 7.6\dbgsrv\android_server64" /data/local/tmp/ adb push android_server64 /data/local/tmp/
adb forward tcp:23946 tcp:23946 adb forward tcp:23946 tcp:23946
adb shell adb shell
cd /data/local/tmp/ cd /data/local/tmp/
chmod +x ./android_server64 chmod +x ./android_server64
su su
./android_server64 ./android_server64
``` ```
接下来打开`ida`,选择`Debugger->Attach->Remote Arm linux/android debugger`,在`hostname`选项中填本地回环地址`127.0.0.1`,如下图。 接下来打开`ida`,选择`Debugger->Attach->Remote Arm linux/android debugger`,在`hostname`选项中填本地回环地址`127.0.0.1`,如下图。
@ -101,7 +98,7 @@ su
成功挂起调试后,检查日志中的 `ppid`,发现并没有任何变化,依然是`zygote`作为父进程。 成功挂起调试后,检查日志中的 `ppid`,发现并没有任何变化,依然是`zygote`作为父进程。
当使用 `IDA` 进行调试时,`IDA` 会创建一个调试器进程,并将其作为目标进程的父进程。但是,由于目标进程最初是由 `zygote `进程` fork `出来的,因此在查询其父进程` id` 时,仍然会返回` zygote `进程的 `id`。这并不意味着调试器进程没有被正确设置为目标进程的父进程。实际上,在`IDA`调试过程中,目标进程的执行状态确实是由调试器进程所控制的。因此,即使查询到的父进程`id`不正确,也不会影响`IDA`对目标进程的控制和调试操作。 当使用 `IDA` 进行调试时,`IDA` 会创建一个调试器进程,并将其作为目标进程的父进程。但是,由于目标进程最初是由 `zygote`进程`fork`出来的,因此在查询其父进程`id`时,仍然会返回`zygote`进程的`id`。这并不意味着调试器进程没有被正确设置为目标进程的父进程。实际上,在`IDA`调试过程中,目标进程的执行状态确实是由调试器进程所控制的。因此,即使查询到的父进程`id`不正确,也不会影响`IDA`对目标进程的控制和调试操作。
尽管查询`ppid`无法判断出进程被调试了,但是依然有其他地方会出现被调试的信息,例如`/proc/<pid>/status`文件中的字段`TracerPid`,就能看到调试进程的`id`。下面查看该文件。 尽管查询`ppid`无法判断出进程被调试了,但是依然有其他地方会出现被调试的信息,例如`/proc/<pid>/status`文件中的字段`TracerPid`,就能看到调试进程的`id`。下面查看该文件。
@ -157,7 +154,7 @@ ptrace_stop
t 1027 1027 0 0 -1 1077952832 29405 4835 0 0 81 9 0 0 20 0 19 0 424763 15088168960 24987 18446744073709551615 1 1 0 0 0 0 4612 1 1073775864 0 0 0 17 1 0 0 0 0 0 0 0 0 0 0 0 0 0 t 1027 1027 0 0 -1 1077952832 29405 4835 0 0 81 9 0 0 20 0 19 0 424763 15088168960 24987 18446744073709551615 1 1 0 0 0 0 4612 1 1073775864 0 0 0 17 1 0 0 0 0 0 0 0 0 0 0 0 0 0
``` ```
### 11.1.2 根据ptrace的特性检测 ### 11.1.2 禁止调试器附加
由于动态调试基本都是依赖`ptrace`对进程追踪,那么可以通过了解`ptrace`的使用特性,来针对性的检查自身是否被调试了。由于`ptrace`附加进程时,目标进程同时只能被一个进程附加,第二次附加就会失败,那么通过对自身进行`ptrace`处理,如果发现对自己进行附加失败,说明已经被调试了。同时对自身附加后,也能阻止其他进程再对其进行附加调试。下面看实现代码。 由于动态调试基本都是依赖`ptrace`对进程追踪,那么可以通过了解`ptrace`的使用特性,来针对性的检查自身是否被调试了。由于`ptrace`附加进程时,目标进程同时只能被一个进程附加,第二次附加就会失败,那么通过对自身进行`ptrace`处理,如果发现对自己进行附加失败,说明已经被调试了。同时对自身附加后,也能阻止其他进程再对其进行附加调试。下面看实现代码。
@ -216,7 +213,45 @@ Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
![image-20230405162058014](.\images\ida_attach_err.png) ![image-20230405162058014](.\images\ida_attach_err.png)
### 11.1.3 其他检测方式 ### 11.1.4 检测跟踪工具
除了常规调试器一些安全分析工具都具备调试跟踪功能。比如Frida可以向目标程序中注入Javascript脚本来Hook跟踪与修改程序的执行状态。在执行反调试检测时这类工具也是需要重点关注的对象。
使用Frida时需要在设备上执行`frida-server`命令。检测Frida的思路来源于该工具运行时的文件与进行特征信息。例如执行Frida注入代码到一个程序后它的`/proc/pid/maps`中有留有注入的动态库的痕迹:
```
$ adb shell pidof com.android.settings
$ frida -U -p 27507
____
/ _ | Frida 16.0.12 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
[Android Emulator 5554::PID::27507 ]->
[Android Emulator 5554::PID::27507 ]-> Process.enumerateModules().filter(m => {
// console.log(m.path)
var ret = false
if (m.path) {
if (m.path.includes("frida")) {
console.log(m.path)
}
}
})
/data/local/tmp/re.frida.server/frida-agent-64.so
[]
```
`Process.enumerateModules()`执行后返回的是`/proc/self/maps`的内容,这里过滤显示留住`frida`字符串的路径。可以看到,输出中有`/data/local/tmp/re.frida.server/frida-agent-64.so`。这是`frida-server`注入代码时释放的动态库。检测它就能检测到程序注入了Frida。
### 11.1.4 系统调试检测接口
除了以上这两种比较常见的检测方式外,还有很多种方式进行检测,这些检测大多都是围绕着调试过程会产生的特征来进行检测,在真实的保护场景下,开发者会结合多种方案检测来防止被攻击者动态调试。以下是其他检测方案的介绍。 除了以上这两种比较常见的检测方式外,还有很多种方式进行检测,这些检测大多都是围绕着调试过程会产生的特征来进行检测,在真实的保护场景下,开发者会结合多种方案检测来防止被攻击者动态调试。以下是其他检测方案的介绍。
@ -230,9 +265,30 @@ Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
由于大多数情况下,反调试手段会被攻击者使用各种`Hook`的方式进行替换处理,所以有些开发者会采用非常规的手段来获取,用来判断是否为调试状态的信息。例如内联汇编通过`svc`来执行对应的系统调用。 由于大多数情况下,反调试手段会被攻击者使用各种`Hook`的方式进行替换处理,所以有些开发者会采用非常规的手段来获取,用来判断是否为调试状态的信息。例如内联汇编通过`svc`来执行对应的系统调用。
## 11.2 系统层面的反调试
了解常见的反调试检测后,就可以对症进行修改,这些修改并不会完美解决反调试的所有问题,主要是处理掉一些常规的检测办法。来尽量减少分析成本。下面开始简单的对几种检测方式进行修改处理。 ## 11.2 常见反调试绕过方案
围绕常见的反调试技术,都有相应的反反调试,也就是反调试绕过技术方案。
1. Hook技术
Hook技术是一种常见的反调试绕过方案。它可以修改目标进程的内存数据与代码绕过应用程序的反调试技术。Hook技术在程序运行时替换函数的实现从而绕过应用程序的反调试技术。例如使用Frida注入程序然后修改进程的调试标志读取接口修改返回的内容即可完成自身的调试标志检测。
2. 内存修改技术
内存修改技术是一种常见的反调试绕过方案,它可以让黑客修改应用程序的内存,从而绕过应用程序的反调试技术。例如,可以使用内存修改工具来修改应用程序的内存,从而绕过应用程序的反调试技术。
3. 反编译技术
反编译技术是一种常见的反调试绕过方案通过分析App程序的代码反编译修改掉代码中的反调试检测逻辑部分从而绕过应用程序的反调试技术。这种技术需要对目标程序进行大量的分析工作结合多个工具执行反编译与重打包在安卓安全技术相对不成熟的时期这种方案被大量使用。目前使用Hook方案与系统级别反反调试技术的场景会更加普遍。
4. 系统级别反反调试技术
系统级别反反调试技术是一种底层的反调试绕过方案,它通过修改系统反调试相关的代码逻辑,让系统输出程序为非调试状态。这种绕过应用程序的反调试技术比较稳定,且不易被检测,是一种常用的反反调试技术。
## 11.3 系统层面解决反调试
了解常见的反调试检测后,就可以对其进行修改,这些修改并不会完美解决反调试的所有问题,主要是处理掉一些常规的检测办法。来尽量减少分析成本。下面开始简单的对几种检测方式进行修改处理。
### 11.3.1 过系统调试检测接口检测
然后修改属性`ro.debuggable`的值让其固定显示为0修改文件`build/make/core/main.mk`,修改代码如下。 然后修改属性`ro.debuggable`的值让其固定显示为0修改文件`build/make/core/main.mk`,修改代码如下。
@ -255,7 +311,13 @@ int __android_log_is_debuggable() {
} }
``` ```
除此之外,还有多个针对文件检测的处理,修改文件`android-kernel/private/msm-google/fs/proc/array.c`,修改如下。 这样修改后的系统代码,检测"ro.debuggable"将永远返回0但不影响我们使用调试相关的能力。
### 11.3.2 过调试标志检测
除此之外,还有多个针对调试标志检测的处理,修改内核文件`fs/proc/array.c`,修改如下。
```c++ ```c++
static inline void task_state(struct seq_file *m, struct pid_namespace *ns, static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
@ -325,7 +387,7 @@ static inline const char *get_task_state(struct task_struct *tsk)
} }
``` ```
最后处理`wchan`的对应代码,修改文件`android-kernel/private/msm-google/fs/proc/base.c`,相关修改如下。 最后处理`wchan`的对应代码,修改内核文件`fs/proc/base.c`,相关修改如下。
```c++ ```c++
static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns, static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
@ -351,133 +413,9 @@ static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
} }
``` ```
## 11.3 Android下的硬件断点 ## 11.4 集成反反调试功能
在调试中,可以通过对程序下不同类型的断点,来辅助分析代码,其中最常见的就是软件断点,软件断点是通过将原有的指令进行替换,在`ARM64`架构中,软件断点通常是通过将原有的指令替换为`BRK`指令`opcode为0xD4200000`来实现的。当程序执行到该指令时,处理器会触发一个异常`trap exception`,从而停止程序的运行。
在软件断点的基础上添加条件判断,就是一个条件断点了,只有满足指定条件才会触发该软件断点。
内存断点,是通过修改指定内存的访问属性,让其触发异常,来实现中断的效果。在程序运行时,将要监视的内存地址标记为不可访问,当程序尝试访问该地址时,会触发一个异常,并且操作系统会中断该进程的执行。然后,调试器会根据异常信息来确定是哪个内存地址引起了中断,并且可以进行相应的处理和调试工作。内存断点的实现与硬件断点或软件断点不同,它需要操作系统提供的支持才能实现。由于内存断点的实现需要修改系统的内存映射表等底层数据结构,因此可能会影响程序的性能和稳定性。应该谨慎地选择要监视的内存地址,并避免过多地使用内存断点。
### 11.3.1 什么是硬件断点
硬件断点是通过CPU内置的调试功能实现的。当程序执行到设置了硬件断点的地址时CPU会发出一个异常信号从而让操作系统停止当前进程的执行。然后操作系统将控制权转移给调试器并通知调试器哪个线程触发了异常以便调试器可以进行相应的调试工作。
在ARM架构下硬件断点主要有两种类型执行断点和数据断点。执行断点可以用于监视代码执行当程序尝试执行指定的指令时触发中断数据断点则可以用于监视内存读写操作当程序尝试访问指定的内存地址时触发中断。执行断点和数据断点都由CPU硬件实现因此响应速度很快但数量有限。
### 11.3.2 开启Android的硬件调试
在开始硬件断点的使用前,首先要进行环境的准备,下面的测试案例将使用`22.0`版本`ndk`中的`gdb`来调试。然后检查当前内核编译选项中是否开启了硬件断点支持。下面是查询过程。
```bash
adb shell
zcat /proc/config.gz |grep -i BREAKPOINT
// 显示内容如下
CONFIG_HAVE_HW_BREAKPOINT=y
```
如果你的结果显示为`n`,则说明需要在内核中修改配置,不要直接去修改`defconfig`配置,而是使用命令生成`.config`文件,然后修改`.config`文件,再由该文件生成对应的`defconfig`,再将其覆盖原文件,最后重新编译。具体的操作过程如下。
```
cd /root/android_src/android-kernel/private/msm-google
// b1c1_defconfig是当前设备使用的对应配置b1c1表示的是pixel3和pixel3 XL的代号
// 第一步会在当前目录生成.config文件
make ARCH=arm64 b1c1_defconfig
// 使用图形界面来开启配置,修改完成后记得保存
make ARCH=arm64 menuconfig
// 如果不想在图形界面编辑,可以直接修改.config文件
vim .config
// 添加选项
CONFIG_HAVE_HW_BREAKPOINT=y
CONFIG_HAVE_ARCH_TRACEHOOK=y
// 选项添加完成后保存配置将会生成新的b1c1_defconfig文件
make ARCH=arm64 savedefconfig
// 替换原文件
cp defconfig arch/arm64/configs/b1c1_defconfig
cd /root/android_src/android-kernel
// 重新编译
./build/build.sh
```
编译完成,重新刷入手机后,再次查询配置就能看到该选项被开启了。
`GEFGDB Enhanced Features`是一个用于`GDB`调试器的`Python`扩展框架,提供了一些额外的功能,使得在调试过程中更加便捷和高效。`GEF`具有丰富的命令行界面和可扩展性,可以通过编写`Python`脚本来自定义其功能。以下是`GEF`的特点:
1. 命令行界面友好:`GEF`提供了易于使用的命令行界面,支持自动补全、历史记录和语法高亮等功能,使得调试过程更加简单和快速。
2. 调试功能强大:`GEF`提供了一系列额外的调试功能,如内存断点、硬件断点、`ASM`混淆解析等,可以显著提高调试效率。
3. 可扩展性好:`GEF`基于`Python`开发,支持编写自定义脚本来扩展其功能,用户可以根据自己的需求进行定制化。
4. 平台支持广泛:`GEF`支持多种操作系统和处理器架构,如`Linux、macOS、Windows`,以及`ARM、x86`等常见的处理器架构。
5. 社区活跃:`GEF`是一个开源项目,拥有庞大的用户群体和贡献者团队,开发进程活跃,问题能够及时得到解决。
安装`GEF`非常简单,一句命令即可完成安装。
```
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
```
### 11.3.3 硬件断点测试
环境准备就绪后,开发一个简单的应用作为被硬件断点的目标,声明两个全局变量,分别为`int`类型和`char`数组,然后分别对两个变量进行访问和写入。下面是样例代码。
```c++
int test1=1024;
char test2[100];
extern "C" JNIEXPORT jstring JNICALL
Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
memset(test2,0,100);
strcpy(test2,"demo");
ALOGD("test1",test1);
ALOGD("test2",test2);
return env->NewStringUTF(hello.c_str());
}
```
安装该测试样例后,接着将`ndk`中的`gdbserver`传入手机中。命令如下。
```
adb push '~/Android/Sdk/ndk/23.1.7779620/prebuilt/android-arm64/gdbserver/gdbserver' /data/local/tmp/
adb shell
su
cd /datalocal/tmp
chmod +x ./gdbserver
// 在手机上打开测试应用后查看该应用的pid
ps -e|grep nativedemo
// 设置
./gdbserver :1234 --attach 5991
```
接下来使用`gdb`连接上手机中的`gdbserver`,这里需要注意,使用的`gdb``gdbserver`的版本需要对应,否则就会导致连接错误的问题。下面是连接的相关操作。
```
// 将gdbserver监听的端口转发到本地
adb forward tcp:1234 tcp:1234
gdb
// 连接监听的端口
target remote :1234
```
所有这些对系统的修改都是针对不同场景反调试而产生的对应解决方案需要重新编译系统代码。涉及内核代码的部分需要重新编译内核涉及framework的部分编译生成ROM。整个过程可以编写自动化操作脚本将重复性的工作做简化处理。
在实践过程中,调试与反调试技术都是随时攻防的不断升级实时变化的,例如,有一些软件壳会对系统状态与接口作检测,这个时候,这里介绍的一些公开的方法可能就失效了。这种情况下,需要结合实际,使用安全分析技术,对目标程序做进一步的分析,确定其使用的反调试技术,重新调整系统文件修改点,然后编译打包测试效果。

View File

@ -108,9 +108,7 @@
### 9.1 Xposed ### 9.1 Xposed
### 9.2 Xposed实现原理 ### 9.2 Xposed实现原理
### 9.3 常见的Hook框架 ### 9.3 常见的Hook框架
### 9.4 集成pine ### 9.4 集成pine
### 9.5 集成dobby ### 9.5 集成dobby
### 9.6 实战测试 ### 9.6 实战测试
@ -125,12 +123,8 @@
## 第11章 反调试 ## 第11章 反调试
### 11.1 反调试常见手段 ### 11.1 反调试常见手段
### 11.2 常见反调试绕过方案 ### 11.2 常见反调试绕过方案
### 11.3 系统层面如何解决反调试 ### 11.3 系统层面解决反调试
### 11.4 集成反调试功能 ### 11.4 集成反反调试功能
### 11.5 Android下的硬件调试
### 11.5.1 什么是硬件调试
### 11.5.2 开启Android的硬件调试
### 11.5.3 硬件调试测试
## 第12章 基于定制系统的逆向实战 ## 第12章 基于定制系统的逆向实战
### 12.1 案例1 ### 12.1 案例1