在介绍O-MVLL之前,首先介绍什么是代码混淆以及基于LLVM的代码混淆,O-MVLL项目正是基于此而开发来的。
有关O-MVLL的概括介绍以及安装和基本使用方式,可参见另一篇随笔
https://www.cnblogs.com/level5uiharu/p/16912019.html
代码混淆是将代码转换成另一种功能上等价,但更难以阅读的形式,是一种对抗逆向工程的手段,也是一种保护源代码和程序的手段。
例如修改各种函数、变量名称以消除其语义,使用非正常逻辑实现功能、使指令复杂化等等。代码混淆不能从根源上对抗逆向工程,只能增加逆向工程的分析成本,因此还需要结合其他手段来获得更强的安全性。同时,常见的代码混淆方式往往会引入大量无关指令,或者用于复杂化程序的指令,尽管引入指令越多安全性越强,但通常还会增加程序体积并降低运行效率。因此在使用代码混淆时,要平衡好效率和安全性。


目前代码混淆仍是一个小众方向,相关研究和进展不多。此外,代码混淆还可以应用于恶意代码检测领域,对恶意程序进行混淆从而生成更多的恶意代码样本,一方面扩充了模型训练的数据集,另一方面代码混淆对恶意代码进行的修改可能会隐藏其某些特征。
那么什么是基于LLVM的代码混淆呢?
代码混淆有多种实现途径,根据目标编程语言、架构等不同有不同方式。最初的代码混淆是直接在源代码上进行修改然后编译,这样虽然保护了源代码,但也增加了调试和开发者自己理解源码的成本。Java则由于其字节码的存在,通常是对存储在class文件中的字节码进行混淆,这样就不必修改源码,但仍能得到一份混淆后的可执行文件,将可执行文件发行即可。
基于LLVM的代码混淆正是采用了和Java混淆类似的思路,LLVM编译框架大致分为前端、中端、后端三段:

前端:进行词法分析、语法分析、语义分析等,生成中间代码IR
中端:优化器,会在此处对中间代码IR进行修改优化,中端会有名为Pass的文件,每一个Pass都会依照自身的逻辑对IR进行修改完成优化
后端:完成连接、汇编、生成目标文件的工作
如上图所示,对于不同的编程语言,都会在前端转换成格式相同的IR文件后交由中端优化器处理。同时,LLVM提供了Pass开发的API,可以根据自身需求开发特定功能的Pass。
因此基于LLVM的代码混淆实际上是通过开发Pass的方式,在中端优化器中混淆IR文件,再讲混淆后的IR文件连接汇编,从而得到混淆的可执行文件的。
O-MVLL项目灵感源自OLLVM,后者则是最著名的基于LLVM的代码混淆器之一,实现了指令替代、控制流平坦化、虚假控制流这三种代码混淆方式。O-MVLL在OLLVM的基础上增强了这三种代码混淆方式,同时新增了一些代码混淆方式(当然,以Python API的形式调用代码混淆也是其创新和特点,在上一篇文章中有所介绍)
下面介绍O-MVLL中使用的代码混淆方式
使用方式:重写anti_hooking方法
def anti_hooking(self, mod: omvll.Module, func: omvll.Function) -> omvll.AntiHookOpt:
if func.name in ["encrypt", "has_secure_enclave"]:
return True
return False
以上的代码能够将这种代码混淆方式作用于函数名为encrypt、has_secure_enclave的函数。
对抗hook技术是另一门值得深入研究的学问,因此O-MVLL对此进行的保护适用范围有限,安全性也有限。
该方式只适用于对抗frida,这跟它的设计有关。通常来说,hook框架需要使用几个临时的寄存器来重新定位或访问当前函数的原数据,对于frida来说,它需要使用x16,x17两个寄存器之一。这一点可以在frida项目的文件gumarm64relocator.c中分析出来:
if (available_scratch_reg != NULL)
{
gboolean x16_used, x17_used;
guint insn_index;
x16_used = FALSE;
x17_used = FALSE;
...
if (!x16_used)
*available_scratch_reg = ARM64_REG_X16;
else if (!x17_used)
*available_scratch_reg = ARM64_REG_X17;
else
*available_scratch_reg = ARM64_REG_INVALID;
}
因此如果在函数的序言开始的地方插入指令,占用x16,x17这两个寄存器,就能够让frida抛出错误。O-MVLL也正是这样做的,具体做法为在函数的开头插入
mov x17,x17;mov x16,x16或者mov x16,x16;mov x17,x17两条语句,插入哪组指令则是由随机数随机选择
可以参见O-MVLL/src/passes/anti-hook/AntiHook.cpp中的定义
static const std::vector<PrologueInfoTy> ANTI_FRIDA_PROLOGUES = {
{R"delim(
mov x17, x17;
mov x16, x16;
)delim", 2},
{R"delim(
mov x16, x16;
mov x17, x17;
)delim", 2}
};

