应用安全 --- 逆向技巧 之 IDA和claude反编译缺陷
主要缺点
1.我认为最大的缺陷是常量值错误,比如打印hello world,但是代没有展示字符串,只有引用的地址,导致反编译失真,解决方法是提前将字符串定义在函数开头的位置,或者直接写入目标引用
比如
// 反编译还原(错误)
static uint32_t dword_4BF8 = 0; // 未初始化!
// 真实源码(正确)
static uint32_t dword_4BF8 = 0x0100;
实际上它是有默认值的
.data:0000000000004BF8 dword_4BF8 DCD 0x200000 ; DATA XREF: LOAD:00000000000000F8↑o
.data:0000000000004BF8 ; Java_lab_galaxy_yahfa_HookMain_init:loc_153C↑o ...
这里的解决方法就是将这里的全局变量提前引入方法前定义
2. AI识别命名相反,解决方法就是引入常量定义并对照源码
// 反编译还原(错误方向)
if (SDKVersion >= 29)
cur &= ~g_kAccNative; // ❌ 错误:SDK29+清除kAccNative
if (SDKVersion <= 29)
cur |= g_kAccFastInterpreterToInterpreterInvoke; // ❌ 错误
// 真实源码(正确逻辑)
if (SDKVersion >= __ANDROID_API_Q__) { // SDK29
access_flags &= ~kAccFastInterpreterToInterpreterInvoke; // 清除fast标志
}
if (SDKVersion <= __ANDROID_API_Q__) { // SDK<=29
access_flags |= kAccNative; // 设置native标志
}
// SDK30+: 不设置kAccNative(会导致查找真实native指针而非entry_point)
3.claude的 jni.h的函数名称识别错误
// 反编译还原(错误)
env->DeleteLocalRef(hookMethod); // ❌ 删除局部引用 (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 168LL))(a1, a4);
if (backupMethod)
env->DeleteLocalRef(backupMethod);
// 真实源码(正确)
(*env)->NewGlobalRef(env, hook); // ✅ 创建全局引用,防止GC回收
if(backup) (*env)->NewGlobalRef(env, backup);
4.内联汇编实现无法识别,只是汇编代码
// 反编译还原(不完整)
__int64 Java_lab_galaxy_xjhook_HookMain_00024Utils_getThread() {
__int64 v0; // x19 ← 仅注释提示
return v0;
}
// 真实源码(裸函数+内联汇编)
jlong __attribute__((naked))
Java_lab_galaxy_yahfa_HookMain_00024Utils_getThread(...) {
#if defined(__aarch64__)
__asm__("mov x0, x19\n" "ret\n"); // x19存储ART Thread*
#elif defined(__arm__)
__asm__("mov r0, r9\n" "bx lr\n");
#endif
}
影响:反编译工具无法还原裸函数的汇编语义,仅识别出寄存器 x19,但丢失了多架构实现。
5.条件编译丢失,这个不能是缺陷但是也是问题
// 反编译还原(不完整)
bool sub_24C8() {
return SDKVersion >= 30; // 仅考虑SDK版本
}
// 真实源码(还考虑架构)
static int shouldVisiblyInit() {
#if defined(__i386__) || defined(__x86_64__)
return 0; // x86/x86_64 不需要
#else
return SDKVersion >= __ANDROID_API_R__;
#endif
}
影响:x86架构下应始终返回0,反编译丢失了架构条件编译信息。
宏条件丢失
// 真实源码:仅 aarch64 需要
#if defined(__aarch64__)
#define NEED_CLASS_VISIBLY_INITIALIZED
#endif
// 反编译还原:无此条件,始终启用
// (因为反编译的二进制本身就是 aarch64,条件编译已展开)
还有全局数组被识别为单个字节,正确的识别特征是
宏定义消失
1. 类型信息丢失(最核心缺陷)
C
// 原始代码
struct User {
char name[32];
int age;
float salary;
};
// IDA 反编译后(类型信息严重退化)
int sub_401000(int a1) {
*(_DWORD *)(a1 + 32) = 0; // age 变成了偏移计算
*(float *)(a1 + 36) = 0.0; // 结构体完全丢失语义
}
2. 变量识别错误
C
// 原始代码
int a = 10;
int b = 20;
int c = a + b;
// IDA 可能合并为
int v1 = 30; // 常量折叠后变量被合并,语义丢失
常见问题:
- 局部变量被错误合并
- 同一内存地址在不同上下文被复用(变量生命周期分析失败)
- 寄存器变量难以准确还原
3. 间接调用 / 函数指针识别困难
C
// 原始代码
void (*func_ptr)(int) = get_handler();
func_ptr(42);
// IDA 反编译后
((void(__cdecl *)(int))some_var)(42); // 无法确定实际调用目标
// 甚至可能显示为
CALL dword ptr [eax] // 直接放弃反编译
4. 控制流还原失真
C
// 原始 switch 语句
switch(x) {
case 1: do_a(); break;
case 2: do_b(); break;
case 3: do_c(); break;
}
// IDA 可能还原为跳转表形式(难以辨认)
if ((unsigned)(x - 1) <= 2)
(jpt_table[x - 1])();
// 或者变成一堆 goto
5. 字符串与数据类型混淆
C
// 原始代码
const char *msg = "Hello";
// IDA 可能识别为
int v1 = 0x6C6C6548; // 把字符串当整数处理
int v2 = 0x006F;
6. 编译器优化导致代码变形
| 优化手段 | 反编译后的问题 |
|---|---|
| 内联展开 | 函数边界消失,代码膨胀 |
| 循环展开 | 循环结构无法还原 |
| SIMD 指令 | intrinsics 难以识别 |
| 尾调用优化 | 函数调用变成 JMP |
| 死代码消除 | 逻辑不完整 |
C
// 原始循环
for(int i = 0; i < 4; i++)
sum += arr[i];
// 编译器展开后,IDA 反编译
v1 = arr[0] + arr[1];
v2 = v1 + arr[2];
result = v2 + arr[3]; // 完全看不出是循环
7. 调用约定识别错误
C
// 原始代码(__fastcall)
int __fastcall calc(int a, int b) { return a + b; }
// IDA 误识别为 __cdecl
int calc(int a1) { // 参数数量错误
return a1 + some_global; // 把寄存器参数当全局变量
}
8. 面向对象代码还原困难
C
// 原始 C++ 代码
obj->method(arg);
// IDA 反编译(虚表调用变得极其晦涩)
(*(void (__thiscall **)(_DWORD, int))(*(DWORD*)obj + 0xC))(obj, arg);
// ↑ vtable偏移0xC处的函数,语义完全丢失
9. 全局变量与局部变量混淆
C
// IDA 经常将栈上变量识别为
int v1; // 实际上可能是数组或结构体
*(DWORD*)(v1 + 4) = 0; // 只能看到偏移访问
10. 异常处理(SEH/C++异常)
C
// 原始代码
try {
risky_operation();
} catch(Exception& e) {
handle(e);
}
// IDA 反编译后充满编译器运行时调用
__CxxFrameHandler3(...); // 晦涩难懂的异常框架代码
总结对比
text
原始代码清晰度 ████████████ 100%
IDA反编译结果 ████░░░░░░░░ 40%~70%(视情况而定)
影响因素:
- 有调试符号(PDB) → 质量大幅提升
- 开启高优化(O2/O3)→ 质量大幅下降
- 有混淆保护 → 几乎无法阅读
最根本的原因:编译是单向有损压缩过程,语义信息在汇编层面无法完整保留。
一、符号信息丢失类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 1.1 | 函数名丢失 | 静态函数名完全丢失,以地址命名 | setNonCompilable → sub_1B04 |
| 1.2 | 全局变量名丢失 | 有意义的常量名变为地址标签 | kAccCompileDontBother → dword_4BFC |
| 1.3 | 结构体字段名丢失 | 偏移量取代字段名 | OFFSET_access_flags_in_ArtMethod → dword_4C54 |
| 1.4 | 数组名丢失 | 数组退化为全局地址 | trampoline[9] → byte_4C11 |
| 1.5 | 宏定义丢失 | 宏展开后无法还原 | roundUpToPtrSize(...) → (v5+7) & 0xFFFFFFFFFFFFFFF8 |
| 1.6 | 枚举/常量语义丢失 | 具名常量变为数字字面量 | PROT_READ|PROT_WRITE|PROT_EXEC → 7 |
| 1.7 | 包名/类名变更无法感知 | 混淆或改名后无从区分 | yahfa → xjhook 仅从字符串可见 |
| 1.8 | 注释完全丢失 | 所有开发者注释消失 | // keep a global ref so that hook method would not be GCed → 无 |
二、类型信息丢失类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 2.1 | 返回类型误判 | void函数被判为有返回值 | void setFlags() → __int64 sub_1DB8() |
| 2.2 | 参数类型退化 | 具名JNI类型退化为整数 | JNIEnv *env, jobject hook → __int64 a1, __int64 a4 |
| 2.3 | 函数指针类型丢失 | typedef的函数指针类型消失 | InitClassFunc → __int64(__fastcall*)(_QWORD,_QWORD,_QWORD) |
| 2.4 | bool/int混淆 | 布尔语义丢失 | int shouldVisiblyInit() → bool sub_24C8() (反而多了信息) |
| 2.5 | 有符号/无符号混淆 | 符号性推断错误 | uint32_t access_flags → int v4 / unsigned int v5 拆分 |
| 2.6 | 指针const属性丢失 | const修饰符消失 | const void *targetMethod → __int64 a1 |
| 2.7 | 数组退化为指针 | 数组类型信息丢失 | unsigned char trampoline[24] → byte_4C08 起始的若干字节 |
| 2.8 | size_t语义丢失 | 大小类型退化 | size_t trampolineSize → __int64 v2 |
三、控制流失真类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 3.1 | 条件取反 | if条件被编译器优化后取反 | if (!doBackupAndHook()) → if (sub_1978()) 逻辑反转 |
| 3.2 | 条件合并 | 多层if被合并为单一复合条件 | if(!a||!b){if(f()!=0){err}} → if(a&&b || !f()){ok}else{err} |
| 3.3 | fall-through语义丢失 | switch的贯穿逻辑变为goto | case 27: kAcc=0x2000000; //fall-through → case 27: dword=0x2000000; goto LABEL_8 |
| 3.4 | goto引入 | 反编译器用goto还原复杂跳转 | 源码无goto → 反编译出现 goto LABEL_3/4/8 |
| 3.5 | 提前返回丢失 | early return被重构为条件嵌套 | if(SDK<24) return; 逻辑被合并进大if块 |
| 3.6 | 循环变量类型变化 | 循环控制变量类型改变 | int i → unsigned int i (影响负数判断) |
四、函数调用失真类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 4.1 | JNI调用变为vtable偏移 | JNI函数名消失,变为数字偏移 | (*env)->GetMethodID(env,...) → (*(_QWORD*)a1+264LL)(a1,...) |
| 4.2 | 系统调用包装替换 | 编译器替换为安全版本 | memcpy(...) → __memcpy_chk(..., -1) |
| 4.3 | 宏函数展开 | 宏调用变为内联表达式 | open(...) → __open_2(...) |
| 4.4 | errno访问方式变化 | 全局宏展开为函数调用 | strerror(errno) → v0=__errno(); strerror(*v0) |
| 4.5 | log返回值被当函数返回值 | void函数末尾log的返回值被误用 | LOGI(...); //void return → return __android_log_print(...) |
| 4.6 | 可变参数信息丢失 | 变参函数原型退化 | __android_log_print(int,char*,char*,...) → 参数直接展开 |
五、内存操作失真类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 5.1 | 指针运算语义丢失 | 有意义的偏移计算变为数字 | (char*)method + OFFSET_access_flags → *(a1 + dword_4C54) |
| 5.2 | 读写宏展开 | readAddr/writeAddr宏变为指针解引用 | writeAddr(from+OFFSET, val) → *(_QWORD*)&a1[dword_4C58]=v6 |
| 5.3 | 数组下标语义丢失 | 有意义的下标变为偏移量 | trampoline[9] |= offset<<4 → byte_4C11 |= 16*result |
| 5.4 | 结构体访问退化 | 结构体字段访问变为原始偏移 | method->entry_point → *(a1+48) |
| 5.5 | 局部变量被合并 | 两个变量共用同一栈地址 | v6/v7 共用 [xsp+8h] 但语义不同 |
六、架构特性丢失类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 6.1 | naked函数属性丢失 | 无栈帧函数被误加函数框架 | __attribute__((naked)) → 普通函数形式 |
| 6.2 | 内联汇编完全丢失 | asm块语义只能部分还原 | __asm__("mov x0, x19\nret\n") → __int64 v0; //x19 return v0 |
| 6.3 | 寄存器语义丢失 | 特殊寄存器用途无法表达 | x19=ART线程寄存器 → 仅标注//x19 |
| 6.4 | 栈canary artifact | 栈保护代码产生多余伪调用 | 多出 _ReadStatusReg(TPIDR_EL0) 调用 |
| 6.5 | SIMD/特殊指令丢失 | 向量指令无法正确反编译 | (本项目未涉及,常见于加密/媒体代码) |
| 6.6 | 调用约定推断错误 | fastcall/thiscall等推断失误 | 部分函数被标为__fastcall但实际为其他约定 |
七、语义等价但信息损失类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 7.1 | 位运算等价替换 | 移位被替换为乘除 | offset << 4 → 16 * result |
| 7.2 | 常量折叠 | 编译期计算结果替代表达式 | roundUpToPtrSize(4*4+2*2)+pointer_size*2 → 40 |
| 7.3 | 变量拆分 | 一个变量被拆为多个 | int res → int v4 + unsigned int v5 |
| 7.4 | 中间变量消除 | 有意义的临时变量被优化掉 | void *originEntrypoint = readAddr(...) → 直接作为参数 |
| 7.5 | 参数传递方式改变 | 值传递/引用传递语义改变 | &toMethod 传指针 → v8数组 包装后传递 |
| 7.6 | 运算顺序改变 | 编译器重排指令顺序 | 多处变量赋值顺序与源码不同 |
八、完全无法反编译类
| # | 缺陷类型 | 简要说明 | 本项目实例 |
|---|---|---|---|
| 8.1 | PLT/GOT表项无法反编译 | 动态链接跳转表 | _android_log_print_00004C98 → // 反编译失败 |
| 8.2 | thunk函数语义退化 | 跳转桩函数自引用 | close(fd) → return close(fd) 自调用形式 |
| 8.3 | 数据段代码混合 | 代码被识别为数据或反之 | trampoline字节数组被执行时IDA可能识别为数据 |
| 8.4 | 间接跳转无法追踪 | 计算跳转目标丢失 | sub_2850: JUMPOUT(0) 完全无法分析 |
| 8.5 | 动态生成代码 | 运行时写入的代码不可见 | genTrampoline生成的代码在IDA静态视图中不存在 |
缺陷影响程度速查
text
严重程度: ████ 极高 ███ 高 ██ 中 █ 低
符号丢失 ████ → 需要大量上下文推断才能还原
类型失真 ███ → 影响指针运算和边界判断的正确性
控制流失真 ███ → 可能误判程序逻辑分支
调用失真 ██ → 函数语义可还原但需查vtable
架构特性 ████ → naked/asm几乎完全丢失,无法还原
完全失败 ████ → PLT条目/动态代码静态分析无效
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号