第三章:常规手段全面失效

44 阅读12分钟

第三章:常规手段全面失效

本章字数:约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 FrameworkAPP检测到并警告             │
│  ├── VirtualXposedAPP检测到虚拟环境           │
│  ├── 太极(TaiChi)                  → APP检测到                   │
│  └── LSPosed                       → 不稳定,偶尔成功            │
│                                                                  │
│  静态分析(部分成功)                                            │
│  ├── APKTool反编译                 → 成功,获得smali代码         │
│  ├── jadx反编译                    → 成功,获得Java代码          │
│  ├── IDA Pro分析SO                 → 部分成功,代码被混淆        │
│  └── Ghidra分析SO                  → 部分成功,代码被混淆        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3.1.2 失败原因分析

为什么这些方法都失败了?让我们深入分析:

网络层失败的原因

  1. APP实现了代理检测,检测到HTTP代理后绕过系统网络栈
  2. APP使用了自定义网络实现,不走系统的网络API
  3. APP可能使用了非标准端口,绑过了iptables规则
  4. 所有通信都使用TLS 1.3加密,即使抓到也看不到内容

动态调试失败的原因

  1. APP在Native层实现了多重检测
  2. 检测在库加载时执行,比注入更早
  3. 检测方法多样化:端口、进程、内存、ptrace等
  4. 检测到后立即崩溃,不给攻击者反应时间

静态分析的局限

  1. 核心逻辑在Native层,反汇编后可读性差
  2. 代码经过混淆,函数名、变量名都是无意义的字符
  3. 存在反调试代码,静态分析难以完全理解运行时行为
  4. 加密算法的具体实现难以从汇编代码中还原

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/x86RootAPP运行结果
红手指ARM支持检测到云环境
多多云ARM支持检测到云环境
雷电云x86支持检测到模拟器
网易MuMu云x86支持检测到模拟器

结果:所有云手机都被检测到。

3.3.3 云手机检测原理

APP可能通过以下方式检测云手机:

  1. IP地址检测:云手机的IP通常属于数据中心
  2. 设备指纹:云手机的设备信息可能有规律
  3. 性能特征:云手机的性能表现与真机不同
  4. 网络延迟:云手机的网络延迟模式异常

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层:

  1. Native层校验:SO库中也有签名校验代码
  2. DEX校验:检测DEX文件是否被修改
  3. 资源校验:检测资源文件的哈希值
  4. 多点校验:在多个地方进行校验,修改一处不够

这是一个纵深防御的典型案例。


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;
}

关键发现

  1. 检测代码在库加载时就执行(__attribute__((constructor))
  2. 有一个后台线程持续检测
  3. 检测到威胁后触发SIGSEGV导致崩溃

3.6.4 修改SO库的困难

要修改SO库,需要:

  1. 理解汇编代码:ARM64汇编,学习曲线陡峭
  2. 找到所有检测点:可能有多处检测
  3. 保持代码完整性:修改后不能破坏其他功能
  4. 绑过完整性校验: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 关键发现

经过本章的探索,我们得出了以下结论:

  1. 常规方法全部失效

    • 网络抓包:代理检测 + 自定义网络栈
    • 动态调试:Native层多重检测
    • 模拟器:全面的环境检测
    • APK修改:签名校验 + 完整性校验
  2. APP的防护体系

    • 纵深防御:多层检测,每层独立
    • 早期检测:在库加载时就执行
    • 持续检测:后台线程持续监控
    • 快速响应:检测到立即崩溃
  3. 突破口:Unidbg

    • 不运行完整APP,绑过检测
    • 可以直接调用SO库中的函数
    • 完全可控的模拟环境

3.8.2 技术启示

这款APP的安全防护给我们的启示:

  1. 安全是一个系统工程:单点防护容易被绑过,需要多层防护
  2. Native层更安全:Java层容易被反编译和修改
  3. 早期检测很重要:越早检测,攻击者越难干预
  4. 没有绝对的安全:再强的防护也有突破口

3.8.3 下一步

既然找到了突破口,接下来我们需要:

  1. 深入学习Unidbg:了解其原理和使用方法
  2. 搭建Unidbg环境:配置开发环境
  3. 实现签名调用:调用SO库中的签名函数
  4. 构建完整方案:实现可用的数据获取系统

本章思考题

  1. 为什么APP选择在检测到威胁后"崩溃"而不是"静默失败"?

  2. 如果你是安全工程师,如何防御Unidbg这类离线模拟执行工具?

  3. 除了Unidbg,还有哪些方法可以在不触发检测的情况下分析Native代码?

章节附录

A. 本章涉及的工具

工具用途官网
IDA Pro反汇编分析hex-rays.com/ida-pro/
Ghidra反汇编分析ghidra-sre.org/
APKToolAPK反编译ibotpeaches.github.io/Apktool/
UnidbgNative库模拟github.com/zhkl0228/un…

B. 常见检测方法汇总

检测类型检测方法绑过难度
Root检测su文件、Superuser.apk
模拟器检测Build属性、传感器、文件
Frida检测端口、进程、内存特征
Xposed检测堆栈特征、类加载器
调试器检测ptrace、TracerPid
签名校验PackageManager

C. 参考资料

  1. Android安全机制:source.android.com/security
  2. Unidbg项目:github.com/zhkl0228/un…
  3. Unicorn引擎:www.unicorn-engine.org/

本章完