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  # 二进制清单 → 包名、权限、四大组件(Activity/Service/Broadcast/ContentProvider)
├── 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 高级隐藏(对抗反调试)。