2023工业信息安全技能大赛—鹏城锦标赛复现-工控maze

img

这道所谓的工控题目其实就是一道CTF的流量分析+逆向的迷宫题。鉴于是第一次认真复现和总结流量分析和逆向的题目,感觉学到了不少东西,写篇博客记录一下。

题目附件

链接:https://pan.baidu.com/s/1RFHOLYmpAFbkPiR29dnA7g?pwd=c1cs
提取码:c1cs

流量分析

开始拿到流量包是这么一个情况

image-20231024164647831

选择一个 TCP 包,追踪流

image-20231024164744234

选择红框标注的这个箭头,可以往后翻找数据流

image-20231024164912089

再往后翻一个流的时候,发现是 7z 字符串开头的,猜测这个流量里可能藏了一个 7z 文件

image-20231024165234880

查看一下这个流对应的几个数据包,发现最后的包是 RST 标志位,这表明因为异常中断了这次的通信连接,所以这个流显示的并不是文件完整的数据。

image-20231024165406121

因为之后会换个端口继续重传,结合这第一部分的信息可知,用 (ip.src == 192.168.40.199 && ip.dst == 192.168.40.41) && (tcp.flags.push == 1) 过滤出 192.168.40.199192.168.40.41 通信时在应用层传输有效数据的报文(如下所示)

image-20231024171057410

用命令 tshark -r maze.pcapng -Y "(ip.src == 192.168.40.199 && ip.dst == 192.168.40.41) && (tcp.flags.push == 1)" -T fields -e tcp.payload > 1.txt

将数据以文本形式导入到 1.txt 文件中,再用 010editor 打开是这个样子的

image-20231024171302683

选择 010editor 里面的导入十六进制,导入的文件用的就是 1.txt

image-20231024171333047

这样就拿到了这个 7z 文件的原始数据

image-20231024171449459

将其保存后,就能正常解压里面的文件。拿到后发现是个 exe 文件,直接放到 IDA 里逆向分析

image-20231024171553486

代码逆向

通过下面的关键代码能猜测出这是一个迷宫游戏(部分变量已将其重命名)

image-20231024171731597

结合着运行程序和静态分析时的关键字符串,能够判断出走通迷宫的字符串,能输出 Congratulations! ,那么这个字符串就是 flag

image-20231024172004109

结合着 IDA 动态调试,首先能够确定输入函数和输出函数。当输入完数据后,确定了两个变量,分别是输入的字符串 buf 和字符串长度 buf_length(已经对其重命名)

image-20231024172352249

迷宫题通常会用一个一维数组来表明一个图(团队里的逆向手说的 😋),因此上下移动的话,移动距离应该加一行的长度,而左右移动的话距离只加很短,由此能判断是横向移动还是纵向移动。

通过这个规律,最终能重命名出表示横、纵、单次移动距离、移动次数这些变量名称(具体逆向过程不再记录,下面给出我重命名变量后的代码)

switch ( buf[(_DWORD)v7] )
{
case 'B':
heng = v18 + 2;
goto LABEL_18;
case 'H':
++zong;
move_num += 10;
++step;
if ( zong < 1 )
goto LABEL_27;
v9 = zong <= 8;
goto LABEL_20;
case 'K':
heng = v18 - 1;
goto LABEL_18;
case 'M':
zong += 2;
move_num += 20;
++step;
if ( zong < 1 )
goto LABEL_27;
v9 = zong <= 8;
goto LABEL_20;
case 'P':
heng = v18 - 2;
goto LABEL_18;
case 'Q':
--zong;
move_num -= 10;
++step;
if ( zong < 1 )
goto LABEL_27;
v9 = zong <= 8;
goto LABEL_20;
case 'V':
zong -= 2;
move_num -= 20;
++step;
if ( zong < 1 )
goto LABEL_27;
v9 = zong <= 8;
goto LABEL_20;
case 'W':
heng = v18 + 1;
LABEL_18:
++step;
v18 = heng;
if ( heng >= 1 )
{
v9 = heng <= 8;
LABEL_20:
if ( v9 && *((_DWORD *)v16 + heng + move_num) != 1 )
goto LABEL_22;
}
goto LABEL_27;

移动的规则明白后,就剩起点和终点的坐标,以及地图和地图边界问题了。

起点坐标就是变量 heng zong 的初始值 (1,1)

终点坐标通过下面三行代码可以分析出来,输出 Congratulations! 的条件就是移动次数要小于等于 12hengzong 要为 8 ,所以判断出终点坐标为 (8,8)

if ( step > 12 || zong != 8 || heng != 8 )
goto LABEL_27;
v10 = sub_2916F0(std::cout, "Congratulations!");

再看下输出 Congratulations! 的条件,每次执行任何一个操作(移动策略),都会到 LABEL_20 代码处进行判断,也就是每次移动后都要判断当前位置是否已经到达终点。

image-20231024174245709

关键代码 if ( v9 && *((_DWORD *)v16 + heng + move_num) != 1 ) 表面 heng zong 都要小于等于 8 ,且 (v16+heng+move_num) 不等于 1 才能往下走。第一个条件的 8 有可能就代表地图的边界,也就是一行只有八个可移动点,第二个条件则代表当前位置是否位于墙,也就是不可走的点位(众所周知,迷宫一定都是有墙的,这里的 1 能猜测到就是墙的意思)

如果说地图边界是 8的话,就很难解释纵向移动 1 或者 2 时,为什么 move_num 这个变量(已被重命名)加减的是 10 或者 20 了。

image-20231024174944908

经过分析后,只有一种解释。横向和纵向可移动的点位只有 8 个,但是横纵的边界是有墙的。而 move_num 每次加一行算的是所有可移动点位和不可移动点位。(大致轮廓确定如下,1 代表墙 0 代表可走的范围)

1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1
1 1 1 1 1 1 1 1 1 1

最后一步就是根据数组画出真正的图,根据上面分析,已知存在一百个点位。观察数组 v16 ,一共有 25 个元素,每个元素占 16 个字节,总共是 400 字节。能猜测到 4 字节来表示一个点位。

image-20231024180212398

v16[0] 为例,IDA 看到的数据是这样的,这样的显示误导我了很久(我一直以为这个直接看到的就是最终的点位图,但这和前面分析的不一样,连周围的墙都没有…)

image-20231024180617688

实际上应该将其转为 dd (双字型,Define Double Word),这样看到的数据才是对的。把所有的数据全部转换出来,按照每十个点位一行来把图画出来

image-20231024180855089

最终的地图如下

1, 1, 1, 1, 1, 1, 1, 1, 1, 1
1, 0, 0, 1, 0, 0, 0, 1, 0, 1
1, 1, 0, 1, 0, 0, 0, 1, 0, 1
1, 0, 0, 0, 0, 1, 1, 0, 0, 1
1, 0, 1, 1, 1, 0, 0, 0, 0, 1
1, 0, 0, 0, 1, 0, 1, 0, 1, 1
1, 0, 1, 0, 0, 0, 1, 0, 0, 1
1, 0, 1, 1, 1, 1, 1, 1, 0, 1
1, 1, 0, 0, 0, 0, 0, 0, 0, 1
1, 1, 1, 1, 1, 1, 1, 1, 1, 1

按照这个走法,输入对应的字符即可

image-20231024181113991

image-20231024181215291

所以最后的 flagflag{WMKMBHBVBMWM}

参考文章

2023工业信息安全技能大赛—鹏城锦标赛 WriteUp - wgf4242 - 博客园 (cnblogs.com)