出题战队:laughlaugh
设计思路
题目设计
序列号=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
破解步骤
50211202 * 3 = 150633606
50211202的前面加1位,使运算继续成立,那就只能是’0′
202112050
python calculate.py
赛题解析
使用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的44数组, 将输入坐标位置设置为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 大赛 | 第九题《第一次接触》设计思路及解析