DiceCTF-Rev Scrambled-up
字数 1749 2025-08-18 17:33:44

DiceCTF逆向题目"Scrambled-up"深度解析

题目概述

"Scrambled-up"是DiceCTF中的一个逆向题目,采用了数据流混淆技术而非传统的执行流混淆。题目使用了一种类似Lisp的函数式编程风格,通过数据流而非控制流来组织程序逻辑。

程序结构分析

主要数据结构

程序使用了三种核心数据结构:

  1. Inst结构体:表示指令的基本信息
struct Inst {
    Block *next_call;
    __int64 line;
    __int64 argc;
    __int64 slot_cnt;
    __int64 exec_type;
    Var var;
};
  1. Edge结构体:表示块之间的连接关系
struct Edge {
    Edge *next_cond;
    __int64 dst_line;
    __int64 src_line;
};
  1. 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; // 指针指向内容的长度
};

程序执行流程

初始化阶段

  1. 信号处理:注册SIGSEGV信号处理函数
  2. 输入处理:读取用户输入的flag
  3. 内存映射:使用mmap分配大块内存(0xF00000字节)
  4. 指令读取:从全局变量inst_edge和inst_code读取数据初始化Block

执行阶段

程序执行分为两部分:

  1. 初始化Block和Edge:通过read_inst函数完成
  2. 执行指令:通过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

  1. 在ExecFunc8(flag检查函数)处下断点
  2. 获取Block和Edge的内存布局
  3. 编写脚本解析数据结构关系

第二阶段:flag有效性检查

发现多个检查逻辑:

  1. 长度检查:flag长度必须为0x26(38)字节
  2. 格式检查:必须以"dice{"开头,"}"结尾
  3. 复杂校验:多轮异或、乘法和加法运算校验
  4. 迷宫校验:通过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]))

总结

  1. 数据流编程特点

    • 所有逻辑都会被执行
    • 只有输入和输出变量,输入不变
    • 条件判断简单(只有0和非0)
  2. 与传统执行流编程对比

特点 执行流编程 数据流编程
执行范围 部分逻辑执行 所有逻辑执行
变量修改 变量可能被修改 输入变量不变
条件判断 丰富多样 只有0和非0
  1. 解题关键
    • 理解数据流传递关系
    • 在程序结尾处dump完整状态
    • 分析变量间的约束关系
    • 结合动态分析和静态分析

这个题目展示了数据流混淆在逆向题目中的应用,解题需要从传统的控制流分析转向数据流分析,理解变量如何在不同块之间传递和转换。

DiceCTF逆向题目"Scrambled-up"深度解析 题目概述 "Scrambled-up"是DiceCTF中的一个逆向题目,采用了数据流混淆技术而非传统的执行流混淆。题目使用了一种类似Lisp的函数式编程风格,通过数据流而非控制流来组织程序逻辑。 程序结构分析 主要数据结构 程序使用了三种核心数据结构: Inst结构体 :表示指令的基本信息 Edge结构体 :表示块之间的连接关系 Block结构体 :表示内存中的程序块 变量表示 程序使用Var结构表示变量: 程序执行流程 初始化阶段 信号处理 :注册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约束求解 总结 数据流编程特点 : 所有逻辑都会被执行 只有输入和输出变量,输入不变 条件判断简单(只有0和非0) 与传统执行流编程对比 : | 特点 | 执行流编程 | 数据流编程 | |------|-----------|------------| | 执行范围 | 部分逻辑执行 | 所有逻辑执行 | | 变量修改 | 变量可能被修改 | 输入变量不变 | | 条件判断 | 丰富多样 | 只有0和非0 | 解题关键 : 理解数据流传递关系 在程序结尾处dump完整状态 分析变量间的约束关系 结合动态分析和静态分析 这个题目展示了数据流混淆在逆向题目中的应用,解题需要从传统的控制流分析转向数据流分析,理解变量如何在不同块之间传递和转换。