IDA 逆向入门方法笔记

最近做SCTF的slang题目,逆向卡了很久,经claude指点,学到了不少,遂作此笔记。

slangalloc_scan_stmt,ida刚打开的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
__int64 __fastcall alloc_scan_stmt(__int64 a1, unsigned int *a2, unsigned int a3)
{
unsigned int v5; // r13d
__int64 result; // rax
__int64 v7; // rdx
int v8; // eax
__int64 v9; // rdx
_DWORD *v10; // rcx
unsigned int *v11; // rdx
__int64 v12; // r12
__int64 v13; // rsi
__int64 *v14; // rbx
__int64 v15; // r13
__int64 v16; // rsi
int v17; // eax
__int64 *v18; // r12
__int64 v19; // r13
__int64 v20; // rsi

v5 = *(_DWORD *)(a1 + 32);
result = *a2;
v7 = v5 + 1;
*(_DWORD *)(a1 + 32) = v7;
switch ( (_DWORD)result )
{
case 0:
v8 = local_index(*(_QWORD *)a1, *((_QWORD *)a2 + 1));
if ( v8 >= 0 )
{
v9 = 4LL * v8;
v10 = (_DWORD *)(v9 + *(_QWORD *)(a1 + 24));
v11 = (unsigned int *)(*(_QWORD *)(a1 + 16) + v9);
if ( *v10 )
{
if ( (int)v5 > (int)*v11 )
*v11 = v5;
}
else
{
*v10 = 1;
*(_DWORD *)(*(_QWORD *)(a1 + 8) + 4LL * v8) = v5;
*v11 = v5;
}
}
return alloc_scan_expr(a1, *((_QWORD *)a2 + 2), v5);
case 1:
result = find_proto(*((char **)a2 + 1));
if ( !a3 || !result || *(_DWORD *)(result + 8) != 3 )
{
result = a2[10];
v12 = 0;
if ( (int)result > 0 )
{
do
{
v13 = *(_QWORD *)(*((_QWORD *)a2 + 4) + 8 * v12++);
result = alloc_scan_expr(a1, v13, v5);
}
while ( (int)a2[10] > (int)v12 );
}
}
break;
case 2:
alloc_scan_expr(a1, *((_QWORD *)a2 + 3), v5);
result = a2[14];
v14 = (__int64 *)*((_QWORD *)a2 + 6);
if ( (int)result > 0 )
{
v15 = (__int64)&v14[(unsigned int)(result - 1) + 1];
do
{
v16 = *v14++;
result = alloc_scan_stmt(a1, v16, a3);
}
while ( v14 != (__int64 *)v15 );
}
break;
case 3:
v17 = a2[14];
v18 = (__int64 *)*((_QWORD *)a2 + 6);
if ( v17 > 0 )
{
v19 = (__int64)&v18[(unsigned int)(v17 - 1) + 1];
do
{
v20 = *v18++;
alloc_scan_stmt(a1, v20, 1);
}
while ( (__int64 *)v19 != v18 );
v7 = *(unsigned int *)(a1 + 32);
}
result = alloc_scan_expr(a1, *((_QWORD *)a2 + 3), v7);
++*(_DWORD *)(a1 + 32);
break;
case 4:
return alloc_scan_expr(a1, *((_QWORD *)a2 + 2), v5);
}
return result;
}

0. 总流程

逆向一个函数,按这个固定流程做,效率最高:

1
F5 反编译 → 改函数原型 → 定义结构体 → 定义枚举 → 重命名临时变量 → 再 F5

核心思想一句话:先重命名/定类型,再理解
不要对着一堆 v5/v8/result 硬读,先把类型信息喂给 Hex-Rays,让它替你把代码变好看。

1. 为什么伪代码里全是 v5 / v8 / result 这种奇怪变量?

