第三章:常规手段全面失效
本章字数:约8000字 阅读时间:约25分钟 难度等级:★★★★☆
声明:本文中的公司名称、包名、API地址、密钥等均已脱敏处理。
引言
经过前两章的探索,我们已经尝试了安全研究员工具箱中的大部分常规武器:
- 网络层:Charles抓包失败,VPN抓包失败,iptables转发失败
- 运行时:Frida注入崩溃,Xposed被检测,各种Hook框架都失效
此时的我,坐在电脑前,看着屏幕上一次又一次的"Process crashed",陷入了深深的沉思。
这款APP的防护水平,远超我之前遇到的任何目标。
但作为一名有十年经验的安全研究员,我知道:没有绝对安全的系统。
本章将记录我在"山穷水尽"之际的最后挣扎,以及最终找到突破口的过程。
3.1 回顾与反思
3.1.1 已尝试的方法清单
让我们先整理一下已经尝试过的所有方法:
┌─────────────────────────────────────────────────────────────────┐
│ 已尝试方法汇总 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 网络层攻击(全部失败) │
│ ├── Charles Proxy HTTP代理 → 抓不到任何数据 │
│ ├── HttpCanary VPN抓包 → 抓不到任何数据 │
│ ├── iptables流量转发 → 抓不到任何数据 │
│ ├── Wireshark路由器抓包 → 只能看到加密流量 │
│ └── mitmproxy透明代理 → 抓不到任何数据 │
│ │
│ 动态调试(全部失败) │
│ ├── Frida标准注入 → APP崩溃 │
│ ├── Frida Gadget嵌入 → APP检测到修改 │
│ ├── Frida改名+改端口 → APP仍然崩溃 │
│ ├── Xposed Framework → APP检测到并警告 │
│ ├── VirtualXposed → APP检测到虚拟环境 │
│ ├── 太极(TaiChi) → APP检测到 │
│ └── LSPosed → 不稳定,偶尔成功 │
│ │
│ 静态分析(部分成功) │
│ ├── APKTool反编译 → 成功,获得smali代码 │
│ ├── jadx反编译 → 成功,获得Java代码 │
│ ├── IDA Pro分析SO → 部分成功,代码被混淆 │
│ └── Ghidra分析SO → 部分成功,代码被混淆 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.1.2 失败原因分析
为什么这些方法都失败了?让我们深入分析:
网络层失败的原因:
- APP实现了代理检测,检测到HTTP代理后绕过系统网络栈
- APP使用了自定义网络实现,不走系统的网络API
- APP可能使用了非标准端口,绑过了iptables规则
- 所有通信都使用TLS 1.3加密,即使抓到也看不到内容
动态调试失败的原因:
- APP在Native层实现了多重检测
- 检测在库加载时执行,比注入更早
- 检测方法多样化:端口、进程、内存、ptrace等
- 检测到后立即崩溃,不给攻击者反应时间
静态分析的局限:
- 核心逻辑在Native层,反汇编后可读性差
- 代码经过混淆,函数名、变量名都是无意义的字符
- 存在反调试代码,静态分析难以完全理解运行时行为
- 加密算法的具体实现难以从汇编代码中还原
3.2 最后的尝试:模拟器方案
3.2.1 为什么考虑模拟器?
既然真实设备上的调试工具都被检测,那么在模拟器中运行APP会怎样?
模拟器的优势:
- 完全可控的环境
- 可以修改系统底层
- 可以绑过某些硬件检测
- 调试更方便
3.2.2 Android Studio模拟器
首先尝试官方的Android Studio模拟器:
# 创建AVD(Android Virtual Device)
avdmanager create avd -n test_device -k "system-images;android-30;google_apis;x86_64"
# 启动模拟器
emulator -avd test_device
安装APP并运行:
adb install android-dreamworld-arm64-v8a-prod-v8.x.x.apk
adb shell am start -n com.dreamworld.app/.MainActivity
结果:APP启动后立即退出,日志显示"检测到模拟器环境"。
3.2.3 模拟器检测分析
APP是如何检测模拟器的?常见的检测方法:
// 模拟器检测示例代码
public class EmulatorDetector {
public static boolean isEmulator() {
return checkBuild() || checkSensors() || checkFiles() || checkProperties();
}
// 检测Build信息
private static boolean checkBuild() {
return Build.FINGERPRINT.contains("generic")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("vbox");
}
// 检测传感器
private static boolean checkSensors() {
SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
// 模拟器通常缺少某些传感器
return sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null
|| sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE) == null;
}
// 检测特征文件
private static boolean checkFiles() {
String[] knownFiles = {
"/dev/socket/qemud",
"/dev/qemu_pipe",
"/system/lib/libc_malloc_debug_qemu.so",
"/sys/qemu_trace",
"/system/bin/qemu-props"
};
for (String file : knownFiles) {
if (new File(file).exists()) return true;
}
return false;
}
// 检测系统属性
private static boolean checkProperties() {
String[] props = {"ro.kernel.qemu", "ro.hardware.audio.primary"};
for (String prop : props) {
String value = SystemProperties.get(prop);
if ("1".equals(value) || "goldfish".equals(value)) return true;
}
return false;
}
}
3.2.4 尝试绑过模拟器检测
方法1:修改Build属性
# 在模拟器中修改build.prop
adb shell
su
mount -o rw,remount /system
echo "ro.product.model=Pixel 6" >> /system/build.prop
echo "ro.product.manufacturer=Google" >> /system/build.prop
结果:仍然被检测到。
方法2:使用Genymotion + ARM翻译
Genymotion是一款商业模拟器,支持ARM翻译层:
# 安装Genymotion
# 创建设备,选择带ARM翻译的镜像
# 安装APP
结果:APP启动后显示"设备环境异常",部分功能被禁用。
方法3:使用真机模拟器(如Anbox)
Anbox在Linux容器中运行Android:
# 安装Anbox
sudo apt install anbox
anbox session-manager
# 安装APP
adb install app.apk
结果:APP无法正常运行,缺少必要的Google服务。
3.3 云手机方案
3.3.1 什么是云手机?
云手机是运行在云端服务器上的虚拟Android设备,通过网络远程访问。
优势:
- 真实的ARM硬件(部分厂商)
- 可以Root
- 可以安装调试工具
3.3.2 测试多家云手机
我测试了市面上几家主流的云手机服务:
| 云手机 | ARM/x86 | Root | APP运行结果 |
|---|---|---|---|
| 红手指 | ARM | 支持 | 检测到云环境 |
| 多多云 | ARM | 支持 | 检测到云环境 |
| 雷电云 | x86 | 支持 | 检测到模拟器 |
| 网易MuMu云 | x86 | 支持 | 检测到模拟器 |
结果:所有云手机都被检测到。
3.3.3 云手机检测原理
APP可能通过以下方式检测云手机:
- IP地址检测:云手机的IP通常属于数据中心
- 设备指纹:云手机的设备信息可能有规律
- 性能特征:云手机的性能表现与真机不同
- 网络延迟:云手机的网络延迟模式异常
3.4 APK修改方案
3.4.1 思路
既然无法在运行时调试,能否直接修改APK,去掉检测代码?
3.4.2 定位检测代码
通过静态分析,我在smali代码中找到了一些可疑的检测类:
# 搜索可能的检测类
grep -r "Emulator" smali_classes*/ --include="*.smali"
grep -r "isRooted" smali_classes*/ --include="*.smali"
grep -r "frida" smali_classes*/ --include="*.smali"
grep -r "xposed" smali_classes*/ --include="*.smali"
找到的文件:
smali_classes2/com/dreamworld/security/DeviceChecker.smali
smali_classes2/com/dreamworld/security/EnvironmentDetector.smali
smali_classes2/com/dreamworld/security/IntegrityVerifier.smali
3.4.3 尝试修改smali代码
修改DeviceChecker.smali:
# 原始代码
.method public static isEmulator()Z
.registers 2
# 复杂的检测逻辑...
const/4 v0, 0x1 # 返回true(检测到模拟器)
return v0
.end method
# 修改后
.method public static isEmulator()Z
.registers 2
const/4 v0, 0x0 # 始终返回false
return v0
.end method
3.4.4 重新打包APK
# 重新打包
java -jar apktool.jar b dw_mall_unpacked -o dw_mall_modified.apk
# 生成签名密钥
keytool -genkey -v -keystore my.keystore -alias mykey -keyalg RSA -keysize 2048 -validity 10000
# 签名APK
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore my.keystore dw_mall_modified.apk mykey
# 对齐APK
zipalign -v 4 dw_mall_modified.apk dw_mall_aligned.apk
3.4.5 安装测试
# 卸载原版
adb uninstall com.dreamworld.app
# 安装修改版
adb install dw_mall_aligned.apk
结果:APP启动时显示"应用完整性校验失败",拒绝运行。
3.4.6 完整性校验分析
APP实现了签名校验和完整性校验:
// 签名校验示例
public static boolean verifySignature(Context context) {
try {
PackageInfo info = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature signature : info.signatures) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(signature.toByteArray());
String currentHash = Base64.encodeToString(hash, Base64.NO_WRAP);
// 与预期的签名哈希比较
if (!EXPECTED_SIGNATURE_HASH.equals(currentHash)) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
由于我们使用了自己的密钥签名,签名哈希与原始APK不同,校验失败。
3.5 绕过签名校验的尝试
3.5.1 Hook签名校验
如果能Hook签名校验函数,让它返回true,就能绕过检测。
但问题是:我们无法使用Frida等Hook工具(会被检测)。
3.5.2 修改签名校验代码
找到签名校验的smali代码并修改:
# 原始代码
.method public static verifySignature(Landroid/content/Context;)Z
.registers 5
# 复杂的校验逻辑...
# 校验失败时返回false
const/4 v0, 0x0
return v0
.end method
# 修改后
.method public static verifySignature(Landroid/content/Context;)Z
.registers 5
# 直接返回true
const/4 v0, 0x1
return v0
.end method
3.5.3 再次测试
重新打包、签名、安装...
结果:APP仍然检测到修改。
3.5.4 原因分析
APP的完整性校验不仅仅在Java层:
- Native层校验:SO库中也有签名校验代码
- DEX校验:检测DEX文件是否被修改
- 资源校验:检测资源文件的哈希值
- 多点校验:在多个地方进行校验,修改一处不够
这是一个纵深防御的典型案例。
3.6 Native层分析
3.6.1 使用IDA Pro分析SO库
既然Java层的修改会被Native层检测,我们需要分析Native层的代码。
# 使用IDA Pro打开libSecurityCore.so
ida64 libSecurityCore.so
3.6.2 找到关键函数
通过字符串搜索和交叉引用,找到了一些关键函数:
┌─────────────────────────────────────────────────────────────────┐
│ libSecurityCore.so 关键函数 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 初始化函数 │
│ ├── JNI_OnLoad → 动态注册JNI方法 │
│ ├── .init_array → 库加载时执行的函数 │
│ └── anti_debug_init → 反调试初始化 │
│ │
│ 检测函数 │
│ ├── check_frida → Frida检测 │
│ ├── check_xposed → Xposed检测 │
│ ├── check_root → Root检测 │
│ ├── check_emulator → 模拟器检测 │
│ ├── check_debugger → 调试器检测 │
│ └── verify_signature → 签名校验 │
│ │
│ 核心功能函数 │
│ ├── do_rsa_sign → RSA签名 │
│ ├── do_hmac_sign → HMAC签名 │
│ ├── do_aes_encrypt → AES加密 │
│ ├── do_aes_decrypt → AES解密 │
│ └── get_device_key → 获取设备密钥 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.6.3 反调试代码分析
在.init_array段中,找到了反调试初始化代码:
// IDA反编译的伪代码
void __attribute__((constructor)) anti_debug_init() {
// 创建检测线程
pthread_t thread;
pthread_create(&thread, NULL, detection_thread, NULL);
// 设置信号处理
signal(SIGTRAP, crash_handler);
signal(SIGBUS, crash_handler);
// 初始检测
if (check_frida() || check_debugger() || check_root()) {
// 触发崩溃
raise(SIGSEGV);
}
}
void* detection_thread(void* arg) {
while (1) {
// 持续检测
if (check_frida() || check_debugger()) {
raise(SIGSEGV);
}
usleep(100000); // 100ms
}
return NULL;
}
关键发现:
- 检测代码在库加载时就执行(
__attribute__((constructor))) - 有一个后台线程持续检测
- 检测到威胁后触发SIGSEGV导致崩溃
3.6.4 修改SO库的困难
要修改SO库,需要:
- 理解汇编代码:ARM64汇编,学习曲线陡峭
- 找到所有检测点:可能有多处检测
- 保持代码完整性:修改后不能破坏其他功能
- 绑过完整性校验:SO库本身可能也有校验
这需要大量的时间和专业知识。
3.7 转折点:Unidbg的发现
3.7.1 偶然的发现
就在我几乎要放弃的时候,我在一个技术论坛上看到了一篇文章,介绍了一个叫Unidbg的工具。
Unidbg是一个基于Unicorn引擎的Android Native库模拟执行框架,可以在PC上直接运行Android的SO库,无需真实的Android设备。
这让我眼前一亮!
3.7.2 Unidbg的优势
┌─────────────────────────────────────────────────────────────────┐
│ Unidbg vs 传统方法 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 传统方法的问题: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 需要在真实设备/模拟器上运行APP │ │
│ │ ↓ │ │
│ │ APP启动时执行检测代码 │ │
│ │ ↓ │ │
│ │ 检测到调试工具/异常环境 │ │
│ │ ↓ │ │
│ │ APP崩溃或拒绝运行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Unidbg的方法: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 在PC上创建模拟的Android环境 │ │
│ │ ↓ │ │
│ │ 只加载需要的SO库 │ │
│ │ ↓ │ │
│ │ 直接调用目标函数(如签名函数) │ │
│ │ ↓ │ │
│ │ 获取返回结果 │ │
│ │ │ │
│ │ ✓ 不运行完整APP,检测代码不会执行 │ │
│ │ ✓ 完全可控的环境,可以Hook任意函数 │ │
│ │ ✓ 可以单步调试Native代码 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.7.3 为什么Unidbg能绑过检测?
关键在于:Unidbg不运行完整的APP,只模拟执行SO库中的特定函数。
APP的检测逻辑通常在:
- Application.onCreate()
- Activity.onCreate()
- SO库的.init_array
但如果我们只调用签名函数,不触发这些初始化代码,检测就不会执行!
3.7.4 初步验证
让我快速验证一下Unidbg是否可行:
// 简单的Unidbg测试
public class QuickTest {
public static void main(String[] args) {
// 创建模拟器
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
// 创建虚拟机
VM vm = emulator.createDalvikVM();
// 加载SO库
DalvikModule dm = vm.loadLibrary(new File("libSecurityCore.so"), false);
// 调用JNI_OnLoad
dm.callJNI_OnLoad(emulator);
System.out.println("SO库加载成功!");
}
}
运行结果:
SO库加载成功!
没有崩溃! SO库成功加载了!
这意味着Unidbg确实可以绑过APP的检测机制!
3.8 本章小结
3.8.1 关键发现
经过本章的探索,我们得出了以下结论:
-
常规方法全部失效
- 网络抓包:代理检测 + 自定义网络栈
- 动态调试:Native层多重检测
- 模拟器:全面的环境检测
- APK修改:签名校验 + 完整性校验
-
APP的防护体系
- 纵深防御:多层检测,每层独立
- 早期检测:在库加载时就执行
- 持续检测:后台线程持续监控
- 快速响应:检测到立即崩溃
-
突破口:Unidbg
- 不运行完整APP,绑过检测
- 可以直接调用SO库中的函数
- 完全可控的模拟环境
3.8.2 技术启示
这款APP的安全防护给我们的启示:
- 安全是一个系统工程:单点防护容易被绑过,需要多层防护
- Native层更安全:Java层容易被反编译和修改
- 早期检测很重要:越早检测,攻击者越难干预
- 没有绝对的安全:再强的防护也有突破口
3.8.3 下一步
既然找到了突破口,接下来我们需要:
- 深入学习Unidbg:了解其原理和使用方法
- 搭建Unidbg环境:配置开发环境
- 实现签名调用:调用SO库中的签名函数
- 构建完整方案:实现可用的数据获取系统
本章思考题
-
为什么APP选择在检测到威胁后"崩溃"而不是"静默失败"?
-
如果你是安全工程师,如何防御Unidbg这类离线模拟执行工具?
-
除了Unidbg,还有哪些方法可以在不触发检测的情况下分析Native代码?
章节附录
A. 本章涉及的工具
| 工具 | 用途 | 官网 |
|---|---|---|
| IDA Pro | 反汇编分析 | hex-rays.com/ida-pro/ |
| Ghidra | 反汇编分析 | ghidra-sre.org/ |
| APKTool | APK反编译 | ibotpeaches.github.io/Apktool/ |
| Unidbg | Native库模拟 | github.com/zhkl0228/un… |
B. 常见检测方法汇总
| 检测类型 | 检测方法 | 绑过难度 |
|---|---|---|
| Root检测 | su文件、Superuser.apk | 中 |
| 模拟器检测 | Build属性、传感器、文件 | 高 |
| Frida检测 | 端口、进程、内存特征 | 高 |
| Xposed检测 | 堆栈特征、类加载器 | 中 |
| 调试器检测 | ptrace、TracerPid | 中 |
| 签名校验 | PackageManager | 高 |
C. 参考资料
- Android安全机制:source.android.com/security
- Unidbg项目:github.com/zhkl0228/un…
- Unicorn引擎:www.unicorn-engine.org/
本章完