Python Opcode逃逸笔记
不同版本python的PyCodeObject参数数量有一定差异,但大同小异,本文在Python3.8环境下进行探索
PyFrameObject
在Python里,一切皆对象,函数也不例外
Python虚拟机的执行环境基于PyFrameObject栈帧,一个线程有一个栈帧链,在栈帧环境中根据执行PyCodeObject对象,从中取出对应的字节码序列在执行机中执行。
栈帧构造如下
struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
/* Next free slot in f_valuestack. Frame creation sets to f_valuestack.
Frame evaluation usually NULLs it, but a frame that yields sets it
to the current stack top. */
PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */
char f_trace_lines; /* Emit per-line trace events? */
char f_trace_opcodes; /* Emit per-opcode trace events? */
/* Borrowed reference to a generator, or NULL */
PyObject *f_gen;
int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field
directly. As of 2.3 f_lineno is only valid when tracing is
active (i.e. when f_trace is set). At other times we use
PyCode_Addr2Line to calculate the line from the current
bytecode index. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
};PyCodeObject
先来看看python中CODE对象的构造
(不同版本python有一定区别,新版本加入了几个新的强制参数)
统计必要参数数量,这个地方不同版本的python会有所差异。
去看下CPython中相关的实现
其中有关PyCode对象的新建
据此构造在python中利用类型方法构造出一个对应的__code__对象改写原函数对象的逻辑
整理一下这些参数的作用文档
看文档不如看源码
整理出表格
属性
描述
co_argcount
位置参数总数(包括仅位置参数和具有默认值的参数)
co_posonlyargcount
仅位置参数(包括具有默认值的参数)的数量
co_kwonlyargcount
仅关键字参数(包括具有默认值的参数)的数量
co_nlocals
函数使用的局部变量的数量(包括参数)
co_stacksize
所需的堆栈大小
co_flags
存放着函数的组合布尔标志位(Code Objects Bit Flags)
co_code
二进制格式的字节码(Bytecode Instructions)
co_consts
常量列表
co_names
字符串列表
co_varnames
包含局部变量名称的元组(以参数名称开头)
co_filename
代码文件名称
co_name
函数名称
co_firstlineno
函数的第一行号
co_lnotab
编码从字节码偏移量到行号的映射
co_freevars*
包含自由变量名称的元组
co_cellvars*
包含嵌套函数引用的局部变量的名称
保证参数对接能够一致不出错的前提下,可以自由修改这些参数
接下来演示另一个具有freevars的样例
构造__code__对象
可以看到这里通过覆盖修改原函数的__code__对象使其成功输出了原函数域内的变量flag,此外,这里由于只接受一次输入,把这个过程压缩在一行里,也可以用一个while循环达成无限次数的输入,但有些时候也可能并没有这样一个继续交互的机会
实际上利用__code__对象完全可以执行任意操作码(opcode) ,比如我们构造一个通过os模块getshell的对象
输入上面的样例就会运行shell,当然,os和system肯定是被过滤的关键词,但这里字符串形式的关键词想必大家也有各种编码和拼接的办法来绕过过滤的
当然,这样还是依赖os模块
不过由上面的例子我们可以理解,通过构造__code__,我们完全可以按自己的想法执行任意的opcode而没什么约束,从而通过opcode达成外部任意读写从而为所欲为
Opcode操作码
python的文本源码经过ast分析与有限的优化后转化成最终的字节码,pyc文件
字节流形式的执行码称之为字节码,而每个字节码有个对应的可理解的符号形式,称其为opcode操作码,通过opcode库我们可以从opcode得到bytecode,而通过dis库,可以将byetecode转化成opcode的可阅读形式
二者关系有如汇编与二进制
由于opcode有一百多个,直接贴 文档,一些常用的操作有
读写指令
指令名
操作
LOAD_GLOBAL
从co_names[namei]入栈
STORE_GLOBAL
出栈到co_names[namei]
LOAD_FAST
从co_varnames[var_num]入栈
STORE_FAST
出栈到co_varnames[var_num]
LOAD_CONST
从co_consts[consti]入栈
控制指令
指令名
操作
CALL_FUNCTION
函数调用,弹出所需参数,新栈帧,返回值压栈
RETURN_VALUE
函数返回,退出栈帧
POP_JUMP_IF_FALSE
当条件为假的时候跳转
JUMP_FORWARD
直接跳转
布尔运算
COMPARE_OP
不同的操作码对应了不同的操作,在Python/ceval.c中的switch里定义了对应的一系列虚拟机操作,构成了Python虚拟机的执行核心。
而具体的opcode在 opcode.h 进行了定义,可以在其中看到所有的opcode,修改调换其中字节码的值再编译即可达成混淆字节码的目的,当然,直接编译是不行的,因为opcode还在opcode_targets.h与 opcode.py 进行了定义,需要保证三者opcode定义上的一致性。
根据上述内容,我们甚至可以拓展opcode,加入一些我们自定义的花指令提高python逆向的难度,不过这不是本文讨论的重点。
Python debug环境
编译debug版本python
debug版本代码编译前可能需要根据具体情况做些调整
gdb调试python
python-dbg中包含了调试符号并把libpython.py工具安装到gdb的auto-load下
不同版本的libpython.py存放在https://github.com/python/cpython/tree/<version>/Tools/gdb,可以手动安装对应版本的libpython.py
安装后即可通过交互式方式从gdb启动python进程
或通过快速方式
或是attach到现有进程
加载core file
常用指令
准备好前置知识与调试环境,就可以在python虚拟机里愉快玩耍啦。
最后更新于
这有帮助吗?