assembly-python

学习pyc逆向过程的一个总结

python执行过程

  1. Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行。
  2. 字节码在Python虚拟机程序里对应的是PyCodeObject对象。.pyc文件是字节码在磁盘上的表现形式。
  3. PyCodeObject对象的创建时机是模块加载的时候,即import。
  4. Python test.py会对test.py进行编译成字节码并解释执行,但是不会生成test.pyc。
  5. 如果test.py加载了其他模块,如import util,Python会对util.py进行编译成字节码,生成util.pyc,然后对字节码解释执行。
  6. 如果想生成test.pyc,我们可以使用Python内置模块py_compile来编译。
  7. 加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。

具体的pyc文件结构和PyCodeObject对应格式就不在这说了,我总结的还没网上总结的好呢。

这只说几种反编译的方法:

1.uncompyle2最好用的方法

新建一个需要反编译的py文件代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add(a):
b = 1
a += b
return a
class world:
def __init__(self):
pass
def sayHello(self):
print 'hello,world'
w = world()
w.sayHello()
if __name__=="__main__":
print add(1)

利用python -m py_compile fun.py这样就产生了pyc文件了。
再新建一个decom.py文件代码如下:

1
2
3
4
5
6
7
8
9
10
#coding:utf8
import marshal
import uncompyle2
funfile = open('fun.pyc','rb')
funfile.read(8) #先读出前8个字节
content = funfile.read()
a = marshal.loads(content)
decomfile = open('fun_decom.py','w')
x = uncompyle2.uncompyle('2.7',a,decomfile)

这里为什么要先读出前8个字节呢,根据pyc的格式决定的:

  • 因为pyc文件的最开始4个字节是一个Maigc int, 标识此pyc的版本信息, 不同的版本的 Magic 都在 Python/import.c 内定义
  • 接下来四个字节还是个int,是pyc产生的时间(1970.01.01到产生pyc时候的秒数)
  • 接下来是个序列化了的 PyCodeObject(此结构在 Include/code.h 内定义),序列化方法在 Python/marshal.c 内定义

所以这里我们只需要最后的PyCodeObject的内容,所以就先将前8个字节读出来不要了。

marshal.loads()的作用是反序列化,因为从py文件生成为pyc文件的时候,python内部会有一个序列化的步骤,所以我们需要在反编译之前先反序列化一下。

在这里marshal有两个函数可以利用,一个就是上面用到的loads函数,他接收的参数是string,也就是pyc除去前8个字节的string;另一个是load函数,他接收的参数是file,所以上面代码需要改一点如下:

1
2
3
funfile = open('fun.pyc','rb')
funfile.read(8) #先读出前8个字节
a = marshal.load(funfile)

执行完这个decom.py文件后,就会在当前目中中生成一个fun_decom.py的文件,可以拿这个文件和fun.py文件比较一下,是完全一样的。这样就反编译成功了。

2.pycdc

pycdc是github上的一个c++编写的python反编译开源项目,地址为:https://github.com/zrax/pycdc,他的介绍里面有编译过程,当编译完成后会产生两个可执行程序pycdas和pycdc

pycdc是将pyc文件直接反编译为py文件,使用方法为pycdc fun.pyc 这样反编译好的py文件会直接输出到屏幕上,如果想输出到文件中可以用linux中的重定向符”>”,具体为pycdc fun.pyc > decom.py即可

pycdas是将pyc文件反编译到字节码,输出也是默认到屏幕上。

3.Easy Python Decompiler

这个工具是windows下的python反编译工具。到处都可以下载到,他的内部其实也是利用了uncompyle2模块,然后在上面封装了一下而已。
界面如下:

这是个windows版的工具,可以反编译单个pyc,pyo 文件,或者选定反编译一个指定文件夹下面的pyc,pyo 文件。
反编译的结果的名字为原来的名字+”pyc_dic”, 用文本编辑器打开就可以看到源码

以下是我在反编译程序时遇到的问题

我遇到一个文件大小为300k左右,以前也遇到过这么大的pyc文件都是可以反编译的,但是这个pyc利用上面的Easy Python Decompiler会出错什么输出都没有;利用pycdc可以输出,但是只能输出一大部分,然后会报错;利用uncompyle2会没有反应,刚开始我以为是出错了,但是没有任何反应,只能看见内存一直在增长,以为是有内存泄露什么的,所以就给关了。

最终我抱着试一试的态度,让他一直运行,然后看着内存在疯狂的增长,cpu占用100%,这里说一下我的物理内存是16g,交换分区也是16g,等了差不多两个多小时后,物理内存全部用完,交换分区用了8g左右,然后cpu的占用率就下去了,但是内存没有下去,这样又等了半个小时以后,最终的py文件被完全反编译出来了。

然后看这个py文件,里面有一个字典,它的条目数是12000左右,所以后来分析反编译的过程,得到我的结论是pyc文件中,所有的变量、函数、类、字典类的是数据结构,字符串等都作为反编译的条目,然后这里就会有个递归的过程,学过c语言的都应该知道,如果是数量级大的递归的话需要消耗非常多的内存。这里就是这样的。所以说反编译还是最好用python的uncompyle2模块。

下面是一个解析pyc文件到xml的代码,我就是从这里面的stacksize猜测出需要递归的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import dis, marshal, struct, sys, time, types
def show_file(fname):
f = open(fname, "rb")
magic = f.read(4)
moddate = f.read(4)
modtime = time.asctime(time.localtime(struct.unpack('<l', moddate)[0]))
print "magic %s" % (magic.encode('hex'))
print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
code = marshal.load(f)
show_code(code)
def show_code(code, indent=''):
old_indent = indent
print "%s<code>" % indent
indent += ' '
print "%s<argcount> %d </argcount>" % (indent, code.co_argcount)
print "%s<nlocals> %d</nlocals>" % (indent, code.co_nlocals)
print "%s<stacksize> %d</stacksize>" % (indent, code.co_stacksize)
print "%s<flags> %04x</flags>" % (indent, code.co_flags)
show_hex("code", code.co_code, indent=indent)
print "%s<dis>" % indent
dis.disassemble(code)
print "%s</dis>" % indent
print "%s<names> %r</names>" % (indent, code.co_names)
print "%s<varnames> %r</varnames>" % (indent, code.co_varnames)
print "%s<freevars> %r</freevars>" % (indent, code.co_freevars)
print "%s<cellvars> %r</cellvars>" % (indent, code.co_cellvars)
print "%s<filename> %r</filename>" % (indent, code.co_filename)
print "%s<name> %r</name>" % (indent, code.co_name)
print "%s<firstlineno> %d</firstlineno>" % (indent, code.co_firstlineno)
print "%s<consts>" % indent
for const in code.co_consts:
if type(const) == types.CodeType:
show_code(const, indent+' ')
else:
print " %s%r" % (indent, const)
print "%s</consts>" % indent
show_hex("lnotab", code.co_lnotab, indent=indent)
print "%s</code>" % old_indent
def show_hex(label, h, indent):
h = h.encode('hex')
if len(h) < 60:
print "%s<%s> %s</%s>" % (indent, label, h,label)
else:
print "%s<%s>" % (indent, label)
for i in range(0, len(h), 60):
print "%s %s" % (indent, h[i:i+60])
print "%s</%s>" % (indent, label)
show_file(sys.argv[1])

使用方法为python showfile.py fun.pyc > fun.xml,里面可以详细的看出pyc的结构,这方面的只是就需要去深入了解pyc的文件结构了