第二十九讲:移动应用逆向(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逆向的核心知识结构:
静态分析帮我们理解代码结构,动态分析让我们看到运行时行为。两者结合,才能完成一次完整的逆向分析。我个人习惯先做静态分析,理清整体逻辑,再用Frida验证关键点。这样效率最高。
法律提醒:逆向分析请遵守当地法律法规。未经授权逆向他人App,可能涉及侵权。我分享这些技术,是为了让大家更好地保护自己的应用安全,而不是用于非法目的。
好了,Android逆向的基础知识就讲到这里。记住,工具只是辅助,真正的功夫在于理解底层原理。多动手、多实践,你也能成为逆向高手。
公众号:蓝海资料掘金营,微信deep3321