背景
上一篇文章中介绍了xorstr的原理和最小化验证概念的代码,这篇文章来看下这种已经被广泛应用于各恶意样本以及安全组件中的技术如何还原,如果还没看上篇建议先看下了解其实现后再看本篇文章
xorstr的现状
随着相关技术的应用越来越广,各种攻击样本都用上了这一工具,威胁样本分析变得越来越耗时。这就是为什么需要一个对抗上述混淆技术的还原工具的原因,两个开源的还原工具flare-floss和AntiXorstr,工具的应用让这种二进制隐藏的字符串还原难度大幅降低
还原方案分析
flare-floss是mandiant公司开发并开源出来的针对病毒分析的自动化二进制字符串启发式查找工具,针对此类加密方式flare-floss提供了两种还原逻辑(stack string & tight string),两者基于不同的假设,但都可以用来处理上述加密,下面给出他的实现原理的简要分析摘要
stack string:flare-floss的这种还原方式基于一种非常宽的假设,即这种字符串必须基于栈,在栈上构造并解密。所以他使用模拟执行的方式对每个函数逐汇编代码的进行模拟执行,并在遇到函数调用指令时对当前栈进行全dump,最后使用字符串明文算法在dump中查找疑似的明文字符串,保存结果。
由于其假设过宽,致使其几乎拥有对当前开源的xorstr类似项目拥有100%的覆盖率,但过宽的假设导致大量的误报,对安全分析来说过多的垃圾信息反而干扰了正确的判断,这个误报在其代码中特别标注为“don’t run this on functions with tight loops as this will likely result in FPs”
floss-stack string | floss-stack string |
---|---|
覆盖率 | 优+ |
绕过难度 | 优+ |
误报率 | 差- |
精确性 | 差- |
tight string:而flare-floss为了解决上述解决方案过宽假设带来的大量False Postive的问题,在去年更新了floss2加入tight string还原模式,这种模式基于这样的假设:存在栈加密字符串的函数存在一个循环Block块,这个Block块出去时栈为解密状态,也即他瞄准的时上述代码中的decrypt内联函数部分,decrypt中的for循环结构即为tight string关注的循环Block,而他也从每个函数调用转为每个循环block出口时对函数栈进行dump并搜索明文字符串
floss-tight string | floss-tight string |
---|---|
覆盖率 | 优- |
绕过难度 | 中+ |
误报率 | 中+ |
精确性 | 差- |
AntiXorstr:去年写这个工具的时候并没有关注到floss项目因此实现的逻辑和他完全不同。区别于tightstring对decrypt特征的关注,该工具关注的是类构造函数。基于这样的假设:“栈加密字符串的加密数值必须是编译期计算出来的”。工具会对函数的栈进行预分析,并对栈进行立即数染色,被非0立即数染色过的联通区域标记为高可疑区域,并在后续的模拟执行过程中关注此类区域的读写并输出结果
AntiXorstr | AntiXorstr |
---|---|
覆盖率 | 中+ |
绕过难度 | 优- |
误报率 | 优+ |
精确性 | 优+ |
tight string的绕过
在对floss的tight string逻辑分析的过程中发现他的假设实际上并非是栈字符串的必须的,但确实是当前几乎所有开源的实现一定会存在的特征,即解密函数流程图中表现为环的形式。所以对floss-tight string的绕过即实现一个不会成环的字符串解密函数。首先我们先看下当前常规字符串的解密逻辑的反汇编流程图特征,demo如下
这里的loc_1400010A0即为一个循环的Block,Block中的代码就是for循环中解密原始数据的decrypt函数的汇编码。这里让decrypt函数内部不生成循环即可实现对此还原方式的绕过,那么如何消环呢?这里用模板编程的思路去考虑一下,可以把循环使用模板递归展开的方式在编译期给消除掉,最终需要的效果如下
__forceinline char* decrypt()
{
for (auto index = 0; index < N; index++)
{
encBuffer[index] -= 1;
}
return encBuffer;
}
//N = 5
__forceinline char* decrypt_noloop()
{
encBuffer[0] -= 1;
encBuffer[1] -= 1;
encBuffer[2] -= 1;
encBuffer[3] -= 1;
encBuffer[4] -= 1;
return encBuffer;
}
剩下就是如何使用C++模板生成decrypt_noloop代码,使用递归展开的方式生成并对N=0做个特化终止即可消除loop环,并且还有一些细节这里限于篇幅不做展开,实现DEMO已开源:xorstr_s.h
测试demo
void test()
{
printf(Enc("Samsung\n"));
wprintf(Enc(L"Apple\n"));
printf(Enc("Xiaomi\n"));
wprintf(Enc(L"Oppo Group\n"));
printf(Enc("vivo\n"));
wprintf(Enc(L"Transsion\n"));
std::cout << Enc("Honor") << std::endl;
std::wcout << Enc(L"Realme") << std::endl;
std::cout << Enc("Motorola") << std::endl;
std::wcout << Enc(L"Huawei") << std::endl;
std::cout << Enc("Others") << std::endl;
}
测试结果:STACK & TIGHT 绕过,DECODED模式还原出一部分
─────────────────────
FLOSS STACK STRINGS
─────────────────────
─────────────────────
FLOSS TIGHT STRINGS
─────────────────────
───────────────────────
FLOSS DECODED STRINGS
───────────────────────
Samsung
Apple
Xiaomi
Oppo Group
vivo
Transsion
Honor
更鲁棒的绕过策略
上述通过详细分析floss还原策略写的xorstr_s似乎显得不是很鲁棒,仅仅针对一个开源的工具去实现定制化的绕过方案显得成本过高,那么是否有更鲁棒的反还原方式呢,暂时有两种方式,其中一种的核心原理是:”基于堆的明文展开“,使得Floss这样对堆明文做监控的模式彻底失效。
读过上一篇文章的应该可以知道栈中保存了字符串密文,但并不是一定要就地解密,完全可以对栈数据只读而解密后的明文写入堆中,并利用临时对象的析构函数完成堆的释放即可。实现DEMO已开源:xorstr_h.h,测试FLOSS的效果如下
FLOSS STACK STRINGS
─────────────────────
o]QOIR[6<
}<L<L<P<Y<6<<<
dU]SQU6<
s<L<L<S<
<{<N<S<I<L<6<<<
JUJS6
h<N<]<R<O<O<U<S<R<6<<<
tSRSN
n<Y<]<P<Q<Y<<<
qSHSNSP]<
t<I<]<K<Y<U<<<
sHTYNO<
─────────────────────
FLOSS TIGHT STRINGS
─────────────────────
───────────────────────
FLOSS DECODED STRINGS
───────────────────────
o]QOIR[6<
测试结果:Floss所有模式下的绕过