2025 KCTF 2025-08-29 18:00 上海
本题共19支战队成功提交flag
2025 KCTF 于8月15日中午12点正式开赛!比赛设置了多维度的评分体系,包括难度值、火力值和精致度积分。今天中午12点,第七题《危局初现》已截止答题。
*注意:签到题《废柴少年觉醒》持续开放,整个比赛期间均可提交答案获得积分
KCTF评委团队点评:
本题结合了 数学填数谜题 + QNX 特殊运行环境 + 干扰项,要求选手既要具备静态逆向分析能力,又要能抽象出方程组并求解。总体来说是一道 考察逆向推理与解方程建模能力的综合题,兼顾创意与挑战性。
一起来看看本题的设计思路和解题思路吧~
一、
设计思路
出题人:geekfire
来自【天外星系】战队
题目名称:guess
运行环境:QNX
输出提示:key正确则输出提示ok
题目运行环境:
ctf平台 qnx.rar
解压密码:qnxqnx233
启动虚拟机后,输入账户密码 root/root登陆系统。按win键在菜单Utilities->Terminal打开终端窗口,即可在/root目录下运行程序。
题目设计思路
完成一个填数字的游戏,程序会初始化一个二维数组,然后输入的key-5(限制为1-10)作为数字填入下图红色圆圈中。
如果每条红线上的3个数字之和为19,则输入正确。
key最后4位 表示加减运算符号,填充后:
使得123 45 67 8 9 =100
正确答案为:89cefabd76asas
二、
赛题解析
该解析由论坛会员ID:【htg】提供
工具:IDA、VWmare
一、观察程序运行结果
此题存在多解(暴力破解,有470个解)。正确输出 ok,错误输出 no。
二、程序主要逻辑
guess 需要在 给定的虚拟机下运行,其无法在 Ubuntu或Centos操作系统下运行,一时半会找不到动态调试的方法【放弃】,直接采用静态调试方法分析代码。
其核心是:序列号为 14 个字符,其中前10个字符为小写16进制字符,即[0-9a-f],最后4个字符为"asas"。而前10个字符需满足设定的条件,可以通过求解方程来获取,可以用线性方程组【详见第四部分 线性方程组求解】(降低暴力破解时间开销)来获取解答,也可以通过编程暴力破解。
伪代码及主要等式如下:
SN = "".join([S0,S1.S2.S3,S4,S5,S6,S7,S8,S9]) + "asas";
S0、S1、S2、S3、S4、S5、S6、S7、S8、S9 为整数,取值为 [0,15]
S0+S2+S3 = 34
S1+S3+S6 = 34
S2+S4+S8 = 34
S4+S7+S9 = 34
S5+S6+S7 = 34
4 + 5 + 7 + 8 + S9 + [0,1,2] = 8*S0-34;//其中的列表值为不确定值,可以根据求解的结果去虚拟机里面验算。
10个未知数,6个方程组,虽然有取值范围限制,但是若没有设定好边界条件或等式关系,极有可能造成多解。
三、IDA代码分析
程序的代码主要逻辑还是比较清晰,只是存在几点常量代换、运算代换,以及调用虚拟机内部的关键处增加干扰等。
前10个小写16进制字符转换成对应的16进制数值:
for ( i = 0; inputSN[i] && i <= 9; ++i ) // 前 10 个 字符均为小写的16进制字符
{
if ( inputSN[i] <= '0' || inputSN[i] > '9' )
{
if ( inputSN[i] == 'a' )
{
v41[5][index++] = 0xA;
}
else if ( inputSN[i] == 'b' )
{
v41[5][index++] = 0xB;
}
else if ( inputSN[i] == 'c' )
{
v41[5][index++] = 0xC;
}
else if ( inputSN[i] == 'd' )
{
v41[5][index++] = 0xD;
}
else if ( inputSN[i] == 'e' )
{
v41[5][index++] = 0xE;
}
else if ( inputSN[i] == 'f' )
{
v41[5][index++] = 0xF;
}
else
{
if ( inputSN[i] == '0' )
v41[5][index] = 0;
else
v41[5][index] = 0x10;
++index;
}
}
else
{
v41[5][index++] = inputSN[i] - '0';
}
}
两个重要的二维数组的内部布局,可通过模拟执行代码输出结果分析处理,后面的代码涉及对 v41 和 v40 二维数组的操作。
小整数常量替换
程序采用 string_to_code(const str) 将循环变量进行了替换,初看代码比较费劲,可考虑直接拷贝代码后,进行整体替换处理。
替换前:
j = string_to_code("act");
v47 = string_to_code("act"); // 0
v48 = string_to_code("con"); // 0
v49 = string_to_code("con"); // 0
v50 = string_to_code("con"); // 0
v51 = string_to_code("con"); // 0
v52 = string_to_code("con"); // 0
v53 = string_to_code("act"); // 0
for ( i = 0; i <= 4; ++i )
{
for ( j = string_to_code("act"); j <= 4; ++j )
{
v4 = string_to_code("con");
if ( v4 == i && (v5 = string_to_code("stop"), v5 == j) )
{
v41[i][j] = v41[5][v47++];
v40[0][v48++] = v41[i][j];
}
else
{
v6 = string_to_code("abort");
if ( v6 == i && (v7 = string_to_code("con"), v7 == j) )
{
v41[i][j] = v41[5][v47++];
v40[1][v49++] = v41[i][j];
}
else
{
v8 = string_to_code("cancel");
if ( v8 == i && (v9 = string_to_code("enable"), v9 == j) )
{
v41[i][j] = v41[5][v47++];
v40[0][v48++] = v41[i][j];
v40[2][v50++] = v41[i][j];
可以将直接将 string_to_code 函数拷贝到 VS 里,然后输出对应的结果,之后替换源码。
replace_rules = {
"string_to_code(\"act\")":"0",
"string_to_code(\"con\")":"0",
"string_to_code(\"stop\")":"3",
"string_to_code(\"abort\")":"1",
"string_to_code(\"cancel\")":"1",
"string_to_code(\"enable\")":"2",
"string_to_code(\"start\")":"2",
"string_to_code(\"reboot\")":"4",
"string_to_code(\"reset\")":"4",
"string_to_code(\"run\")":"2"
}
替换后:可以更为清晰看清楚代码结构。
j = 0;
v47 = 0; // 0 //v41[5]的索引,指向的是用户输入的10个16进制字符
v48 = 0; // 0 //v40[0]的索引,指向的是第0行,拷贝前面用户输入的前10个字符中的部分内容
v49 = 0; // 0 //v40[1]的索引,指向的是第1行,拷贝前面用户输入的前10个字符中的部分内容
v50 = 0; // 0 //v40[2]的索引,指向的是第2行,拷贝前面用户输入的前10个字符中的部分内容
v51 = 0; // 0 //v40[3]的索引,指向的是第3行,拷贝前面用户输入的前10个字符中的部分内容
v52 = 0; // 0 //v40[4]的索引,指向的是第4行,拷贝前面用户输入的前10个字符中的部分内容
v53 = 0; // 0 //累加
for ( i = 0; i <= 4; ++i )
{
for ( j = 0; j <= 4; ++j )
{
v4 = 0;
if ( v4 == i && (v5 = 3, v5 == j) ) // i==0 %% j==3
{
v41[i][j] = v41[5][v47++];
v40[0][v48++] = v41[i][j];
}
else
{
v6 = 1;
if ( v6 == i && (v7 = 0, v7 == j) ) // i==1 %% j==0
{
v41[i][j] = v41[5][v47++];
v40[1][v49++] = v41[i][j];
}
else
{
v8 = 1;
if ( v8 == i && (v9 = 2, v9 == j) ) // i==1 %% j==2
{
v41[i][j] = v41[5][v47++];
v40[0][v48++] = v41[i][j];
v40[2][v50++] = v41[i][j];
}
等式相关:获取 v40 的第四行 数值之和
v22 = 4;
if ( v22 == i )v53 += v41[i][j];//4 + 5 + 7 + 8 + S9
干扰项:利用虚拟机执行函数的结果来影响分析1
if ( stat("/etc/rc.d", (struct stat *)&v43) )// 重点关注此处是否有修改?此时不能为真。stat 获取文件的状态成功,即返回0。否则 V40[i][j]将会读取未赋初值的区域。
v53 += v40[i][j];
等式:建立5个等式,即每行数值之和需要等于 34 = 5*3+19
for ( i = 0; i <= 4; ++i ) // 要求 v40 的每行的3个元素之和为 34
{
v57 = 0;
for ( j = 0; j <= 2; ++j )
{
v57 += v40[i][j];
v57 -= 5;
}
if ( v57 != 19 )v54 = 0;
}
干扰项:利用虚拟机执行函数的结果来影响分析2:在虚拟机里验算得到的3种序列号
//此处对应于上面提到的方程组中的[0,1,2],也就是 v53 到底累加0,还是1,抑或2,暂时留下来,后面通过注册机运算,得到的序列号,去虚拟机验算,发现只有累积0的时候,才正确。
fd = open("/proc/self/as", (int)"r"); // fd!=-1
if ( fd == -1 )++v53; // 此时不能为真,此处是否有修改
if ( devctl(fd, 0x41100801, (int)&s, 0x110, 0) )// 十六进制0x41100801是 QNX 系统中预定义的命令宏DCMD_PROC_ASINFO的数值,作用是获取进程的地址空间信息(Address Space Info)
++v53;
等式:建立1个等式,v53的值(v40第4行之和)应等于v41首行累乘积(除了第0个外)与v41首行首列的值(实际为0)、(v39 & 0x80)之和(为0)
v23 = v53;
v24 = v41[0][0] + v41[0][3] * v41[0][2] * v41[0][1] * v41[0][4] + (v39 & 0x80);//也可以进行测试,(v39 & 0x80)只有两种值,0 或者 0x80,而0x80时无解。
v25 = 4 + 1;//5
if ( v23 != v24 + v25 * ~4 - 9 )v54 = 0;//if ( v23 != v24 -34 )v54 = 0;
这里应用到了两处干扰项,(v39 & 0x80),此时的v39尚未赋值,其运算的结果为0;v25=5,~4(反码的内存表现形式)实际就是 -5(补码的内存表现形式)。实际上就是 4+5+7+8+S9 = 8*S0-34
数值的反码和补码表现形式
最后4个字符判断:实际上就是通过对 45、67、8、9进行加减运算,结果与 123 相加,最终得到 100。只有一种 123+45-67+8-9,对应的字符就是 asas
v56 = 123;
v26 = 2 * 4; // 最后应该是 as
if ( inputSN[v26 + 2] == 'a' )// 满足
v56 += 45;
v27 = 2 * 4;
if ( inputSN[v27 + 2] == 's' )// 不满足
v56 -= 45;
v28 = 2 * 4;
if ( inputSN[v28 + 3] == 'a' )// 不满足
v56 += 67;
v29 = 2 * 4;
if ( inputSN[v29 + 3] == 's' )// 满足
v56 -= 67;
v30 = 2 * 4;
if ( inputSN[v30 + 4] == 'a' )// 满足
v56 += 8;
v31 = 2 * 4;
if ( inputSN[v31 + 4] == 's' )// 不满足
v56 -= 8;
v32 = 3 * 4;
if ( inputSN[v32 + 1] == 'a' )// 不满足
v56 += 9;
v33 = 3 * 4;
if ( inputSN[v33 + 1] == 's' )// 满足
v56 -= 9;
v34 = 4;
v35 = 1;
if ( 20 * (v34 + v35) != v56 )
v54 = 0;
四、线性方程组求解
结果就是 S0至S9的通解公式,我们知道各自取值为[0,15],那就可以以 S6(k4)、S7(k3)、S8(k2) 作为循环变量[0,15],并确保所计算的其他数值结果为 [0,15],可以加快寻求所有有效解的速度。
五、注册机代码
import os
# 初始化计数器
solution_count = 0
# 遍历所有可能的取值组合
for S0 in range(16):
for S1 in range(16):
for S2 in range(16):
# 根据第一个方程计算S3
S3 = 34 - S0 - S2
# 检查S3是否在有效范围内
if S3 < 0 or S3 > 15:
continue
for S4 in range(16):
# 根据第三个方程计算S8
S8 = 34 - S2 - S4
if S8 < 0 or S8 > 15:
continue
for S5 in range(16):
for S6 in range(16):
# 检查第二个方程
if S1 + S3 + S6 != 34:
continue
for S7 in range(16):
# 检查第五个方程
if S5 + S6 + S7 != 34:
continue
# 根据第四个方程计算S9
S9 = 34 - S4 - S7
if S9 < 0 or S9 > 15:
continue
# 检查第六个方程
# 累加 第 4 行的所有元素
tmp1 = 4 + 5 + 7 + 8 + S9
# fd = open("/proc/self/as", (int)"r"); 经验证返回 fd != -1,即不做累加
tmp2 = 0
# devctl(fd, 0x41100801, (int)&s, 0x110, 0);经验证返回 0 ,即不做累加
tmp3 = 0
left = tmp1 + tmp2 + tmp3
right = 8 * S0 - 34
if left == right:
solution_count += 1
print(f"当前解 {solution_count}:")
print(f"{hex(S0)[2:]}{hex(S1)[2:]}{hex(S2)[2:]}{hex(S3)[2:]}{hex(S4)[2:]}{hex(S5)[2:]}{hex(S6)[2:]}{hex(S7)[2:]}{hex(S8)[2:]}{hex(S9)[2:]}asas")
#os.system("pause")
#print("---------------------")
print(f"共找到 {solution_count} 个解")
#共找到 470 个解
部分解
当前解 1:
84bfd4ffa6asas
当前解 2:
84bfe5fe96asas
当前解 3:
84bff6fd86asas
当前解 4:
85bfd5efa6asas
当前解 5:
85bfe6ee96asas
当前解 6:
85bff7ed86asas
当前解 7:
85ced4ff96asas
当前解 8:
85cee5fe86asas
当前解 9:
85cef6fd76asas
..........................
扫码参赛
第八题《暗云涌动》火热挑战中
关于KCTF
看雪CTF(简称KCTF)是圈内知名度最高的技术竞技,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
扫码进入2025 KCTF
主办方
支持单位
看雪CTF比赛分为两个阶段,所有论坛会员均可参与,第一阶段是防守篇,防守方根据比赛要求制作题目,根据题目被破解的时间排名,被破解时间长者胜出。第二阶段为攻击篇,攻击第一阶段的题目,根据攻击成功的时间与题目排名,破解时间短且破解题目数多者胜。既给了防守方足够的施展空间,也避免过度浪费攻击方的时间。从攻防两个角度看,都是个难得的竞技和学习机会。
看雪CTF比赛历史悠久、影响广泛。自2007年以来,看雪已经举办十多个比赛,与包括金山、360、腾讯、阿里等在内的各大公司共同合作举办赛事。比赛吸引了国内一大批安全人士的广泛关注,历年来CTF中人才辈出,汇聚了来自国内众多安全人才,高手对决,精彩异常,成为安全圈的一次比赛盛宴,突出了看雪论坛复合型人才多的优势,成为企业挑选人才的重要途径,在社会安全事业发展中产生了巨大的影响力。
- End -
球分享
球点赞
球在看
