3. 开发语言基础:Java、Kotlin、OC与Swift
做逆向,说白了就是跟代码打交道。你面对的是一个黑盒,但黑盒里跑的逻辑,终究是用某门语言写出来的。我见过不少新手,上来就掏IDA、Frida,结果连个类继承关系都理不清,更别说看懂JNI调用栈了。
嗯,这一章咱们就聊聊逆向必须掌握的语言基础。我不打算给你念语法手册,而是挑那些逆向时真正会用到的核心知识点。
3.1 Java基础:面向对象与JVM机制
Android逆向,Java逃不掉。哪怕现在Kotlin当道,反编译出来你看到的还是Java字节码。
3.1.1 面向对象:你看到的类不一定是真的类
逆向时你经常要回答一个问题:这个对象到底是什么类型?
- 继承与多态:父类引用指向子类对象。反编译看到
BaseActivity,但运行时实际跑的是MainActivity。我遇到过有人对着BaseActivity的方法一顿Hook,结果发现方法根本没被调用——因为子类重写了它。 - 接口与抽象类:很多加固壳会动态生成代理类。反编译看不到具体实现,只能看到
InvocationHandler。这时候你得顺着invoke方法去追。 - 内部类与匿名类:反编译后你会看到
MainActivity$1、MainActivity$InnerClass这种名字。它们其实是独立的class文件。我曾经在分析一个支付SDK时,发现关键逻辑藏在一个匿名内部类里,名字叫PayManager$3。
逆向视角:反编译工具(如Jadx、Bytecode Viewer)会把内部类拆开显示。如果你看到类名带$符号,别奇怪,那是内部类的编译产物。
3.1.2 JVM机制:Dalvik与ART的区别
Android不用标准JVM,它用Dalvik(4.4以前)和ART(5.0以后)。但核心机制是相通的。
| 概念 | 说明 | 逆向意义 |
|---|---|---|
| 类加载机制 | 双亲委派模型,但Android有PathClassLoader和DexClassLoader | 插件化、热修复都靠自定义ClassLoader。逆向时看到DexClassLoader,基本可以断定有动态加载 |
| 垃圾回收 | 标记-清除、复制算法 | 内存dump时,GC可能把对象挪走。我踩过这个坑——Hook一个对象后,GC触发导致地址变了,Frida脚本直接崩了 |
| 栈帧与操作数栈 | 每个方法调用对应一个栈帧 | Smali调试时,你看locals和registers就是在看栈帧布局 |
个人习惯:我分析一个App时,会先看它有没有自定义ClassLoader。如果有,十有八九用了加固或插件化。这时候我会重点关注loadClass的调用点。
3.2 Kotlin基础:与Java的互操作
现在新App用Kotlin的越来越多。反编译Kotlin代码,你会看到一堆奇怪的类名和函数名。
3.2.1 空安全与平台类型
Kotlin的空安全在字节码层面会变成null检查。反编译后你会看到类似这样的代码:
// Kotlin源码
fun getName(): String? {
return null
}
// 反编译后的Java
@Nullable
public String getName() {
return null;
}
嗯,这里要注意:Kotlin的String?对应Java的@Nullable String,String对应@NonNull String。但Java没有强制空安全,所以互操作时经常出问题。
3.2.2 协程与挂起函数
Kotlin协程在字节码里会被转换成状态机。反编译后你会看到Continuation参数和label字段。
我分析过一个用协程写的网络库,反编译后方法签名变得很长:
// 反编译后的Java
public final Object fetchData(@NotNull Continuation<? super String> continuation) {
// 状态机实现
}
如果你看到Continuation,说明这是挂起函数。Hook它的时候,不能只Hook入口,还要处理状态机的各个分支。
避坑指南:我曾经试图Hook一个Kotlin协程的挂起函数,只Hook了入口方法,结果发现返回值一直是COROUTINE_SUSPENDED。后来才明白,协程的返回值是在状态机不同分支里设置的。你得把每个label对应的分支都Hook到。
3.2.3 伴生对象与顶层函数
Kotlin的companion object在字节码里会变成静态内部类。顶层函数会变成文件名Kt类的静态方法。
举个例子:
// Kotlin源码
class MyClass {
companion object {
fun create() = MyClass()
}
}
// 反编译后的Java
public final class MyClass {
public static final MyClass.Companion Companion = new MyClass.Companion();
public static final class Companion {
@NotNull
public final MyClass create() {
return new MyClass();
}
}
}
逆向时,如果你看到Companion类,就知道这是Kotlin的伴生对象。Hook的时候要HookCompanion.create(),而不是直接HookMyClass.create()。
3.3 Objective-C与Swift基础:Runtime与ARC
iOS逆向,OC和Swift是绕不开的。OC的Runtime机制是逆向的核心,Swift虽然更安全,但底层还是依赖Runtime。
3.3.1 Runtime机制:消息传递与Method Swizzling
OC的方法调用本质上是发消息。你写[obj doSomething],编译器会把它变成objc_msgSend(obj, @selector(doSomething))。
这意味着什么?意味着你可以在运行时替换方法的实现。这就是Method Swizzling。
// OC Runtime核心函数
// 获取方法实现
Method method = class_getInstanceMethod([obj class], @selector(doSomething));
IMP originalImp = method_getImplementation(method);
// 替换方法实现
class_replaceMethod([obj class], @selector(doSomething), newImp, "v@:");
逆向时,我经常用class_copyMethodList来枚举一个类的所有方法。有一次分析一个金融App,发现它用Runtime动态添加了几个方法,反编译的二进制里根本看不到——只有运行时才能抓到。
核心概念:OC的isa指针指向类对象,类对象里有个methodLists,存着所有方法。Method Swizzling就是交换这个列表里的IMP指针。
3.3.2 ARC:自动引用计数
ARC是编译器帮你插入retain和release。逆向时,你会看到很多objc_retain、objc_release的调用。
嗯,这里有个坑:ARC下,局部变量默认是__strong,出了作用域会自动释放。但如果你用__unsafe_unretained,指针不会自动置nil,容易产生野指针。
我遇到过一个问题:一个App在某个版本频繁崩溃,反编译后发现是ARC和MRC混编导致的。一个MRC写的第三方库返回了autorelease对象,ARC的调用方没有retain,结果对象被提前释放了。
3.3.3 Swift与OC的互操作
Swift类在OC里会被桥接。Swift的class会生成一个隐藏的OC类,名字类似_TtC12MyApp7MyClass。
逆向Swift代码时,你会看到很多$s开头的符号名,这是Swift的mangling规则。比如:
// Swift方法名
func doSomething(param: String) -> Int
// Mangling后的符号
$s12MyApp7MyClass11doSomething5paramSi_tF
这个符号怎么读?$s是Swift符号前缀,12MyApp是模块名长度+模块名,7MyClass是类名长度+类名,11doSomething是方法名长度+方法名,5param是参数名长度+参数名,Si是返回值类型(String),tF是方法类型标识。
个人经验:分析Swift二进制时,我会先用nm或strings导出所有符号,然后过滤$s开头的。这样能快速定位到Swift类和方法。配合swift demangle命令,可以把mangled符号还原成可读的方法名。
3.4 知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一个检查清单——做逆向时,遇到哪块不懂,就回来翻翻。
这张图把三大语言的核心知识点串起来了。你不需要一次性全记住,但做逆向时遇到相关问题,能快速定位到对应的知识点就行。
好了,这一章的内容就这些。记住一句话:语言只是工具,逆向的核心是理解运行时发生了什么。不管Java、Kotlin、OC还是Swift,最终都要落到内存布局和指令执行上。