HITCTF pipe_snake详解
字数 1292 2025-08-22 12:22:15
HITCTF pipe_snake 逆向分析详解
1. 程序概述
pipe_snake 是一个结合了贪吃蛇游戏逻辑和加密算法的 CTF 逆向题目。程序通过验证输入 flag 的正确性来控制游戏流程,最终需要分析游戏逻辑和加密机制来获取正确 flag。
2. 主要函数分析
2.1 主函数流程
程序主要执行流程:
- 首先进入
sub_401110函数 - 然后进入关键的
check函数(实际为__security_check_cookie) - 通过检查后会进入贪吃蛇游戏逻辑
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 游戏关键机制
-
分数系统:
- 初始分数为5(
dword_40D618) - 每次移动分数减1
- 吃到食物时:
- 增加分数(
dword_40D61C,初始值为3) - 每次吃食物后
dword_40D61C增加2 - 蛇身长度增加
- 增加分数(
- 初始分数为5(
-
胜利条件:
- 当分数
dword_40D618 == 8时返回-1,表示成功 - 其他情况返回错误代码
- 当分数
-
方向控制:
- 3389: 左移
- -1: 下移
- 443: 右移
- 8080: 上移
-
食物投放:
- 食物位置由
dword_40E194和dword_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 % 5和v11 / 5决定 v11 == 25时不下发食物
4. Flag生成逻辑
-
格式要求:
- 总长度36字节
- 以"flag{"开头,"}"结尾
- 中间30个字符(15字节的十六进制表示)
-
内容生成:
- 分析贪吃蛇路径和食物投放点
- 根据游戏逻辑推导出正确的字节序列
- 转换为十六进制字符串格式
-
最终flag:
flag{b3b3b3b340b3b3b3cfb3b3b3b3b3fd}
5. 解题步骤总结
- 识别程序基本结构和加密函数
- 分析输入格式要求和转换逻辑
- 理解贪吃蛇游戏的控制机制和胜利条件
- 导出并分析隐藏数据
- 根据游戏逻辑推导出正确的flag字节序列
- 将字节序列转换为十六进制字符串格式
- 组合成完整flag格式验证
6. 关键数据
- 字符串加密密钥:各字符串的倒数第二个字节
- 方向控制值:
- 左:3389
- 下:-1
- 右:443
- 上:8080
- 初始分数:5
- 食物加分初始值:3,每次增加2
- 胜利分数:8