最近做SCTF的slang题目,逆向卡了很久,经claude指点,学到了不少,遂作此笔记。
slang的alloc_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; __int64 result; __int64 v7; int v8; __int64 v9; _DWORD *v10; unsigned int *v11; __int64 v12; __int64 v13; __int64 *v14; __int64 v15; __int64 v16; int v17; __int64 *v18; __int64 v19; __int64 v20;
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) *(_DWORD *)(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_stmt、parse_*)。
那里能直接看到 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 里定义结构体 / 枚举
定义结构体
- 菜单 View → Open subviews → Local Types,或按 Shift+F1。
- 窗口里右键 → Insert(或按
Insert 键)。
- 粘贴 C 声明,OK。IDA 会自动处理对齐(不用手写 padding)。
- 回到伪代码窗口,光标点在变量(如
a2)上按 Y(Set lvar type),
输入 Stmt *,回车。
- 按 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; char *name; void *expr; void *cond; void **args; int nargs; void **body; int nbody; };
struct Proto { char *name; int rettype; };
struct Alloc { void *func; int *first_use; int *last_use; int *seen; int time; };
|
定义枚举(干掉魔法数字 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 字段类型改成 StmtKind、rettype 改成 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;
|
对那个 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; int kind; __int64 v7; int v8; __int64 v9; int *v10; int *v11; Proto *proto; __int64 i; void *v14; int nbody; void **body; __int64 v17; Stmt *v18; int v19; void **v20; __int64 v21; Stmt *v22;
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 |