程序用 MFC 编写,查壳,没壳。直接运行,输入注册码,注册错误,弹框显示错误信息。可见程序执行逻辑很简单,并且猜测弹框显示是调用了 MessageBox 函数。
用 IDA 打开,直接 F5 查看函数,发现在sub_4016E0 和 sub_401720 这两个函数里面都调用了 MessageBox 函数。
通过 IDA 的交叉引用功能,发现在.text:00401617 处调用了 sub_4016E0,往前翻可以看到很多信息,比如说对sub_401630 和 UpdateDate 函数的调用,以及.text:004015FB 和 .text:00401613 处的两个跳转。
于是猜测程序在这里通过UpdateDate 获取了注册码编辑框的输入注册码,然后先判断注册码长度是否为 0x21,接着在sub_401630 里面对注册码进行进一步验证,注册码正确则通过 sub_4016E0 弹出正确信息,错误则通过 sub_401720 弹出错误信息。
照这种思路,那么 sub_401630 验证函数就比较关键了,我们用 F5 查看如下:
可以知道,在函数里面对内存中的两处字符串进行了逐字符循环比较,这两处字符串应该就是我们输入的注册码和内存中的真码。在这里每一轮循环的种子和产生的随机数都是固定不变的,确保了正确的注册码的唯一性。
这时候就可以上 OD 调试了,在004015E0 处下断,输入任意长度为 0x21 的注册码,然后程序被断下。长度验证正确后单步进入 sub_401630,这时候在 00401669 处下断就可以观察到注册码的每一个字符了。
在Linux 下用 file 命令查看,可知这是一个ELF64 文件,并且符号表没有被抽取掉,调试起来会好一点。
用 IDA 打开,直接 F5 查看main 函数,然而报错。没关系,直接看图表视图就好。
在红框这一部分可以看到,程序通过 scanf 读取输入的 flag,然后有一个关于验证 flag 长度的跳转,正确长度为 0x0E。
这一部分红框可以看到,长度验证正确后调用 judge 函数进一步验证 flag,然后通过 puts 输出结果信息。
很自然这时候我们应该重点关注 judge 函数,我们双击 judge 跳转,可以知道 judge 的起始地址是 0x600B00,然而此时 judge 函数被加密了,解密过程在下图中的红框部分:
好,整个流程到现在已经分析清楚了,我们用 gdb 调试,在靠近 call judge 的地方下个断点。注意此时不能直接在 judge 函数里面下断点,否则解密时断点信息会被冲刷掉。
输入任意长度为 0x0E 的 flag,然后断了下来,单步步入 judge 函数里面。此时 judge 函数已经解密完成,分析可知:
上图红框为内存中存放的 flag。
上图红框部分对输入的 flag 做一个异或运算。
上图红框部分比较运算后的 flag 和内存中存放的 flag。
现在我们得到了内存中存放的 flag,那么只需要将这个 flag 做一个异或的逆运算就可以得到输入正确的 flag 了。
查壳,没有壳。用 IDA 打开,F5 分析 main 函数如下:
大概分析可得,输入的 flag 长度应为 19,长度验证正确后会先调用 sub_401220 函数,该函数功能未知。然后通过 CreateFileA 和 WriteFile 将输入的 flag 写入到一个文件里面,双击 FileName 可知是写入到本地目录下的 Your_Input 文件里面。接着调用 sub_401240 函数,因为传入了 buffer 和NumberOfBytesWritten 的地址,结合紧跟着 sub_401240 后面的一个关键判断,所以很可能 sub_401240 就是对 flag 进行验证的关键函数了。
从上面分析得知,输入的长度为 19 的 flag 会被写入到文件里面。然而测试之后发现,写入到文件里面的 flag 发生了改变。回顾 main 函数的执行流程,可知对 flag 的修改要么发生在 sub_401220 函数里面,要么是WriteFile 函数出了问题。由于题目的名称给出了 hook 的提示信息,于是猜测在 sub_401220 里面 hook 了WriteFile 函数。直接 F5 分析 sub_401220 函数,报错。查看汇编代码得知猜测正确,分析如下:
在 sub_401220 中获取进程句柄后跳转到loc_4011B0。
在 loc_401180 里面通过GetProcAddress 获取WriteFile 的地址,然后调用了sub_4010D0。注意红框框处的几个数据。
F5 查看 sub_4010D0,可知调用 VirtualProtectEx和 WriteProcessMemory 修改了 WriteFile 函数的起始 5 个字节。动态调试后发现修改为跳转到 sub_401080 的一条无条件跳转指令,从而实现了 hook WriteFile 的功能。
那么当 main 函数调用 WriteFile 的时候,程序将转去执行 sub_401080 函数。来看一下sub_401080:
首先调用 sub_401000 函数,分析参数传递可知参数 lpBuffer 即是我们输入的 flag,那么很有可能就是在 sub_401000 里面对 flag 进行了修改。然后调用 sub_401140 函数,接着又一次调用了WriteFile 函数。我们知道虽然 WriteFile 被 hook 了,但最后确实是把 flag 写入到了文件里面,所以很有可能是在 sub_401140 里面对 WriteFIle 进行了 hook 还原。最后对 sub_401000 的返回值进行判断,如果非 0 则将NumberOfBytesWritten 置 1。
先分析 sub_401140,证实了里面的 hook 还原操作。然后看一下 sub_401000,可知里面主要是两个循环处理:
这个循环处理比较简单,就不分析了。看接下来的一个循环:
这里就有一个很明显的字符串比较的意图了,结合上面的分析可知,我们输入的长度为 19 的 flag 经过第一个循环的处理之后,如果和 byte_40A030 指向的全局字符串相同,那么 sub_401000 返回 1。在 sub_401080 里面对sub_401000 的返回值进行判断,如果返回值为 1,则将NumberOfBytesWritten 置 1。然后在最外层的 main函数里面进行判断,如果NumberOfBytesWritten 为 1,则输出正确的提示信息。
所以,我们只需要将 byte_40A030 指向的字符串做一次 sub_401000 函数里面第一个循环处理的逆运算,就可以得到输入正确的 flag 了。
当然别忘了在 main 函数里面的 sub_401240 函数,我们刚开始时分析认为在 sub_401240 里面对输入的 flag 做了关键验证,但事实上真正的验证函数是 sub_401000,只要 sub_401000 验证正确即可。sub_401240 只是一个幌子,纯属糊弄人的。