首先下载运行ConsoleApplication1.exe:
是需要输入一个字符串或者数字之类的
之后PEid查壳:
32位的控制台程序,没有壳。
1、IDA静态分析
再用IDA打开静态分析,找到main函数,按F5反编译:
main_0函数带注释的源代码:
__int64 main_0()
{
int v0; // edx
__int64 v1; // ST00_8
signed int i; // [esp+D0h] [ebp-48h]
char v4[16]; // [esp+DCh] [ebp-3Ch]
char v5; // [esp+ECh] [ebp-2Ch]
sub_45E9C1("welcome to zsctf!\n");
sub_45E9C1("show me your key:");
sub_45D846("%s", v4);
if ( sub_45B1C7() )
j__exit(0);
if ( j__strlen(v4) == 24 ) // 字符串的长度是24
{
v5 ^= 1u;
sub_45C748(v5); // 对v5字符串做变换
for ( i = 0; i < 24; ++i )
{
if ( (v4[i] < 48 || v4[i] > 57) && (v4[i] < 97 || v4[i] > 102) )// 要求每个字符大小在48~57、97~102之间,也就是“0~9、a~f”之间
{
sub_45BF7D(); // 输出“error key”
goto LABEL_16;
}
}
if ( sub_45E593((int)v4) ) // 判断字符串
{
sub_45E9C1("done!!!The flag is your input\n");
sub_45D9C7(4);
sub_45E1B5(a1); // 生成一张图片flag.png
}
else
{
sub_45BF7D();
}
}
else
{
sub_45BF7D();
}
LABEL_16:
HIDWORD(v1) = v0;
LODWORD(v1) = 0;
return v1;
}
2、主函数分析
程序首先是限定了字符串长度为24,之后调用sub_45C748(v5)函数对v5字符串做变换:
其中主要是sub_45DCD3((int)v4):
根据_int8 a1大小为0~255,做不同的变换,case分支太多了没办法逐个函数直接逆向分析,所以我们用OD去动态调试一下,看经过sub_45C748(v5)函数,v5字符串发生了什么变化。
3、OD动态分析
打开OD运行,输入字符串“000000000000000000000000”:
找到sub_45C748函数,在函数附近下两个断点:
单步运行,发现字符串在进入sub_45C748函数之前,对字符串的第17个字符进行了异或操作,让字符串变成了“000000000000000010000000”:
运行完之后的字符串:
我们跟进这个函数,找到让字符串变化的关键函数:
进入这个函数,发现运行过之后字符串变成了“0123456789:;<=>?!!"#$%&’”
再换个输入“111111111111111111111111”,查看字符串变化:
字符串变为“1032547698;:=<?> #"%$’&”
再换个输入“12345678901234567890abcd”,查看字符串变化:
字符串变为“1317131?19;9?9;9&)+#uwus”
其实就是进行了先对第17位字符进行异或1:char[16]^0x1
,再进行按位与的操作:char[i]=char[i]^i
。
4、关键sub_463480函数分析
再回到程序:
要求每个字符大小在48-57、97-102之间,也就是“0-9、a-f”之间,之后通过sub_45E593((int)v5)这个函数判断字符串是否正确,正确就输出"done!!!The flag is your input\n"和一张图片flag.png,否则输出"key error\n"结束程序。
我们接下来分析sub_45E593()函数,查看sub_45E593():
调用了sub_463480(a1)函数:
所以我们主要就是分析sub_463480(a1)函数
sub_463480()函数带注释的源代码:
BOOL __cdecl sub_463480(int a1)
{
int v1; // ST18_4
int v2; // ST18_4
BOOL result; // eax
int v4; // ST18_4
int v5; // [esp+D8h] [ebp-38h]
int v6; // [esp+E4h] [ebp-2Ch]
int v7; // [esp+F0h] [ebp-20h]
int v8; // [esp+FCh] [ebp-14h]
int v9; // [esp+108h] [ebp-8h]
v9 = 0;
v6 = 0;
v5 = 0;
while ( 2 )
{
v1 = v9++;
if ( v1 >= 12 ) // v1 = 0~12,12的时候返回
return v5 == 311; // 返回的时候v5 = 311则返回true,得到flag;否则返回false
if ( sub_45B1C7() ) // 确定调用进程是否由用户模式的调试器调试
j__exit(0); // 被动态调试就返回
v2 = *(char *)(v6++ + a1); // a1是字符串的首地址,v2相当于取字符串中的0、2、4……位置上的字符字符
switch ( v2 ) // v2大小是48~52,也就是数字0~4,否则退出
{
case 48:
v8 = 0; // v8,大小0~4,下面byte_541168数组的下标,决定调用哪个函数
goto LABEL_12;
case 49:
v8 = 1;
goto LABEL_12;
case 50:
v8 = 2;
goto LABEL_12;
case 51:
v8 = 3;
goto LABEL_12;
case 52:
v8 = 4;
LABEL_12:
v4 = *(char *)(v6++ + a1); // a1是字符串的首地址,v2相当于取字符串中的1、3、5……位置上的字符字符
switch ( v4 ) // v4的大小是53~57、97~102,也就是数字5~9、a~f,否则退出
{
case 53:
v7 = 5; // v7,大小5~15,下面byte_541168数组的下标,决定函数的第二个参数
goto LABEL_25;
case 54:
v7 = 6;
goto LABEL_25;
case 55:
v7 = 7;
goto LABEL_25;
case 56:
v7 = 8;
goto LABEL_25;
case 57:
v7 = 9;
goto LABEL_25;
case 97:
v7 = 10;
goto LABEL_25;
case 98:
v7 = 11;
goto LABEL_25;
case 99:
v7 = 12;
goto LABEL_25;
case 100:
v7 = 13;
goto LABEL_25;
case 101:
v7 = 14;
goto LABEL_25;
case 102:
v7 = 15;
LABEL_25:
switch ( byte_541168[v8] ) // 决定调用哪个函数,byte_541168[]数组就是'delru0123456789'
{ // v5是最终返回的结果311,byte_541168[v7] - 48控制循环次数
case 100: // d,增加循环次数个26
sub_45CC4D((int)&v5, byte_541168[v7] - 48);
continue;
case 108: // l,减少循环次数个1
sub_45D0A3((int)&v5, byte_541168[v7] - 48);
continue;
case 114: // r,增加循环次数个1
sub_45CB0D((int)&v5, byte_541168[v7] - 48);
continue;
case 117: // u,减少循环次数个26
sub_45D0E9((int)&v5, byte_541168[v7] - 48);
continue;
default:
result = 0;
break;
}
break;
default:
result = 0;
break;
}
break;
default:
result = 0; // 返回0,表示错误
break;
}
return result;
}
}
1、程序首先通过v9和v1来控制循环12次;
2、v1等于12时返回,返回的时候判断结果v5,等于311则返回true,得到flag;否则返回false;
3、sub_45B1C7()调用IsDebuggerPresent()函数确定调用进程是否由用户模式的调试器调试,被动态调试就返回;
4、循环12次就是把24个字符分成了12组,每两个字符一组,通过v2获得获得第一个字符,v2大小在4852之内则将字符转化为数字04给v8,否则退出;v4获得第二个字符,v4大小在5357、97102之内则将字符转化为数字5~15给v7,否则退出;
5、根据switch ( byte_541168[v8] ) 决定调用下面四个中的一个函数:
(1)byte_541168数组分析
这四个函数的代码逻辑相似,只是具体功能不同,两个参数是(int)&v5和byte_541168[v7] - 48,v5是最终返回的结果311,byte_541168[v7] - 48控制循环次数:
查看byte_541168数组:
64就是’d’,其实它和下面的’elru0123456789’是一起的,这里只是IDA对它们的识别格式不一样,64h被识别成了单字节,我们把
它们转化一下,先全部选中右键转化为未定义:
然后在00541168这一行右键就可以看到,IDA已经重新识别到了字符数组‘delru0123456789’:
而在下面的其他行比如0054116B,IDA识别到的字符数组就是后面的‘ru0123456789’:
在00541168这一行右键,或者按快捷键‘A’解释光标的地址为字符数组的首地址,转化为字符数组‘delru0123456789’,0:
最后的那个0是C、C++字符数组中最后表示结束位置的’\0’。
我们再回到代码,就很好理解了:v8大小04,表示下面byte_541168数组的04个数“delru”的下标;v7大小515,表示下面byte_541168数组的第515个数“0123456789”,0的下标。
(2)迷宫游戏的方向和形状
6、(1)byte_541168[v8]的值为d(100)则调用第1个函数sub_45CC4D:
目的是让v5增加循环次数个26;
(2)byte_541168[v8]的值为l(108)则调用第2个函数sub_45D0A3:
目的是让v5减少循环次数个1;
(3)byte_541168[v8]的值为r(114)则调用第3个函数sub_45CB0D:
目的是让v5增加循环次数个1;
(4)byte_541168[v8]的值为u(117)则调用第4个函数sub_45D0E9:
目的是让v5减少循环次数个26;
(5)byte_541168[v8]的值为e(101)则退出;
到这,我们大概明白题目的意思了,结合题目的名称Take the maze(走迷宫),迷宫大概是下面这样:
我们要从迷宫的左上角v5=0走到右下角311,方法就是我们输入的字符串,24个字符分成12组,每组两个字符,第一个字符负责方向就是往上(减26)、往下(加26)、往左(减1)、往右(加1),第二个字符负责走几步(循环次数),碰到最外面四周的墙壁就返回,由四个函数中的这一行代码处理:
if ( i / 26 > 10 )return result; // i/26的最大值是11,走到了最下边,返回当前行数
if ( i %26 < 1 )return result; // i%26的最小值是0,走到了最左边,返回当前行数
if ( i % 26 > 24 )return result; // i%26最大值是25,走到了最右边,返回当前行数
if ( i / 26 < 1 )return result; // i/26的最小值是0,走到了最上边,返回当前行数
7、最重要的就是下面这四个判断分别在下四个函数里:
if ( dword_540548[i] ^ dword_540068[i] ) return result;//非0即真,循环的次数i要确保这两个数组中的相同位置的值异或为0也就是相等,出现不相等即异或不为0就返回
if ( dword_5404DC[i] ^ dword_53FFFC[i] )return result;
if ( dword_5404E4[i] ^ dword_540004[i] )return result;
if ( dword_540478[i] ^ dword_53FF98[i] )return result;
相当于每个位置i的能走的方向,只要这个方向上两个数组中的相同位置的值相等。所以对于i大小范围:0~311中,每个i让这四个判断中的某个判断不成立则表示这个i位置上,可以往某个方向走;成立则直接返回,表示不能往这个方向走。
所以我们接下来就是要得到每个位置i能走的方向的迷宫地图,可以把这8个数组都复制下来,对i:0~311计算每个位置上异或的结果;也可以直接在IDA里写idc或者Python的脚本,直接利用现有的8个数组,计算得到地图。
5、脚本寻找路径
把8个数组都复制下来的过程太多了,这里就用的是写idc脚本的方式了。
文件->脚本命令,打开IDC和Python的命令行:
idc脚本源代码:
auto i;
for(i = 0;i <= 311; ++i){
if(i % 26 == 0)
Message("\n");
if(Dword(0x540548 + i * 4) ^ Dword(0x540068 + i * 4))
Message("-");
else
Message("d");
if(Dword(0x5404DC + i * 4) ^ Dword(0x53FFFC + i * 4))
Message("-");
else
Message("l");
if(Dword(0x5404E4 + i * 4) ^ Dword(0x540004 + i * 4))
Message("-");
else
Message("r");
if(Dword(0x540478 + i * 4) ^ Dword(0x53FF98 + i * 4))
Message("-");
else
Message("u");
Message(" ");
}
return 0;
运行结果:
根据地图,我们只能转12次方向,得到的路径:
方向和步数是d,1,r,1,d,3,r,1,r,6,d,3,d,2,r,9,d,1,r,4;
其中方向dlru对应的是v8的0234,对应转化后的字符串v4的0234;
步数0-9对应的是v7的5-15,对应转化后的字符串v4的5-f;
所以进入迷宫前的字符串是:0,6,3,6,0,8,3,6,3,b,0,8,0,7,3,e,0,6,3,9;
我们再把06360836063b0839073e0639进行按位异或的操作就行了,按位异或的操作是可以直接逆向的,我们可以把06360836063b0839073e0639输入程序得到按位异或后的字符串或者用代码完成对第17位字符的异或和按位异或的操作。
6、路径转换得到flag
把06360836063b0839073e0639输入程序:
得到对第17位字符的异或和按位异或后的字符串:“07154=518?9i<5=6!&!v$#%.”
或者自己用代码完成对第17位字符的异或和按位异或的操作:
得到flag的源代码:
#include<stdio.h>
int main(){
char str[24] = {'0','6','3','6','0','8','3','6','0','6','3','b','0','8','3','9','0','7','3','e','0','6','3','9'};
int i;
str[16] ^= 1;
for( i=0 ; i<24 ; i++){
str[i] ^= i;
///07154=518?9i<5=6 &!v$#%.
printf("%c",str[i]);
}
return 0;
}
运行结果:
我们把“07154=518?9i<5=6!&!v$#%.”输入ConsoleApplication1.exe:
也得到了一张flag.png图片:
打开是一张二维码,用手机扫描二维码:
Flag还要加上作者的名字Docupa,所以最终的flag是:zsctf{07154=518?9i<5=6!&!v$#%.Docupa}。