GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 逆向技巧 之 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,条件编译已展开)

 

 

 

还有全局数组被识别为单个字节,正确的识别特征是

你在现实中怎么认出"数组"?

text
一排格子,每格大小相同,可以按编号访问

IDA 里完全一样,就找这三个现象:


现象1:一排相同大小的格子(DCB重复)

asm
trampoline DCB 0       ← 格子1
           DCB 0       ← 格子2  
           DCB 0       ← 格子3
           DCB 0x60    ← 格子4
           ...连续24个
text
就像超市货架:
一排摆了24个同规格的盒子
= 这肯定是数组,不是单个变量

现象2:被整体搬走(memcpy)

C
memcpy(dst, &trampoline, 24);
text
没人会这样操作单个变量:
memcpy(dst, &my_int, 4)  ← 奇怪,直接赋值就行

但数组必须整体复制:
memcpy(dst, &arr, 24)    ← 正常,这就是数组的用法

现象3:按编号取某一格(下标访问)

C
byte_4C11  ← trampoline[9]   // 取第9格
byte_4C12  ← trampoline[10]  // 取第10格
text
trampoline基址 0x4C08
byte_4C11      0x4C11
差值           = 9  ← 就是下标

一句话

text
连续的DCB + memcpy整体复制 + 按偏移取单个字节

= 就是 unsigned char 数组,没别的可能
 

宏定义消失

通俗解释:对齐到8字节

什么是对齐?

text
内存就像停车场,每个车位是8字节
你的数据必须停在车位的起点,不能停在中间

举个例子

text
你有24字节数据:
24 ÷ 8 = 3  整除,正好停3个车位  → 不用调整,直接用24

你有28字节数据:
28 ÷ 8 = 3.5  不整除,停到第4个车位中间了
→ 必须补到32才能对齐

源码宏在做什么

C
roundUpToPtrSize(v)
= v + 7 - ((v+7) & 7)

// 翻译成人话:
// 先加7(留出余量)
// 再把末尾不足8的部分去掉
// = 向上凑整到8的倍数

编译器优化后(IDA看到的)

C
(v5 + 7) & 0xFFFFFFFFFFFFFFF8

// 0xFFFFFFFFFFFFFFF8 的低3位全是0:
// ...1111 1000
//         ↑↑↑
//         强制低3位=0
//         = 强制是8的倍数

两种写法结果完全一样

text
v=24:  (24+7) & ~7 = 31 & ~7 = 24  ← 本来就对齐,不变
v=28:  (28+7) & ~7 = 35 & ~7 = 32  ← 补到下一个对齐点

源码用减法凑整
编译器改成位与清零
结果一样,位与更快

一句话

text
roundUpToPtrSize = 把任意数字向上凑到8的倍数
编译器把这个操作优化成了"低3位清零"
IDA看到的就是优化后的版本
 

 

 

 

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条目/动态代码静态分析无效

posted on 2026-04-05 09:07  GKLBB  阅读(25)  评论(0)    收藏  举报