君は春の中にいる、かけがえのない春の中にいる.

你驻足于春色中,于那独一无二的春色之中.

Python与它的opcode

0CTF中有一道opcode修改的题,当时觉得除了暴力尝试还原外没有什么更好的思路,结果后来看到了别人的write up,还真是暴力尝试~~

0x01 PyCodeObject

Python代码在运行时,将我们写的源码转换成字节码,再由python解释器来执行字节码。而字节码就是一个PyCodeObject对象。一个相应的对象如下:文件在/include/code.h

/* Bytecode object */
typedef struct {
    PyObject_HEAD
    int co_argcount;        /* #arguments, except *args */
    int co_nlocals;        /* #local variables */
    int co_stacksize;        /* #entries needed for evaluation stack */
    int co_flags;        /* CO_..., see below */
    PyObject *co_code;        /* 编译所得的字节码指令序列 */
    PyObject *co_consts;    /* Block中所有常量的元组 */
    PyObject *co_names;        /* 所有名字的元组 */
    PyObject *co_varnames;    /* 在本代码段中赋值,但没有被内层代码引用的变量*/
    PyObject *co_freevars;    /* 在本层引用,在外层赋值的变量*/
    PyObject *co_cellvars;      /* 本层赋值,且被内层代码段引用的变量*/
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;    /* python文件路径*/
    PyObject *co_name;        /* 函数名或类名*/
    int co_firstlineno;        /* Block所在的起始行*/
    PyObject *co_lnotab;    /* 字节码指令与pyc文件中的source code对应关系*/
    void *co_zombieframe;     /* 优化(see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
} PyCodeObject;

从这个定义中,我们可以简单了解一下,该对象有哪些参数组成:

1 co_argcount,一个Code Block的参数计数。

作为萌新,这里再多提一下python中的几种参数形式,python中的参数形式可以分成位置参数和关键字参数两类,位置参数即参数所在的位置影响它被处理的逻辑,关键字参数即参数所在的位置与它的处理逻辑无关。

另一个概念是,Code Block,这个概念是说我们所写的Python代码中的每一个名字空间在编译时都会对应一个Code Block,每一个Block会创建一个PyCodeObject对象

那么这样的话,我们就可以理解这个参数做了什么,我们写一段代码来做个测试,看这样一段代码:

def co_arg(fn):
    print fn.__name__,fn.__code__.co_argcount
@co_arg
def f1():
    pass
@co_arg
def f2(a):
    pass
@co_arg
def f3(a,b):
    pass
@co_arg
def f4(a,b=1):
    pass
@co_arg
def f5(a,b,*c):
    pass
@co_arg
def f6(a,b,*c,**d):
    pass

执行结果如下:

>>f1 0
>>f2 1
>>f3 2
>>f4 2
>>f5 2
>>f6 2

OK,我们可以看到这个变量记录了函数的参数个数,但不记录tuple参数和dict参数

2 co_nlocals,一个Code Bloack的局部变量计数,包括所有参数和局部变量。

同样看如下代码:

def co_nlo(fn):
    print fn.__name__,fn.__code__.co_nlocals
@co_nlo
def f1():
    pass
@co_nlo
def f2(a):
    pass
@co_nlo
def f3(a):
    b=1
@co_nlo
def f4(a,*b,**c):
    d=1

执行结果:

>>f1 0
>>f2 1
>>f3 2
>>f4 4

3 co_stacksize,运行这段Code Block需要的栈空间

剩下的解释就直接标注在上面的type结构中。

了解过PyCodeObject后,我们就可以来看看pyc,一般情况下,翻译的字节码会写入内存中,当程序结束后,会根据程序运行的方式来决定是否将字节码写入pyc或者其他格式的存储文件中。

我们来看一下一个简单的pyc文件。

0x02 pyc

我们编译如下代码到pyc来进行学习:

def foo():
    print 1
if __name__ == '__main__':
    foo()

python -m py_compile hello.py

编译后的代码如下:

看上述pyc图,头4个字节,03f3 0d0a 是pyc的MAGIC数,用来表示python的版本。接下来的4个字节是时间。头声明完后的是一个Block的开头,即为63,后面的4个字节是PyObject中的argument数,这里为0,接着4个字节是nlocals,这里还是0。接着是4个字节的栈空间,0200 0000

接着4个字节的flags 4000 0000。flag后面就是一些具体到代码的字节码。

开始是co_code,0x73,代表类型s(string),接下来的4个字节代表长度 2300 0000
使用小端模式,表示有35个字节长度。

接下来我们向后取35个字节长度,这时的字节码就可以对应上opcode,都2个字节对应相应opcode操作,如果操作后带有值,那么就在紧跟其后的4个字节中表示。

完整的版本如下:

03f3 0d0a 版本
76e2 d458 时间
63  block
0000 0000 argument
0000 0000 nlocals
0200 0000 栈空间
4000 0000 flags
73 类型 string
2300 0000 长度 35 bytes
64 00 00 LOAD_CONST  0
84 00 00 MAKE_FUNCTION 0
5a 00 00 STORE_NAME 0
65 01 00 LOAD_NAME 1
64 01 00 LOAD_CONST 1
6b 02 00 COMPARE_OP 2
72 1f 00 POP_JUMP_IF_FALSE
65 00 00 LOAD_NAME 0
83 00 00 CALL_FUNCTION 0
01 POP_TOP
6e 00 00 JUMP_FORWARD 0
64 02 00 LOAD_CONST 2
53 RETURN_VALUE

28 (
0300 0000 

63 
0000 0000
0000 0000
0100 0000
4300 0000
73 
0900 0000 
64 01 00
47
48
64 00 00 
53

28 (
0200 0000 
4e N
69 0100 0000 1

28 (
0000 0000
28 (
0000 0000 
28 (
0000 0000
28 (
0000 0000 

73 string  co_filename
3000 0000
屏蔽敏感信息

74 t co_name
0300 0000 
666f6f  foo

0100 0000
73 
0200 0000 
00 01 
74
0800 0000
5f5f 6d61 696e 5f5f

4e N
28
0200 0000
52 R
0000 0000 
74
0800 0000
5f 5f6e 616d 655f5f

28 0000 0000 
28 0000 0000
28 0000 0000

73  co_filename
30 0000 00
屏蔽敏感信息

74  co_name
0800 0000 
3c6d 6f64 756c 653e <module>

0100 0000 

73
0400 0000
0903 0c01 lnotab

解释一下,第一个block其实可以看成是main主程序的执行,第二个block就是foo函数中执行print的逻辑。

看这两张图,在对比我上面的字节码,应该就能懂了。

block 1

block 2

而opcode对应的操作,可以查看python目录下的opcode.py

0x03 一道0ctf题

在明白了上述原理后,可以参考这篇文章

http://0x48.pw/2017/03/20/0x2f/