【攻防世界】BABYRE
收获
了解 SMC 代码自修改的原理,使用 IDA 脚本破解 SMC 代码自修改
新版 IDA 与 旧版 IDA 由于 API 改变,脚本写法有所不同
通过 IDA 远程调试绕过 SMC 代码自修改
思路一
IDA 打开,定位到主函数:
逻辑感觉很简单
首先对 judge[]
数组做异或操作,跟进 judge[]
要求用户输入 s
,v5
是 s
的长度,只要满足 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
后面的内容,但是内容都是这样的十六进制数据:
联想到程序中的数据、指令、代码其实都是二进制数据形式存放的
于是猜测这个 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
等错误,因为PatchByte
、Byte
已经不能直接用于新版的 IDA 了详见本站《IDA新版与旧版的API变更》一文
可以看到脚本执行前后 judge
中数据的变化
脚本执行前:
脚本执行后:
接下来把这些数据转化为代码:
从 judge
的首地址 0x600B00
开始,到结尾的位置 0x600BB5
,一路按 快捷键 C
将数据转化为代码,遇到 IDA 弹窗的,直接转代码,无视即可(或者直接在 judge
的首地址 C
一下,IDA 会一路将可以转代码的地址全部转过来,最后 P
一下生成函数)
全部 C
完之后如下:
然后在 judge
首地址的位置,按 快捷键 P
将代码生成函数
最后,像正常函数一样按 快捷键 F5
就可以快乐反编译了
得到 judge()
函数的内容如下:
代码逻辑比较简单,定义了 v2 = "fmcd\x7F"
,v3 = "k7d;V`np"
将输入异或后要与 v2
相等,根据循环次数 14 可知,这里是将 v2
和 v3
拼接起来了
也可以通过汇编代码查看:
接下来编写脚本即可
脚本
key = "fmcd\x7Fk7d;V`;np"
flag = ""
for i in range(0, 14):
flag += chr(ord(key[i]) ^ i)
print(flag)
思路二
考虑到 judge
是在程序执行过程中自己解码的,所以可以通过动态调试下断点,先让程序自己解码,然后我们再观察解码后的内容
由于是 Linux 端的 elf 文件,开启远程调试,远程调试的方法详见本站《IDA的基础和远程调试》一文
在调试之前:
- 先在第一条指令
push rbp
的地方下一个断点 - 然后在输入
call ___isoc99_scanf
的地方下断点 - 开始调试
程序开始时停在我们下的第一个断点处
在第二个断点处,右键 --> Run to cursor
直接让程序运行到这里
可以看到,程序的 RIP
指向了我们第二个断点的地方:
然后 F8
单步步过
Linux 中程序已经开始让我们输入
这里先随便输入一个值,例如我输入:1
程序越过了 scanf
输入,RIP
指向下一条命令
由于下面的 jnz short loc_400698
这一条指令会跳转到 loc_400698
这个位置是输出 “Wrong!” 用的,并且会导致程序直接结束
所以我们需要在 jnz short loc_400698
这一条指令之后,设置一个新的 RIP
这样一来,输入 “Wrong!” 后,可以迫使程序继续跳转到我们设置的 RIP
的地方,从而绕过输入错误导致的退出
但也要注意,我们的目的是让程序自己解码 judge
函数后查看 judge
的内容,所以这个 RIP 一定要设置在调用 judge
函数之前,不然无法进入到 judge
函数
例如,我将 RIP
设置在 jnz short loc_400698
的后面一句,即:地址 0x40067A
处,然后 右键 --> set IP
同时,在 call rdx ; judge
调用 judge
函数的地方 F2
下一个断点
然后我们直接 F8
就会跳转到刚刚设置 RIP
的位置,从而绕过 loc_400698
继续 F8
执行到调用 judge
函数的地方,IDA 会询问是否进入这个地址
选择 "Yes"
,程序就会进入 judge
函数:
这个就是程序自己解码出来的 judge
函数的内容
然后选择 judge
函数的内容,使用 快捷键 P
将代码生成函数
最后 F5
即可将 judge
函数反汇编
结果
flag{n1c3_j0b}