ollvm反混淆学习

VSole2021-10-16 16:57:55

看了@无名侠大佬发的一篇关于使用unicorn模拟执行还原ollvm的贴子受到了很大的启发, 自己也基于这个思路做了些样本学习,下面来探讨一下。

ollvm原理

Ollvm大致可分为 bcf(虚假块), fla(控制流展开), sub(指令膨胀), Split(基本块分割)

bcf:

克隆一个真实块,并随机替换其中的一些指令,然后用一个永远为真的条件建立一个分支。克隆后的块是不会被执行的。

Fla:

将所有的真实块使用一个switch case结构包裹起来,每个真实块执行完毕后都会重新赋值switch var,对于有分支的块会使用select指令,并跳转到switch起始代码块(分发器)上,根据switch var来执行下一个真实块。

Sub:

指令膨胀,将一条运算指令,替换为多条等价的运算指令。

Split:

利用随机数产生分割点,将一个基本块分割为两个,并使用绝对跳转连接起来。

关于ollvm具体的实现,可参考源码。

还原思路

网上有很多还原ollvm的脚本,但是只能还原特征很明显的ollvm,或者说只是debug版的ollvm。在debug版中ollvm的特征非常明显,一个分发器,和引用了这个分发器的真实块。但经过编译器优化后,分发器可能会变成多个,基本块会合并造成虚假块也可能会和真实块合并,等等。

现实情况是,你基本上碰不到简单的ollvm,所以那些东西个人感觉意义不是很大,还是需要靠自己。

谈下还原思路

Bcf:

Bcf块是执行不到的块,所以说当使用unicorn 跑过一遍函数后,其中没有执行到的块肯定有包括bcf块,我们只需要将它挑出来标记下就好。

但函数中可能存在分支,只跑一遍函数是无法覆盖到所有分支的,所以要想办法找到函数的所有分支。一开始采用的是无名侠大佬的方法,当碰到csel指令时人工干预让其覆盖所有分支,但整个函数经常陷入死循环,分析过后发现虚假块的跳转也有可能使用csel指令。

后来想到了在二进制漏洞挖掘中的思路fuzz(模糊测试),即变异函数的参数传递给函数,来覆盖更多的分支。这样做也不能说能够找到函数的所有分支。影响一个函数的分支执行大概有三种情况,参数,全局变量,内部函数调用的返回值。后两种情况的话留意下模糊执行的trace应该能找到些蛛丝马迹,可能会比较麻烦。

Fla

这个环节会产生控制流块,我们只需要将这些块挑出来标记,找出所有的真实块,并通过模拟执行还原真实块之间的关系就好。

控制流块的剔除采用了无名侠大佬对基本块签名的方法。

Sub:

指令膨胀的还原,使用llvm的pass优化效果还可以,但目前一些ir翻译工具对arm64的支持不怎么样。

Split:

基本块分割更多是用来增加bcf和fla效果的。

总结整体思路:

(1)利用模拟执行和fuzz技术,找出bcf块并剔除。

(2)使用基本块签名剔除控制流块。

(3)将剩余的块标记为真实块,并使用模拟执行找出对应关系。

(4)根据对应关系,重构cfg。

实战

自己编译的一个样本如下:

void HexDump(char *buf,int len,int addr)__attribute((__annotate__(("split"))))__attribute((__annotate__(("fla"))))__attribute((__annotate__(("bcf")))){    int i,j,k;    char binstr[80];     for (i=0;i        if (0==(i%16)) {            sprintf(binstr,"%08x -",i+addr);            sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]);        } else if (15==(i%16)) {            sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]);            sprintf(binstr,"%s  ",binstr);            for (j=i-15;j<=i;j++) {                sprintf(binstr,"%s%c",binstr,('!''~')?buf[j]:'.');            }            printf("%s",binstr);        } else {            sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]);        }    }    if (0!=(i%16)) {        k=16-(i%16);        for (j=0;j            sprintf(binstr,"%s   ",binstr);        }        sprintf(binstr,"%s  ",binstr);        k=16-k;        for (j=i-k;j            sprintf(binstr,"%s%c",binstr,('!''~')?buf[j]:'.');        }        printf("%s",binstr);    }}

先找出所有的基本块(以跳转指令结尾的块)

这里需要注意下由于编译器优化的关系,基本块会合并,有些基本块并不是以跳转指令结尾,就如这样:

这些情况,是因为两个基本块同时引用了这个块,所以需要将这个块拷贝一份,并将另一个块的引用修改为新拷贝的块,不然还原关系的时候会乱掉。

我这里占用了main函数的空间。

找出所有的基本块后开始fuzz执行,并统计所有被执行到的块。这里fuzz采用了,先使用peach编写规则生成参数的语料库保存到文件中,然后读取文件中的内容当作参数传递给函数, 当然如果不关心函数的其他分支,fuzz的步骤感觉可以跳过,例如一些纯算法函数。

经过几十轮fuzz后,共统计到如下被执行了的块。

这些块中肯定是包含了控制流块的,所以现在用签名法来过滤掉控制流块。

过滤后还剩下169个块,这些块就是真实块了,为了保险起见我还人工过滤了一下,基本没什么问题。