使用方法:重写obfuscate_arithmetic方法
def obfuscate_arithmetic(self, mod: omvll.Module,
fun: omvll.Function) -> omvll.ArithmeticOpt:
if func.name == "encode":
return omvll.ArithmeticOpt(8)
上述配置会将该代码混淆方式应用于encode函数,并且迭代混淆8次
这种方式是将运算指令复杂化,能够被复杂化的运算指令包括加、减、与、或、异或,乘法和除法由于其运算的复杂性和溢出、借位等操作难以实现。
具体来说,它会将这些运算使用混合布尔算术(MBA)构造的等价式替代,这些等价式和原本的运算指令之间的映射关系如下

迭代混淆会在上一轮混淆的基础上再次调用该混淆方式,多次的迭代将产生大量的混淆代码,因此一定要考虑安全性和运行效率的平衡。
以下是混淆前后的对比,迭代次数为1


使用这种方式混淆出来的特征明显,且由于每种运算指令和替代式一一对应,因此每一种特定的混淆形式都能唯一确定相应的运算指令,也能由混淆形式还原。这种混淆方式往往需要和其他代码混淆方式结合使用才能发挥更大的威力。
使用方式:重写obfuscate_constants方法
def obfuscate_constants(self, mod: omvll.Module, func: omvll.Function):
# Logic goes here
在函数的返回值方面,作者进行了设计,目前提供以下几个返回值的处理:
1.BOOL:返回true时启动混淆,false时不启动
2.返回一个整型常量的list:混淆list中出现的常量
3.返回omvll.OpaqueConstantsLowerLimit(n),混淆不小于n的常量
与运算混淆类似,这里则是使用构造的复杂等价式替换掉程序中出现的常量,这对于一些加密算法的特征常量(例如AES的S盒)的保护效果很好。
对于用来替代常量的复杂等价式的构造,作者采用以下三个方式:
1.0的构造
0 = MBA(X ^ Y) - (X ^ Y)
0 = (X | Y) - (X & Y) - (X ^ Y)
2.1的构造
LSB = 当前栈顶地址
Odd = 随机生成的奇数
1 = (LSB + Odd) % 2
由于栈地址一定要满足对齐的条件因此低位一定是0,即栈地址是一个偶数,这样就能保证LSB + Odd一定等于一个奇数
3.其他值的构造
Split = random(1,min(255,var))
LHS = var - Split + 0
RHS = Split + 0
var = LHS + RHS
通过上述构造的替换,逆向工程时能看到的原常量var被替换成LHS + RHS。此外LHS和RHS都分别加上了0,这个0会被之前提到的0的构造替换。并且整个Opaque Constants会默认启用运算混淆,迭代次数为1,其中相应的运算指令也会被复杂化。
下图为混淆前后的对比图,展示的是0的构造替代。


可以看到尽管没有在配置中启用运算混淆,但是常量混淆的混淆代码中有明显运算混淆的特点。
使用方式:重写break_control_flow方法
def break_control_flow(self, mod: omvll.Module, func: omvll.Function):
if func.name == "break_control_flow":
return True
return False
上述配置会将该混淆方式应用于break_control_flow函数
该混淆方式破坏控制流,准确地说是破坏函数调用的控制流。但本质上并没有改变函数调用的流程图,而是将被保护的函数中的指令复制到另一个函数当中,再删除原函数的指令并添加混淆指令,最后使用隐含的方式跳转到复制函数中以保证功能不变。
具体而言,它做了三件事:
1.clone克隆
克隆原函数的所有指令,并记录克隆函数的地址。
2.插入混淆指令
删除原函数的指令,并插入混淆指令,这些混淆指令包含类似于ldr x0,#offset的指令,pc+#offset处则是要保护的函数的地址,添加这种指令会让反汇编器认为这个偏移处的内容可能不是指令而是数据,然而实际上它就是指令。
此外还会添加一些运算,运算结果为克隆函数地址,将地址保存到局部变量中,并使用运算混淆和不透明常量进行联合保护。
3.添加跳转
在原函数的结尾处插入跳转指令,跳转到保存了原函数正常功能的克隆函数处,以完成正常功能。
首先会从局部变量中取出克隆函数的地址到寄存器中,再使用BLR指令跳转到寄存器中保存的地址。

以这样的方式跳转,克隆函数不会出现在函数调用的流程图当中,相比于硬编码使用函数地址完成跳转来说是一种隐含的函数调用
下图是该混淆方式工作的模式图

以下是混淆前后函数的对比


混淆后函数原先的代码被存放在了克隆函数sub_18D8当中,并通过最后一条指令跳转到克隆函数,克隆函数当中的内容,与未混淆前原函数的内容相同

使用方式:重写flatten_cfg函数
def flatten_cfg(self, mod: omvll.Module, func: omvll.Function):
if func.name == "check_password":
return True
return False
如上所示,将会对函数check_password使用该混淆方式。
控制流平坦化是对程序中出现的分支和跳转进行修改,全部转换为switch的形式,从控制流程图的角度看,就像是把控制流程图给压平了,如下图所示

