初探 python 栈帧逃逸
前言
以前在一些大型比赛就遇到这种题,一直没时间去研究,现在康复训练下:)
生成器介绍
生成器(Generator)是 Python 中一种特殊的迭代器,它可以在迭代过程中动态生成值,而不需要一次性将所有值存储在内存中。
Simple Demo
这里定义一个生成器函数, 生成器使用yield
语句来产生值,每次调用生成器的next()
方法时,生成器会执行直到遇到下一个yield
语句为止,然后返回yield
语句后面的值,也就是a
的值
也可以遍历获取所有的自增值
生成器表达式
生成器表达式是一种在 Python 中创建生成器的紧凑形式。类似于列表推导式,生成器表达式允许你使用简洁的语法来定义生成器,而不必显式地编写一个函数。生成器表达式的语法与列表推导式类似,但是使用圆括号而不是方括号。生成器表达式会逐个生成值,而不是一次性生成整个序列。这有利于提高内存的利用率
生成器属性
gi_code
: 生成器对应的 code 对象。
gi_frame
: 生成器对应的 frame(栈帧)对象。
gi_running
: 生成器函数是否在执行。生成器函数在 yield 以后、执行 yield 的下一行代码前处于 frozen 状态,此时这个属性的值为 0。
gi_yieldfrom
:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals
:一个字典,包含生成器当前帧的本地变量。
gi_frame 使用
gi_frame
是一个与生成器(generator)和协程(coroutine)相关的属性。它指向生成器或协程当前执行的帧对象(frame object),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息。
以上例子展示了如何获取生成器的帧信息
栈帧(frame)介绍
在 Python 中,栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。(跟 c/c++中的栈类似,懂点逆向知识应该很好理解)
栈帧包含了以下几个重要的属性:f_locals
: 一个字典,包含了函数或方法的局部变量。键是变量名。f_globals
: 一个字典,包含了函数或方法所在模块的全局变量。f_code
: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。f_lasti
: 整数,表示最后执行的字节码指令的索引。f_back
: 指向上一级调用栈帧的引用,用于构建调用栈。
利用栈帧(frame)逃逸沙箱
原理就是通过生成器的栈帧对象通过 f_back(返回前一帧)从而逃逸出去获取 globals 符号表,例如:
逃逸出来我们就可以调用沙箱外的方法来执行恶意命令了
globals 中的__builtins__字段
__builtins__
模块是 Python 解释器启动时自动加载的,其中包含了一系列内置函数、异常和其他内置对象。
当代码这么设计时:
这里将沙箱中的__builtins__
置为空,也就是说沙箱中不能调用内置方法了,那我们这段代码运行就会报错了(next 方法不能使用),那么该如何代替 next 方法来拿到生成器的值,还记得上面说可以遍历的形式来获取生成器的值:
这样可以成功拿到 key 的值,不过这里需要注意的是在给b
赋值时,多加了一个f_back
,因为我们用这种列表推导式拿到生成器的值,它的 code 对象是不同的:
列表推到式拿到的生成器的 code 对象是listcomp
,所以我们还得拿上一个栈帧,所以需要再f_back
一下
一些简便写法
例如:
第一行生成器创建时,并不会直接执行,只是存储在内存中。
第二行对生成器解包,解包的同时会触发生成器调用,此时才开始执行 q.gi_frame.f_back.f_back.f_globals。
exec 调用时,创建一个新栈帧;生成器 q 被执行时,又创建一个新栈帧。所以当我们拿到 q.gi_frame 时,需要回溯两次才到达 exec 之外。
再拿一个 f_globals,就得到了沙箱外的的 globals
文章转载自:F12~
评论