2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

WriteUp 2周前 admin
96 0 0
2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析
2024 KCTF 大赛于8月15日正式开赛!比赛设置了多维度的评分体系,包括难度值、火力值和精致度积分,旨在引导竞赛的难度和趣味度,使其更具挑战性和吸引力。同时,也为参赛选手提供了更加公平、有趣的竞赛平台。

今天中午12点,第九题《第一次接触》已截止答题,本题共有17支战队成功破解,【hzqmwne】战队用时1小时20分55秒抢先拿下此题,第二名来自【CrackE】战队、第三名来自【COMPASS】战队。
*注意:签到题《逐光启航》持续开放,整个比赛期间均可提交答案获得积分

一起来看看本题设计思路和解析吧!

出题战队:laughlaugh


战队成员ID:奔跑的阿狸、misskings

2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析


设计思路


题目设计


这道题没用复杂算法,主要用到几个小坑。
序列号=hellocatx202112050a3b1c4d29,它是27位,必须平均分成3段,每段9位,但是会误导每段是8位。

第一段:hellocat x
第二段:20211205 0
第三段:a3b1c4d2 9

第一段校验

strncmp(第一段, "hellocat", 8)
第9位不提


第二段校验

reverse("20211205")="50211202"
atoi("50211202") * 3 = 150633606; 但代码里藏着一个无声的反调试,会将3变成2
第9位也不提


第三段校验

四皇后问题,每个直线、竖线、斜线上只能有一个皇后!
a3 b1 c4 d2,就是4个皇后的坐标,转换成二维数组坐标
国际象棋坐标系:
4 1
3 1
2 1
1 1
a b c d
二维数组坐标:
0 1 2 3
0 1
1 1
2 1
3 1

先介绍2个全局变量:
char g_serial3[8] = { 0 }; //第三段serial会拷贝进去
int g_serial3_verify = 0; //第三段serial最终校验数

有意思的校验方式来了!
g_serial3_verify 最终等于 0x10000039 才能校验通过
但是一开始 g_serial3_verify |= 0x10000000 = 0x10000000

如果4个坐标都正确,g_serial3_verify = 0x10000000
如果4个坐标都错,g_serial3_verify = 0x39
如果4个坐标,错1-3个,g_serial3_verify = 0x100000**

但是只有当4个落子位置都错的时候,才能执行下列4条语句,凑出0x39。但是这样开头的0x10就被清空,最终校验失败
g_serial3_verify = 0b00000001; //清空0x10,因为这里用的是 = ,不是 ^=
g_serial3_verify ^= 0b00001000;
g_serial3_verify ^= 0b00010000;
g_serial3_verify ^= 0b00100000;

所以陷入矛盾。。。

再来看 g_serial3 和 g_serial3_verify 的内存布局是挨着的:
00 00 00 00 00 00 00 00 - 00 00 00 00
模拟下 产生 0x10000039 时,内存布局应该是:
00 00 00 00 00 00 00 00 - 39 00 00 10
所以校验通过的唯一解法就是,让第三段serial是9位,第9位是'9',将 g_serial3_verify 低地址1字节覆盖成0x39


破解步骤


1.有了上面的分析,先确定serial3是9位,因为序列号必须被3整除,所以serial1 和 serial2也都是9位。
因为4皇后问题有2个解,所以 serial3是 a2b4c1d39 或 a3b1c4d29。

2.再推翻第二段的”20211205″,它必须是9位,
50211202 * 3 = 150633606
50211202的前面加1位,使运算继续成立,那就只能是’0′
202112050

3.再推翻第一段的”hellocat”,它必须是9位,末尾可能是(26个小写+10个数字)种情况。

4.所以写脚本,遍历 2 * 36种情况,对比md5即可。
python calculate.py

赛题解析


以下解析由看雪学者【tacesrever】给出,来自【tacesrever】战队。

2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析


使用ida打开程序, 分析main函数的逻辑为:


output_45548D((int)&unk_50AE70, v4);
j__memset(input, 0, 0x12Cu);
input_45591F("%[^n]", (char)input);
length = j__strlen(input);
if ( !(length % 3) && length )
{
for ( i = 0; i < length; ++i )
{
if ( (input[i] < 'a' || input[i] > 'z') && (input[i] < '0' || input[i] > '9') )
goto LABEL_3; // output_45548D((int)"WRONG!n", v5); return 0
}
Count = length / 3;
part1 = (char *)j__malloc(__CFADD__(length / 3, 1) ? -1 : length / 3 + 1);
part2 = (char *)j__malloc(__CFADD__(Count, 1) ? -1 : Count + 1);
part3 = (char *)j__malloc(__CFADD__(Count, 1) ? -1 : Count + 1);
j__memset(part1, 0, length / 3 + 1);
j__memset(part2, 0, length / 3 + 1);
j__memset(part3, 0, length / 3 + 1);
j__strncpy(part1, input, length / 3);
j__strncpy(part2, &input[Count], Count);
j__strncpy(part3, &input[2 * Count], Count);
if ( verify1_456BCB(part1) && verify2_4578CD(part2) && verify3_456E78(part3) )
{
Str1 = (char *)md5_4579F4(input);
if ( !j__strcmp(Str1, "40d511825ecbc207eb6ef9a7b1c6e34b") )
output_45548D((int)"Success~n", v5);
...
}
...
}


首先输入的长度是3的倍数, 而且只包含小写字母与0到9。

