代码混淆技术有哪些(代码混淆定义及操作)

本文首先介绍了Collberg对代码混淆的分类方式以及定义,而后介绍了一些常见的混淆策略,最后讨论了如何构建一个二进制混淆引擎。代码混淆研究综述

>>>>

混淆的分类

早在1997年,学术界就开始了代码混淆的研究。目前使用最广泛的混淆分类方式是Collberg[1]于1997年针对JAVA程序的安全问题提出的。根据Collbreg的定义,混淆可以分为以下四种类型:布局混淆、预防性混淆、数据混淆、控制混淆。布局混淆指删除或修改源代码中的对攻击者由于的信息,如变量名、调试信息等。显然,布局混淆在二进制代码混淆中可以忽略不计。预防性混淆指预先针对特定的反编译工具,解混淆方法进行防备。控制混淆是目前研究的重点。Collbreg将控制混淆分为以下三类:聚合、排序、计算。聚合指将逻辑上相关的计算分割,或者将逻辑上不相干的计算聚合在一起。排序指将计算在程序中的分布随机化处理。计算指插入混淆计算代码来隐藏真实的控制流,包括扩展循环条件,引入不透明谓词等。值得一提的是,Collberg提出的排序混淆在二进制代码中实现难度较大。数据混淆被分为以下三种:聚合、重组、编码。当然,还有许多学者对此做了有关研究,如Chenxi Wang[10],Wroblewsk[9]等。前者是基于C语言程序的,后者是基于二进制代码的。值得一提的是,目前使用非常广泛的控制流平坦化算法正是Wang提出的。>>>>

混淆的定义

Collberg将混淆描述为一个从源程序P到混淆后的程序P’的等价转换。更精确地说,合法的转换必须要满足以下两个条件:如果P终止失败或者异常终止,那么P’可以选择终止或不终止;在其它情况下,P’必须要正常终止,并且产生与P’相同的输出。这里的输出指的是用户可观测到的P的行为,对于那些不可观测到的行为,如创建文件与访问网络等,都是可以在合法转换中出现的。贾福春在[6]对该定义进行了修正,提出了弱语义等价。常见混淆策略

>>>>

基于数据流

指令移动在二十年前,甚至更早,就出现了使用JMP IMM32指令连接乱序后的代码的混淆技术,以此来打乱代码顺序局部性。指令移动与此不同,指令移动是将基本块内的语句进行顺序重排。z0mbie[12]首次引入x86指令对象集的概念,解决了相邻指令的交换。潘雁[7]基于Wroblewski提出的x86系统指令形式化定义,论证了指令交换的充分条件,并提出了针对数学与逻辑运算指令进行交换时,还需要考虑交换律等问题。文献[7]同时提出了一种模拟退火算法的指令随机交换混淆算法。本文提出使用定值分析,构建指令移动混淆,非相邻指令交换。以一段代码为例:

Mov var1.500Mov eax,var1Sub var1,20Add eax,2018Push eaxCallfunc

Mov eax,2010Mov eax,var1Mov eax,2018Callstdcall_funcMov eax,2018Push eaxPush var1CallObfscallObfscall:Push var1Callstdcall_func

这是三个死代码插入的例子。毫无疑问,例1的解混淆是最为简单的,例2使用了函数返回值使用eax传递的技巧,例3的复杂度是较高的。我举出这三个例子并不是为了指出改进死代码生成算法的必要性,而是为了强调将混淆引擎设计为支持轮换调用各种混淆算法的形式的必要性。如果说死代码生成了一个变量,那么在其它混淆算法中最好设法引用该变量。常量展开常量折叠是最早的基本编译优化手段之一。这种优化的目的是将可以在编译期就能推导出结果的计算用结果来替换。

Moveax,2018Add eax,2

显然,如果忽略标志位的变化,该段代码等价于Mov eax,2020。数据储存、编码数据的储存方式都是约定成俗的。如果我们要储存整数1238326,用int类型是首选,当然,用可变类型也是可以的。同理,三维数组也可以降维为一维数组。更进一步地,储存一个int变量,我们可以用连续的4字节内存,也可以采用不连续的4字节内存,当然,使用后者要自己调整加减乘除等运算。对数据进行编码也是一种混淆方案。在对编码后的数据进行操作时,有必要将数据解码,显然,这会暴露解码函数。同态加密可以解决这个问题,使用同态加密能够在数据解码前操作它们,通过对编码后的数据定义一个等价运算。目前已经有不少文献在面向混淆的视角下讨论过这种问题,但可惜这门学科所需要的知识背景远超我的能力范围,有兴趣的读者请自行研究。恒等运算替换使用布尔代数,将一些简单的运算扩展为更为复杂的逻辑运算,该技巧为许多混淆引擎采用,比如“臭名昭著”的VMP万用门。掌握基本的几个等值式就可以进行逻辑运算的推导,该部分内容较为简单,就不再做赘述。模式替换模式替换的概念很简单,混淆者定义一些较复杂的指令序列,在待混淆程序中进行语义等价的替换。