那么如何保证执行的顺序和分支条件不发生变化呢?
假设switch语句根据变量var来进行跳转,那么首先为每个基本块打上标签,从基本块2到基本块5分别为a,b,c,d,e,这五个标签为生成的随机数。
之后在每一个基本块结束的时候对var进行赋值,将var赋值为下一个基本块对应的标签,然后再跳转到分发块,例如基本块2的分支可能跳转到基本块3和基本块4,那么根据判断条件,将var赋值为b或者c,然后跳转到分支块switch,switch语句就会根据基本块2对var的修改来进行相应的跳转。
以上是OLLVM实现的控制流平坦化,O-MVLL对其进行了增强
1.对var进行编码处理
在OLLVM中,可以根据基本块最后的赋值来判断下一个基本块是哪个,你可能能够看到如下的伪代码:
switch(var){
case a: var = b;
case b: ;
...
}
可以很明显地根据对var的赋值判断出基本块a的下一个基本块是基本块b。
但在O-MVLL中,赋值给var的值实际上是经过编码后的值,也就是如下的伪代码:
switch(encode(var)){
case a: var = c;
case b: ;
...
}
而encode(c)= b,这保证了流程的正常执行,但仅仅通过分析switch处的代码,无法得知基本块a的下一个基本块是基本块b,还需要对编码的算法进行分析和破解。
2.在default中填充垃圾代码
由控制流平坦化而来的switch语句中,default所指示的代码块是永远不会被执行的(否则原程序的控制流程被混淆后就发生改变了),因此O-MVLL在default指示的代码块中插入了一些垃圾代码,这些代码不会被执行,但仍会出现在控制流程图中混淆视线。
这里添加的垃圾代码,就像Control-Flow Breaking中添加的混淆指令一样。
以下是混淆前后函数的控制流程图:

使用方法:重写obfuscate_struct_access函数
def obfuscate_struct_access(self, _: omvll.Module, __: omvll.Function,
struct: omvll.Struct):if struct.name == "class.SecretString":
return True
return False
如上所示,将会对名字为SecretString的类进行混淆(该混淆方式也可以应用于结构体)
这种方式能够增加分析结构体和类的难度,在逆向工程时更难分析出类和结构体内部的成员和类型。
通常,对于结构体和类的访问采用
ldr x0, [x1, #offset]
这样的形式,而offset的组成是由局部变量和栈顶的偏移组成的,因此无法直接对#offset进行混淆。
O-MVLL的做法是将上述指令转换如下:
$var := #offset + 0
ldr x0, [x1, $var]
这样就将偏移保存在变量当中,再通过变量进行寻址。此时就可以对var进行混淆了,混淆的方式采用了之前提到的运算混淆和不透明常量两种方式结合。
以下是混淆前后的对比图


使用方式:重写obfuscate_string方法
def obfuscate_string(self, _, __, string: bytes):
if b'debug.cpp' in string:
return 'REMOVED'
该方法支持多种返回值,具体如下:
1.返回一个字符串:将字符串替换为返回的字符串,如果用于替换的字符串长度比原字符串长,则超出部分会被截断。返回空字符串时表示删除
2.返回omvll.StringEncOptGlobal(),加密字符串并存储为全局变量,在.data段可见,一旦程序被加载,该字符串就可以被搜索到
3.返回omvll.StringEncOptStack(),加密字符串并存储在当前栈上
4.返回omvll.StringEncOptStack(loopThreshold=0),加密字符串并存储在当前栈上,解密流程相比3更简单,代码量减少
上述四种方式的实现细节如下:
omvll.StringEncOptGlobal()
这种方式加密后,字符串被保存在.data段上

对应的解密函数为sub_1818,该解密函数被存放在.init_array当中,也就是说它是在程序加载后的初始化当中被调用,因此一旦程序完成加载初始化,字符串就会被还原

omvll.StringEncOptStack()
这种方式会将加密后的字符串作为局部变量保存在函数的栈上,使用该字符串之前的解密步骤也是在使用该字符串的函数中完成

在解密算法调用的时候,也都默认调用了运算混淆和不透明常量两种混淆方式。
没有启用loopThreshould=0时,解密的算法大致如下所示
char OMVLL_DECODED[6];
OMVLL_DECODED[1] = ENC_OMVLL[1] ^ 0xd7;
OMVLL_DECODED[5] = ENC_OMVLL[5] ^ 0x02;
OMVLL_DECODED[2] = ENC_OMVLL[2] ^ 0x77;
OMVLL_DECODED[0] = ENC_OMVLL[0] ^ 0x55;
OMVLL_DECODED[4] = ENC_OMVLL[4] ^ 0x7b;
OMVLL_DECODED[3] = ENC_OMVLL[3] ^ 0x35;
这种方式产生大量指令,但好在打乱了秘钥流,增强了安全性。当字符串很长时,这种方式会非常耗费空间。
因此启用了loopThreshould=0时,解密算法大致可以如下表示:
char OMVLL_DECODED[6];
for (size_t i = 0; i < 6; ++i) {
OMVLL_DECODED[i] = ENC_OMVLL[i] ^ KEY[i];
}
我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源