HITCTF pipe_snake详解
字数 1292 2025-08-22 12:22:15

HITCTF pipe_snake 逆向分析详解

1. 程序概述

pipe_snake 是一个结合了贪吃蛇游戏逻辑和加密算法的 CTF 逆向题目。程序通过验证输入 flag 的正确性来控制游戏流程,最终需要分析游戏逻辑和加密机制来获取正确 flag。

2. 主要函数分析

2.1 主函数流程

程序主要执行流程:

  1. 首先进入 sub_401110 函数
  2. 然后进入关键的 check 函数(实际为 __security_check_cookie
  3. 通过检查后会进入贪吃蛇游戏逻辑

2.2 关键检查函数

void __cdecl __security_check_cookie() {
    // 省略变量声明
    
    // 第一部分:输入格式检查
    v0 = sub_3D1520(&unk_40D59C, 28);
    sub_3D1020(v0);
    sub_3D1520(&unk_40D59C, 28);
    v1 = sub_3D1520(&unk_3F6968, 4);
    sub_3D1080(v1, Str1, 100);
    
    // 检查输入长度和格式
    if ( strlen(Str1) != 36 || 
        (v2 = (const char *)sub_3D1520(&unk_40D594, 7), strncmp(Str1, v2, 5u)) || 
        Str1[35] != 125 ) {
        // 失败处理
        v3 = sub_3D1520(&unk_3F6954, 19);
        sub_3D1020(v3);
        sub_3D1520(&unk_3F6954, 19);
        sub_3D1520(&unk_40D594, 7);
        _loaddll((char *)1);
    }
    
    // 第二部分:flag内容转换
    for ( i = 0; i < 15; ++i ) {
        if ( (unsigned __int8)sub_3D17C0(*((_BYTE *)&dword_40E134 + 2 * i + 1)) && 
             (unsigned __int8)sub_3D17C0(*((_BYTE *)&dword_40E134 + 2 * i + 2)) ) {
            // 十六进制字符转换
            if ( *((char *)&dword_40E134 + 2 * i + 1) <= 57 )
                v6 = *((_BYTE *)&dword_40E134 + 2 * i + 1) - 48;
            else
                v6 = *((_BYTE *)&dword_40E134 + 2 * i + 1) - 87;
            Str1[i] = 16 * v6;
            
            if ( *((char *)&dword_40E134 + 2 * i + 2) <= 57 )
                v5 = *((_BYTE *)&dword_40E134 + 2 * i + 2) - 48;
            else
                v5 = *((_BYTE *)&dword_40E134 + 2 * i + 2) - 87;
            Str1[i] |= v5;
            Str1[i] = Str1[i];
        } else {
            // 失败处理
            v4 = sub_3D1520(&unk_3F6954, 19);
            sub_3D1020(v4);
            sub_3D1520(&unk_3F6954, 19);
            _loaddll((char *)1);
        }
    }
}

关键点:

  • 输入长度必须为36字节
  • 必须以"flag{"开头(unk_40D594解密后为"flag{")
  • 必须以"}"结尾(ASCII 125)
  • 中间30个字符被分成15组,每组2个字符转换为1个字节(十六进制转换)

2.3 字符串加密函数

BYTE *__cdecl sub_3D1520(BYTE *a1, int a2) {
    unsigned int i;
    for ( i = 0; i < a2 - 1; ++i )
        a1[i] ^= a1[a2 - 2];
    return a1;
}

这是一个简单的异或加密函数:

  • 参数1:待加密字符串
  • 参数2:字符串长度
  • 加密方式:每个字节与倒数第二个字节异或

2.4 SM4加密/解密函数

char *__cdecl sub_3D1980(unsigned __int8 a1) {
    // 省略变量声明
    
    v8 = (char *)&unk_40E238 + 8232 * a1;
    sub_3D1940(v8 + 24, Source, 16); // 解密函数
    
    if ( Source[0] == 255 ) {
        // 加密路径
        v3[0] = *(_DWORD *)Str1;
        v3[1] = *(_DWORD *)dword_40E134;
        v3[2] = dword_40E138;
        v4 = word_40E13C;
        v5 = byte_40E13E;
        v6 = a1;
        sub_3D1900(v3, Source, 16); // 加密函数
        memcpy_s(v8 + 4124, 0x1000u, Source, 0x10u);
    } else {
        if ( Source[0] != 15 ) {
            // 存储数据
            dword_40E194[Source[0]] = Source[1];
            dword_442648[Source[0]] = Source[2];
        }
        // 初始化操作
        v1 = v8 + 4124;
        *((_DWORD *)v8 + 1031) = -1;
        v1[1] = -1;
        v1[2] = -1;
        v1[3] = -1;
    }
    return v8;
}

关键点:

  • 使用了标准的SM4加密算法,没有魔改
  • 主要用于数据传输加密
  • Source[0] != 15时,会将Source[1]Source[2]存储到数组中(用于贪吃蛇游戏控制)

3. 贪吃蛇游戏逻辑分析

3.1 主游戏循环

int sub_3D43D0() {
    // 省略变量声明
    
    while ( (unsigned int)dword_4429B8 < 0xF ) {
        // 获取控制指令
        dword_442B58 = dword_40E194[dword_4429B8];
        dword_4429BC = dword_442648[dword_4429B8];
        dword_442B5C = dword_40D5DC[dword_4429B8++];
        
        // 蛇身移动逻辑
        v4 = dword_442B60[0];
        v3 = dword_4429C8[0];
        dword_442B60[0] = dword_4429B4;
        dword_4429C8[0] = dword_4429C4;
        
        for ( i = 1; i < dword_4429C0; ++i ) {
            v2 = dword_442B60[i];
            v1 = dword_4429C8[i];
            dword_442B60[i] = v4;
            dword_4429C8[i] = v3;
            v4 = v2;
            v3 = v1;
        }
        
        // 方向控制
        if ( dword_442B5C > 3389 ) {
            if ( dword_442B5C == 8080 )
                ++dword_4429C4;
        } else {
            switch ( dword_442B5C ) {
                case 3389: --dword_4429B4; break; // 左
                case -1: --dword_4429C4; break;   // 下
                case 443: ++dword_4429B4; break;  // 右
            }
        }
        
        // 碰撞检测
        if ( (unsigned int)dword_4429B4 > 4 || (unsigned int)dword_4429C4 > 4 )
            return 53;
            
        for ( j = 0; j < dword_4429C0; ++j ) {
            if ( dword_442B60[j] == dword_4429B4 && dword_4429C8[j] == dword_4429C4 )
                return 53;
        }
        
        // 食物检测
        if ( dword_4429B4 == dword_442B58 && dword_4429C4 == dword_4429BC ) {
            if ( dword_40D618 >= 2 )
                return 8080;
            dword_40D618 += dword_40D61C;
            dword_40D61C += 2;
            ++dword_4429C0;
            dword_442B58 = 255;
            dword_4429BC = 255;
        } else {
            if ( dword_442B58 != 255 || dword_4429BC != 255 )
                return 0;
            --dword_40D618;
        }
        
        // 分数判断
        if ( dword_40D618 <= 0 )
            return 3306;
        if ( dword_40D618 > 8 )
            return 25565;
        if ( dword_40D618 == 8 )
            return -1;
    }
    return 3306;
}

3.2 游戏关键机制

  1. 分数系统

    • 初始分数为5(dword_40D618
    • 每次移动分数减1
    • 吃到食物时:
      • 增加分数(dword_40D61C,初始值为3)
      • 每次吃食物后dword_40D61C增加2
      • 蛇身长度增加
  2. 胜利条件

    • 当分数dword_40D618 == 8时返回-1,表示成功
    • 其他情况返回错误代码
  3. 方向控制

    • 3389: 左移
    • -1: 下移
    • 443: 右移
    • 8080: 上移
  4. 食物投放

    • 食物位置由dword_40E194dword_442648数组决定
    • 需要分析dump出的数据来获取食物投放逻辑

3.3 数据导出分析

通过IDA脚本导出数据:

#include <idc.idc>
static main() {
    auto i,fp;
    fp=fopen("dump","wb");
    for(i=0x426980;i<=0x43D57F;i++) {
        fputc(Byte(i),fp);
    }
}

分析导出数据发现关键比较:

for ( i = 0; i < 15u; ++i ) {
    if ( v4[i] == (unsigned __int8)byte_414D18[v11] ) {
        LOBYTE(v6[0]) = i;
        if ( v11 == 25 ) {
            *(_WORD *)((char *)v6 + 1) = -1;
        } else {
            BYTE1(v6[0]) = v11 % 5;
            BYTE2(v6[0]) = v11 / 5;
        }
        sub_401220(hNamedPipe, (int)v6, 0x10u, 0);
    }
}
  • v4是输入的flag内容
  • byte_414D18数组比较
  • 食物位置由v11 % 5v11 / 5决定
  • v11 == 25时不下发食物

4. Flag生成逻辑

  1. 格式要求:

    • 总长度36字节
    • 以"flag{"开头,"}"结尾
    • 中间30个字符(15字节的十六进制表示)
  2. 内容生成:

    • 分析贪吃蛇路径和食物投放点
    • 根据游戏逻辑推导出正确的字节序列
    • 转换为十六进制字符串格式
  3. 最终flag:

    flag{b3b3b3b340b3b3b3cfb3b3b3b3b3fd}
    

5. 解题步骤总结

  1. 识别程序基本结构和加密函数
  2. 分析输入格式要求和转换逻辑
  3. 理解贪吃蛇游戏的控制机制和胜利条件
  4. 导出并分析隐藏数据
  5. 根据游戏逻辑推导出正确的flag字节序列
  6. 将字节序列转换为十六进制字符串格式
  7. 组合成完整flag格式验证

6. 关键数据

  • 字符串加密密钥:各字符串的倒数第二个字节
  • 方向控制值:
    • 左:3389
    • 下:-1
    • 右:443
    • 上:8080
  • 初始分数:5
  • 食物加分初始值:3,每次增加2
  • 胜利分数:8
HITCTF pipe_ snake 逆向分析详解 1. 程序概述 pipe_ snake 是一个结合了贪吃蛇游戏逻辑和加密算法的 CTF 逆向题目。程序通过验证输入 flag 的正确性来控制游戏流程,最终需要分析游戏逻辑和加密机制来获取正确 flag。 2. 主要函数分析 2.1 主函数流程 程序主要执行流程: 首先进入 sub_401110 函数 然后进入关键的 check 函数(实际为 __security_check_cookie ) 通过检查后会进入贪吃蛇游戏逻辑 2.2 关键检查函数 关键点: 输入长度必须为36字节 必须以"flag{"开头( unk_40D594 解密后为"flag{") 必须以"}"结尾(ASCII 125) 中间30个字符被分成15组,每组2个字符转换为1个字节(十六进制转换) 2.3 字符串加密函数 这是一个简单的异或加密函数: 参数1:待加密字符串 参数2:字符串长度 加密方式:每个字节与倒数第二个字节异或 2.4 SM4加密/解密函数 关键点: 使用了标准的SM4加密算法,没有魔改 主要用于数据传输加密 当 Source[0] != 15 时,会将 Source[1] 和 Source[2] 存储到数组中(用于贪吃蛇游戏控制) 3. 贪吃蛇游戏逻辑分析 3.1 主游戏循环 3.2 游戏关键机制 分数系统 : 初始分数为5( dword_40D618 ) 每次移动分数减1 吃到食物时: 增加分数( dword_40D61C ,初始值为3) 每次吃食物后 dword_40D61C 增加2 蛇身长度增加 胜利条件 : 当分数 dword_40D618 == 8 时返回-1,表示成功 其他情况返回错误代码 方向控制 : 3389: 左移 -1: 下移 443: 右移 8080: 上移 食物投放 : 食物位置由 dword_40E194 和 dword_442648 数组决定 需要分析dump出的数据来获取食物投放逻辑 3.3 数据导出分析 通过IDA脚本导出数据: 分析导出数据发现关键比较: v4 是输入的flag内容 与 byte_414D18 数组比较 食物位置由 v11 % 5 和 v11 / 5 决定 v11 == 25 时不下发食物 4. Flag生成逻辑 格式要求: 总长度36字节 以"flag{"开头,"}"结尾 中间30个字符(15字节的十六进制表示) 内容生成: 分析贪吃蛇路径和食物投放点 根据游戏逻辑推导出正确的字节序列 转换为十六进制字符串格式 最终flag: 5. 解题步骤总结 识别程序基本结构和加密函数 分析输入格式要求和转换逻辑 理解贪吃蛇游戏的控制机制和胜利条件 导出并分析隐藏数据 根据游戏逻辑推导出正确的flag字节序列 将字节序列转换为十六进制字符串格式 组合成完整flag格式验证 6. 关键数据 字符串加密密钥:各字符串的倒数第二个字节 方向控制值: 左:3389 下:-1 右:443 上:8080 初始分数:5 食物加分初始值:3,每次增加2 胜利分数:8