26、移动应用逆向(Android):APK结构、DEX与Smali、Jadx与Frida基础、常见保护绕过

好,咱们今天聊Android逆向。说实话,这活儿我干了快十年,从最早用ApkTool解包看XML,到现在用Frida动态插桩,工具链换了好几茬。但核心的东西没变——你得先搞清楚APK这个“壳”里到底装了啥。

APK结构:拆开看看里面有什么

APK其实就是一个ZIP压缩包。你把它后缀改成.zip,解压出来,能看到这么几个关键东西:

  • AndroidManifest.xml:应用的“身份证”。包名、权限、四大组件(Activity、Service、BroadcastReceiver、ContentProvider)全在这里声明。注意,它是二进制XML,得用工具反编译才能看。
  • classes.dex:核心中的核心。所有Java/Kotlin代码编译后的Dalvik字节码文件。一个APK可能有多个dex(比如classes2.dex、classes3.dex),说明方法数超了65535。
  • resources.arsc:资源索引表。字符串、颜色、布局ID都映射在这里。逆向时经常需要查这个文件来找资源ID。
  • lib/:Native库。armeabi-v7a、arm64-v8a、x86这些目录下放的是.so文件。很多核心逻辑或保护代码会写在这里。
  • META-INF/:签名信息。篡改APK后必须重新签名,否则装不上。

我个人习惯:拿到APK第一件事,先看AndroidManifest.xml里的android:debuggableandroid:extractNativeLibs。前者决定能不能直接调试,后者影响Native库的加载方式。很多加固壳会在这里做手脚。

DEX与Smali:字节码的世界

DEX文件是Android的“可执行文件”。它不像Java的.class文件那样直接对应JVM指令,而是用了一套精简的Dalvik指令集。为什么要精简?因为早期手机内存小、CPU慢,DEX的设计目标就是省空间、省带宽。

Smali是什么?它是DEX的反汇编表示。说白了,就是把二进制字节码翻译成人能读懂的文本格式。每个Smali文件对应一个Java类,方法、字段、注解都保留。

看一段简单的Smali代码:

.class public Lcom/example/MainActivity;
.super Landroid/app/Activity;

.method public onCreate(Landroid/os/Bundle;)V
    .registers 3
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    const v0, 0x7f09001c
    invoke-virtual {p0, v0}, Lcom/example/MainActivity;->setContentView(I)V

    return-void
.end method

嗯,这里要注意:.registers 3表示这个方法用了3个寄存器,p0是this引用,p1是第一个参数。invoke-super调用父类方法,const v0, 0x7f09001c加载资源ID,然后setContentView设置布局。

避坑指南:我曾经在分析一个混淆过的应用时,发现Smali代码里大量使用了gotonop指令来打乱控制流。这时候别硬读Smali,用Jadx看Java伪代码效率高得多。Smali更适合做精确的代码修改——比如改if-else条件、注入日志。

Jadx与Frida:静态与动态的黄金搭档

Jadx是我最常用的静态分析工具。它能把DEX直接反编译成可读的Java代码,支持搜索字符串、类名、方法名,还能导出Gradle项目。对于没有加固的应用,Jadx基本能还原90%以上的逻辑。

但静态分析有局限——遇到动态加载、反射调用、代码混淆,Jadx就抓瞎了。这时候得上Frida。

Frida是个动态插桩框架。它允许你在运行时Hook任意Java方法或Native函数,修改参数、返回值,甚至替换整个方法实现。我常用的几个场景:

  • 绕过SSL Pinning:Hook javax.net.ssl.SSLContextinit 方法,替换TrustManager。
  • 查看加密参数:Hook javax.crypto.CipherdoFinal 方法,打印明文和密文。
  • 修改返回值:比如把 isVip() 的返回值从false改成true。

一个简单的Frida脚本示例:

Java.perform(function() {
    var MainActivity = Java.use('com.example.MainActivity');
    MainActivity.isVip.implementation = function() {
        console.log('[+] isVip called, returning true');
        return true;
    };
});

你想想看,这比静态分析灵活多了。不需要重新打包、签名,直接运行时改逻辑。

注意:Frida需要root权限或使用frida-gadget注入。生产环境的应用通常有反调试检测,会检测frida-server进程或/data/local/tmp/下的文件。我遇到过几次,Frida一启动应用就闪退,后来发现是检测了ptrace状态。

常见保护绕过:实战中的那些坑

现在的应用几乎没有裸奔的。加固、混淆、反调试、反Hook,一套组合拳打下来,新手很容易懵。我总结了几种常见的保护及其绕过思路:

保护类型 原理 绕过方法
代码混淆(ProGuard) 类名、方法名、字段名变成a、b、c Jadx反编译后看逻辑,结合字符串搜索定位关键点
资源加密 图片、布局文件被加密,运行时解密 Hook解密函数,dump解密后的资源
反调试(ptrace) 检测自身是否被调试器附加 Frida的--no-pause参数,或修改/proc/self/status的TracerPid
反Hook 检测Frida特征(端口、进程名) 使用frida-gadget注入,或修改frida-server端口
DEX加固 壳程序在运行时解密原始DEX并加载 内存dump,找到解密后的DEX文件

举个例子。我曾经分析过一个金融类应用,它用了360加固。Jadx打开只能看到壳的代码,原始DEX被加密了。怎么搞?

第一步,用Frida脚本Hook dalvik.system.DexClassLoader 的构造函数,打印加载的DEX路径。第二步,在loadClass方法处下断点,等壳把原始DEX加载到内存后,用Java.perform遍历BaseDexClassLoaderpathList字段,找到DexFile对象,然后调用DexFilegetBytes方法把DEX dump出来。

嗯,这里要注意:有些加固壳会检测DexClassLoader是否被Hook,这时候得用更底层的方法——直接读/proc/self/maps,找到DEX的内存映射区域,然后memcpy出来。

核心思路:无论保护多复杂,最终代码都要在CPU上执行。静态分析不行就动态调试,动态调试被检测就改检测逻辑。逆向的本质是“时间差”——你比保护代码跑得快,你就赢了。

知识体系总览

下面这张图是我自己整理的Android逆向知识体系,从APK结构到动态Hook,再到保护绕过,一条线串下来。建议你保存下来,每次遇到新应用时对照着看,缺哪块补哪块。

Android逆向知识体系 APK结构 AndroidManifest.xml classes.dex resources.arsc lib/ (Native库) DEX与Smali Dalvik字节码 Smali反汇编 寄存器与指令 代码修改与注入 Jadx + Frida Jadx:静态反编译Java代码 Frida:动态Hook Java/Native 内存dump与调试 常见保护绕过 代码混淆 反调试 DEX加固 反Hook

好了,这一章的内容就这些。从APK结构到DEX/Smali,再到Jadx和Frida的配合使用,最后聊了保护绕过的思路。这些东西你多练几次,自然就熟了。

个人建议:别光看,动手。找个简单的应用(比如计算器、记事本),用Jadx反编译看看代码结构,再用Frida Hook一个方法试试。踩过坑才能记住。


公众号:蓝海资料掘金营,微信deep3321