第二十九讲:移动应用逆向(Android)——从APK到Smali,再到动态Hook

各位同学,今天我们来聊聊Android逆向。说实话,这个领域水挺深的。我做了这么多年安全研究,Android逆向始终是绕不开的一环。你想想看,现在谁手机里没几十个App?这些App里藏着多少秘密?嗯,我们今天就来揭开这层面纱。

APK文件结构:一个压缩包里的世界

APK文件,说白了就是一个ZIP压缩包。你把它后缀改成.zip,用解压软件打开,就能看到里面的结构。我刚开始学逆向时,第一件事就是拿个APK解压了看看里面都有啥。

一个典型的APK包含这些关键部分:

  • AndroidManifest.xml:App的身份证,记录了权限、组件、入口等信息。不过它是二进制格式,直接打开是乱码。
  • classes.dex:核心代码文件,所有Java/Kotlin代码编译后的产物。
  • resources.arsc:编译后的资源索引表。
  • lib/:Native库,通常是.so文件,C/C++写的。
  • META-INF/:签名信息,篡改APK后这里会报错。
  • res/:原始资源文件,图片、布局等。

核心要点:逆向分析时,我们最关注的是classes.dex和lib/目录。前者包含了Java层的逻辑,后者藏着Native层的秘密。

DEX/ODEX文件格式:Dalvik的字节码

DEX是Dalvik Executable的缩写。Android早期用的是Dalvik虚拟机,后来换成了ART。但不管怎么换,DEX文件格式基本没变。

DEX文件的结构大致是这样的:

  • 文件头:魔数、校验和、各区域偏移量等元信息。
  • 字符串表:所有用到的字符串,包括类名、方法名、字段名。
  • 类型表:所有用到的类型引用。
  • 原型表:方法签名信息。
  • 字段表:类中定义的字段。
  • 方法表:类中定义的方法。
  • 类定义表:所有类的定义信息。
  • 数据区:实际的字节码指令。

ODEX呢?它是优化后的DEX。Android 5.0之后,ART编译器会在安装时把DEX编译成ODEX(实际上是ELF格式的oat文件)。我曾经在分析一个加固App时,发现它的DEX文件被抽空了,真正的代码藏在ODEX里。那会儿折腾了好几天才找到正确的偏移量。

个人经验:分析ODEX时,我建议先用oat2dex工具把它转回DEX,再用Jadx打开。直接硬啃ODEX的二进制结构,效率太低了。

使用Jadx/JEB反编译:从字节码到可读代码

反编译工具是我们的眼睛。我个人最常用的是Jadx,开源免费,效果也不错。JEB是商业软件,功能更强大,但价格不菲。

Jadx的使用

# 命令行反编译
jadx -d output_dir target.apk

# 带反混淆
jadx --deobf -d output_dir target.apk

# 只反编译特定包名
jadx -d output_dir --include "com.example" target.apk

Jadx会把DEX反编译成Java代码,虽然有时候变量名会变成a、b、c这种,但逻辑基本能看懂。我记得有一次分析一个金融App,Jadx直接还原出了完整的加密算法,省了我大量时间。

JEB的使用场景

  • 遇到混淆严重的代码,JEB的Native调试器能帮你动态分析。
  • JEB对OAT/ODEX的支持更好,可以直接打开不转换。
  • JEB的脚本引擎可以批量处理,适合大规模分析。

注意:反编译出来的代码仅供参考。有些App会做控制流混淆,反编译后的代码可能和源码差异很大。这时候就需要结合动态分析了。

Smali语法基础:Dalvik的汇编语言

Smali是DEX字节码的文本表示。你想想看,Java代码被编译成字节码,字节码再被反编译成Smali。所以Smali比Java更接近底层。

一个简单的Smali例子:

.class public Lcom/example/Hello;
.super Ljava/lang/Object;

.method public static sayHello()V
    .registers 2
    
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "Hello, World!"
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    return-void
.end method

Smali的关键语法点:

  • .class:定义类名和访问权限。
  • .super:父类。
  • .method:方法定义,后面跟方法签名。
  • .registers:声明寄存器数量。v0、v1这些就是寄存器。
  • 指令:sget-object、const-string、invoke-virtual等,对应Java里的各种操作。
  • return-void:返回空值。

为什么要学Smali?因为有些时候Jadx反编译出来的Java代码不完整,或者逻辑混乱。这时候直接看Smali反而更清楚。我曾经在分析一个恶意软件时,Jadx反编译出来的代码全是goto,根本没法看。切换到Smali模式,反而理清了它的控制流。

避坑指南:修改Smali代码后重新打包,记得重新签名。我曾经改完代码忘了签名,安装时直接报错,折腾了半天才发现问题。

使用Frida进行动态Hook:运行时修改行为

静态分析只能看到代码的静态结构,动态分析才能看到运行时的真实行为。Frida是我最爱的动态Hook工具,没有之一。

Frida的基本原理:它把JavaScript引擎注入到目标进程,然后你就可以用JS代码来Hook任意函数、修改返回值、调用内部方法。

一个简单的Frida Hook脚本:

// Hook Java方法
Java.perform(function() {
    var MainActivity = Java.use('com.example.MainActivity');
    
    MainActivity.secretMethod.implementation = function() {
        console.log('secretMethod被调用了!');
        // 调用原始方法
        return this.secretMethod();
    };
    
    // Hook构造函数
    MainActivity.$init.implementation = function() {
        console.log('MainActivity被创建了');
        this.$init();
    };
});

// Hook Native函数
Interceptor.attach(Module.findExportByName('libnative.so', 'check_password'), {
    onEnter: function(args) {
        console.log('check_password called');
        console.log('arg0: ' + args[0].readCString());
    },
    onLeave: function(retval) {
        console.log('返回值: ' + retval);
        // 修改返回值
        retval.replace(1);
    }
});

Frida的常用操作:

  • Java.use():获取Java类引用。
  • .implementation:替换方法实现。
  • Interceptor.attach():Hook Native函数。
  • Module.findExportByName():查找导出函数地址。
  • args[]:访问函数参数。
  • retval:修改返回值。

个人经验:Frida脚本调试时,我习惯先用console.log打印关键信息。等逻辑跑通了,再逐步添加Hook逻辑。另外,Frida的-r参数可以热重载脚本,不用每次都重启App。

知识体系总览

下面这张图展示了Android逆向的核心知识结构:

Android逆向核心知识体系 APK文件 classes.dex 静态分析 动态分析 Jadx / JEB 反编译 Smali 语法分析 Frida 动态Hook 运行时行为监控 静态 + 动态 = 完整的逆向分析

静态分析帮我们理解代码结构,动态分析让我们看到运行时行为。两者结合,才能完成一次完整的逆向分析。我个人习惯先做静态分析,理清整体逻辑,再用Frida验证关键点。这样效率最高。

法律提醒:逆向分析请遵守当地法律法规。未经授权逆向他人App,可能涉及侵权。我分享这些技术,是为了让大家更好地保护自己的应用安全,而不是用于非法目的。

好了,Android逆向的基础知识就讲到这里。记住,工具只是辅助,真正的功夫在于理解底层原理。多动手、多实践,你也能成为逆向高手。


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