看雪学院 08月29日
KCTF 2025 比赛回顾:数学谜题与QNX环境的逆向挑战
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

2025 KCTF 比赛已于8月15日开赛,采用多维度评分体系。第七题《危局初现》是一道结合数学填数谜题、QNX特殊运行环境及干扰项的综合性逆向工程挑战,要求选手具备静态分析能力、抽象方程组并求解的能力。题目设计巧妙,考察了选手的逆向推理和建模能力。

🎯 题目《危局初现》的核心挑战在于其混合了数学填数谜题和QNX特定运行环境。选手需要理解并操作一个二维数组,通过输入一个14位字符序列(前10位为16进制字符,后4位为'asas')来填充该数组,并满足特定条件,即每条红线上的三个数字之和必须为19。

⚙️ 该题的难点在于其运行环境为QNX,限制了常规的动态调试方法,迫使选手采用静态分析。代码中存在常量替换和运算代换,增加了分析的复杂度,特别是对`string_to_code`函数的处理,需要将其替换为实际数值才能清晰理解代码逻辑。

🧩 题目的关键是通过建立一个包含10个未知数(S0-S9)的线性方程组来求解。这些未知数代表了输入的16进制字符对应的数值,它们需要满足多个等式,例如S0+S2+S3 = 34,S1+S3+S6 = 34等。此外,还有一项涉及特定计算和随机数(如`/proc/self/as`)的干扰项,增加了解题的难度。

💻 针对该题,研究人员通过静态分析提取出线性方程组,并利用Python编写了注册机来暴力破解。注册机遍历所有可能的取值组合,根据方程组进行验证,最终找到了470个有效的解,其中一个解为`89cefabd76asas`,能够使程序输出`ok`。

💡 题目设计者还巧妙地融入了干扰项,例如在虚拟机中通过`/etc/rc.d`文件的状态以及`devctl`函数调用的结果来影响程序逻辑,增加了分析的迷惑性。最终的验证过程需要将计算出的值与一系列加减运算(如123+45-67+8-9=100)的结果进行比对,确保后四位字符也符合要求。

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 -

球分享

球点赞

球在看

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

KCTF 2025 逆向工程 QNX 数学谜题 线性方程组 静态分析 CTF
相关文章