反混淆:字符串加密

字符串与常量加密也是常规的混淆方案,很常见。

这一章会介绍常见的字符串加密场景,以及静态对抗字符串加密混淆的常用方案

通用的字符串加密对抗

字符串加密有很多种小情况:解密的方式,什么时候解密,解密数据放到哪里,等等

总的来说,有这么几种通用的方法:

  • 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.pyarrow-up-right

https://github.com/wINfOG/IDA_Easy_Life/blob/main/unicorn_fast_string.pyarrow-up-right

解密逻辑在支配树父节点

可以说是上面的升级版本,这个方案会使得字符串加密逻辑更加的复杂,更加的细碎。

原理是:支配树中的父节点是当前basic-block的必经节点,因此只要将当前使用的字符串解密逻辑放到父节点里面去,就确保解密逻辑一定执行。

我没有找到比较合适的案例,因此这部分不再展开了,依照类似的思路应该是能够完成一个较为通用的方案:找到所有的basic-block块 ;判断里面的字符串解密部分;模拟执行或者模式匹配每个basic-block中的字符串解密;把数据填回去。

混合其他混淆的字符串加密

还有一个点不得不说,字符串加密通常不单独出现,他只是作为混淆的基坐。

最典型的场景是和OLLVM一起出现。这导致逻辑更加的难看懂。

Last updated