之后把输入分为等长的三段分别用不同的函数判断, 函数需要返回非0。
再之后对输入进行了个md5的校验。


第一个函数为:


BOOL __cdecl verify1_45C360(char *part1)
{
__CheckForDebuggerJustMyCode(&unk_535028);
to_lower_455816(part1);
return j__strncmp(part1, "hellocat", 8u) == 0;
}


先转小写再确保前8个字符是hellocat; 因为之前确保了输入是小写所以这里的to_lower可以无视。


第二个函数为:


BOOL __cdecl verify2_45C400(char *part2)
{
size_t i; // [esp+D4h] [ebp-14h]
int v3; // [esp+E0h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_535028);
reverse(part2);
v3 = 0;
for ( i = 0; i < j__strlen(part2); ++i )
v3 = v3 + part2[i] - '0';
return v3 == num13_531004 && num3_531008 * parseint(part2) == 150633606;
}


先将输入逆转, 再计算各个字符与’0’的差值和, 即将逆转后输入作为数字的各位数加在一起确保等于13。


之后3*输入数字等于150633606, 即逆转后输入等于50211202。
逆转前的输入为20211205


第三个函数为:


BOOL __cdecl verify3_45C500(char *part3)
{
__CheckForDebuggerJustMyCode(&unk_535028);
j__strcpy(&gpart3_531E94, part3);
if ( j__strlen(&gpart3_531E94) < 8 )
return 0;
dword_531E9C |= 0x10000000u;
if ( gpart3_531E94 >= *(&gpart3_531E94 + 2) )
dword_531E9C |= 0x88u;
if ( *(&gpart3_531E94 + 2) >= *(&gpart3_531E94 + 4) )
dword_531E9C |= 0x90u;
if ( *(&gpart3_531E94 + 4) >= *(&gpart3_531E94 + 6) )
dword_531E9C |= 0xA0u;
if ( !sub_458232((int)&gpart3_531E94) )
dword_531E9C = 1;
if ( !sub_458232((int)&gpart3_2) )
dword_531E9C ^= 8u;
if ( !sub_458232((int)&gpart3_4) )
dword_531E9C ^= 0x10u;
if ( !sub_458232((int)&gpart3_6) )
dword_531E9C ^= 0x20u;
return dword_531E9C == 0x10000039;
}


sub_458232经分析,逻辑为将输入解析为两个字符表示的44坐标, 第一个字符为a-d,第二个字符为1-4;


初始存在一个全0的4
4数组, 将输入坐标位置设置为1, 然后检查同行,同列,以及斜向方向上是否存在1(即已设置)。


如果坐标解析错误或存在则返回0, 不存在就返回1。


回来仔细观察
verify3_45C500的逻辑发现无论如何也不能满足dword_531E9C == 0x10000039。


dword_531E9C初始为0, 要满足条件需要其最低位为1,唯一将最低位设置为1的语句用的是=而非^=,会清除之前的0x10000000。


而且需要sub_458232返回0才置位也很奇怪。


观察gpart3与dword_531E9C的位置发现:


.data:00531E94 ; char gpart3_531E94
.data:00531E94 gpart3_531E94 dw 0 ; DATA XREF: sub_45C500+2C↑o
.data:00531E94 ; sub_45C500+39↑o ...
.data:00531E96 gpart3_2 dw 0 ; DATA XREF: sub_45C500:loc_45C60F↑o
.data:00531E98 gpart3_4 dw 0 ; DATA XREF: sub_45C500:loc_45C62D↑o
.data:00531E9A gpart3_6 dw 0 ; DATA XREF: sub_45C500:loc_45C64B↑o
.data:00531E9C dword_531E9C dd 0


gpart3_531E94的大小为8字节, 如果part3为9字节则part3的最后一个字符会覆盖dword_531E9C的最低位从而可能满足条件。


假设溢出的字符值就是
0x39即字符9, 则sub_458232的判断需要返回1。


之前的比较保证坐标的第一个字符需要从小到大,则需要分别为a,b,c,d。


再回头看前两个函数输入为9字节的情况;
第一段只比较前8字节所以为
hellocat?
第二段parseint允许前缀0所以为202112050
第三段为a?b?c?d?9


写python脚本爆破:


from Crypto.Hash import MD5

serial = bytearray(b'hellocat.202112050')
target_md5 = "40d511825ecbc207eb6ef9a7b1c6e34b"
chars = b"abcdefghijklmnopqrstuvwxyz0123456789"

for i in range(36):
serial[8] = chars[i]
for j in range(4*4*4*4):
n = j % 4
r = b'a'
r += chr(ord('1')+n).encode()
n = (j // 4) & 3
r += b'b'
r += chr(ord('1')+n).encode()
n = (j // 16) & 3
r += b'c'
r += chr(ord('1')+n).encode()
n = (j // 64) & 3
r += b'd'
r += chr(ord('1')+n).encode()
r += b'9'
if MD5.new(serial+r).hexdigest() == target_md5:
print(serial+r)
exit(0)

# bytearray(b'hellocatx202112050a3b1c4d29')



2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析
2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析
今日中午12点,第十题 试探
正式开赛
2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析
2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

球分享

2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

球点赞

2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

球在看


2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

点击阅读原文查看更多

原文始发于微信公众号(看雪学苑):2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析

版权声明:admin 发表于 2024年9月4日 下午6:00。
转载请注明:2024 KCTF 大赛 | 第九题《第一次接触》设计思路及解析 | CTF导航

相关文章