Hex-Rays 是机器自动反编译,它不知道变量的真实含义,只能机械命名:

  • v5 / v8 / v12 … —— 临时变量,名字按它来自哪个 CPU 寄存器生成
    (伪代码注释里能看到 // r13d// rax)。
  • result —— 对应 rax 寄存器。x86 用 rax 传函数返回值,
    这种函数又经常”调别的函数→拿返回值→再返回”,所以 result 被反复复用。
    它是个垃圾桶变量,每处含义都不同,看的时候忽略名字,只看那一行在做什么。

Hex-Rays 知道的越少,代码越丑

反编译时,Hex-Rays 唯一确定知道的只有:

  • 这块内存被当成 4 字节读还是 8 字节读
  • 哪个值进了哪个寄存器
  • 调了哪个函数

它不知道:

  • “偏移 8 这个东西”叫 name
  • “这一坨内存”是个叫 Stmt 的结构体
  • 这个函数的返回值其实没人要

所以在你没告诉它之前,它只能写成最保守、最机械的样子:

1
2
*((_QWORD *)a2 + 1)        // 它只敢说"a2 第8字节处的8字节值"
*(_DWORD *)(result + 8) // 它只敢说"result 加 8 的地方"

2. 读懂指针算术:*((_QWORD *)a2 + 1) 到底是什么

规则只有一条:

指针 + N 的实际字节偏移 = N × 指针所指类型的大小

例子(a2 原本是 unsigned int *,即 4 字节一格):

写法 先看作几字节一格 偏移计算 字节偏移
*a2 / a2[0] 4 字节 0×4 0
a2[10] 4 字节 10×4 40 = 0x28
a2[14] 4 字节 14×4 56 = 0x38
*((_QWORD *)a2 + 1) 8 字节 1×8 8
*((_QWORD *)a2 + 2) 8 字节 2×8 16 = 0x10
*((_QWORD *)a2 + 4) 8 字节 4×8 32 = 0x20

所以 (_QWORD*)a2 + 1 就是”结构体第 8 字节处的一个 8 字节值”。
记住:把指针先 cast 成什么类型,+1 就跳几个字节。


3. 如何恢复结构体字段的”含义”(逆向核心功夫)

偏移是出来的(第 2 节),但”偏移 8 是 name”是出来的。
四招,按可靠度从高到低:

① 看”消费者”——这个值被传给谁、那函数怎么用它(最常用)

把字段的值追踪到使用它的函数,用那个函数的行为反推字段类型:

  • 传进 strcmp / puts / printf("%s")字符串指针
  • arr[i] 的基址、或传进 free指针 / 数组
  • 参与 + - * <、当循环计数 → 整数

实例:alloc_scan_stmt 里偏移 8 的值被传进 find_proto
find_proto 内部对它做 strcmp(PLT 跳转可查到目标是 strcmp)。
能拿去 strcmp 的必是字符串 → 偏移 8 = 名字(name/callee)
另一处 case 0 把它传给 local_index(按名字查变量),两处互相印证。

② 看”生产者”——结构体是在哪被填出来的(最硬的证据)

真正的标准答案在构造 / 解析函数里(如 new_stmtparse_*)。
那里能直接看到 stmt->字段8 = 解析出的标识符 这种赋值。
字段拿不准时,去看它被写入的地方,比看被读取的地方更准。

③ 看访问宽度和位置,做合理猜测

  • 偏移 0 是 _DWORD(4字节) 且马上拿去 switch → 几乎一定是”种类枚举” kind
  • _QWORD(8字节) → 多半是指针或 int64
  • _DWORD(4字节) 且当循环上界 → 计数器(个数)
  • 同一偏移在多个分支被同样使用 → 一致性检验,增强信心

④ 蹭已知的”地面真相”

  • 题目自带的 runtime.c 白送了 vec_t { int64_t *data; int64_t size; }
    → data 在偏移 0、size 在偏移 8,直接抄。
  • libc 函数(strcmp/malloc/puts)行为公开已知,用它们当锚点反推未知结构。

实战结论:slang 的 Stmt 结构体字段表

偏移 Hex-Rays 原始写法 推断依据 含义
0x00 *a2 DWORD,立即 switch kind 语句类型
0x08 *((_QWORD*)a2+1) 进 find_proto/local_index 做名字查找 name / callee
0x10 *((_QWORD*)a2+2) case 0/4 传给 alloc_scan_expr expr(右值/返回值)
0x18 *((_QWORD*)a2+3) case 2/3 传给 alloc_scan_expr cond 条件
0x20 *((_QWORD*)a2+4) case 1 里 args[i] 基址 args 参数数组
0x28 a2[10] case 1 循环上界 nargs 参数个数
0x30 *((_QWORD*)a2+6) case 2/3 里 body[i] 基址 body 语句体数组
0x38 a2[14] case 2/3 循环上界 nbody 语句体条数

4. 在 IDA 里定义结构体 / 枚举

定义结构体

  1. 菜单 View → Open subviews → Local Types,或按 Shift+F1
  2. 窗口里右键 → Insert(或按 Insert 键)。
  3. 粘贴 C 声明,OK。IDA 会自动处理对齐(不用手写 padding)。
  4. 回到伪代码窗口,光标点在变量(如 a2)上按 Y(Set lvar type),
    输入 Stmt *,回车。
  5. F5 刷新 → *((_QWORD*)a2+1) 自动变成 a2->name

slang 题用到的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Stmt {
int kind; // 0x00 0=赋值 1=调用 2=if 3=do-while 4=return
char *name; // 0x08 变量名 / 被调函数名
void *expr; // 0x10 右值 / return 表达式
void *cond; // 0x18 条件 (if / do-while)
void **args; // 0x20 参数数组
int nargs; // 0x28 参数个数
void **body; // 0x30 语句体数组
int nbody; // 0x38 语句体条数
};

struct Proto {
char *name; // 0x00 函数名
int rettype; // 0x08 返回类型 (3 = void)
};

struct Alloc { // 就是函数里的 a1(活跃度分析器状态)
void *func; // 0x00
int *first_use; // 0x08 每个变量"首次使用时间"
int *last_use; // 0x10 每个变量"最后使用时间"
int *seen; // 0x18 是否已登记
int time; // 0x20 单调递增时间戳
};

定义枚举(干掉魔法数字 0/1/2/3)

在 Local Types 里同样 Insert:

1
2
enum StmtKind { ST_ASSIGN=0, ST_CALL=1, ST_IF=2, ST_DOWHILE=3, ST_RETURN=4 };
enum RetType { RT_INT=0, RT_STR=1, RT_VEC=2, RT_VOID=3 };

用法:

  • 把结构体里 kind 字段类型改成 StmtKindrettype 改成 RetType
  • 或更快:伪代码里光标点到常量 3,按 M(Map to enum,选 RetType),
    立刻变成 RT_VOID

5. 一个通用直觉:bug 常藏在”进不去的那个 if”里

Hex-Rays 经常把代码写成 if (正常条件) { 正常处理 } 而没有 else。
读的时候要习惯性追问一句:“不满足条件时会发生什么?”

slang 的漏洞正是这样:

1
2
3
4
5
6
case ST_CALL:
proto = find_proto(stmt->name);
if ( !depth || !proto || proto->rettype != RT_VOID ) // 正常:扫描参数
for (i = 0; i < stmt->nargs; i++)
alloc_scan_expr(a, stmt->args[i], time);
break; // ← 当 (depth>0 && 返回void) 时,直接 break,参数一个没扫 = BUG

对那个 if 取反(德摩根):
“在循环里 + 调用的是 void 函数”时,跳过整个参数扫描 → 变量活跃区间漏记 → slot 误复用 → 类型混淆。

逆向常见 bug 模式:提前 return / break / continue 跳过了某段必要处理
养成专门盯这类”提前退出”的习惯。


6.处理完的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
void __fastcall alloc_scan_stmt(Alloc *a, Stmt *stmt, unsigned int depth)
{
unsigned int time; // r13d
int kind; // eax
__int64 v7; // rdx
int v8; // eax
__int64 v9; // rdx
int *v10; // rcx
int *v11; // rdx
Proto *proto; // rax
__int64 i; // r12
void *v14; // rsi
int nbody; // eax
void **body; // rbx
__int64 v17; // r13
Stmt *v18; // rsi
int v19; // eax
void **v20; // r12
__int64 v21; // r13
Stmt *v22; // rsi

time = a->time;
kind = stmt->kind;
v7 = time + 1;
a->time = v7;
switch ( kind )
{
case 0:
v8 = local_index(a->func, stmt->name);
if ( v8 >= 0 )
{
v9 = v8;
v10 = &a->seen[v9];
v11 = &a->last_use[v9];
if ( *v10 )
{
if ( (int)time > *v11 )
*v11 = time;
}
else
{
*v10 = 1;
a->first_use[v8] = time;
*v11 = time;
}
}
goto LABEL_5;
case 1:
proto = find_proto(stmt->name);
if ( !depth || !proto || proto->rettype != 3 )
{
for ( i = 0; stmt->nargs > (int)i; ++i )
{
v14 = stmt->args[i];
alloc_scan_expr(a, v14, time);
}
}
break;
case 2:
alloc_scan_expr(a, stmt->cond, time);
nbody = stmt->nbody;
body = stmt->body;
if ( nbody > 0 )
{
v17 = (__int64)&body[(unsigned int)(nbody - 1) + 1];
do
{
v18 = (Stmt *)*body++;
alloc_scan_stmt(a, v18, depth);
}
while ( body != (void **)v17 );
}
break;
case 3:
v19 = stmt->nbody;
v20 = stmt->body;
if ( v19 > 0 )
{
v21 = (__int64)&v20[(unsigned int)(v19 - 1) + 1];
do
{
v22 = (Stmt *)*v20++;
alloc_scan_stmt(a, v22, 1u);
}
while ( (void **)v21 != v20 );
v7 = (unsigned int)a->time;
}
alloc_scan_expr(a, stmt->cond, v7);
++a->time;
break;
case 4:
LABEL_5:
alloc_scan_expr(a, stmt->expr, time);
break;
}
}

7. 快捷键速查

操作 快捷键
反编译当前函数 F5
重命名变量/函数 N
设置变量/函数类型(原型) Y
把常量映射成枚举 M
打开 Local Types(定义结构体/枚举) Shift+F1
打开 Structures(老版本) Shift+F9
查看交叉引用(这个值流向哪 / 谁用了它) X
在 Local Types 里新建条目 Insert