把上个博客的坑给填了。讲讲混淆与python内核调试。
Pyc字节码分析 3
Pyc字节码混淆
示例代码
还是老脚本
1 | # test.py |
1 | 42 0d 0d 0a // 版本魔数 |
想法
直接patch脚本,修改PyCodeObject
对象中co_code
的size
和具体内容,来实现魔改。
一(尝试,无混淆)
魔改func
的co_code
,使得print(a,b)
能够执行两次
原来
1 | 2].co_code) dis.dis(b.co_consts[ |
现在
1 | 2].co_code) dis.dis(b.co_consts[ |
很简单其实就是复制了一番。
现在体现在二进制字节码上:
原来
1 | b'd\x01}\x03t\x00|\x00|\x01\x83\x02\x01\x00t\x00|\x02\x83\x01\x01\x00d\x00S\x00' |
现在
1 | b'd\x01}\x03t\x00|\x00|\x01\x83\x02\x01\x00t\x00|\x00|\x01\x83\x02\x01\x00t\x00|\x02\x83\x01\x01\x00d\x00S\x00' |
就是复制了里面的一小段。
然后patch到具体文件上。
同时修改size
,要加10。
导入模块,检验:
1 | >>> import rhstestobfus |
成功了。
二——基于不可到达分支
增添JUMP_FORWARD_A
指令,使得能够向后跳转,从而实现忽略几个字节。
1 | 110 JUMP_FARWARD_A |
所以构造
1 | b'\x6e\x0e' |
即
1 | JUMP_FORWARD 14 |
算上自身2字节,即一共跳了16字节。
在co_code
代码段内任意一个地方塞上这个,然后再添加14字节的随机数据
且记得将size
加16。
导入后和以前一样,不影响逻辑且能正常使用。
反编译/反汇编测试
pydisasm/pycdas
1 | 5: 0 LOAD_CONST (114514) |
一般的反汇编器由于是线性扫描的,2字节2字节的,所以我们这样正好凑偶数地址偶数跳转,就能够正常反汇编。
垃圾数据没识别出来的,直接显示成<invalid>
,不会导致完全失败。
pycdc
反汇编直接报错。说明反汇编器对于pyc代码的纯净度要求特别高。
可以算作是50%的成功,但是确实没能对付好反汇编器的线性扫描。
三——基于奇数地址跳转
一开始的设想是跳转到奇数地址。
我构造的是
1 | JUMP_FORWARD 13 |
那么包括自己2个字节,就会跳到+15的位置执行。
但是按照这样的思路是错的,因为Python3解释器是默认偶数地址执行,所以实际上地址会向前取偶,即从+14开始执行。
这样实现,还是能保证14-2-2=10个字节可以用于垃圾数据,但是后续的指令仍然能够很正常完美的被反汇编器打印出来。
不过用settrace
找出这个特性已经很不错了,我愿意称作半个成功。
四——基于溢出
py27的实现方式
Py27版本的时候,有参汇编指令是3字节的,2个参数字节,所以最大能到65535。有的混淆脚本就是基于这个实现的。
比如
1 | LOAD_CONST [64 FF FF] 65535 |
反汇编器去根据索引乖乖的找数据,结果就会溢出而报错。
py3的尝试
应该也是可以实现的,只不过最大那就只能有255了,因为只有1字节参数。
构造一个
1 | JUMP_FORWARD 2 |
试试,即
1 | 6E0264FF |
pydisasm测试
python原生反汇编器会炸,因为它不检查越界问题
pycdas测试
不会炸,而是会直接显示<INVALID>
,说明已经考虑到越界问题了。
pycdc/uncompyle6测试
会炸。经典的tuple out of range
五——虚假分支(存根,未实现)
通过字节码混淆来保护Python代码_Keep going-CSDN博客_python 项目代码混淆
比较经典的通过异常处理来实现的路径混淆。
1 | #flag可以是一些计算的结果 |
1 | try: |
1 | try: |
1 | try: |
上面是源码实现。可以考虑编译成字节码后进行注入。
六——重叠指令(大概率在py3以上无法实现)
重叠指令在有变长指令的CISC机器(比如x86)上有广泛应用。
python27是变长的,所以似乎解释器也是会灵活变长的,从而有了这种重叠指令的操作。python3定长,且似乎强制偶数地址,似乎这种套路都不行了
尝试了构造这种字节码
1 | 6E 01 | __ 6E | 01 __ |
但是没有成功。
后注:偶地址对齐的缘故,这个操作大概是在py3以上完全无法实现。
七——预加密
测试代码
1 | // hello.py |
编译成hello.pyc
加密
1 | import marshal, zlib, base64 |
执行脚本
1 | import marshal, zlib, base64 |
奇思妙想
这样通过抽取代码直接执行,就跟使用Unicorn
来模拟执行代码片段很像。于是乎我们完全可以在此基础上实现动态注入代码,动态修改代码,边解密边执行,然后重新加密等灵活操作。
总结
类RISC指令集的混淆确实不轻松。
调试
谈谈Python内核调试+settrace
模块级调试
pdb
就不管了,大抵是源码级别的调试器。
PVM调试
环境
1 | python3.7.9 AMD64 |
编译
打开Visual Studio
,找到PCbuild
文件夹下的.sln
项目工程文件。设置下位数为x64
,即可生成。
点击调试,便会弹出交互界面python_d.exe
开始调试。
下断点,找到相应c文件下就好了。
DEBUG编译
直接在ceval.c
顶部添加
1 |
即可。
原来一开始准备在整个项目上开启调试的,通过在pyconfig.h
顶部设置
1 | #define Py_DEBUG |
但是似乎编译报错了。只能放弃。
settrace
1 | import sys |
settrace
只会监视特定动作。
- call: 函数调用
- return: 函数返回
- line: 一行新代码
- exception: 异常事件
局限性较大,且有方法能够检测Python文件正在被调试。
1 | sys.gettrace() |
能够返回现在程序中的trace
函数,从而判断是否在被调试。
LLTRACE
这是一个内核级调试方法。
开启方法见上文。
开启
1 | __ltrace__ = 1 |
注意是一个
l
。而不是两个l
测试
1 | .\python_d.exe |
实现原理
如果定义了LLTRACE
,则会开启以下代码。
1 | // 定义了一个prtrace函数 |
以上会打印诸如
push <built-in function print>
这样的字符串。
下面的则会打印opcode
1 |
|
诸如
2: 100, 0
这类。
现在能够打印字节码了。但是还不够完美。
魔改
LLTRACE
有以下问题:
- 需要程序内部明确定义
__ltrace__
全局变量才会开启。 - 硬核打印字节码,不像
dis
模块那样清晰。 - 打印的是动态执行流,会因为循环展开等影响分析。
其中第三点并不好处理,这是动态跟踪的通病。不过其实影响并不是很大,动态CFG还是具有可读性的。
关于第一第二点,可以通过对源码进行修改来实现。
1
判断lltrace
的存在,是通过
1 |
|
实现的。
其中f
是个PyFrameObject
对象。这里它检查了栈帧对象内的全局变量字典中有没有__ltrace__
存在。
修改它为
1 |
|
让它寻找其上一级的调用栈帧中的全局变量或者此栈帧的全局变量。
或者直接强行设定为1也行。
2
LLTRACE
开启后会打印部分底层汇编和字节码代码。底层汇编打印的是PUSH
,POP
,STACKADJ
,EXT_POP
这几个比Python汇编指令还要底层的指令。且数量有限,不完全,SET_LOCAL
,FAST_DISPATCH
这类都无法打印。
所以这个实际上不一定能提供多少帮助,所以我们还是主要基于魔改
1 |
|
这打印字节码的地方,实现一个dis
函数。
至于如何实现,源码下面就有个核心的switch-case
虚拟机执行函数可以提供参考。
魔改后:
1 |
|
在这里面又嵌入了一个switch-case
,用于打印字节码,模拟dis
。
1 | def func(a): |
得到
1 | .\python_d.exe .\test.py |
格式化上还是有点问题,但是已经很OK了。
dis.dis()
源码
和python27
相比有了更多东西,但是核心代码还是没变。
1 | def dis(x=None, *, file=None, depth=None): |
Py源码混淆
源码混淆常用于解释性语言脚本中。
见到的混淆器
pyminifier
Python Source Obfuscation using ASTs | Development & Security (jbremer.org)
这篇文章讲的大概是如何基于AST树来进行混淆。涉及到编译原理知识,比较高级。
混淆前(举例)
1 | class SampleClass: |
混淆后
1 | class N: |
Pyc各版本结构体比较
待解决
如何发现在被sys.settrace
调试√
如何完全定制Python解释器实现一体化混淆
总结
Python字节码混淆可能对于Py27效果更好一些。
参考
逆python–pyc文件结构及pyc混淆基础_招财猫的小叮当的博客-CSDN博客
如何破解一个Python虚拟机壳并拿走12300元ETH - evilpan
通过字节码混淆来保护Python代码_Keep going-CSDN博客_python 项目代码混淆
如何编译和调试Python内核源码? - shine-lee - 博客园 (cnblogs.com)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2022/02/16/Pyc Reverse 3/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!