好久没刷re了,来刷点re啊。

buuctf的[WUSTCTF2020]level4

静态分析文件

下载发现是一个64位的elf文件,IDA打开分析。照例先看看明显的字符串明文,发现有left,right,然后还有三种打印(type1,type2,type3)。观察符号列表发现有type1和type2函数。跟进去发现跟我们二叉树的递归输出十分相似,并且type1 先递归了a1+1再递归a1+2,差不多他们就是左子树和右子树。那么type1就是一个后根遍历。然后type2是再中间输出的,是一个中根遍历,那么type3大概率就应该是先根遍历,这里他没有,那么先根遍历大概率就是flag。根据中序遍历和其它一个遍历可以求另外一个遍历,这个在数据结构课里有讲。

动态调试

那么先运行一遍可以发现得到了两个结果

正解显然就是考一个数据结构嘛,但是一个题目总得有多种解法,这里我选择修改函数结构,让它从一个后序遍历变成先序遍历,这里需要patch elf我们先找到type2函数观察它的汇编代码

很明显

1
jz      short loc_4007FD

这一条指令对应了if (*a1)的跳转,那么一直到最后的call putchar之前应该都是if范围内的东西,这里需要注意的是,函数调用要把之前的一切准备都算进来。定位找到字节码,然后交换位置,将开头到第二个call type2的指令和之后的到putchar指令对换位置,然后patch上去即可。

满心欢喜patch之后却发现没有得到想要的结果,为什么呢?

这里需要理解一下jmp跳转指令了,call和jmp两个指令实际上都属于无条件跳转指令,为什么加以区分呢,call它在跳转之后一定会有一个返回的动作,而jmp则不需要。如果自己去尝试编码的话就会发现它的编码开头都是E8 +4个字节定位代码位置。然后它是怎么定位的呢?首先E8开始,之后四个字节为小端表示这条指令(jmp xxx)的下一条指令的位置到 目标代码地址位置的差值(后者减前者)。举个例子,比如我有如下的机器代码。

1
2
3
4
5
6
7
8
nop  90//1
nop 90//2
nop 90//3
nop 90//4
jmp xxx E8 ?? ?? ?? ??//5
nop 90//6
nop 90//7
nop 90//8

当编码的4个字节都为00时,那么这条jmp指令的跳转位置就是第六条指令。

如果为01 00 00 00时,那么这条指令跳转的位置是第七条指令。

如果为02 00 00 00时,那么这条指令跳转的位置是第8条指令。

以此类推,注意里面的数值表示字节,而不是指令的数目,如果想往回挑,那么则需要用相应负数的补码表示偏移。

讲完这些之后就能理解为什么简单的交换代码位置会导致patch失败了,因为指令的地址改变了,所以原来这么多偏移量它已经对应不上相应的函数了。因此需要自己手动操作一下,调整call指令的偏移使之成功patch,这里建议使用keypatch插件,在更改这条指令只需直接输入call 地址,则可以快速完成patch,不用自己算偏移编码。

可以看到代码逻辑按照我们预期的方向更改了,那么我们跑一下,直接输出flag。

flag: wctf2020{This_IS_A_7reE}