前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >简单入门python字节码混淆

简单入门python字节码混淆

作者头像
ChaMd5安全团队
发布2019-05-07 14:57:15
2.3K1
发布2019-05-07 14:57:15
举报
文章被收录于专栏:ChaMd5安全团队ChaMd5安全团队

前言

我就是小菜鸡本鸡了,不是很会写东西,请各位大佬多多见谅。本文基于python2.7,因为python3并不是很懂。

python文件如果要发布的话,有时候还是难免想保护一下自己的源码,有些人就直接编译成了pyc文件,因为这样既可以保留跨平台的特性,又可以不能直接看到代码,也看到网上很多人说为了保护自己的代码可以编译成pyc文件。

用pyc文件可以保护python代码的想法其实是不正确的,pyc文件是可以被很容易反编译的,比如说比较著名的uncompyle6库(https://github.com/rocky/python-uncompyle6),用来反编译文件最爽不过了,几乎支持python全版本的pyc文件的反编译。

pyc文件结构

py文件编译成pyc文件可以使用 python -m xxx.py

常规pyc文件的结构如下:

代码语言:javascript
复制
magic 03f30d0a
日期 aa813e59 (Mon Jun 12 19:57:30 2017)
code 代码对象

首先pyc前四个字节是魔术字,魔术字是用来标记python版本的标识。如03f30d0a是python2.7的标识。

在python2.7中,获取魔术字的方式:

代码语言:javascript
复制
import imp
magic = imp.get_magic()
print(magic)

魔术字之后四个字节是时间戳,时间戳解开的方式如下:

代码语言:javascript
复制
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对象的结构如下:

代码语言:javascript
复制
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库进行加载,加载方式如下

代码语言:javascript
复制
# 接上方代码
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对象是可以加载成模块的。

代码语言:javascript
复制
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']

示例的a.py里面没什么东西:

代码语言:javascript
复制
a = 123
b = 456

这文件解释出来的变量就放到m的空间里面了,调用M对象的属性就能调用的这个py文件。

通过上面的方式,我们可以进行编译py文件,提取code对象,然后就可以实现单文件的支持库的加载了。样例:

代码语言:javascript
复制
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)

输出结果:

代码语言:javascript
复制
<module 't_mod' (built-in)>
123
456

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 这个表示的是停止代码,停了,不需要参数。有些字节码是需要参数的,比如 \x64\x00\x00 加载常量表中第1个常量(常量表是元组,常量表可通过code.co_consts访问,第一个成员下标为0)。在python的字节码中有一个分水岭,就是\x5a,在opcode模块中就是90,如果 opcode<90 表示无参数,反之则有参数。

有关于python的字节码都是什么意思,可以参考dis库的帮助文档,由于篇幅过长,就不在这里贴出来了。

py代码混淆

py代码的混淆就是针对写出来的py代码里面插入一些无意义的分之跳转,将原本的有意义的变量名字,改成无意义的名字,为了加大难度,有时候还会将变量名改成类似的名称。

比如iIl1iI1liI1liIl1,就是类似这种相近的名字,在手工进行解密,还是非常头疼的,原本的代码也不清楚什么意图,通过名字也根本猜不出来。

py代码混淆的结果,主要体现在那些只能读懂代码,这种人基本都能拦住一些,但是如果是python大佬级别的,找个IDE,重构一下名字,就很快能看懂什么意思了。

推荐Python source code obfuscator:

https://github.com/astrand/pyobfuscate

对字节码的混淆

先举个例子:

代码语言:javascript
复制
if 1+1 == 2:
    print "hello"
else:
    print "world"

反编译结果是这样子的:

代码语言:javascript
复制
64 05 00 // LOAD_CONST 5 (2)
64 01 00 // LOAD_CONST 1 (2)
6B 02 00 // COMPARE_OP 2 (==)
72 14 00 // POP_JUMP_IF_FALSE 20

64 02 00 // LOAD_CONST 2 ("hello")
47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
6E 05 00 // JUMP_FORWARD 5 (to 25)

64 03 00 // LOAD_CONST 3 ('world')
47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
64 04 00 // LOAD_CONST 4 (None)
53 -- -- // RETURN_VALUE

这个反编译出来的助记符还是比较明白的,大概可以按照这个结构写出来原本的代码的形式。

如果我们想要在其中插入一条指令(插入一条指令就是为了增大难度,同时可能会让反编译工具报错),怎么办呢?

假如说我们插入这样一条混淆指令:

代码语言:javascript
复制
71 06 00 // JUMP_ABSOLUTE 6
64 ff ff // LOAD_CONST    65535