Push imm32Lea esp,[esp-4]Mov [esp],imm32Push reg32Mov reg32,espXchg [esp],reg32Pop espMov esp,[imm32]Push reg32Xor reg32,reg32Dec reg32S1: Inc reg32Cmp reg32,esp32Jnz S1 ;假定esp的值不可能等于-1Xchg [esp],reg32Pop espMov esp,[imm32]

聪明如你,可看出了上面四段代码的等价关系?这三个例子说明,模式替换的匹配替换过程是可以不断继续下去的。模式替换对应的就是编译优化技术中的窥孔优化。第4段代码中对Mov的变形是一位前辈指点我的,据前辈说是污点漂白的一个上不得台面的小trick。常量隐藏将常量值直接暴露在指令的操作数中,可能会向攻击者透露一些有用的信息。将代码中的明文常量值隐藏起来似乎是非常有必要的。

上述两段代码中,1行指令较之0行指令有了一点安全性上的提升,但显然还不够。为了将常量隐藏,我们可以引入一些数学技巧,如sin^2(x) cos^2(x) = 1,以此构造一些返回值恒定的函数。再进一步,我们可以利用一些API的特性,如故意传递错误的参数,利用API返回的异常代码。如果异常代码是200,而我们需要的定值是100,那该怎么办呢?答案当然是,得到异常代码后减去100啊!>>>>

基于控制流

函数内联、外联和混合函数内联的定义不再赘述。函数外联指将某一函数的一部分提取出来单独构成一个函数,并将其替换为对新创建的函数的调用。函数混合指将多个函数混合在一起。在调用混合后的函数前,由混淆代码调整上下文,来指定究竟应该执行哪个函数。以一段代码为例。

fun1() {s1…sn}fun2() {k1…kn}fun_mix(n) {if(n) {s1….sn}else{k1…kn}}

当然,实际混合时不会如此简单,比如可以用多个参数来指示欲执行函数。>>>>

基于控制流与数据流

不透明谓词Collberg把不透明谓词定义为一个布尔表达式。该表达式的值在混淆前是已知的,且混淆后再探明其性质需要付出一定代价。如果该表达式恒为true,则称为永真不透明谓词,记为PT。同理,还有永假不透明谓词(PF),可真可假不透明谓词(P?)。值得一提的是,可真可假不透明谓词的两条分支处的代码块在语义是上等价的,但可以使用不同的混淆策略对其进行混淆。吴伟民[2]等首次提出了N态不透明谓词。并不是说谓词所涉及到的数学知识越多就越强大,足够复杂的谓词确实可以难倒静态分析工具,但人工检测谓词的效率有时却出奇的高。这正是Collberg[1]提出的混淆评估指标中的隐蔽性。为了增强谓词的强度,王志[3]提出了使用数学未解猜想构造不透明谓词。不过要实现它比较困难,其一是如何将某个猜想在混淆的视角下运用,其二是如何逃避人工检测。王志提出了使用角谷猜想。但如何运用,仍然是个头疼的问题。比如最容易想到的一个性质,即角谷猜想生成的数列最终收敛于定值1。

While(x >= 30) {…}While(x >= 30&& x – y <= 29) {…}

其中y是由程序输入引出的一个变量,并按照角谷猜想进行了若干次迭代。类似这种混淆,要抵御人工检查仍是不太现实。路径分支信息隐藏程序的反汇编代码总是会暴露分支的信息,以一段代码为例:

0 Cmp eax,20191 Jnz s0 Push eax1 Callhash2Cmp eax,hash(2019)3Jnz s

第一段代码暴露出了分支的条件是jnz,目标是s,成立条件是eax等于2019。第二段代码是对其进行路径分支信息隐藏后的代码。控制流平坦化与不透明谓词都只适用于无分支代码的混淆,目前的路径分支信息隐藏混淆策略,都是针对约束求解器对于非线性运算求解的困难提出的。王志[4]将保留前缀算法与哈希函数相结合提出了一种新的路径分支混淆策略。因为哈希函数不具备保序性,所以引入保留前缀算法,取能使跳转成立的所有整数的集合并取其前缀后使用哈希函数加密。贾福春等[5]提出使用随机森林构建混淆,使解混淆的难度等价于提取出随机森林的规则。但这些方法都很难抵御最原始的二分查找法暴力搜寻区间边界。控制流平坦化控制流平坦化将基本块直接的联系隐藏在对Dispatch的调用中,最简单的实现是将函用一个switch-case结构重写。以一段代码为例:

