反混淆:字符串加密
Last updated
Last updated
字符串与常量加密也是常规的混淆方案,很常见。
这一章会介绍常见的字符串加密场景,以及静态对抗字符串加密混淆的常用方案。
字符串加密有很多种小情况:解密的方式,什么时候解密,解密数据放到哪里,等等
总的来说,有这么几种通用的方法:
Dump内存:如果解密逻辑都放在初始化中(比如JNI_Onload 或者 init_func),程序执行后Dump内存是一种很棒的方法。但是,现在复杂的混淆都会边执行函数边解密,这就使得dump内存的方法不那么好用了;还存在一些其它的情况,比如解密之后把数据加密回去。
模拟执行:模拟执行也是很通用的方法,通常用于对抗将字符串解密放到函数开头的场景。
还有一些小技巧,比如在IDA中,我们可以将__DATA段设置为只读的,这样IDA通过常量计算的能力,在F5中能看到部分的数据。
比较早期的字符串加密方案,将字符串全部加密存储后,运行时调用解密函数。
分析解密函数的逻辑后,通过XREF找到所有的调用点,分析传入的参数就能很容易的写出解密逻辑。
样例:
如上图,decrypt_str_buffer_1CE30函数将会传入四个参数,进行解密;我们可以简单的编写脚本,进行解密,解密的结果如下:
下面是我写的一个简单的脚本
配合着脚本,解释一些对抗中可能会遇到一些小问题:
如何获得函数传入的参数:静态还原工具不太好直接提取出,为了通用性上面的脚本使用了模拟执行,还有一种做法是向上解析汇编。
解密的结果在动态分配的内存:有的时候,解密的结果会放到malloc的内存中,通过一个全局或者局部的指针使用,这种情况解密的结果需要可能要处理内存对象,不太好处理,比较偷懒的办法就是加注释。
也是很常见的字符串加密,通常来说会有个原子atomic全局变量包裹,整个逻辑inline在每个函数的头部,
案例:
我通常使用模拟执行对抗。模拟执行头部,监控所有的__data段写入;
参考下面我这个通用的代码:
https://github.com/wINfOG/IDA_Easy_Life/blob/main/emu_fast_string.py
https://github.com/wINfOG/IDA_Easy_Life/blob/main/unicorn_fast_string.py
可以说是上面的升级版本,这个方案会使得字符串加密逻辑更加的复杂,更加的细碎。
原理是:支配树中的父节点是当前basic-block的必经节点,因此只要将当前使用的字符串解密逻辑放到父节点里面去,就确保解密逻辑一定执行。
我没有找到比较合适的案例,因此这部分不再展开了,依照类似的思路应该是能够完成一个较为通用的方案:找到所有的basic-block块 ;判断里面的字符串解密部分;模拟执行或者模式匹配每个basic-block中的字符串解密;把数据填回去。
还有一个点不得不说,字符串加密通常不单独出现,他只是作为混淆的基坐。
最典型的场景是和OLLVM一起出现。这导致逻辑更加的难看懂。