Unicorn实战(三):去掉hikari的字符串加密

hikari是近年来市面上开源的最优秀的一款基于llvm的混淆工具了,也提供字符串加密功能,上一篇文章解密了armariris的字符串,本文用unicorn解密一下hikari的字符串。

本文的涉及到个5文件,位于该gist下:https://gist.github.com/LeadroyaL/41deedb95f5fd29d7ee874c08d27db5b 或者github仓库 https://github.com/LeadroyaL/decrypt_str_hikari

共:一个idapython(py2)脚本,一个unicorn(py3)脚本,一个C文件,一个Patch前的SO,一个Patch后的SO。

一、hikari字符串加密实现原理

我曾经移植过它,代码位于https://github.com/LeadroyaL/llvm-pass-tutorial/blob/dev/Hikari/StringEncryption.cpp,(建议先阅读上篇armariris)

与armariris不同的地方是:hikari是在进入函数时进行字符串解密的,只有运行过的函数才会将解密过的字符串存放在内存中,因此不能使用执行一遍datadiv_decode就完事的策略,需要遍历可疑的函数从而在内存中获得解密后的字符串。

使用下面的伪代码可以帮助理解:

原先的代码

hikari 处理过的代码

使用llvm提供的对函数的viewCFG功能,会有下图的CFG

 

由于字符串加密的Pass在注册时,放在比较靠后的位置,所以它一定是出现在整个函数最开始的,范围比较容易界定。

二、制造样本

网上没找到,没办法,自己编译一个出来吧,用脚本生成一些c代码,将随机字符串拼接为新的字符串并且返回,见github里的native_lib.c。

然后加载hikari的字符串加密Pass,可以参考我的github教程(https://github.com/LeadroyaL/llvm-pass-tutorial/blob/dev/),获得一份样本。

如果懒得自己弄,样本是github的libnative-lib.so文件。

三、主要思路

字符串解密在每个函数开头完成的的,因此要对每个函数都解密一遍,就要先找到每个函数,然后只运行最前面的解密代码,之后保存内存,并且运行时不要再次执行解密。

这里我们定义三个BasicBlock,entryBB就是函数入口,可以理解为 if(status==0) 这句话,decryptBB就是执行解密逻辑的代码,可以理解为 xor_decrypt(xxx),originalBB就是原先的业务代码,可以理解为status=1;紧接printf(“Hello”)。

第一个难题是【函数边界】确定各个函数的起始位置,这个很头疼,因为用到的这几款工具都无法直接给出函数的开头地址;

方案一:使用IDAradare2angr 给出的推测

方案二:手写一个简单的寻找函数的轮子

 

最后决定使用IDA提供的函数边界识别。

第二个难题是【ControlFlowGraph如何精准确认StringDecryptBB 的具体位置、精准确认status 被读写的时刻,精准确认 if 函数的位置。

 

方案一:添加rwdata hook,读取 status 时可以知道大致位置,写回时候也知道大致位置,使用它们作为界限

方案二:总结 pattern,根据各个块的汇编特征来确认

方案三:使用IDAradare2angr获取CFG

最终决定使用 IDA 提供的API,菜鸡脱离IDA活不下去,太菜了。。。

写完这三篇文章,突然想一个东西:使用unicorn时一定要忘掉汇编语句这个东西,思路上,把ELF看做一个个Function、BasicBlock、一段段汇编,关注大局而非某条语句,思想上要从逐句执行汇编上解放出来,也是我觉得unicorn比较牛逼的地方。虽然我对unicorn的了解并不深入,但我还是强烈推荐读者也有这样的大局观!

四、实现细节

由于没有办法用python脚本把ida的逻辑和unicorn的逻辑串起来,因此执行完毕后,把重要信息保存到json里,之后再传递给unicorn。(主要是因为ida是python2,而我unicorn喜欢用python3)

1、使用idapython拿到各个函数入口

2、使用idapython拿到函数的CFG(Control Flow Graph),API叫FlowChat,并且根据以下特征找到entryBB、decryptBB、originalBB:

特征一:entryBB一定有两个分支,其中一个是originalBB、另一个是decryptBB,但无法区分出二者。

特征二:decryptBB进入后,无论中间经过多少次跳转,一定不会存在两个后继分支,一定会走到originalBB。可以理解为单向图,decryptBB => AAA => BBB => CCC => originalBB,使用深度优先遍历即可确认两个BB之间的关系,如果错了就说明这个函数没有进行字符串解密。

注意这里要区分开arm和thumb,ida里是通过 “T寄存器”来区分的,我们需要转换为正确的PC值

至此,我们获得了一系列 entryBB、decryptBB、originalBB 的数据,完整代码见github。

3、使用unicorn模拟执行代码

 

这里主要的难点是何时认为字符串已解密完成,向下运行的太多,对我们的分析是有害的,因为业务代码会造成各种非预期,一定要停下来。

 

另一个难点是,使用idapython得到的数据,大部分是可靠的,有一小部分可能是不可靠的,例如业务代码真的写了 if(xxx) {xxx} 这样的代码,这种代码对我们的分析也是有害的,需要额外考虑,我们称之为“非decryptHeaders”。

 

停止的方案一:statusdecrypt的内存访问read/write恰好完全成对出现; -> 极大概率不会被非decryptHeaders的地方干扰

停止的方案二:status的位置已知,decrypt执行完毕;-> 但容易被非decryptHeaders的地方干扰,因为对解密block的鉴定是不准确的

停止的方案三:status的位置已知,当他被赋值为1时,停止执行;-> 大概率不会被非decryptHeaders的地方干扰

 

最后选择了方案三,因为比较好写,如果失败率过高,就需要人工剔筛选一下 decryptHeaders 了。

4、执行停止的方案三,主要使用UC_HOOK_MEM_READ和UC_HOOK_MEM_WRITE

我们将运行分为三段:

entryBB里一定只涉及一次内存读,(因为status初始为0,落在了bss里),可以获取status的内存地址,直到decryptBB会停下。

decryptBB不关心,直接运行即可,直到originalBB会停下。

original运行后,当status被写为1时,停下来

 

这里仍然有Thumb的坑点,入口PC要写加一的值,停止PC要判断去掉一的值,需要多测试。

按照预期,执行三段代码

5、异常case的处理

为了避免“非decryptHeaders”被运行,我做了两种简单的校验,防止异常case出现。

校验一:在entryBB,读bss时,将它的地址放到set里;在originalBB,写bss时,只要涉及到bss写入就停下,并且从set中移除地址。在预期内的话,结束时set时空的。

校验二:在entryBB,读内容一定读到的是4字节的0;originalBB时,写内存一定写到的是4字节的1。

如果出现了非预期,就需要将rwdata恢复,防止影响,因此需要添加一个用于backup的内存监控钩子,如下代码:

6、Patch掉data,Patch掉解密逻辑

data的话直接把rwdata的内容完全覆盖过去,字符串就会出现在那些未知。而Patch解密逻辑有点绕,因为status落在bss上的,所以初始化总是0,无法初始化为1所以patch方式是:在decrypt的第一句,直接BoriginalBB上。

 

五、验证成果

丢到手机里运行,发现patch前后功能没有变化,使用strings和ida都可以看到我们的字符串,完工!

来一张对比图收工!


=============================================================
随着访客的增多,LeadroyaL在本站流量的开支越来越多了,曾经1元能用1个月,现在1元只能用3天。如果觉得本文帮到了你,希望能够为服务器的流量稍微打赏一点,谢谢!

《Unicorn实战(三):去掉hikari的字符串加密》有1个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code