最近做了一道pyc的逆向题,主要难点在于Python环境的opcode被置换,就简单记录一下相关知识。
opcode其实是指Python源码的操作码,Python源代码*.py编译后可以得到二进制文件*.pyc,*.pyc文件中就含有opcode序列。对于不同版本的Python,其opcode是不完全相同的,这也就是为什么某一版本的Python虚拟机不能执行另一个版本的源码。
查看当前版本opcode的方式有很多种,最简单的是直接导入opcode库查看:
import opcode
for key in opcode.opmap.keys():
print (key, opcode.opmap[key])
或者在/Python/Include/opcode.h文件中查看。
一个简单源码的opcode查看如下:
import dis
def foo():
x = 20
y = 10
z = x - y
return z
dis.dis(foo)
输出如下:
3 0 LOAD_CONST 1 (20)
2 STORE_FAST 0 (x)
4 4 LOAD_CONST 2 (10)
6 STORE_FAST 1 (y)
5 8 LOAD_FAST 0 (x)
10 LOAD_FAST 1 (y)
12 BINARY_SUBTRACT
14 STORE_FAST 2 (z)
6 16 LOAD_FAST 2 (z)
18 RETURN_VALUE
或者可以在命令行运行 python -m py_compile filename.pyc,然后利用pycdas等软件去反编译至opcode。
一些逆向题会构建一个修改的Python环境,从而使对应的*.pyc文件只能被本地识别并执行,而不能在别的计算机执行。
参考这篇文章:置换CPython 2.7.13的opcode
那如何构建这样的环境呢,一般是通过源码安装Python的方式实现,在安装前更改opcode的相关文件。对于2.7.13版本以下,似乎只需要更改/Include/opcode.h,/Lib/opcode.py两个文件,而2.7.14及以上版本则还需要修改/Python/opcode_targets.h文件。本文的例子是针对2.7.16的。
构建三个补丁文件(这里以加减法交换为例)如下,分别命名为re1.patch,re2.patch,re3.patch。
--- Include/opcode.h
+++ Include/opcode.h
@@ -30,2 +30,2 @@
-#define BINARY_ADD 23
-#define BINARY_SUBTRACT 24
+#define BINARY_ADD 24
+#define BINARY_SUBTRACT 23
--- Lib/opcode.py
+++ Lib/opcode.py
@@ -65,2 +65,2 @@
-def_op('BINARY_ADD', 23)
-def_op('BINARY_SUBTRACT', 24)
+def_op('BINARY_ADD', 24)
+def_op('BINARY_SUBTRACT', 23)
--- Python/opcode_targets.h
+++ Python/opcode_targets.h
@@ -25,2 +25,2 @@
- &&TARGET_BINARY_ADD,
- &&TARGET_BINARY_SUBTRACT,
+ &&TARGET_BINARY_SUBTRACT,
+ &&TARGET_BINARY_ADD,
下载Python2.7.16的源码:https://www.python.org/ftp/python/2.7.16/Python-2.7.16.tar.xz
运行以下命令:
~$ xz -d Python-2.7.16.tar.xz
~$ tar -xvf Python-2.7.16.tar
~$ cd Python-2.7.16
~/Python-2.7.16$ cp ~/re1.patch re1.patch
~/Python-2.7.16$ cp ~/re2.patch re2.patch
~/Python-2.7.16$ cp ~/re3.patch re3.patch
~/Python-2.7.16$ patch -p0 < re1.patch
~/Python-2.7.16$ patch -p0 < re2.patch
~/Python-2.7.16$ patch -p0 < re3.patch
~/Python-2.7.16$ ./configure --prefix=/usr/local/python
~/Python-2.7.16$ sudo make & make install
此时Python环境编译成功,可以用下面的小例子测试一下:
x = 10
y = input("number:")
print x-y
然后将源码编译为pyc文件:python -m py_compile main.py,分别在不同平台上运行main.pyc如下:
在windows下使用uncompyle6尝试对main.pyc反编译:
D:\>uncompyle6 main.pyc
# uncompyle6 version 3.2.6
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 4 2019, 01:37:19) [MSC v.1500 64 bit (AMD64)]
# Embedded file name: main.py
# Compiled at: 2019-06-20 14:51:58
x = 10
y = input('number:')
print x + y
# okay decompiling main.pyc
我们发现,加减法已经被置换了。
简单置换也可以置换多个opcode,需要注意的是上面三个文件中opcode的顺序必须是一致的,否则make将会失败。
对于普通*.pyc文件的反编译,uncompyle6已经是非常好的工具了,但其显然是针对官方版本的opcode进行识别并反编译,对于修改过后的文件,如果修改处较多,我们逐字节更改就比较困难。推荐一款工具:Decompyle++,下载后可以更改默认的字节码map文件,本例中可以修改/pycdc/bytes/python_27.map,将加减法互换,然后make,官方推荐了cmake。
- Generate a project or makefile with CMake (See CMake's documentation for details)
- Build the generated project or makefile
- For projects (e.g. MSVC), open the generated project file and build it
- For makefiles, just run make
- To run tests (on *nix or MSYS), run make test
使用编译好的pycdc就可以反编译到正确的源码了。
其实我们一般做题很难事先知道正确的opcode顺序是什么,主要还是得通过一次反编译之后的源码,对照其中异常的语句猜测正确的opcode值。