题目链接
https://adworld.xctf.org.cn/challenges/list
题目详情
reverse_re3
解题报告
下载得到的文件使用ida64分析,如果报错就换ida32,得到分析结果,有main函数就先看main
main函数分析
main函数十分的简单,说明关键代码都在函数里了
sub_940是需要关键分析的函数,每次循环都会带回返回值赋值给v4,等于1或者-1时都将使循环停止
sub_11B4函数分析
双击跟进sub_11B4函数,这个函数也是十分简洁,第5行和第7行的__readfsqword(0x28u)经常会出现,看了其他大佬的wp说是用了反调试,但讲原理的目前还没搜到。。先记下来吧,等以后有缘自会理解的
第6行则是对dword_202AB0变量赋值了0,现在还不太清楚这个作用,先留个心眼
sub_940函数分析
双击跟进sub_940函数,这个就是关键函数了,这里的变量v5也是__readfsqword(0x28u)反调试用的
v0
这边有个变量叫v0,在下面被多次引用,我们选中它后按X键去查看引用,发现进行了多次值的判断,分别是100,115,119,27,97
对这类数字、编码之类的要提起一种敏感心理,这里显然是字符的ASCII码值,写个简单脚本或直接查看ASCII码表,分别对应的字符为:d,s,w,ESC(键盘最左上角那个键)和a,经常玩3D冒险/FPS类游戏的人对这四个字母可谓十分敏感了,wsad与ESC,和上下左右、退出等操作挂钩
仔细观察,如果按键是wsad中的一个,都会去调用另一个函数,唯有输入为ESC键时,会直接将【0xFFFFFFFFLL】这个十六进制数返回,这个数其实就是十进制的-1(计算器按一下就知道了),末尾的LL是长整型的标志。而返回-1和main函数中v4被该函数赋值为1时结束循环是相呼应的,说明ESC键被输入的话,整个程序直接停止,更有游戏的韵味了吧
v2
v2的这个变量也被多次引用,它的作用是在各个分支里面接收来自4个函数(见下图)的返回值,最终来控制do...while循环是否停止,显然当这些函数被调用并返回1时会停止do...while循环
v3&v4
v3的作用一目了然,用来作为v4这个字符数组的索引,它的值变换很单一,初始值为0,每次do...while循环自增一次
v4是个字符数组,我点了半天没找到它的值是什么,结果突然发现在12行有个scanf函数,顿时给我气笑了,所以v4是用来接收输入字符的
这下感觉通透多了,输入的字符串用v4存放,通过v3索引按照输入顺序访问这些字符,结合代码中的分支判断,有效的只能是WSAD和ESC这五个按键的字符,很有操作游戏的意思吧
dword_202AB0
注意到这里又出现了之前在sub_11B4函数分析时,被初始化为0的变量dword_202AB0,可以看出它也被用作一个计数功能,可能取值为0,1,2,因为当它为2时整个while循环最终停止,这样代码就会执行到49行的flag获取成功提示以及50行的返回值1(这里和main函数中,v4被该函数的返回值赋值为1时结束循环又呼应上了)
sub_86C函数分析
sub_86C函数关键代码只在8~19行,就是通过一个两层for循环对两个变量(dword_202AB4和dword_202AB8)进行赋值,两层for循环分别循环15次,总计225次
除了一个新数组和两个新变量之外,我们还注意到一个眼熟的变量dword_202AB0,它已经在三个函数中出现了,前两次分别是sub_11B4函数初始化为0,sub_940函数作为计数器控制循环结束,而在这里,它被用于索引的部分参数去访问dword_202020这个数组
前面分析出,dword_202AB0可能取值为0,1,2,那么【225 * dword_202AB0 + 15 * i + j】这个索引表达式的大致范围也就明确了——以255的倍数为一个界限,把一维数组dword_202020划分成了3部分
那就去看看dword_202020这个数组里到底放了什么东西!
双击跟进后发现是这些内容,这里灰色的注释直接表明了这个数组大小是675,因为是反汇编,这里的元素内容看起来有些难受,但大致能明白是些什么东西,比如【5 dup(1)】是连着5个1,其它内容同理了,观察一番发现,dword_202020数组中只存在4种数字:0、1、3、4
分析到这里,很多东西已经清晰了,如果到这里还没意识到这像个迷宫游戏的话,在赛场上也很难ctf了,接下来就带着迷宫的猜测,继续分析后续代码
sub_E23函数分析
这个函数是当判断按键为d,即右移操作时会调用的函数
这里的4个变量在前面都是出现过的,dword_202020是数组(也就是游戏的迷宫图),而另外三个变量dword_202AB0、dword_202AB4和dword_202AB8的用途确实有点难想到,不过我觉得就算不知道它们三的作用,对这题的ctf影响没有很大,我们观察这个函数的逻辑判断——当某某数组元素为1时,它会被赋值为3,且比它索引小1的元素(即)会被赋值为1,而如果你它为4,则函数直接终止(因为返回1了,sub_940函数中的v2接收到1的返回值,会终止do...while循环),由此结合迷宫游戏的逻辑做出猜测:1代表的是走过的地方,3代表的是当前所在的地方(初始时,数组元素为3的地方,就相当于起点!),而4代表的是终点,再结合题目描述的
看来,只要提取出迷宫图,并按照1是走过的路线、3是起点、4是终点这个规则,把路线划出来就能得出线索
另外几个按键触发的函数这里就不一一展示了,因为它们的逻辑基本是一模一样
提取迷宫
抛开代码审计,我认为本题最难的部分就是迷宫提取这步了,因为按常规思维,再简单的迷宫应该也是进行上下左右四个方向的二维地图,通过二维数组来实现才对啊?
但是这里的迷宫数组却是个一维数组,也就是说,它利用了一些运算技巧,用一维数组去实现了二维迷宫图,反正蒟蒻博主是没这个天赋能自己悟出来了,就算看了wp也是想了很久才明白,下面就按我的理解来讲讲
①这个迷宫是分三部分的,数组总大小是675,在sub_940函数和sub_86C函数对dword_202AB0这个变量的运用可以说明这点。dword_202AB0取值可能是0,1,2,在sub_86C函数中以255的倍数为一个界限,把一维数组dword_202020划分成了3部分,也就是三个迷宫
②dword_202AB8变量代表的是列号,dword_202AB4代表的是行号,按键wsad任意一个调用的函数中能分析出这点,以按键d调用的sub_E23函数为例子
只有当列号不为14(即小于14时)才能进行d键(即右移)操作,否则就越界了,这也间接说明了每个小迷宫的列数是15(索引从0开始),同理可以从ws两个键调用的函数分析出每个小迷宫的行数也是15,即15*15*3(3个小迷宫) = 675,这么巧合的数字,说明猜测是正确的!
③快捷键【shift+e】把dword_202020数组的元素提取出来,这里要注意,一定要选择下图的【initialized C variable】才能获取正确的数组元素,其它要么是16进制的,要么是16进制转10进制的
④写脚本把一维数组转换成二维数组,以便判断上下左右四个方向的移动路径
这里直接把数组内容全部复制到记事本中,然后去头掐尾把数组定义格式给删除,只留数组元素,注意这里删完后把第一个元素和其它元素对齐一下,接下来,直接把每行的空格、数组、逗号看成一行字符串,写脚本输入进去,并用数组保存索引为0的字符(博主用的C++编写,可以直接忽略输入的空格,所以数字是第一个元素,索引为0),最后再输出一遍保存在数组中的数字元素,同时控制行列即可
1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 3, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0
提取脚本
#include<iostream> using namespace std; int main(){ string ins; string ous[675]; for(int i = 0; i < 675; i++){ cin >> ins; ous[i] = ins[0]; } int row = 0, col = 0; for(int i = 0; i < 675; i++){ cout << ous[i] << " "; col++;//累计输出几列 if(col == 15){ cout << endl; col = 0; row++;//累计输出几行 } if(row == 15){ cout << endl; row = 0; } } }
迷宫
1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 3 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 1 1 1 1 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 0 0 0 0 0 0 0 0 1 0 1 1 1 1 1 0 0 0 0 0 0 0 0 4 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 3 1 1 1 1 1 0 0 0 0 0 0 1 1 0 1 1 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 1 1 0 0 0 1 1 1 1 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 1 0 0 1 1 0 1 1 0 0 0 0 0 1 1 1 1 0 1 1 0 1 1 0 0 0 0 0 1 0 0 1 0 1 1 0 1 1 0 0 0 0 0 1 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 0 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 4 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0
提取路线
有了迷宫,剩下的步骤就很简单了,按照1是走过的路线、3是起点、4是终点的规则以及上下左右分别为wsad字符的规则,写出路线
然后汇总成一个总串:ddsssddddsssdssdddddsssddddsssaassssdddsddssddwddssssssdddssssdddss
再按照sub_940函数的提示,对这个字符串使用md5加密,得到最终flag
总结
本题基本没啥知识点,完全是初学者都能看懂的if...else代码,但如何从中察觉到是迷宫游戏并分析出对应元素的含义,以及写脚本提取出迷宫的操作是个大难点,能够很好地锻炼初学者的代码审计能力