接下来开始模拟执行找出他们之间的对应关系了,当碰到一个真实块时记录下它上一个执行的真实块,并保存起来。

传递给函数的参数也需要使用上面fuzz使用的参数,这样才能执行到每一个块。

模拟执行后,基本块之间的关系如下:

如果数组中只有一个基本块的话,那么他们是一个顺序关系,如果有两个的话则是分支关系, 如果2个以上则有三种情况:

(1)漏了真实块;

(2)该块不是一个真实块;

(3)该块是一个分支共用块。

经排查这里是第三种情况,如下:

9e8这个块被两个基本块引用,并两个基本块都是一个分支块,所以会出现这种情况。具分析其中一个块的分支对应的是bcf,不会被执行到,所以数组中是3个基本块而不是4个。对于这个情况也需要将9e8这个块copy一份,将两个基本块中的其中一个引用修改为copy后的块。

修改完毕后,记得将copy块添加到真实块中,并重试。

可以看到问题解决了。

找出对应关系后需要接着还原分支关系,当条件为真时跳到那个块,为假时跳转到那个块。因为每个分支块都会有一条cmp 和csel指令, 如果找到的分支块中没有这两条指令,那么就是漏了真实块。

还原他们的关系,只需要在模拟执行时,记录cmp的返回值,和返回值对应的真实块即可,这里会比较麻烦,需要手动找到cmp的地址, 左右值, 和比较关系。

模拟分支块的关系如下:

我这里根据记录的条件,翻译成了汇编。

最后根据这些真实块之间的关系patch即可, 注意在patch分支块时需要注意csel和cmp的关系,像这种:

如果我们如果在基本块的最后patch b.ne xxx b xx, 那么标志位就会被上面的一个cmp干扰,所以需要将上面 一个cmp也patch掉。 

好了现在大功告成,直接来看伪代码。

把伪代码拿出来编译测试:

写在最后

目前还在测试大概还原了5 6个样本,可能还有一些细节方面未考虑到,所以发出来希望听下大佬的意见。

之所以没贴代码出来是因为代码太杂了和篇幅太大了,实在是不太方便,有需要的话可以参考无名侠大佬的帖子和源码,我都是基于他之上的。

如果大家感觉以上有不妥或者不理解的地方,欢迎和我一起探讨一起学习。

代码混淆汇编指令
本作品采用《CC 协议》,转载必须注明作者和本文链接
加密算法共4种,第二个任务注册机,缺一个算法的解密算法,其他三个算法均已写好C实现的解密算法。随后在xxx函数通过frida分析找到XTEA加密,然后用frida在内存中找到并提取了密钥。Dump && Recover IL2CPP虽然用修改后的frida去hook libsec2023.so仍然会被检测,但是hook其他库没有出现问题。
免杀知识汇总
2021-08-25 23:11:00
免杀知识汇总
奇安信的报告《使用和海莲花相似混淆手法的攻击样本分析》[1]中分析了一个和APT32使用相同混淆方法的样本。本文根据奇安信的报告以及报告中提到的参考文章和代码[2]对该样本进行去混淆
得益于Unicorn的强大的指令trace能力,可以很容易实现对cpu执行的每一条汇编指令的跟踪,进而对ollvm保护的函数进行剪枝,去掉虚假块,大大提高逆向分析效率。
少量虚假控制流混淆后的算法还原案例分析!
结果表明,那些想要保持匿名的程序员需要采取极端的应对措施来保护他们的隐私。因此,我们为可执行二进制作者溯源设计了一个特征集,目标是准确地表示与程序员风格相关的可执行二进制文件的属性。从头到尾反汇编二进制文件,遇到无效指令时跳过该字节。特别而言,反编译器可以重构控制结构,如不同类型的循环和分支结构。因此,需要进行特征选择的降维操作。
机器学习模型对这种变化称为概念漂移,使用旧数据训练的模型在处理前所未见的新样本时挑战极大。为了构建有效且稳健的分类器,必须能够检测同一恶意软件家族中漂移的 IoT 变种,并解释漂移的成因。通过 VirusTotal 的分析报告,使用 AVClass 对其进行处理聚合家族归属。一共确定了 44 个 Mirai 的变种与 11 个 Gafgyt 的变种。相比 Gafgyt 来说,Mirai 的连接更为紧密。
由于init函数是linker调用的,所以没法做加密。所以我们合理怀疑初始化函数位置找错了。其实之所以会搞错,是因为错误的section header干扰了ida的解析。这通常是因为代码中有花指令的缘故,我们要考虑去除花指令了。所以有理由怀疑,这里就是花指令,用来干扰ida解析的。执行完后再加上0x20,栈是平衡的。所以我们确信,中间的ret部分就是花指令
Frida工作原理学习
2022-07-12 16:28:29
frida是一款便携的、自由的、支持全平台的hook框架,可以通过编写JavaScript、Python代码来和frida_server端进行交互,还记得当年用xposed时那种写了一大堆代码每次修改都要重新打包安装重启手机、那种调试调到头皮发麻的痛苦,百分之30的时间都是在那里安装重启安装重启。
VSole
网络安全专家