赞
踩
我们一般对反汇编代码进行还原时,默认CALL就是对一个函数的调用,碰到RET就是函数返回,条件分支两侧的代码都有可能被执行。而代码混淆就是打破了这种思维惯性,让逆向工程变得更加复杂。说到混淆,就不得不提到编译原理。编译器在把中间代码翻译为目标程序时,会先经过一个代码优化器来处理。而混淆,就是代码优化器的逆过程。
源程序 - > 前端 - > 中间代码 - > 代码优化器 - > 中间代码 - > 代码生成器 - > 目标程序
基本分类:
控制流混淆的目的是改变控制流或将程序原有控制流复杂化,使程序更难破译。通常程序中代码块都是按照逻辑顺序有序划分与组合,并且将相关的代码放在一起。在不改变程序功能的前提下,通过拆分重组程序代码等方式打破这种常规逻辑,使代码间的关系变得模糊,以此来保护程序的源码。
控制流混淆是代码混淆技术研究中最为广泛的一种,控制流混淆的方法非常多,常用的包括不透明表达式,压扁控制流,插入多余控制流等。
在混淆时,如果一个表达式的值已知,但是破解者却很难通过表达式本身推断它的值,那么它就是一个不透明表达式。最常见的不透明表达式即不透明谓词。不透明谓词是永真或永假或时真时假的布尔表达式。
图1.1 恒正或者恒假不透明谓词
图1.2 永正或者永假不透明谓词
在如下程序段中,我们构造了永假的不透明谓词(x²+x)%2,无论x为何值,此表达式结果永为false,即程序永远不会执行到a * b这一基本块
OBFWHKD算法是常用的混淆工具,利用它可以将程序中原有的嵌套循环和条件转移语句平展开。压扁控制流的通常过程有两步:
我们通过next的值对程序的正确流程进行维护,在不断更新next变量的值的过程中使程序正确执行。不过OBFWHKD压扁控制流算法的开销较高,需要更多优化。同时该算法的混淆强度也不算太高,可以选择构造不透明表达式去计算next值以加强混淆力度。
压扁控制流算法OBFWHKD是通过重新组织控制流,使静态分析工具无法构建出原有控制流。插入多余控制流算法OBFCTJbogus是通过向程序中插入多余控制流的方法来实现控制流的复杂化。这一算法的主要实现方法是分离程序中的基本块并插入不透明表达式。
图1.2 循环条件中插入不透明谓词
在循环条件P中加入不透明谓词,使程序看上去必须要在P和PT都为真的时候才能继续执行。
按照结构化的编程规则,用嵌套的判断和循环语句编程,程序的控制流图就是可归约的。这一类程序的控制流相对清晰。如果在程序中加上直接跳转到循环内部的语句,那么这个循环就会多出一个入口。这样生成的控制流图就是不可归约的例如某程序段:
构造出图2-5所示控制流图后可以很明显看出分析难度加大了。
图1.3 不可归约控制流图
另一种控制流混淆的基本方法是通过跳转函数来执行无条件转移指令。下图1.4所示即采用了这种思想实现的混淆算法OBFLDK的基本过程。
图1.4 OBFLDK算法基本过程
用函数bf实现原本无条件跳转指令jmp b的功能,用一个哈希函数计算返回值a的hash值,再用这个hash值查表T来实现跳转。这一做法可以有效对抗静态分析,但是抗动态分析能力不足。
用pyc文件可以保护python代码的想法其实是不正确的,pyc文件是可以被很容易反编译的,比如说比较著名的uncompyle6库(https://github.com/rocky/python-uncompyle6),用来反编译文件最爽不过了,几乎支持python全版本的pyc文件的反编译。
py文件编译成pyc文件可以使用 python -m xxx.py
常规pyc文件的结构如下:
magic 03f30d0a
日期 aa813e59 (Mon Jun 12 19:57:30 2017)
code 代码对象首
先pyc前四个字节是魔术字,魔术字是用来标记python版本的标识。如03f30d0a是python2.7的标识。在python2.7中,获取魔术字的方式:
import imp
magic = imp.get_magic()
print(magic)
魔术字之后四个字节是时间戳,时间戳解开的方式如下:
import time
import struct
content = open("a.pyc","rb").read()
timestamp = content[4:8]
timestamp = struct.unpack("<I"timestamp)[0]
time_str = time.strftime("%Y-%m-%d%H:%M:%S",time.localtime(timestamp))
print(time_str)
2019-02-21 10:47:59
去掉前8个字节,剩下的就是个code的对象,code对象的结构如下:
typedef struct { PyObject_HEAD int co_argcount; /* 位置参数个数 */ int co_nlocals; /* 局部变量个数 */ int co_stacksize; /* 栈大小 */ int co_flags; PyObject *co_code; /* 字节码指令序列 */ PyObject *co_consts; /* 所有常量集合 */ PyObject *co_names; /* 所有符号名称集合 */ PyObject *co_varnames; /* 局部变量名称集合 */ PyObject *co_freevars; /* 闭包用的的变量名集合 */ PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合 */ PyObject *co_filename; /* 代码所在文件名 */ PyObject *co_name; /* 模块名|函数名|类名 */ int co_firstlineno; /* 代码块在文件中的起始行号 */ PyObject *co_lnotab; /* 字节码指令和行号的对应关系 */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ } PyCodeObject;
这个code对象可以使用marshal库进行加载,加载方式如下
# 接上方代码
code_bytes = content[8:]
import marshal
co = marshal.loads(code_bytes)
print co
<code object <module> at 0x10f8f85b0, file "a.py", line 3>
加载起来之后是code对象,code对象是可以加载模块的
import imp
# 创建一个空的模块
m = imp.new_module("test module")
# 这个co对象是一个code对象,这个对象里面包含的内容是一系列的操作,
# 执行这个code对象,解释出来的变量值都会放到m对象的空间里面
exec(co,m.__dict__)
print dir(m)
['__builtins__', '__doc__', '__name__', '__package__', 'a', 'b']
这文件解释出来的变量就放到m的空间里面了,调用M对象的属性就能调用的这个py文件。
通过上面的方式,我们可以进行编译py文件,提取code对象,然后就可以实现单文件的支持库的加载了。样例:
import marshal import sys import imp code = """ a = 123 b = 456 """ c = compile(code, "<string>", "exec") m = imp.new_module("t_mod") exec (c, m.__dict__) sys.modules["t_mod"] = m import t_mod print(t_mod) print(t_mod.a) print(t_mod.b)
pyc里面有code对象,code对象中的功能部分都在code.co_code中,该属性的内容是字符串对象,实际上是一串动作的集合。
python中有一个反编译的字节码到助记符的库,叫dis,这个库的功能就和Windows中静态分析二进制的工具很像,把二进制文件转成汇编代码。在dis库的帮助文档(https://docs.python.org/2/library/dis.html)中有描述每个字节码的用途,每个字节码名字找不到的可以去python的库opcode 中看一下。
就比如NOP 在opcode中是这样添加进来的def_op(‘NOP’, 9),所以x09 就是NOP的意思。
一般的,每三个字节码或者一个字节码是一组。有些字节码是不需要参数的,比如 x00 这个表示的是停止代码,停了,不需要参数。有些字节码是需要参数的,比如 x64x00x00 加载常量表中第1个常量(常量表是元组,常量表可通过code.co_consts访问,第一个成员下标为0)。在python的字节码中有一个分水岭,就是x5a,在opcode模块中就是90,如果 opcode<90 表示无参数,反之则有参数。
有关于python的字节码都是什么意思,可以参考dis库的帮助文档,由于篇幅过长,就不在这里贴出来了。
参考作者:ChaMd5安全团队
安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/106129.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。