收获

  • 了解 SMC 代码自修改的原理,使用 IDA 脚本破解 SMC 代码自修改

  • 新版 IDA 与 旧版 IDA 由于 API 改变,脚本写法有所不同

  • 通过 IDA 远程调试绕过 SMC 代码自修改


【攻防世界】BABYRE


思路一

IDA 打开,定位到主函数:

攻防世界_BABYRE1.png

逻辑感觉很简单

首先对 judge[] 数组做异或操作,跟进 judge[]

攻防世界_BABYRE2.png

要求用户输入 sv5s 的长度,只要满足 if(v5 == 14 && (*judge)(s)) 就能得到 flag

但是乍一看,这个条件好像有哪里怪怪的,v5 == 14 这个好理解,但是 (*judge)(s) 就很奇怪

前面的 jugde[] 是个数组,怎么这里变成一个函数了?

本来一度摸不着头脑,注意到刚开始的异或操作:for ( i = 0; i <= 181; ++i ),这里循环了 182 次,我们再回到 judge[] 定义的地方

judge 的起始地址是 0x600B00,182 就是 0xB6,那结尾的地址就是 0x600B00 + 0xB6 - 1 = 0x600BB5

从上往下跟过去看一下 judge 后面的内容,但是内容都是这样的十六进制数据:

攻防世界_BABYRE3.png

联想到程序中的数据、指令、代码其实都是二进制数据形式存放的

于是猜测这个 judge 可能本来就不是一个数组,而是函数,即:这些内容其实就是函数里的数据,只是经过了加密处理,程序开头的异或操作可能就是一种对 judge 的解密,把 judge 恢复成了正常的函数 (其实就是 SMC 代码自修改)

首先按照这个主函数给出的逻辑对 judge 进行解密

因为异或的数据比较多,输入快捷键 shift + F2 打开 IDA 的脚本编辑器,输入如下脚本:(适用于 IDA 7.0 以后的版本)

address = 0x600B00

for i in range(182):
	ida_bytes.patch_byte(address + i, idc.get_wide_byte(address + i) ^ 0xC)

print("Done")

注意:
在 IDA 7.0 以前的版本中,这个脚本应该这么写(网上很多 WP 就是这么写的):

add = 0x600b00  
for i in range(182): 
PatchByte(add + i, Byte(add + i) ^ 0xC)

但是 IDA 7.0 以后,官方对 API 进行了更改
如果还是按老版本来写,会报出:
NameError: name 'PatchByte' is not defined
NameError: name 'Byte' is not defined
等错误,因为 PatchByteByte 已经不能直接用于新版的 IDA 了

详见本站《IDA新版与旧版的API变更》一文

可以看到脚本执行前后 judge 中数据的变化

脚本执行前:

攻防世界_BABYRE4.png

脚本执行后:

攻防世界_BABYRE5.png

接下来把这些数据转化为代码:

judge 的首地址 0x600B00 开始,到结尾的位置 0x600BB5,一路按 快捷键 C 将数据转化为代码,遇到 IDA 弹窗的,直接转代码,无视即可(或者直接在 judge 的首地址 C 一下,IDA 会一路将可以转代码的地址全部转过来,最后 P 一下生成函数

全部 C 完之后如下:

攻防世界_BABYRE6.png

攻防世界_BABYRE7.png

然后在 judge 首地址的位置,按 快捷键 P 将代码生成函数

攻防世界_BABYRE8.png

最后,像正常函数一样按 快捷键 F5 就可以快乐反编译了

得到 judge() 函数的内容如下:

攻防世界_BABYRE9.png

代码逻辑比较简单,定义了 v2 = "fmcd\x7F"v3 = "k7d;V&#96;np"

将输入异或后要与 v2 相等,根据循环次数 14 可知,这里是将 v2v3 拼接起来了

也可以通过汇编代码查看:

攻防世界_BABYRE11.png

接下来编写脚本即可


脚本

key = "fmcd\x7Fk7d;V`;np"  
flag = ""  
  
for i in range(0, 14):  
    flag += chr(ord(key[i]) ^ i)  
  
print(flag)

思路二

考虑到 judge 是在程序执行过程中自己解码的,所以可以通过动态调试下断点,先让程序自己解码,然后我们再观察解码后的内容

由于是 Linux 端的 elf 文件,开启远程调试,远程调试的方法详见本站《IDA的基础和远程调试》一文

在调试之前:

  1. 先在第一条指令 push rbp 的地方下一个断点
  2. 然后在输入 call ___isoc99_scanf 的地方下断点
  3. 开始调试

程序开始时停在我们下的第一个断点处

攻防世界_BABYRE12.png

在第二个断点处,右键 --> Run to cursor 直接让程序运行到这里

攻防世界_BABYRE13.png

可以看到,程序的 RIP 指向了我们第二个断点的地方:

攻防世界_BABYRE14.png

然后 F8 单步步过

攻防世界_BABYRE15.png

Linux 中程序已经开始让我们输入

这里先随便输入一个值,例如我输入:1

攻防世界_BABYRE16.png

程序越过了 scanf 输入,RIP 指向下一条命令

由于下面的 jnz short loc_400698 这一条指令会跳转到 loc_400698

这个位置是输出 “Wrong!” 用的,并且会导致程序直接结束

攻防世界_BABYRE17.png

所以我们需要jnz short loc_400698 这一条指令之后,设置一个新的 RIP

这样一来,输入 “Wrong!” 后,可以迫使程序继续跳转到我们设置的 RIP 的地方,从而绕过输入错误导致的退出

但也要注意,我们的目的是让程序自己解码 judge 函数后查看 judge 的内容,所以这个 RIP 一定要设置在调用 judge 函数之前,不然无法进入到 judge 函数

例如,我将 RIP 设置在 jnz short loc_400698 的后面一句,即:地址 0x40067A 处,然后 右键 --> set IP

攻防世界_BABYRE18.png

同时,在 call rdx ; judge 调用 judge 函数的地方 F2 下一个断点

然后我们直接 F8 就会跳转到刚刚设置 RIP 的位置,从而绕过 loc_400698

继续 F8 执行到调用 judge 函数的地方,IDA 会询问是否进入这个地址

攻防世界_BABYRE20.png

选择 "Yes",程序就会进入 judge 函数:

攻防世界_BABYRE21.png

这个就是程序自己解码出来的 judge 函数的内容

然后选择 judge 函数的内容,使用 快捷键 P 将代码生成函数

攻防世界_BABYRE22.png

最后 F5 即可将 judge 函数反汇编

攻防世界_BABYRE23.png


结果

flag{n1c3_j0b}

攻防世界_BABYRE10.png