13、SO文件逆向入门:ARM与Thumb指令集、IDA Pro静态分析、JNI函数定位
各位同学,欢迎来到SO文件逆向的世界。说实话,很多搞Android逆向的朋友,一开始都卡在SO文件这一关。Java层反编译看看逻辑还行,一碰到Native代码就头大。我当年也是这样,对着IDA Pro里一堆十六进制字节码发呆,完全不知道从哪下手。
但你别怕。SO文件逆向,说白了就是跟ARM指令打交道。你只要搞懂ARM和Thumb这两种指令模式的区别,学会用IDA Pro这把利器,再掌握JNI函数的定位技巧,基本上就能在Native层横着走了。今天我就带你把这几个硬骨头啃下来。
核心要点:SO文件逆向的本质,是把二进制机器码还原成可读的汇编指令,然后理解它的执行逻辑。ARM和Thumb是两种不同的指令编码方式,IDA Pro是我们的翻译官,JNI函数定位则是找到Native代码的入口。
13.1 ARM与Thumb指令集:两种不同的“方言”
ARM处理器支持两种指令集模式:ARM模式和Thumb模式。你可以把它们想象成同一种语言的两种方言。ARM模式指令长度固定为32位,功能强大,但占空间。Thumb模式指令长度可以是16位或32位,代码密度高,省空间。
为什么会有Thumb?我猜你也能想到——移动设备内存金贵啊。早期ARM处理器为了在有限的内存里塞下更多代码,就搞出了Thumb这种压缩指令集。现在虽然内存不是大问题了,但这个传统保留了下来。很多Android Native库为了减小APK体积,都会选择Thumb模式编译。
| 特性 | ARM模式 | Thumb模式 |
|---|---|---|
| 指令长度 | 固定32位 | 16位或32位(Thumb-2) |
| 代码密度 | 较低 | 较高(节省约30%空间) |
| 寄存器访问 | 所有16个通用寄存器 | 受限(通常只能访问低8个) |
| 性能 | 较高 | 略低(但Thumb-2已大幅改善) |
| 常见场景 | 启动代码、中断处理 | 应用层Native库 |
这里有个关键点:处理器怎么知道当前执行的是ARM还是Thumb指令?答案是靠地址的最低有效位(LSB)。如果跳转地址的LSB为1,处理器就切换到Thumb模式;如果为0,就是ARM模式。嗯,这个细节在JNI函数定位时会用到,你先记着。
我的经验:在IDA Pro里,你看到的函数地址如果末尾是奇数(比如0x12341),那这个函数就是Thumb模式的。我曾经因为没注意这个细节,反汇编出来的代码全是乱码,折腾了半天才发现是模式搞错了。
13.2 IDA Pro静态分析:把二进制变成你能看懂的东西
IDA Pro是逆向工程师吃饭的家伙。对于SO文件分析,我个人习惯用IDA Pro 7.x版本,加载速度快,反汇编质量高。你直接把SO文件拖进IDA Pro,它会自动识别文件格式(ELF),然后问你几个问题。
第一个问题:是否加载PDB符号?选No。Android的SO文件基本不带PDB,那是Windows的玩意儿。
第二个问题:是否进行递归下降反汇编?选Yes。这个选项会让IDA Pro自动追踪代码路径,把能识别的函数都反汇编出来。我建议你勾上,能省不少事。
加载完成后,你会看到IDA Pro的主界面。左边是函数窗口(Functions window),列出了SO文件中所有的函数。右边是反汇编窗口,显示具体的汇编指令。最下面是输出窗口,会打印一些加载日志。
我个人习惯先看函数窗口。你按一下Ctrl+E,可以按函数名排序。找那些名字里带“Java_”前缀的函数——这些就是JNI函数,是Java层和Native层的桥梁。
IDA Pro常用快捷键:
- G:跳转到指定地址
- N:重命名当前函数或变量
- X:查看交叉引用(谁调用了这个函数)
- F5:生成伪代码(Hex-Rays插件)
- Ctrl+S:查看段信息
- Alt+M:添加书签
说到F5生成伪代码,这功能简直是神器。它能把汇编指令还原成类似C语言的伪代码,可读性大大提高。但你要注意,伪代码不是100%准确的,有时候反编译结果会有偏差。我遇到过好几次,伪代码看起来逻辑很清晰,但实际执行结果跟预期不符。这时候就得切回汇编视图,逐条指令核对。
避坑指南:我曾经接手过一个被VMP(虚拟机保护)加固的SO文件,IDA Pro的F5完全失效,生成的伪代码全是乱跳转。这种情况下,静态分析基本废了,得靠动态调试。所以别太依赖F5,汇编基础一定要扎实。
13.3 JNI函数定位:找到Native世界的入口
JNI函数是Java层调用Native代码的入口。在SO文件里,JNI函数的命名有固定格式:Java_包名_类名_方法名。比如Java_com_example_app_NativeLib_nativeInit。
在IDA Pro里定位JNI函数,最直接的方法就是在函数窗口搜索“Java_”。但有时候,函数名会被混淆或剥离。这时候怎么办?
我教你一个土办法:看导出表。SO文件有一个.dynsym段,里面记录了所有导出的符号。JNI函数必须导出,否则Java层找不到它。你可以在IDA Pro里按Ctrl+E打开导出表,或者用readelf -s命令查看。
如果导出表也被清理了(有些加固会这么做),那就得靠特征定位。JNI函数的第一个参数是JNIEnv*指针,第二个参数是jclass或jobject。在ARM汇编里,函数参数通过R0-R3寄存器传递。所以JNI函数的开头,通常会有对R0和R1的操作。
; 典型的JNI函数入口(Thumb模式)
.text:00001234 Java_com_example_app_NativeLib_nativeInit
.text:00001234 PUSH {R4-R7, LR} ; 保存寄存器
.text:00001236 MOV R7, R0 ; R0 = JNIEnv*
.text:00001238 MOV R6, R1 ; R1 = jclass/jobject
.text:0000123A LDR R0, [R7] ; 从JNIEnv*获取函数表
.text:0000123C LDR R0, [R0, #0x2C] ; 获取FindClass函数地址
.text:0000123E BLX R0 ; 调用FindClass
你看,JNI函数入口的代码模式很固定:先压栈保存现场,然后把R0(JNIEnv*)和R1(jclass/jobject)移到其他寄存器,接着通过JNIEnv*的函数表指针调用JNI API。你只要在IDA Pro里搜索这种模式,就能找到被隐藏的JNI函数。
我的技巧:在IDA Pro里,你可以用Alt+I打开搜索面板,搜索LDR R0, [R7]这种特征指令。如果找到的地址附近有PUSH {R4-R7, LR},那基本就是JNI函数入口了。这个办法我用了好几年,屡试不爽。
13.4 实战:手把手分析一个SO文件
光说不练假把式。我们来走一遍完整的分析流程。假设你拿到了一个libnative.so文件,目标是找到它的核心加密逻辑。
第一步:加载SO文件
把libnative.so拖进IDA Pro,选择“New”并确认选项。等待反汇编完成。
第二步:定位JNI函数
按Ctrl+E打开导出表,搜索“Java_”。假设你找到了Java_com_example_crypto_NativeCipher_encrypt。双击跳转过去。
第三步:分析函数逻辑
按F5生成伪代码。你会看到类似这样的结构:
jstring __fastcall Java_com_example_crypto_NativeCipher_encrypt(JNIEnv *env, jclass clazz, jstring input)
{
const char *v3; // 输入字符串
char *v4; // 加密结果
jstring v5; // 返回的Java字符串
v3 = (*env)->GetStringUTFChars(env, input, 0);
v4 = encrypt_core(v3); // 核心加密函数
v5 = (*env)->NewStringUTF(env, v4);
(*env)->ReleaseStringUTFChars(env, input, v3);
return v5;
}
第四步:追踪核心函数
看到encrypt_core这个函数了吗?双击跳进去。这里就是真正的加密逻辑。你可以继续用F5看伪代码,或者逐条分析汇编指令。
第五步:提取关键数据
在encrypt_core函数里,你可能会看到一些硬编码的常量,比如密钥、S盒等。把这些数据提取出来,就能还原加密算法。
实战心得:我在分析一个金融类App的SO文件时,发现它的加密函数被分成了十几个小函数,互相调用,逻辑非常绕。后来我用了IDA Pro的“函数调用图”功能(按Ctrl+F12),把调用关系可视化,才理清脉络。遇到复杂逻辑时,别硬啃,善用工具。
13.5 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图。你可以把它当作一个导航地图,随时回来对照。
这张图把本章的三个核心知识点串联起来了。ARM/Thumb指令集是基础,IDA Pro是工具,JNI函数定位是切入点。三者结合,你就能在SO文件的海洋里找到方向。
好了,关于SO文件逆向入门,我就讲这么多。记住,逆向分析没有捷径,多练、多踩坑、多总结,你也能成为高手。下次遇到一个陌生的SO文件,别慌,按我今天教你的步骤来,一步步拆解它。
公众号:蓝海资料掘金营,微信deep3321