上面这段混淆的指令如果是使用uncompyle的情况下,肯定是反编译失败的,因为插入的第二条指令加载了一个不存在的常量,导致下标超出,引发异常。

如果是使用dis库直接反编译code对象,也是反编译失败的,原因是dis库在反编译code对象的时候会尝试去常量表里面把变量取出来打印。但是可以dis库的进行反编译 code.co_code 属性,就可以反编译成功,原因是这个对象为纯粹的字节码,并不会包含常量表,所以不会出现下标异常。

那么接下来我们把上面这段代码放到我们原本的代码中,挑一个比较典型的位置。

代码语言:javascript
复制
64 05 00 // LOAD_CONST 5 (2)
64 01 00 // LOAD_CONST 1 (2)
6B 02 00 // COMPARE_OP 2 (==)
72 14 00 // POP_JUMP_IF_FALSE 20

// 插入的段
71 12 00 // JUMP_ABSOLUTE 18 
64 ff ff // LOAD_CONST    65535

64 02 00 // LOAD_CONST 2 ("hello")
47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
6E 05 00 // JUMP_FORWARD 5 (to 25)

64 03 00 // LOAD_CONST 3 ('world')
47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
64 04 00 // LOAD_CONST 4 (None)
53 -- -- // RETURN_VALUE

如果我们把它放到这个位置,这个第二段的位置,因为我们使用的是一个绝对跳转的指令,所以我们需要查出来jump需要跳到哪里合适,可以跳过我们的异常代码部分。计算出来的是如果我们要跳到原本第二段的位置就需要跳到18的位置(从第一个字节查下标)

当我们插入进去后,发现又出问题了,我们需要考虑第一段中最后一个跳转语句,它原本是要跳到20的,但是我们在它之后又插入了6字节长度的字节码,所以导致它跳转的位置是有问题的,我们需要修复这个跳转问题,这个修复的逻辑我觉得是这样的,如果插入的位置,在影响范围内,就对跳转地址进行更改,增加插入字节的长度。

经过我长(san)久(tian)的研究,发现python字节码混淆主要的成功和失败原因都在跳来跳去。先说个坑,以前的时候我手动改的时候,改完之后发现如果只是修改不插入进去,字节码就能运行,如果长度有变化,字节码就坏了,后来发现前面有长度的计算的。

代码语言:javascript
复制
64 05 00 // LOAD_CONST 5 (2)
64 01 00 // LOAD_CONST 1 (2)
6B 02 00 // COMPARE_OP 2 (==)
72 1a 00 // POP_JUMP_IF_FALSE 26 !需要修复的位置

// 插入的段
71 12 00 // JUMP_ABSOLUTE 18 
64 ff ff // LOAD_CONST    65535

47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
6E 05 00 // JUMP_FORWARD 5 (to 25)

64 03 00 // LOAD_CONST 3 ('world')
47 -- -- // PRINT_ITEM
48 -- -- // PRINT_NEWLINE
64 04 00 // LOAD_CONST 4 (None)
53 -- -- // RETURN_VALUE

修复以后就是这样的样子了,因为这里面只存在一个跳转语句,也就是说我们插入数据只能影响到一条跳转的执行。

重叠指令

