python利用帧栈沙箱逃逸
基础知识
生成器
在Python中,这种一边循环一边计算的机制,称为生成器:generator。
generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
举个简单栗子:
1 | def f(): |
如果我们给a定义一个范围,a<=100 ,可以使用for语句一次性输出
1 | def f(): |
生成器表达式
generator和list做区别的是最外层的()
,而list是[]
。
1 | x=(i * i for i in range(10)) |
上面一堆听不太懂也没多大关系,只需要知道一点generator
获取yield
定义的值通过next()
函数 or for循环(for循环的原理是生成器是特殊的迭代器)
生成器属性
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),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息。
比如:
1 | def my_generator(): |
帧栈(stack frame)
栈帧包含了以下几个重要的属性:
f_locals
: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。f_globals
: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。f_code
: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。f_lasti
: 整数,表示最后执行的字节码指令的索引。f_back
: 指向上一级调用栈帧的引用,用于构建调用栈。
每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧 !!!!!
利用帧栈沙箱逃逸
原理就是通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals全局符号表
栗子:
1 | s3cret="this is flag" |
1 | s3cret = "this is flag" |
代码说明:
- 之前提到的
next
获取yield定义的值,这里获取的就是g.gi_frame.f_back
- 使用
g.gi_frame.f_back
的话,那么g = f()
就必须为g,用的就是这个生成器对象的栈帧 compile(codes, "test", "exec")
就是设置了名称为test
的python沙箱环境
理解一下逃逸的顺序:
1 | s3cret="this is flag" |
即:
1 | f -> waff -> <module>(test) -> <module>(ex1.py) |
成功逃逸
获取到外部的栈帧,就可以用f_globals
去获取沙箱外的全局变量了
但是yield g.gi_frame.f_back
并不能修改为yield g.gi_frame
这样获取到的栈帧经过f_back
后获得的是None
要是再来一个f_back
就会报错
frame.f_back.f_back.f_back.f_back
尝试访问的是 waff
函数的外部调用者的栈帧对象,但是在这个例子中,waff
是最外层函数,没有外部函数调用它
再写一层就能理解了:
1 | s3cret="this is flag" |
globals中的__builtins字段
__builtins__
模块是 Python 解释器启动时自动加载的,其中包含了一系列内置函数、异常和其他内置对象。
使用 dir(__builtins__
) 来查看所有可用的内置函数和异常的列表
1 | dir(__builtins__) |
L3HCTF2024
源码
1 | import sys |
上述代码大概就是通过exec执行任意代码,返回的p和q需要满足if语句
过滤了__
,还有过滤一些其他,这导致不能使用gc去获取对象引用(下面的据说可以用gc秒)
1 | if "__" in codes: |
最关键的是 {"__builtins__": None}
置空了__builtins__
1 | exec(code,{"__builtins__": None},locals) |
根据上述条件,这道题的解题思路就是通过栈帧对象逃逸出沙箱从而获取到沙箱外的globals
1 | a=(a.gi_frame.f_back.f_back for i in [1]) |
1 | a=(a.gi_frame.f_back.f_back for i in [1]) |
然后将沙箱外部的int函数修改为fakeint函数即可
1 | def fake_int(i): |
第九届中国海洋大学信息安全竞赛
源码
1 | from flask import * |
flag在源码中,但是源码被删除,没有 /proc
目录
要获得被覆写的 flag 内容只剩一个地方可以找,就是依靠 python 解析自身进程的内存
cpython 的实现中暴露了获取 python 栈帧的方法
而每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧
而对 flag 的赋值的字节码肯定存在于某个栈帧中,我们只需要从当前栈帧向上找就行了
法一
利用 ctypes
模块的指针,将flag
地址周围的值读一下,实现一个从内存读源码
因为真正的flag在覆盖的flag之前,所以读到假的flag的地址后,往前读取即可
这里用了char 指针,读出来的是一个字符串
最细节的是每次位移8的倍数。(可以自行对比任意两个变量的地址,可以发现它们的差值都是8的倍数)
1 | a=(a.gi_frame.f_back.f_back for i in [1]) |
官方wp
使用的是非常普通的继承链获取globals对象,然后从线程上去找栈帧
而且flask 使用了多线程去处理每个请求,这导致直接在当前线程的栈帧向上找会找不到主线程的 flag,需要从主线程栈帧向上找
1 | sys = print.__globals__["__builtins__"].__import__('sys') |
gc
L3HCTF那题禁用了gc,但是这题没有,有师傅用这个秒了好像
1 | print([].__class__.__base__.__subclasses__()[84].load_module('gc').get_objects()) |
东西太多了,有点卡
题外话:
这回国赛遇到了我竟然没做???!!!,没做???(我真该死)
参考: