App逆向初探

📚 本文将带你从「静态看APK」到「动态改行为」,入门移动端安全分析/爬虫前置技能。


课程目标

用3分钟明确本次学习的可落地成果

  • 30秒快速拆解APK核心组成
  • 1分钟选定场景启动反编译工具
  • 复制粘贴即可用的3类Frida Hook脚本(SSL/Root/通用函数)
  • 加密App抓包+还原签名逻辑的完整4步流程

1. APK解析基础

1.1 APK核心结构「先拆再看」

很多新手以为APK是「特殊的二进制文件」,其实它就是加了签名的标准ZIP压缩包!我们可以直接用解压工具或者Python原生库扫描:

# Windows:直接用WinRAR/7-Zip解压(记得先备份!)
# Mac/Linux:unzip -d ./test_apk example.apk

高频接触的7个核心目录/文件

example.apk/
├── AndroidManifest.xml  # 二进制清单 → 含包名、权限、四大组件(活动/服务/广播/内容提供)
├── classes*.dex         # Dalvik/ART字节码 → Java/Kotlin代码编译成的可执行文件
├── lib/                 # 原生库目录 → arm64-v8a/armeabi-v7a/x86等ABI对应的so文件(高性能/底层加密常用)
├── res/                 # 编译资源 → 布局、图片、字符串的索引版
├── resources.arsc       # 资源索引表 → 把res里的索引映射成内存地址
└── META-INF/            # 签名校验区 → 确保APK没有被篡改

新手友好的「一键扫描脚本」(Python原生)

无需安装额外工具,快速定位敏感文件:

import zipfile

def quick_scan_apk(apk_path: str):
    try:
        with zipfile.ZipFile(apk_path, "r") as z:
            all_files = z.namelist()
            print(f"✅ APK解析成功!总文件数: {len(all_files)}\n")
            
            # 1. 找DEX文件(有多个说明代码量可能大/混淆拆分过)
            dex_list = [f for f in all_files if f.endswith(".dex")]
            print(f"📝 DEX文件: {dex_list}\n")
            
            # 2. 找原生库ABI(选设备对应ABI的so分析,比如手机一般选arm64-v8a)
            abi_list = list(set([f.split("/")[1] for f in all_files if f.startswith("lib/")]))
            print(f"🔧 支持的原生库ABI: {abi_list}\n")
            
            # 3. 找签名文件(后续重打包要替换)
            sign_list = [f for f in all_files if f.startswith("META-INF/")]
            print(f"🔒 签名文件: {sign_list}")
    except Exception as e:
        print(f"❌ 解析失败: {e}")

if __name__ == "__main__":
    quick_scan_apk("example.apk") # 换成你的APK路径

1.2 反编译三剑客「按需选用」

选对工具能节省90%的时间!按推荐优先级排序

工具名称核心能力推荐场景
jadx-guiDEX → 带语法高亮的Java代码;自动脱常见轻量混淆壳绝大多数场景首选,快速定位加密/网络核心代码
apktool完整解码/编码smali代码;无损修改资源/布局;重打包基础需要改smali逻辑、加调试开关、替换证书时用
dex2jar+JD-GUI轻量DEX → JAR;快速浏览第三方库(如okhttp/glide)代码临时看某段第三方逻辑,不想装jadx-gui时用

jadx-gui 核心使用(超简单!)

  1. 下载:jadx GitHub Release页,选对应系统的压缩包
  2. 运行:
    # Mac/Linux终端
    ./jadx/bin/jadx-gui example.apk
    # Windows双击jadx-gui.bat,选APK
  3. 搜索技巧:顶部搜索栏选「Method Name」「Class Name」「Text」,输入sign/encrypt/OkHttpClient快速定位

2. Hook技术之神:Frida

Frida是无需重打包、无需Root(可选)、跨平台的「动态行为注入工具」——简单来说就是在App运行时,把一段JS代码塞进去,修改它的函数逻辑

2.1 5分钟快速搭建环境

PC端(Python3+已装好)

pip install frida-tools # 自动安装frida和frida-tools

