DiceCTF-Rev Scrambled-up
字数 1749 2025-08-18 17:33:44
DiceCTF逆向题目"Scrambled-up"深度解析
题目概述
"Scrambled-up"是DiceCTF中的一个逆向题目,采用了数据流混淆技术而非传统的执行流混淆。题目使用了一种类似Lisp的函数式编程风格,通过数据流而非控制流来组织程序逻辑。
程序结构分析
主要数据结构
程序使用了三种核心数据结构:
- Inst结构体:表示指令的基本信息
struct Inst {
Block *next_call;
__int64 line;
__int64 argc;
__int64 slot_cnt;
__int64 exec_type;
Var var;
};
- Edge结构体:表示块之间的连接关系
struct Edge {
Edge *next_cond;
__int64 dst_line;
__int64 src_line;
};
- Block结构体:表示内存中的程序块
struct Block {
__int64 line;
int exec_type;
int field_C;
__int64 argc;
Var *argv;
__int64 slot_cnt;
Var *slot_buffer;
Var value;
};
变量表示
程序使用Var结构表示变量:
struct Var {
__int64 var_type; // 1=指针,2=值,0=无效
__int64 var_or_ptr;
__int64 var_length; // 指针指向内容的长度
};
程序执行流程
初始化阶段
- 信号处理:注册SIGSEGV信号处理函数
- 输入处理:读取用户输入的flag
- 内存映射:使用mmap分配大块内存(0xF00000字节)
- 指令读取:从全局变量inst_edge和inst_code读取数据初始化Block
执行阶段
程序执行分为两部分:
- 初始化Block和Edge:通过read_inst函数完成
- 执行指令:通过parser_inst函数完成
执行逻辑特点:
- 线性执行所有逻辑块
- 输入变量恒定不变,修改通过输出到其他块实现
- 条件判断只有0和非0两种情况
关键函数分析
exec_inst函数
该函数包含11个不同类型的执行函数:
| 类型 | 函数作用 |
|---|---|
| 1 | 所有argv相加,结果赋值 |
| 2 | 所有argv相乘,结果赋值 |
| 3 | 将var位置的block赋值 |
| 4 | 调用函数指针并将结果赋值 |
| 5 | 条件赋值:argv[0]==0则用argv[1],否则用argv[2] |
| 8 | 检查flag是否正确 |
| 9 | 调用read函数读取flag到全局变量 |
| 10 | 合并两个指针指向的内存到新内存 |
| 11 | 获取argv[0][argv[1]]的值并赋值 |
类型4函数(CallFunc)
这些是被调用的函数指针:
| 函数名 | 作用 |
|---|---|
| xor_all_argv | 所有参数异或 |
| or_all_argv | 所有参数或 |
| and_all_argv | 所有参数相与 |
| check_if_zero | 检查第二个参数是否为0 |
| get_index_from_input | 获取输入的第index参数 |
| reorder | 按指定顺序重构输入(16字节) |
| maze_step | 走迷宫函数 |
解题过程
第一阶段:数据流dump
- 在ExecFunc8(flag检查函数)处下断点
- 获取Block和Edge的内存布局
- 编写脚本解析数据结构关系
第二阶段:flag有效性检查
发现多个检查逻辑:
- 长度检查:flag长度必须为0x26(38)字节
- 格式检查:必须以"dice{"开头,"}"结尾
- 复杂校验:多轮异或、乘法和加法运算校验
- 迷宫校验:通过flag值控制迷宫行走路径
第三阶段:迷宫分析
迷宫特点:
- 起点:(0x40, 0x40)
- 终点:(0x45, 0x8)
- 每个flag字符控制3步移动
- 必须在96步内完成
- 不能碰到障碍(-1)
迷宫移动有9种方向:
- 常规4方向
- 4个斜方向
- 原地不动
解题脚本关键部分
Z3约束求解
from z3 import *
BITS = 8
solver = Solver()
flags = []
# 初始化flag变量
for i in range(0x20):
each_flag = BitVec("flag%d"%i ,BITS)
flags.append(each_flag)
solver.add(each_flag <= 0x7f)
solver.add(each_flag >= 0x10)
# 添加已知字符约束
solver.add(flags[2] == ord('w'))
solver.add(flags[6] == ord('e'))
# ... 其他已知字符约束
# 添加校验约束
res = 0
for i in range(len(total_flag)-1):
res = res ^ i ^ total_flag[i] ^ (total_flag[i]*0xe+total_flag[i+1])
res = res + total_flag[len(total_flag)-1]*7
solver.add(res == 0x784)
# 求解
while solver.check() == sat:
model = solver.model()
flag = ""
for each_flag in flags:
flag += chr(model[each_flag].as_long())
print(flag)
solver.add(Or([ model[v]!=v for v in flags]))
总结
-
数据流编程特点:
- 所有逻辑都会被执行
- 只有输入和输出变量,输入不变
- 条件判断简单(只有0和非0)
-
与传统执行流编程对比:
| 特点 | 执行流编程 | 数据流编程 |
|---|---|---|
| 执行范围 | 部分逻辑执行 | 所有逻辑执行 |
| 变量修改 | 变量可能被修改 | 输入变量不变 |
| 条件判断 | 丰富多样 | 只有0和非0 |
- 解题关键:
- 理解数据流传递关系
- 在程序结尾处dump完整状态
- 分析变量间的约束关系
- 结合动态分析和静态分析
这个题目展示了数据流混淆在逆向题目中的应用,解题需要从传统的控制流分析转向数据流分析,理解变量如何在不同块之间传递和转换。