intcode = 0;while(1){switch(code){case0:i = 50;code = 1;break;case1:if(i < 20)code = 2;elsecode = 3;break;case2:if(i >= 0)code = 4;elsecode = 5;break;case3:if(i <= 100)code = 6;elsecode = 5;break;case4:i -= 3;code = 2;break;case5:return0;case6:i = 3;code = 3;

基于二进制代码实现混淆器

>>>>

静态分析方法

while(某个IN值发生改变) {for(除exit之外的每个基本块B) {OUT[B] = IN[B的后继1] ∪ IN[B的后继2] ∪ … ∪ IN[B的后继i] //1IN[B] = useB ∪ (OUT[B] – defB) //2}}

1. 说明一个寄存器在一个基本块的出口处活跃当且仅当它在该基本块的某个后继入口处活跃。2. 说明一个寄存器要在进入一个基本块时活跃,必须满足两个条件中的一个:要么它在基本块内被重新定值之前就被使用;要么它在基本块的出口处活跃且基本块没有对它进行重新定值。实际对代码进行分析时,要首先初始化exit块的出口处的寄存器活跃状况,使用基本块内的活跃分析计算出其入口处的活跃状况。再开始基本块间的活跃分析,迭代求出所有除exit之外的基本块的出口处的活跃状况,再使用基本块内的活跃分析其每一条指令上的活跃状况。初始化exit块的出口处的寄存器活跃状况时,可以直接初始化所有寄存器为活状态,或者取所有调用约定的保护寄存器的并集赋值为活状态,这又是一个保守与冒险的问题。附录

>>>>

本文的姊妹篇

代码混淆之我见代码混淆之我见(三)代码混淆之我见(四)>>>>

参考文献

[1]COLLBERG, THOMBORSONC, LOW D. A taxonomy of obfuscating transformations[R]. Department of computer science the university of auckland New Zealand, 1997.[2]吴伟民, 林水明, 林志毅. 一种基于混沌不透明谓词的压扁控制流算法[J]. 计算机科学, 2015, 42(5):178-182.[3]王志. 二进制代码路径混淆技术研究[D]. 南开大学.[4]王志,贾春福,刘伟杰,王晓初,张海宁,于晓旭,陈.一种抵抗符号执行的路径分支混淆技术[J].电子学报,2015,(第5期).[5]王志, 贾春福, 刘伟杰, et al. 一种抵抗符号执行的路径分支混淆技术[J]. 电子学报, 2015, 43(5):870-878.[6]陈喆, 贾春福, 宗楠, et al. 随机森林在程序分支混淆中的应用[J]. 电子学报, 2018, 46(10):156-164.[7]潘雁,祝跃飞,林伟.基于指令交换的代码混淆方法[J].软件学报,2019,(第6期).[8]数据流分析(二) https://blog.csdn.net/dashuniuniu/article/details/50709160[9]C. Wang. A security architecture for survivability mechanisms[D]. University of Virginia, 2001[10]G. Wroblewski. General method of program code obfuscation[D]. Wroclaw University of Technology, Institute of Engineering Cybernetics. 2002[11]吴适. 基于二进制代码混淆的软件保护研究[D]. 电子科技大学.[12]http://z0mbie.daemonlab.org/pcond.txt

感慨这篇文章从去年10月开始写起,修修补补弄了一年多,玩壳也断断续续地搞了一年左右,粗略计算我扔了80h进去。然而我这80h算打水漂了,真打水漂了,就给我留了一地肤浅的鸡毛。有时候想起一年前做的那些蠢事,都能把自己逗笑。不过写完这最后一个字,我就彻底和2018的我“决裂”啦!

代码混淆技术有哪些(代码混淆定义及操作)– End –

看雪ID:三十二变

https://bbs.pediy.com/user-783210.htm

推荐文章

*使用IDA Python寻找二进制漏洞

*看雪CTF从入门到存活(六)主动防御

*Web漏洞分析之zzcms多个CVE分析

*常见的WMI攻击检测方法

*Web漏洞分析之——CVE-2019-1010153

进阶安全圈,不得不读的一本书

﹀﹀﹀

发表评论

登录后才能评论