设备端(以Android真机为例,Root/非Root都可以,但新手推荐Root机)

  1. 查看设备ABI:终端输入adb shell getprop ro.product.cpu.abi
  2. 下载对应版本的frida-server:frida GitHub Release页(文件名格式:frida-server-版本号-android-ABI
  3. 推送并启动:
    # 推送到临时目录(Root机专用)
    adb push frida-server-16.4.11-android-arm64 /data/local/tmp/frida
    # 赋予执行权限
    adb shell chmod 755 /data/local/tmp/frida
    # 后台启动(Root机)
    adb shell su -c "/data/local/tmp/frida &"
  4. 验证连接:
    frida-ps -U # 列出USB设备上的所有进程(能看到说明连接成功!)

2.2 复制粘贴即可用的3类Frida Hook脚本

脚本1:通用Java方法Hook(打印入参/返回值/调用栈)

快速定位加密函数的输入输出调用来源,新手必备!

// hook_common.js
Java.perform(function() {
    // 1. 替换成目标类的完整包名
    var TargetClass = Java.use("com.example.app.utils.SignUtils");
    // 2. 替换成目标方法名(支持重载:["getSign", "[B", "java.lang.String"])
    var TargetMethod = TargetClass["getSign"];

    // 重写目标方法
    TargetMethod.implementation = function() {
        console.log("\n========== 目标方法被调用 ==========");
        console.log(`🔍 调用类: ${this.getClass().getName()}`);
        
        // 打印所有入参
        console.log(`📥 入参数量: ${arguments.length}`);
        for (let i = 0; i < arguments.length; i++) {
            let arg = arguments[i];
            // 自动识别类型并转成字符串
            let argType = arg ? arg.getClass().getName() : "null";
            let argStr = arg ? (argType === "[B" ? "Hex: " + byteArrayToHex(arg) : arg.toString()) : "null";
            console.log(`  参数${i}[${argType}]: ${argStr}`);
        }

        // 调用原方法获取返回值
        let result = this.getSign.apply(this, arguments);
        
        // 打印返回值
        let resType = result ? result.getClass().getName() : "null";
        let resStr = result ? (resType === "[B" ? "Hex: " + byteArrayToHex(result) : result.toString()) : "null";
        console.log(`📤 返回值[${resType}]: ${resStr}`);

        // 打印调用栈(想看是谁调用了目标方法时启用)
        // console.log("\n📌 调用栈:");
        // console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));

        console.log("=====================================\n");
        return result; // 必须返回原方法的结果,不然App会崩!
    };

    // 辅助函数:字节数组转Hex字符串
    function byteArrayToHex(byteArray) {
        var hex = "";
        for (var i = 0; i < byteArray.length; i++) {
            var b = byteArray[i] & 0xff;
            if (b < 0x10) hex += "0";
            hex += b.toString(16);
        }
        return hex;
    }
});

运行方式:

# 附加到已启动的App(App要先在前台)
frida -U com.example.app -l hook_common.js
# 或者从Frida启动App(更常用,不会错过启动时的Hook)
frida -U -f com.example.app -l hook_common.js --no-pause

脚本2:SSL Pinning绕过(通用版,覆盖90%+场景)

抓包前的第一道坎!这个脚本覆盖了OkHttp3、Android 7+系统证书、WebView的Pinning:

// ssl_pinning_bypass.js
console.log("[.] 开始绕过SSL Pinning...");

Java.perform(function() {
    // 1. 绕过OkHttp3/4 CertificatePinner
    try {
        var CertificatePinner = Java.use("okhttp3.CertificatePinner");
        CertificatePinner.check.overload("java.lang.String", "java.util.List").implementation = function() {};
        CertificatePinner.check.overload("java.lang.String", "java.security.cert.Certificate", "java.util.List").implementation = function() {};
        console.log("[+] OkHttp3/4 CertificatePinner 绕过成功");
    } catch(e) {}

    // 2. 绕过Android 7+ Conscrypt TrustManagerImpl
    try {
        var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
        TrustManagerImpl.verifyChain.implementation = function() {
            return arguments[0]; // 直接返回证书链,不校验
        };
        console.log("[+] Android 7+ Conscrypt TrustManagerImpl 绕过成功");
    } catch(e) {}

    // 3. 绕过Android 6+ X509TrustManager扩展
    try {
        var X509ExtendedTrustManager = Java.use("javax.net.ssl.X509ExtendedTrustManager");
        var EmptyTrustManager = Java.registerClass({
            name: "com.example.EmptyTrustManager",
            implements: [X509ExtendedTrustManager],
            methods: {
                checkClientTrusted: function() {},
                checkServerTrusted: function() {},
                getAcceptedIssuers: function() { return []; },
                checkClientTrustedSSLEngine: function() {},
                checkServerTrustedSSLEngine: function() {},
            }
        });
        var SSLContext = Java.use("javax.net.ssl.SSLContext");
        SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(km, tm, sr) {
            var emptyTm = Java.array("Ljavax.net.ssl.TrustManager;", [EmptyTrustManager.$new()]);
            return this.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, km, emptyTm, sr);
        };
        console.log("[+] X509ExtendedTrustManager 绕过成功");
    } catch(e) {}

    // 4. 绕过WebView SSL错误(可选,抓H5包时用)
    try {
        var WebViewClient = Java.use("android.webkit.WebViewClient");
        WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
            handler.proceed();
        };
        console.log("[+] WebView SSL错误 绕过成功");
    } catch(e) {}

    console.log("[✅] 所有SSL Pinning绕过完成!");
});

