原文点我
我的一个朋友讲,他可以直接看出一个pyc和pyo的源代码来,也就是说可以他可以作为一个人肉的python的decompiler,这项功夫兄弟羡慕得紧,想要讨教,却不知从何问起,在google上搜索,居然没什么文章介绍这方面的东西,原来有人做过这方面的工具,后来居然不发布了,靠这个作为服务来收钱了。好在最近有朋友给了我一些资料,我从本周开始了这方面的”研究”工作,有了一点点的心得,特记录在这里。这项工作我希望最后能做成一个decompiler的工具。
好了,言归正传,我们正式开练,下面我们从”hello world”开始:
Hello.py
Print “hello world”
好了,我们先编译它。
c:Python24python
Python 2.4.3 (#69, Mar 29 2006, 17:35:34) [MSC v.1310 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import compiler
>>> compiler.compileFile("c:temphello.py")
>>>
我们生成了hello.pyc,下面是文件内容:
Offset 0 1 2 3 4 5 6 7 8 9 a b c d e f
00000000 6D F2 0D 0A 36 1B 79 45 63 00 00 00 00 00 00 00 m?.6.yEc.......
00000016 00 01 00 00 00 40 00 00 00 73 09 00 00 00 64 01 .....@...s....d.
00000032 00 47 48 64 00 00 53 28 02 00 00 00 4E 73 0B 00 .GHd..S(....Ns..
00000048 00 00 68 65 6C 6C 6F 20 77 6F 72 6C 64 28 00 00 ..hello world(..
00000064 00 00 28 00 00 00 00 28 00 00 00 00 28 00 00 00 ..(....(....(...
00000080 00 73 10 00 00 00 63 3A 5C 74 65 6D 70 5C 68 65 .s....c: emphe
00000096 6C 6C 6F 2E 70 79 73 08 00 00 00 3C 6D 6F 64 75 llo.pys....<modu
00000112 6C 65 3E 01 00 00 00 74 00 00 00 00 le>....t....
好,我们开始分析这个文件的内容:
0x00 ? 0x03
6D F2 0D 0A代表的是编译器的版本,(2.4版本),是特征值
同理,其它版本的特征值如下:
99 4e 0d 0a python 1.5
Fc c4 0d 0a python 1.6
87 c6 0d 0a python 2.0
2a eb 0d 0a python 2.1
2d ed 0d 0a python 2.2
3b f2 0d 0a python 2.3
6d f2 0d 0a python 2.4
B3 f2 0d 0a python 2.5
0x04 ? 0x07
36 1b 79 45
这个文件生成的时间,是小端在前的格式。
好了,下面进入了我们的核心内容了,
0x8
63 -à ‘c’
这个代表什么?我们有必要下载一份python的源代码来看看了,在python/marshal.c中,你会看到:
#define TYPE_NULL '0'
#define TYPE_NONE 'N'
#define TYPE_FALSE 'F'
#define TYPE_TRUE 'T'
#define TYPE_STOPITER 'S'
#define TYPE_ELLIPSIS '.'
#define TYPE_INT 'i'
#define TYPE_INT64 'I'
#define TYPE_FLOAT 'f'
#define TYPE_COMPLEX 'x'
#define TYPE_LONG 'l'
#define TYPE_STRING 's' //A-1
#define TYPE_INTERNED 't'
#define TYPE_STRINGREF 'R'
#define TYPE_TUPLE '(' //A-3
#define TYPE_LIST '['
#define TYPE_DICT '{'
#define TYPE_CODE 'c' //A-2
#define TYPE_UNICODE 'u'
#define TYPE_UNKNOWN '?'
对了,这个’c’代表TYPE_CODE (A-2 ),那么我们看看这个TYPE_CODE的结构是如何的,同样,在marshal.c中我们找到:
int argcount = r_long(p); // B-1
int nlocals = r_long(p); // B-2
int stacksize = r_long(p); // B-3
int flags = r_long(p); // B - 4
PyObject *code = r_object(p); // B ? 5
PyObject *consts = r_object(p); //B-6
PyObject *names = r_object(p); //B-7
PyObject *varnames = r_object(p);
PyObject *freevars = r_object(p);
PyObject *cellvars = r_object(p);
PyObject *filename = r_object(p);
PyObject *name = r_object(p);
int firstlineno = r_long(p);
PyObject *lnotab = r_object(p);
那么,我们知道了,
0x9 ? 0xc 字节: 00 00 00 00是argcount (B-1),
0xd-0x10 : 00 00 00 00是nlocals,(B-2)
0x11 ? 0x14 : 01 00 00 00 是 stacksize,(B-3)
0x15 ? 0x18 : 40 00 00 00 是 flags, (B-4)
下面又是一个object, 0x73 = ‘s’ ,是另一个标志符了,看看上面,你就知道这代表TYPE_STRING(A-1),我们来看看TYPE_STRING是如何处理的:
case TYPE_STRING:
n = r_long(p); //C-1
if (n < 0) {
PyErr_SetString(PyExc_ValueError, "bad marshal data");
return NULL;
}
v = PyString_FromStringAndSize((char *)NULL, n);
if (v != NULL) {
if (r_string(PyString_AS_STRING(v), (int)n, p) != n) {
Py_DECREF(v);
v = NULL;
PyErr_SetString(PyExc_EOFError,
"EOF read where object expected");
}
}
if (type == TYPE_INTERNED) {
PyString_InternInPlace(&v);
PyList_Append(p->strings, v);
}
return v;
看到了吧?0x1a - 0x 1d 09 00 00 00是代表string的长度(C-1),
0x1e ? 0x26 64 01 00 47 48 64 00 00 53 是 code的 内容,
从B-6可以知道,下面我们要读入consts了,这个标志符是0x28 = ‘(‘,
代表他是一个TYPE_TUPLE(A-3),我们来看看如何处理这个TYPE_TUPLE,
n = r_long(p); //D-1
if (n < 0) {
PyErr_SetString(PyExc_ValueError, "bad marshal data");
return NULL;
}
v = PyTuple_New((int)n);
if (v == NULL)
return v;
for (i = 0; i < n; i++) {
v2 = r_object(p); //D-2
if ( v2 == NULL ) {
if (!PyErr_Occurred())
PyErr_SetString(PyExc_TypeError,
"NULL object in marshal data");
Py_DECREF(v);
v = NULL;
break;
}
PyTuple_SET_ITEM(v, (int)i, v2);
}
return v;
从D-1,D-2我们知道这个tuple有两个元素,第一个的标志符是0x4e, Type_None,
第二个是0x73 = ‘s’ ,字符串型,长度为0x0b,内容就是”hello world”,
下面我们从B-7知道应该是names了,是tuple 类型,长度为0,
同理,varnames = freevars = cellvars = (),
Filename = “c:temphello.py”
Name = “<module>”
Firstlineno = 1
Lnotab的标志符为0x74, TYPE_INTERNED,这种类型的跟TYPE_STRING是一样处理的。
好了这整个文件我们都过了一遍了,很显然接下来要把重点放到code的内容上来,这个内容一共9个字符:64 01 00 47 48 64 00 00 53 , 什么意思???
请先阅读这段东西:
http://www.python.org/workshops/1998-11/proceedings/papers/aycock-211/aycock211.html
显然这段东西应该是opcode 加上操作数,opcode的定义在哪里???
找啊找,我们找到了,在/include下面有opcode.h的说明,我们对着说明来看看,
#define LOAD_CONST 100 /* Index in const list */
第一个是0x64 = 100 ,100对应得是LOAD_CONST ,后面有注明是index in const list,后面
那个参数是00 01 (小端在前) ,我们看看前面的consts的第二个内容是”hello world”,对不对?
所以这个就是 64 01 00 == LOAD_CONST “ hello world”
第二个是0x47 = 71, 没参数的, #define PRINT_ITEM 71 , 没错,就是它 ,
第三个是0x48 = 72 没参数的, #define PRINT_NEWLINE 72 没错,就是它
第4个又是0x64 = 100,那个参数是00 00 (小端在前) ,我们看看前面的consts的第1个内容是None,对不对?
第5个是0x53 = 83,#define RETURN_VALUE 83 , 哈哈,全对,我们没有指定返回值, 翻译成python的汇编就是:
LOAD_CONST “ hello world”
PRINT_ITEM
PRINT_NEWLINE
LOAD_CONST None
RETURN_VALUE
好,我们终于实现了pyc 和 pyo 的disassemble, 但离我们的decompiler好像还有很长的距离要走,没关系,慢慢来
(待序)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课