这是在大佬的博客(https://blog.csdn.net/ir0nf1st/article/details/61650984)看到的东西:

代码语言:javascript
复制
# 例1 Python单重叠指令
0 JUMP_ABSOLUTE        [71 05 00]     5 
3 PRINT_ITEM           [47 -- --]
4 LOAD_CONST           [64 64 01]     356
7 STOP_CODE            [00 -- --]

# 例1 实际执行
0 JUMP_ABSOLUTE        [71 05 00]     5 
5 LOAD_CONST           [64 01 00]     1

这个例子里面就是第一句的JUMP_ABSOLUTE跳到了一个很神奇的地方,如果按照dis库解释字节码的操作来看,就是会出问题的,因为第一行跳到5的位置刚好是第三行,LOAD_CONST的参数地址的位置,但是这个参数地址的位置和后面又拼接完整了一条指令,所以在dis解释的时候因为字节码重叠的问题导致输出有问题,我觉得这个还可以改一下,改成以下形式:

代码语言:javascript
复制
71 04 00 // JUMP_ABSOLUTE 4
64 71 09 // LOAD_CONST 2417
00 -- -- // STOP_CODE
64 ff ?? // LOAD_CONST xxxx

最后一条指令故意空缺了一个字节的位置,这个字节是故意为了拼接原来的字节码,如果运气好的话,原来的字节码会有一部分会变成看起来是乱数据的字节码。

就比如在后面插入一条三个字节码的指令,我们插入LOAD_CONST 1,对应的字节码为64 01 00,放到这个后面就会出现:

代码语言:javascript
复制
71 04 00 // JUMP_ABSOLUTE 4
64 71 09 // LOAD_CONST 2417
00 -- -- // STOP_CODE
64 ff 64 // LOAD_CONST 25855
01 -- -- // POP_TOP
00 -- -- // STOP_CODE

就会有如上的效果,单纯的看dis反编译的结果就会发现是乱序的,当然这些只是对工具有一些作用,对人工的对抗还是不太好。

基础代码的混淆知识讲解到此结束。

接下来写一写如何把自己的想法转变成代码,自己写过很多次混淆的工具,写是写,但是总是会出各种奇怪的bug。问题的根源就是跳转的位置计算有问题,每次都想插入不同的payload,还想换着法的插入,查来查去就容易算错。其他方面也有一些问题,就是插入的位置不对,有些字节码前面不能插入东西,插进去就报错。

总结了一下就是下面几个opcode

代码语言:javascript
复制
71  # 跳转绝对位置
64  # 加载常量
6c  # 导入支持库
65  # 通过变量名加载内容
84  # 创建函数
72  # 如果上方表达式不成立跳转到
5b  # DELETE_NAME
48  # 换行
6e  # 跳出循环

在这些字节码前面插入东西是没什么问题的,可能还有其他的字节码前面插入东西也没什么问题,但是懒得找了,只是找一些常用指令,问题不太大就行。如果要混淆,首先插入的位置一定要是上一条指令的结束,下一指令开始之前。还要随机选择出来一段是以上面开头的opcode,所以我选择的方式是,按着字节码的结构解析出来指令的表,解析过程中,按照上面的表,分成列表。

这一段已经写成了代码,会贴在最后面。

一些其他的想法

你手里面有一段代码,然后把它混淆一下内容,获取到字节码序列化对象(利用marshal.dumps),获取到后,对字节码进行压缩,为了防止好辨认,再反转一下。

首先有个大家需要先知道的前提,就是python里面的函数,字符串什么的是放到常量表中的,并不是在代码段,我做的就是想把数据放到代码段,然后外面套个简单的壳子,写点伪代码描述一下:

代码语言:javascript
复制
t_code = "
import zlib
import marshal
def add(a,b):
    return a+b
print "Run Func 'add' func_code"
exec (marshal.loads(zlib.decompress(add.func_code.co_code[::-1])))
"

t_insert_code = "
print("xdcfgvbhjnkjbhvgcf")
print("这里是测试要加密的代码")
"

t_code.co_consts[add] = zlib.compress(marshal.dumps(t_insert_code))[::-1]

差不多就是上面这个样子,第一段代码中的add函数就是个壳子,为的是让代码中可以不通过常量表访问数据,虽然这个add函数对象也在常量表中放着,但是我们对内容进行了压缩和反转,就算拿到了这个函数,打印出来也是下面这样子的:

代码语言:javascript
复制
<code object add at 0x10b8624b0, file "code_replace.py", line 8>

这样一段东西,打印出来还是个有名字的code,我觉得怀疑的几率不是很高吧~

结尾

感谢各位大佬看我废话这么久,最后还是要说一,即使已经做了这么多工,你的代码还是完全有可能被逆向,暂时还没有完美的防止逆向的方案,起码我现在还没想到,虽然现在我们可以进行简单的混淆欺骗一下机器,但是如果想欺骗人就很难了,因为代码是死的。

我们可能实际上需要的并不是为了防止反编译,我们实际上需要的是保护自己的代码,增加对抗的成本,让对方反编译你的代码感觉到头疼,并且付出大量的时间和精力。假如说你的代码里面插入了10个混淆指令,可能对方稍微费些心思就会完成逆向,如果你的字节码里面插入了和正常代码等比例的混淆代码呢?如果是两倍三倍的垃圾数据呢?这个过程需要耗费的精力就很大了,可能对方就对解密你的代码要放弃了。

对于混淆推荐的套路是,先代码混淆,然后字节码混淆一下,这样出来的代码,恐怕是少有人能看懂了,看得懂也要付出很大的精力才能进行还原。

最后贴一下最近两天才拼了个混淆的渣渣库:

https://github.com/c0cc/code_obfuscate

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ChaMd5安全团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • pyc文件结构
  • py代码混淆
  • 对字节码的混淆
  • 重叠指令
  • 一些其他的想法
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档