脚本3:Root检测绕过(通用基础版)

抓包+Hook前的第二道坎!这个脚本覆盖了常见的文件检查、系统属性检查、Build字段检查:

// root_bypass.js
console.log("[.] 开始绕过Root检测...");

Java.perform(function() {
    // 1. 隐藏常见的Root相关文件
    try {
        var File = Java.use("java.io.File");
        var rootFiles = [
            "/sbin/su", "/system/bin/su", "/system/xbin/su",
            "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
            "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su",
            "/sbin/which", "/system/bin/which", "/system/xbin/which",
            "/system/app/Superuser.apk", "/system/app/SuperSU.apk", "/system/app/Magisk.apk"
        ];
        
        // 重写File构造函数
        File.$init.overload("java.lang.String").implementation = function(path) {
            if (rootFiles.indexOf(path) !== -1) path = "/nonexistent_file_xyz";
            return this.$init.overload("java.lang.String").call(this, path);
        };
        
        // 重写File.exists()
        File.exists.implementation = function() {
            var path = this.getAbsolutePath().toString();
            if (rootFiles.indexOf(path) !== -1) return false;
            return this.exists.call(this);
        };
        
        console.log("[+] Root相关文件隐藏成功");
    } catch(e) {}

    // 2. 修改Build.TAGS为release-keys
    try {
        var Build = Java.use("android.os.Build");
        Build.TAGS.value = "release-keys";
        console.log("[+] Build.TAGS 修改成功");
    } catch(e) {}

    // 3. 修改系统属性ro.debuggable/ro.secure(可选)
    try {
        var SystemProperties = Java.use("android.os.SystemProperties");
        SystemProperties.get.overload("java.lang.String").implementation = function(key) {
            if (key === "ro.debuggable") return "0";
            if (key === "ro.secure") return "1";
            if (key === "ro.build.type") return "user";
            return this.get.overload("java.lang.String").call(this, key);
        };
        console.log("[+] 系统属性修改成功");
    } catch(e) {}

    console.log("[✅] 所有Root检测绕过完成!");
});

3. 实践思路:4步快速分析加密App

假设目标App有sign签名参数SSL Pinning,结合上面的工具和脚本,完整流程如下:

  1. 静态定位:用jadx-gui打开APK,搜索sign/encrypt/OkHttpClient定位SignUtils.getSign()和网络请求逻辑
  2. 环境准备:配置Burp Suite代理,安装CA证书到设备用户证书目录
  3. 动态启动:从Frida启动App并同时运行双脚本:
    frida -U -f com.example.app -l ssl_pinning_bypass.js -l hook_common.js --no-pause
  4. 还原逻辑:操作App触发请求,Burp抓包看sign的实际值,Frida打印getSign的入参,手动写Python脚本还原签名逻辑

4. 合法使用声明

本文内容仅用于合法的安全研究、自有App的漏洞排查、教学演示,不得用于未经授权的App逆向、数据窃取、商业盈利等非法用途,违反者需自行承担所有法律责任。


本章总结

  • 静态分析:APK是ZIP压缩包,jadx-gui首选快速看Java代码
  • 动态分析:Frida无需重打包,通用脚本覆盖90%+入门场景
  • 入门实践:先抓包(绕SSL),再Hook(看入参返回),最后还原逻辑

后续可以深入学习:smali语法修改、so层Hook(Frida Interceptor)、Frida高级隐藏(对抗反调试)。