这道题目是南京邮电CTF平台的reverse题目single(最后一道,但不是最有难度的一道)
题目链接如下:https://cgctf.nuptsast.com/challenges#Re
不多讲,拖进IDA分析,如下:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s; // [rsp+0h] [rbp-70h]
unsigned __int64 v5; // [rsp+68h] [rbp-8h]
v5 = __readfsqword(0x28u);
memset(&s, 0, 0x64uLL);
puts("Input string:");
scanf("%s", &s);
sub_40070E(&s); // 简单限制字符范围、长度
sub_40078B(&s, (__int64)&unk_602080);
sub_400AD4((__int64)&unk_602080);
puts("Congratulations!");
printf("flag{%s}", &s);
return 0LL;
}
在总体上,可以清楚的分析出程序的大致逻辑:读入flag,经过三个函数的检查来判断是否是真正的flag。
下面逐个分析这三个函数。
1.
size_t __fastcall sub_40070E(const char *a1)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
if ( strlen(a1) > 0x51 )
wrong();
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
if ( a1[i] <= 47 || a1[i] > 57 ) // flag字符ASICL在[48,57]
wrong();
}
return result;
}
意图很明显,限制flag的长度不超过0x51,且flag的组成字符只限于0\1\2\3\4\5\6\7\8\9。
2.
size_t __fastcall sub_40078B(const char *a1, __int64 a2)
{
size_t result; // rax
int i; // [rsp+1Ch] [rbp-14h]
for ( i = 0; ; ++i )
{
result = strlen(a1);
if ( i >= result )
break;
if ( a1[i] != 48 ) // a[i]!='0'
{
if ( !a1[i] || *(_BYTE *)(i + a2) ) // if ( a2[i]!=0 )
wrong(); // flag不为0的时候,会改变
*(_BYTE *)(i + a2) = a1[i] - 48; // a2[i]=a1[i]-'0'
}
}
return result;
}
这是一个通过遍历flag的每一个字符来为一个数组a2初始化的函数,逻辑就是初始化的规则:
当flag[i]!='0'时,a2[i]一定为0(否则flag错误),并且要重新赋值a2[i]=flag[i]-'0';
3.
unsigned __int64 __fastcall sub_400AD4(__int64 a1)
{
sub_400833(a1);
sub_4008FE(a1);
return sub_4009C9(a1);
}
第三个函数其实就是判断的关键所在了,这里面有三个子函数。逐个分析
(1)
unsigned __int64 __fastcall sub_400833(__int64 a1)
{
signed int i; // [rsp+18h] [rbp-28h]
signed int j; // [rsp+1Ch] [rbp-24h]
signed int k; // [rsp+1Ch] [rbp-24h]
char s[24]; // [rsp+20h] [rbp-20h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
for ( i = 0; i <= 8; ++i )
{
memset(s, 0, 0xAuLL);
for ( j = 0; j <= 8; ++j )
++s[*(unsigned __int8 *)(9 * i + j + a1)];// s[ a1[9*i+j] ]++
for ( k = 1; k <= 9; ++k )
{
if ( s[k] != 1 )
wrong();
} // 每一行都必须全有1\2\3\4\5\6\7\8\9
}
return __readfsqword(0x28u) ^ v6;
}
这里面需要尤其注意, ++s[*(unsigned __int8 *)(9 * i + j + a1)]和9*9的嵌套循环 足以让你怀疑是一个9*9的数组。
而需要注意到,判断部分是在for(i=0;i<9;i++)i里面的,这就很容易知道其实就是需要满足每一行必须有1\2\3\4\5\6\7\8\9
(2)和(1)逻辑一样,不过是限制每列都有1\2\3\4\5\6\7\8\9
(3)
unsigned __int64 __fastcall sub_4009C9(__int64 a1)
{
signed int i; // [rsp+1Ch] [rbp-34h]
int j; // [rsp+20h] [rbp-30h]
signed int l; // [rsp+20h] [rbp-30h]
int k; // [rsp+24h] [rbp-2Ch]
signed int v6; // [rsp+28h] [rbp-28h]
signed int v7; // [rsp+2Ch] [rbp-24h]
char s[24]; // [rsp+30h] [rbp-20h]
unsigned __int64 v9; // [rsp+48h] [rbp-8h]
v9 = __readfsqword(0x28u);
v6 = 3;
v7 = 3;
for ( i = 0; i <= 8; ++i )
{
memset(s, 0, 0xAuLL);
for ( j = v6 - 3; j < v6; ++j )
{
for ( k = v7 - 3; k < v7; ++k )
++s[*(unsigned __int8 *)(9 * j + k + a1)];// 从上到下 从左到右的 不重合的 3*3需要是1\2\3\4\5\6\7\8\9
}
for ( l = 1; l <= 9; ++l )
{
if ( s[l] != 1 )
wrong();
}
if ( v7 == 9 )
{
v7 = 3;
v6 += 3;
}
else
{
v7 += 3;
}
}
return __readfsqword(0x28u) ^ v9;
}
分析一下嵌套循环以及判断函数,发现其实还是相似的。不同的是,这里每次判断的九个数是3*3的小数组,按照从上到下,从左到右不重合的顺序检查。
这样一来,其实可以知道这是一个“数独”游戏。根据原有数据,在线填写之后,不要忘记了 把原有数据更改为0.就达到了flag.
flag{401095728057800001802040305000321589500479002923586000105060203300008950269750804}。