*本文原创作者:antiuto,本文属FreeBuf原创奖励计划,未经许可禁止转载
老规矩,坐下,打开电脑,最好能泡一杯茶,手边再放一个肉夹馍。。。
由于上一次吃过没查壳的亏,所以这次要先看一看有没有壳,用PEiD打开看一看:
很好,没有壳,先直接运行看一看,找一找关键字符串:
这样,我们就有了关键字符串,直接IDA打开(一定要先用IDA打开,高手都是这样的,即使不是,也要假装是),然后通过IDA的字符串查看功能(shift+F12)来找我们需要的字符串:
除了我们想要的字符串还有满满的意外收获,先等等再说,直接双击“welcome to zsctf”,然后按下X,通过交叉引用来到了我们想要的函数:
这是函数最开始反编译以后的样子,很多函数名称不是很友好,在这里,我想很多老师傅应该都会上手修改函数名称,改成对人阅读友好的名字,举个例子,第9行和第10行一眼就能看出来这是C里面的printf函数,这里,单击一下这个函数,然后按下n,就会跳出来修改函数名称的窗口:
直接修改为printf,ok以后,就能看到所有函数名称一样的地方全部变成了我们修改的字样:
通过不断地双击函数,查看函数的功能,修改函数或者变量的名称,最终修改函数名称变成了这个样子:
第17行的函数进入以后是这个样子的:
好多函数,不可能一一分析,那么猜测应该是花指令,这段代码有可能是对输入进行判断也有可能是处理输入字符串,等等再说,先看后面的函数。从第14行我们能得到输入的字符串长度是24,第20行应该是对处理过的字符串进行验证,只能是0-9,a-f。会不会是进行了md5加密?不管他,后面还有一个关键的函数,check。进入check函数以后,这个check卡了我很长时间,很长时间,有多久往下看:
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 )
return v5 == 311;
if ( (unsigned __int8)sub_45B1C7() )
j__exit(0);
v2 = *(char *)(v6++ + a1);
switch ( v2 )
{
case 48:
v8 = 0;
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);
switch ( v4 )
{
case 53:
v7 = 5;
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] )
{
case 100:
sub_45CC4D(&v5, byte_541168[v7] - 48);
continue;
case 108:
sub_45D0A3(&v5, byte_541168[v7] - 48);
continue;
case 114:
sub_45CB0D(&v5, byte_541168[v7] - 48);
continue;
case 117:
sub_45D0E9(&v5, byte_541168[v7] - 48);
continue;
default:
result = 0;
break;
}
break;
default:
result = 0;
break;
}
break;
default:
result = 0;
break;
}
return result;
}
}
第一眼看上去简直复杂得不得了,这还怎么看???继续改名字!!!一边改名字一边分析各个函数,比如变量a1就是我们输入的字符串,那么a2的类型就不是int,应该是char类型的,这里按下键盘上的y来修改变量类型:
修改完以后我们看起来费力的第23行一下子变得简单起来了,前后对比如下:
按照这样的思路各种修改同时也是分析程序的各个功能,最终得到这样的逻辑:经过处理的字符串传递进来以后,每两个字符一组,第一个字符对应第一个switch,第二个字符对应第二个witch。结合此函数的结尾部分:
不难得出,第一个表示方向,第二个表示步长,通过两个switch语句达到这个功能,byte_541168[]开始的地方是一串字符串【delru0123456789】。在这里的时候我就疑惑了,从d到9一共15个字符,整个选择一共有16个,这是什么意思?搞不懂啊,卡在这里很久很久····于是放弃思考15与16的问题,打算进入down_check函数看一看:
我基本就歇菜到这四个检查函数里了,这四个检查函数大同小异,都是判断,然而就是这个判断逻辑搞不懂。做不出来题目就认真玩游戏,认真玩游戏,认真玩游戏,重要的话说三遍!!
于是在我翻车以后我打算继续逆向,还是回到熟悉的地方重新开始,正好这个时候学了一些东西,get到了以前一直忽略的一个地方,就是函数的类型,不是所有的函数都有返回值,就像这几个函数一样:
函数返回以后没有对其他进行赋值操作,那么,这四个函数应该都是void类型的函数,然而,在我打开down_check函数的时候,简直亮瞎我的眼:
怎么可能是int类型?!!改!单击sub_462D60以后按下y,改成void以后,世界清晰了许多,感觉人生特别美好,阳光特别灿烂,再来一次翻车都没问题。
于是,按照这样的思路依次修改这四个文件,并且把传递的参数也进行名字上的修改,一边改,又一边分析了程序,然后阳光再次普照大地,还是以down_check为例:
如上图,传递两个参数,一个是当前已经走的步数,一个是要走几步,这里,如果要想将i的值重新赋给*steps,必须满足两个条件:
1:i/26<=10
2:dword_540548[i]^dword_540068[i]==0
两个条件同时满足才能避免return,并且完成i的自增。这一点非常重要,因为先判断dword_540548[i]^dword_540068[i]=0以后才进行i的自增。表示先判断能不能往下走,再把步数加上去。或者说站在迷宫中的一个点的时候先判断能往哪里走,再把步数加上来。如果判断顺序颠倒一下,表示在迷宫的一个点可以从哪里来。这是一个往哪里走和从哪里来的问题。
还有值得一提的就是,在对四个函数进行分析的时候,通过他一直和26做除法能推断出来这个迷宫一行26个,一共12行。没错,就是12行!你也许会问:在down_check里明明是i/26>10嘛,你想想,如果你在第12行,你上面至少11行,那么,这个时候,你是不能down的,只有在第11行的时候,你能down,此时你的i/26取整等于10。
怪不得把我卡得死死的。到这里,距离我坐下,打开电脑已经三个月过去了,当然,中间有其他事情,还有过年。
现在,我们找到了四个不同方向的检查函数,问题来了,迷宫呢?!!!四个check函数里做异或的地址都不一样,这是想干什么?这里又卡了好久好久,真的好久,久到开始怀疑人生。后来又翻了一次车,才想明白一件事情:每个检查函数里都有做异或的字符串,表示这个点能向哪个方向移动,那么我把同样的一个i,对应的四个check函数合并起来,是不是就得到了迷宫!!!迷宫就是一个12×26的矩阵!!!
有了想法立刻行动,直接用IDA的python接口写脚本,把每个方向的check函数中做异或的地方提取出来,比如提取up_check出来看看:
提取出来是这样的:
然后把提出来的四个列表合起来,我用Pycharm跑了一次试了试:
显示效果不好,把他放在notepa++上效果好很多:
问题又来了:我在哪儿?我要去哪儿?回到IDA,找到了这个线索:
最开始的时候steps为0,在20行又有311的数字,结合上面的12×26=312,推断出就是从迷宫左上角走到右下角了。
路线图是这样的:
根据整个switch语句的判断,能得到这样的一串数字:【06360836063b0839073e0639】。此时大家别忘了,还有一个函数在孤独的等着我:
这个函数就是我刚开始怀疑的那个md5加密,因为转换过后的字符是有限制的。IDA我是不看了,那个太复杂,干脆动态调试一下试试:
前面就不说怎么找关键位置,直接从定位关键字以后开始输入字符串:
此时根据输入的字符串定位到加密的地方:
在这里要根据堆栈段的内容将内存段的地址改过去,方便查看数据的变动,注意地址,0x4EFD58是我们输入字符串的原始存储地,0x56D090相当于复制了一份。在一步一步跟的时候发现是先将输入字符串的第17个字符和1做了异或然后到这里的。然后继续F8,一步一步跟,跟到下面这个地方的时候一直在反复循环,于是我打算在这里下两个端点,一个是每次循环,一个是整个循环跳出:
此时要重点关注数据窗口的数据:
此时多按几次F9,发现数据窗口的内容发生了改变:
这里看到了第一个字符,再次多按F9的时候,变成了这样:
数字4的ASCII码为0x34,用字符串的表示为str[3],第四个字符,数字对上了,根据前面第17个字符先和1做异或,这里猜测应该是这样的逻辑:
a=’123456′
a[0]则表示1
a[1]则表示2
那么
b = a[i]^i
让程序跑完这一段,得到加密后的字符串:
程序跑完以后再F8几次,注意寄存器的表示,能发现想下面这样一样,把原来存放字符串的地方的值变成了加密后的字符串,也就是0x4EFD58这个地方:
然后继续F8,看看程序要到哪里去:
这里很明显有四个比较,而且0x30是数字0,0×39是数字9,0x61是字母a,0x66是字母f,返回IDA查看一下我们的宏观判断:
我们已经成功跳出了加密函数,进入了字符合法性判断,还剩下一个就是验证自己的算法猜想。
自己写一段python代码验证一下:
猜想得到验证,搞定这题!
直接上代码找flag~!
最后把第17个字符替换一下就行。
然而我提交以后发现不对。不对?!!!!!为啥?
此时想到了用程序判断一次试试,然后我发现我忽略了一个点:
最开始的时候意外收获的字符串给忘了,程序运行完以后会生成一个flag.png文件,这个文件不完整,我用电脑软件扫描不出来,后来进入IDA亲自手拖这段数据(强迫症是不会用手机直接扫描的)
在这里直接复制到hex里面保存。注意png文件的开头和结尾是:文件头:89504E47 文件尾:AE426082
最后成功扫描出了结果:
此题逆向至此结束,结合了在IDA里重命名,更改类型,IDA脚本,OD的熟练使用,我都怀疑我杀鸡用了牛刀,老师傅有简单的路子请教教我,毕竟这道题让我迷了四个月(泪奔呐。。。。)
题目连接:链接: https://pan.baidu.com/s/1mBSysZ6hPcOJYDjuGo_W3Q 密码: xic3
*本文原创作者:antiuto,本文属FreeBuf原创奖励计划,未经许可禁止转载
来源:freebuf.com 2018-04-09 08:00:20 by: antiuto
请登录后发表评论
注册