WHS 2nd , WhiteGang — PalAnticheatEx: 커널 Anti-Cheat

WHS 2nd , WhiteGang — PalAnticheatEx: 커널 Anti-Cheat

기간
Aug 6, 2024 → Sep 25, 2024
분야
보안
개발
page icon
외부 핵의 프로세스 핸들 획득 지점을 Windows 커널에서 제어하는 PalAnticheatEx 프로토타입을 구현했습니다.
ObRegisterCallbacks 사전 콜백으로 비인가 요청의 위험한 접근권한을 제한하고, 게임 실행 상태에 따른 드라이버 활성화와 PalGuard–드라이버 IOCTL 통신까지 연결했습니다.
page icon

왜 이 프로젝트를 시작했는가

선행 사용자 모드 프로젝트는 일반적인 LoadLibraryA 기반 DLL 로딩 경로를 관찰할 수 있었지만, 외부 프로세스가 핸들을 얻어 WriteProcessMemory로 게임 메모리를 바꾸는 경로와 사용자 모드 우회를 포괄하지 못했습니다. 고도화 프로젝트에서는 외부 핵이 반드시 거쳐야 하는 프로세스 핸들 생성과 권한 부여를 새로운 방어 경계로 정하고, Windows 커널에서 이를 통제할 수 있는지 검증했습니다.
모든 분석과 검증은 교육 목적의 로컬 테스트 환경에서 수행했습니다. 이 페이지는 커널 모드 고도화 프로젝트만 다루며, Detours 기반 사용자 모드 안티치트는 선행 프로젝트로 분리했습니다.

PROJECT OVERVIEW

항목
내용
기간
2024.08.06–2024.09.25 · 고도화 프로젝트
수행 형태
WhiteHat School 2기 · WhiteGang 팀 프로젝트
문제
외부 프로세스가 Palworld 핸들을 열어 메모리 쓰기·변경 권한을 확보하는 경로를 커널에서 제한하는 문제
데이터·대상·환경
Windows · Steam Palworld · 외부 메모리 변조 테스트 프로그램 · 테스트 서명 드라이버 환경
분석·구현 범위
프로세스 핸들 위협 모델, ObRegisterCallbacks 사전 콜백, 요청자·권한 정책, 게임 시작 감시 드라이버, PalGuard–드라이버 IOCTL 통신
결과
통제된 테스트 경로에서 비인가 프로세스의 위험한 핸들 접근권한 요청을 제거하는 커널 드라이버 구조 구현
선행 프로젝트
PalAnticheatEx
foxirain
 
핵심 질문 — 외부 핵이 Palworld 메모리를 변경하기 전에 필요한 프로세스 핸들 권한을 커널 객체 관리 경계에서 선별적으로 제한할 수 있는가?

Project Flow

단계
판단 기준
산출물
1. 외부 핵 분석
게임 메모리 읽기·쓰기에 반드시 필요한 커널 자원은 무엇인가?
OpenProcess → WPM/RPM과 프로세스 핸들 권한의 관계 정리
2. 커널 콜백 구현
핸들이 부여되기 전에 요청을 검사할 수 있는가?
ObRegisterCallbacks 사전 콜백과 DesiredAccess 제어
3. 정책 개선
게임·시스템 프로세스의 정상 동작을 유지하면서 위험 요청만 제한할 수 있는가?
요청자·대상·권한을 구분한 접근 정책
4. 활성화·통신
게임 실행 구간에 맞춰 방어 모듈을 제어하고 상태를 전달할 수 있는가?
시작 감시 드라이버, PalGuard, IOCTL 통신
5. 검증
테스트 프로그램의 위험 권한 요청이 실제로 제거되는가?
드라이버 적용 전후 비교와 시연 영상

ROLE

Windows Kernel Security Researcher · Kernel Anti-Cheat Developer
  • 직접 담당 — 프로세스 핸들 권한 정책과 ObRegisterCallbacks 사전 콜백 설계·구현
  • 직접 담당 — 허용 목록과 시스템 경로 판별, 위험 접근권한 조건 정의
  • 직접 담당 — 게임 시작·종료에 따른 핵심 드라이버 활성화 흐름 구현
  • 직접 담당 — PalGuard의 드라이버 상태 조회 및 IOCTL 통신 구현, 통제된 테스트 환경에서 동작 검증
  • 공동 수행 — 외부 핵 작성·방어 조사와 최종 통합 방향 결정

TECH STACK

C · C++ · Windows Driver Kit (WDK) · Windows Kernel Driver · WinDbg · ObRegisterCallbacks · IOCTL · Windows Object Manager

MY WORK

1. External Cheat 분석과 커널 방어 지점 선정

판단 원칙 — 변하는 치트 코드보다 공격이 반드시 의존하는 지점을 통제합니다.
분석 대상은 Palworld 외부에서 동작하는 External Cheat였습니다. 특정 기능을 하나씩 탐지하기보다, 메모리 변조가 성립하는 공통 조건을 찾아 커널 방어 지점으로 연결하는 것을 목표로 했습니다.
분석 과정
1) 오프셋과 메모리 접근 경로 추적
기능별 오프셋이 실제 주소로 계산되는 과정과 공통 read·write 래퍼를 확인했습니다. 서로 다른 치트 기능도 최종적으로 ReadProcessMemory·WriteProcessMemory 호출로 수렴했습니다.
notion image
기능에서 사용할 주소를 계산하기 위해 정의된 오프셋 영역
notion image
RPM/WPM을 공통화한 read·write 래퍼
오프셋은 게임 업데이트마다 달라질 수 있지만, 외부 프로세스가 다른 프로세스의 메모리를 조작하려면 운영체제가 부여한 프로세스 핸들과 접근권한이 필요합니다. 이에 기능이나 오프셋이 아니라 두 메모리 API가 공유하는 hProcess의 생성 지점을 역추적했습니다.
2) OpenProcess → HANDLE → RPM/WPM 의존성 확인
OpenProcess가 Palworld PID로부터 핸들을 만들고, 이 핸들이 이후 모든 읽기·쓰기 함수에 전달되는 것을 확인했습니다.
오프셋은 ‘어디를 조작할지’를 정하지만, 핸들의 GrantedAccess는 ‘그 조작을 허용할지’를 결정합니다. 따라서 PROCESS_VM_WRITEPROCESS_VM_OPERATION을 얻지 못하면 주소를 알아도 WPM 기반 변조를 완성할 수 없다고 판단했습니다.
3) WinDbg로 핸들 구조 검증
소스 분석만으로 결론을 내리지 않기 위해 OpenProcess로 테스트 프로세스 핸들을 여는 최소 재현 프로그램을 작성했습니다. 출력된 값을 WinDbg !handle로 조회해 핸들이 실제 Handle Table의 Process 객체를 가리키며, 요청 권한이 GrantedAccess에 기록되는 것을 확인했습니다.
notion image
External Cheat의 OpenProcess 호출부 — 이후 RPM/WPM이 사용할 핸들을 확보합니다.
notion image
최소 재현 실험의 WinDbg !handle 결과 — Handle Table, Process Object, GrantedAccess를 교차 확인했습니다.
관찰
판단
설계 반영
오프셋·기능 코드는 변경 가능
시그니처 탐지는 업데이트에 종속
치트 코드 밖의 공통 경계를 선택
RPM/WPM이 동일한 핸들에 의존
핸들이 메모리 접근의 선행 조건
핸들 생성 시점을 통제
Handle Table에 권한이 기록됨
부여 전에 위험 권한 조정 가능
ObRegisterCallbacks 선택
방어 지점 선정. 파일명·해시 탐지는 재빌드에 취약하고, 유저모드 API 후킹은 동일 권한의 공격자가 우회할 수 있습니다. 반면 ObRegisterCallbacks는 Object Manager가 핸들을 부여하기 전에 대상·요청자·DesiredAccess를 확인할 수 있습니다. 이에 비인가 요청의 PROCESS_VM_WRITE, PROCESS_VM_OPERATION, PROCESS_DUP_HANDLE을 제거하는 방식을 핵심 방어로 선택했습니다.
⚠️
검증 범위는 콜백 등록 이후 새로 생성되는 유저모드 핸들입니다. 기존 핸들, 커널 권한 공격자, 실제 정책 분기가 구현되지 않은 복제 핸들은 후속 보완 범위로 남겼습니다.
결과. 정적 분석과 WinDbg 검증을 연결해 방어 기준을 ‘알려진 치트 탐지’에서 프로세스 메모리 접근권한이 부여되는 순간의 통제로 전환했습니다.

2. Windows 커널 드라이버 개발·디버깅 환경 구축

커널 드라이버는 잘못된 포인터나 메모리 처리 하나만으로도 시스템 전체를 중단시킬 수 있습니다. 따라서 안티치트 구현에 앞서 개발 PC와 분리된 VM에서 빌드·적재·디버깅을 반복할 수 있는 루프를 구축했습니다.
구성
역할
검증 기준
Windows 10 VMware
커널 크래시 격리
장애 후 다시 테스트 가능한가
Visual Studio · SDK · WDK
드라이버 빌드 환경
테스트 서명된 .sys가 생성되는가
Test Signing · sc
드라이버 서비스 등록·적재
DriverEntry와 Unload가 실행되는가
WinDbg · DbgPrint
커널 실행 흐름 관찰
로그·중단점으로 동작을 확인할 수 있는가
1) 격리된 개발 환경 구성
Windows 10 VM에 Visual Studio 2022, Windows SDK와 WDK를 설치하고 Kernel Mode Driver 템플릿을 구성했습니다. 테스트 서명 모드를 활성화하고 호스트 WinDbg와 VM을 시리얼 포트로 연결해, 드라이버 오류가 실제 개발 환경에 영향을 주지 않도록 분리했습니다.
notion image
 
2) 환경 검증용 커널 드라이버 개발
초기 WDF Hello Driver는 안현진 팀원의 코드를 사용했습니다. 이를 출발점으로 DriverEntry와 Unload 루틴, 콜백 등록·해제, NTSTATUS 반환값과 DbgPrint 로그를 직접 다루며 실제 드라이버 구조로 확장했습니다.
먼저 PsSetCreateProcessNotifyRoutineEx를 이용해 프로세스 생성 시 이미지 이름을 출력하는 드라이버를 작성했습니다. 로드 시 콜백을 등록하고 언로드 시 동일 콜백을 해제하도록 생명주기를 구성했습니다.
status = PsSetCreateProcessNotifyRoutineEx(ProcessCreateNotifyEx, FALSE); DriverObject->DriverUnload = UnloadDriver; // Unload PsSetCreateProcessNotifyRoutineEx(ProcessCreateNotifyEx, TRUE);
이후 PsSetCreateThreadNotifyRoutine을 이용한 스레드 생성 감지 PoC도 작성했습니다. 다만 원격 스레드와 정상 스레드를 판별하는 정책까지 완성한 것은 아니므로, 이 단계는 커널 이벤트 콜백을 등록하고 관찰하는 실험으로 구분했습니다.
3) 문제 해결 — 콜백 등록 시 STATUS_ACCESS_DENIED
프로세스 생성 콜백 드라이버를 sc start로 적재했지만 0xC0000022 (STATUS_ACCESS_DENIED)가 반환되었습니다. 단순 적재 실패로 넘기지 않고 반환값을 기준으로 공식 동작 조건을 확인해, 콜백을 포함한 이미지에 IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY가 필요하다는 원인을 찾았습니다.
Visual Studio의 링커 → 명령줄 → 추가 옵션/INTEGRITYCHECK를 적용해 PE 이미지에 무결성 검사 플래그가 포함되도록 수정했습니다. 이후 Calculator 실행 시 ProcessCreateNotifyEx 로그에 Calculator.exe가 출력되는 것을 확인했습니다.
notion image
콜백 등록 단계에서 확인한 STATUS_ACCESS_DENIED
notion image
/INTEGRITYCHECK 적용 후 프로세스 생성 로그 검증
4) 문제 해결 — 비공개 API 링크 실패와 시그니처 탐색
초기 핸들 차단 PoC는 PID를 상수로 지정했기 때문에 Palworld를 재실행하면 보호 대상을 다시 찾을 수 없었습니다. 프로세스 이름으로 PID를 찾기 위해 PsGetNextProcessPsGetProcessImageFileName을 사용했지만, WDK 빌드에서 해결할 수 없는 외부 기호 오류가 발생해 직접 링크할 수 없었습니다.
notion image
PsGetNextProcess 계열을 직접 사용하며 발생한 외부 기호 링크 오류
대안을 조사하면서 함수의 바이트 배열을 시그니처로 만들고, ntoskrnl 이미지의 ImageBaseAddress부터 ImageSize 범위를 순회하며 RtlCompareMemory로 동일한 바이트열을 찾는 FindFunctionInModule 방식을 검토했습니다. 일치 주소를 함수 포인터로 변환하면 비공개 함수를 호출할 수 있다는 접근이었습니다.
시그니처 탐색은 비공개 심볼을 찾는 방법을 이해하고 직접 구현해 본 시도였지만, Windows 빌드마다 함수 바이트가 달라질 수 있어 유지보수성과 안정성이 낮았습니다. 따라서 최종 프로세스 열거는 ZwQuerySystemInformation(SystemProcessInformation)으로 전환했습니다. 이 과정에서 ‘동작 가능성’보다 버전 변화에 견딜 수 있는 구현인지를 선택 기준으로 삼았습니다.
5) 빌드에서 커널 실행까지 검증 루프 확립
작성한 드라이버를 sc create/start로 등록·적재하고, DriverEntry 진입, 콜백 로그, 언로드를 WinDbg로 반복 검증했습니다.
이 과정에서 완료 기준을 ‘컴파일 성공’으로 두지 않고, 실제로 커널에 적재되어 실행 흐름을 관찰하고 다시 내릴 수 있는 상태로 정의했습니다.
notion image
WinDbg에서 드라이버 적재를 확인한 기록
6) 장애를 환경 문제와 코드 문제로 분리
환경 구축 과정에서도 여러 실패를 겪었습니다. Visual Studio SignTask 접근 거부는 관리자 권한으로 해결했고, 보이지 않던 커널 로그는 레지스트리의 Debug Print Filter를 설정하고 재부팅해 확인했습니다.
드라이버 적재 후 VM이 복구 불가능한 상태에 빠진 경우에는 Windows ISO를 두 차례 다시 내려받고 설치 위치까지 변경해 환경을 재구축했습니다. 당시 해당 장애의 단일 원인은 끝내 특정하지 못했으므로 해결했다고 단정하지 않고, 동일 작업을 다시 수행할 수 있도록 실험 환경을 복구한 과정으로 남겼습니다.
 
결과. 단순히 WDK를 설치한 것이 아니라, 프로세스·스레드 콜백 드라이버를 직접 작성하고 적재 오류와 링크 문제를 원인별로 추적하는 개발 루프를 만들었습니다. 이 과정에서 확보한 콜백 생명주기 관리, 무결성 플래그, 동적 프로세스 탐색 경험이 이후 ObRegisterCallbacks 기반 안티치트 구현의 기반이 되었습니다.
근거 기록: 8월 31일 보고서

3. ObRegisterCallbacks 기반 최소 권한 핸들 제어

판단 원칙 — 강한 차단보다 공격에 필요한 권한만 제거하고 정상 동작을 보존합니다.
1차 구현. 특정 PID의 핸들 DesiredAccess를 0으로 만드는 콜백으로 차단 가능성을 확인했습니다. 그러나 PID는 실행마다 달라졌고, 모든 권한을 제거하면 디버거·백신·정상 시스템 프로세스까지 영향을 받았습니다. ‘차단 성공’만으로는 실제 환경에서 사용할 수 없다는 문제를 확인했습니다.
동적 대상 식별. 비문서화된 PsGetNextProcess·PsGetProcessImageFileName 사용을 시도했지만 외부 기호 해결과 버전 의존 문제가 발생했습니다. 시그니처 스캔 가능성까지 검토한 뒤 유지보수성을 우선해 ZwQuerySystemInformation 기반 프로세스 열거로 전환했고, Palworld의 PID를 실행 시점에 찾도록 바꿨습니다.
권한 정책 개선. 전체 접근을 제거하는 대신, 외부 메모리 변조에 직접 필요한 권한만 비트 단위로 제거했습니다.
권한
공격에서의 의미
정책
PROCESS_VM_WRITE
타깃 프로세스 메모리 쓰기
제거
PROCESS_VM_OPERATION
타깃 주소 공간에 대한 메모리 연산
제거
PROCESS_DUP_HANDLE
기존 핸들 복제로 정책 우회 가능
제거
조회·동기화 등 비위험 권한
정상 상태 확인과 시스템 동작에 필요
유지
호출 주체와 대상이 같으면 허용하고, 시스템 경로와 정상 실행에 필요한 프로세스는 예외 처리했습니다. 아래 실험은 정책을 ‘허용/차단’ 두 방향에서 함께 검증한 과정입니다.
notion image
시스템 프로세스 및 비위험 권한 접근 허용
notion image
위험 권한 차단 확인 — 이후 정상 프로세스는 화이트리스트로 정교화
결과. 커널 콜백을 단순한 전면 차단기가 아니라, 호출 주체·대상·요청 권한을 함께 평가하는 최소 권한 정책 지점으로 발전시켰습니다.

4. 상시 콜백을 줄이기 위한 이중 드라이버 설계

설계 기준 — 보호 기능의 강도뿐 아니라, 언제 활성화하고 언제 해제할지도 보안 기능의 일부로 다뤘습니다.
문제 발견. 초기 드라이버는 Palworld가 실행되지 않은 상태에서도 프로세스·스레드 생성과 핸들 요청을 계속 처리했습니다. WinDbg 로그에서 콜백이 반복되는 것을 확인했고, 게임이 없을 때까지 집중 보호 로직을 유지할 필요는 없다고 판단했습니다.
notion image
게임 실행 여부와 관계없이 반복되던 콜백 활동
1) 역할과 생명주기 분리
하나의 드라이버에 상태 감시와 핸들 보호를 모두 두는 대신, 시작 드라이버와 집중 보호 드라이버로 역할을 분리했습니다.
구성 요소
담당 역할
활성 구간
ExternalStart.sys
Palworld 시작·종료 감지, 보호 드라이버 로드·언로드
상시 실행
External.sys
ObRegisterCallbacks 등록과 위험 핸들 권한 제어
게임 실행 중
2) 첫 시도 실패 — NtLoadDriver
게임 실행 시 보호 드라이버를 동적으로 올리기 위해 먼저 유저모드의 비문서화된 NtLoadDriver 경로를 시도했습니다. 그러나 Event Viewer에 오류가 남고 정상적으로 실행되지 않았으며, 당시 기록에서는 ntdll 관련 메시지 이상의 원인을 특정하지 못했습니다.
notion image
NtLoadDriver 경로를 시도하며 확인한 실행 오류
원인이 불명확한 비문서화 경로를 계속 사용하지 않고, 드라이버 로드 책임을 커널의 시작 드라이버로 옮겼습니다. 보호 드라이버의 서비스 레지스트리 경로를 UNICODE_STRING으로 구성하고 ZwLoadDriver·ZwUnloadDriverNTSTATUS를 로그로 확인하는 방식으로 전환했습니다.
3) 프로세스 시작·종료 이벤트와 드라이버 상태 연결
ExternalStart.sysPsSetCreateProcessNotifyRoutineEx로 프로세스 콜백을 등록했습니다. 생성 이벤트에서 CreateInfo->ImageFileName을 대상 경로와 비교하고, 일치하면 PID를 저장한 뒤 보호 드라이버를 로드했습니다. 종료 이벤트에서는 저장한 PID와 ProcessId가 같은지 확인한 뒤 보호 드라이버를 언로드했습니다.
if (CreateInfo != NULL && IsTargetProcess(CreateInfo->ImageFileName)) { targetPID = ProcessId; ZwLoadDriver(&focusDriverRegistryPath); } else if (CreateInfo == NULL && ProcessId == targetPID) { ZwUnloadDriver(&focusDriverRegistryPath); }
이 구조를 통해 External.sysObRegisterCallbacks는 게임 실행 구간에만 등록되고, 게임 종료 시 DriverUnload 경로에서 콜백과 리소스를 정리하도록 생명주기를 연결했습니다.
4) 통제된 전환 검증
초기에는 Notepad를 대상으로 생성·종료 이벤트와 드라이버 로드·언로드를 검증한 뒤 Palworld 경로에 적용했습니다. 시작 드라이버가 집중 탐지용 드라이버를 실제로 적재하는지 WinDbg 로그와 드라이버 상태로 확인했습니다.
notion image
ExternalStart가 집중 보호 드라이버를 로드한 검증 기록
📏
성과의 범위. CPU·메모리·지연 시간은 정량 측정하지 않았으므로 성능 향상 수치를 주장하지 않았습니다. 확인한 성과는 상시 활성화되던 핵심 핸들 콜백의 생명주기를 게임 실행 구간으로 제한한 것입니다. 현재 구현의 실행 파일 경로와 서비스 레지스트리 경로는 하드코딩되어 있어 설치 경로 변경, 재시작, 중복 로드 상태는 추가 검증이 필요합니다.
결과. 콜백 호출량을 보고 끝내지 않고, 커널 모듈을 상태 감시와 집중 보호로 분리했습니다. 비문서화 로드 방식의 실패를 ZwLoadDriver 기반 구현으로 전환하고, 프로세스 생성·종료 이벤트를 드라이버 로드·언로드와 연결해 보호 모듈의 실행 범위를 직접 제어했습니다.

5. PalGuard·IOCTL 통신과 환경 의존성 제거

설계 기준 — 커널은 보호 정책에 집중하고, 환경 탐색과 사용자 상태 전달은 유저모드 제어면으로 분리했습니다.
문제 발견. 초기 구현은 시스템 프로세스를 판별하기 위해 \Device\HarddiskVolume3\Windows\System32\를 커널에 고정했습니다. 제 VM에서는 동작했지만 볼륨 번호가 달라지는 다른 환경에서는 같은 경로가 성립하지 않았고, 매번 fltmc volumes로 값을 확인해 코드를 수정하는 방식도 배포 가능한 구조가 아니었습니다. 동시에 드라이버 상태를 DbgPrint로만 확인해서 사용자는 게임 감지와 보호 모듈 활성화 여부를 알 수 없었습니다.
두 문제를 개별 예외가 아니라 커널이 환경 정보와 사용자 인터페이스까지 모두 책임진 구조의 문제로 판단했습니다. 이에 PalGuard를 단순 실행 파일이 아니라 런타임 환경을 발견하고 커널 상태를 중계하는 제어면으로 설계했습니다.
notion image
PalGuard, 시작 감시 드라이버, 집중 보호 드라이버의 통신을 먼저 정리한 당시 설계 메모
1) 세 구성요소의 책임을 분리했습니다
구성요소
책임
주요 교환 정보
PalGuard.exe
런타임 환경 탐색과 사용자 상태 표시
C:의 NT 디바이스 경로, 게임·보호 모듈 상태
ExternalStart.sys
Palworld 실행 상태와 집중 보호 드라이버의 생명주기 관리
PALSTART와 드라이버 활성화 상태
External.sys
핸들 권한 정책 적용과 동적 System32 기준 경로 사용
IOCTL_SEND_CDRIVER_NAME으로 받은 볼륨 경로
이 분리를 통해 4번에서 구현한 드라이버 생명주기에 사용자 모드 제어면을 연결했습니다. 게임 존재 여부, 보호 드라이버 상태, 환경별 경로를 한 모듈에 섞지 않고 각 구성요소가 필요한 정보만 주고받도록 했습니다.
2) 사용자–커널 통신 경로를 직접 구현했습니다
이전까지는 커널 로그만 출력했기 때문에 사용자 모드에서 드라이버에 요청을 보낼 대상이 없었습니다. 드라이버에 Device Object를 만들고 Symbolic Link를 연결한 뒤, IRP_MJ_DEVICE_CONTROL 디스패치에서 IOCTL을 처리하도록 구현했습니다. PalGuard는 CreateFileW로 장치 핸들을 열고 DeviceIoControl로 상태 조회와 경로 전달 요청을 보냈습니다.
HANDLE hDevice = CreateFileW( L"\\\\.\\ExternalStartDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); DeviceIoControl( hDevice, IOCTL_RECEIVE_STARTPAL, NULL, 0, &palStarted, sizeof(palStarted), &bytesReturned, NULL);
3) 고정 볼륨값을 런타임 정보로 바꿨습니다
PalGuard가 현재 시스템에서 C:에 대응하는 \Device\HarddiskVolume... 값을 찾고, 이를 집중 보호 드라이버에 전달하도록 했습니다. 드라이버는 받은 ANSI 문자열을 RtlMultiByteToUnicodeN으로 변환하고 \Windows\System32\를 결합해 프로세스 경로 판정에 사용할 기준값을 구성했습니다.
notion image
IOCTL로 전달된 볼륨 경로로 System32 기준 경로를 구성하고 프로세스 경로를 판정한 로그
핵심은 특정 PC의 HarddiskVolume3을 다른 값으로 교체한 것이 아니라, 환경마다 달라지는 값은 실행 시점에 발견해 명시적인 인터페이스로 전달한다는 구조로 전환한 점이었습니다.
4) 문자열 경계에서 발생한 BSOD를 추적했습니다
IOCTL 연동 후 IsInSystem32Directory에서 시스템이 중단됐습니다. WinDbg의 !analyze -v와 로컬 PDB를 연결해 접근 위반이 RtlPrefixUnicodeString을 호출한 122번째 줄에서 발생한 것을 확인했습니다.
FAULTING_SOURCE_LINE_NUMBER: 122 SYMBOL_NAME: ExternalAnticheat!IsInSystem32Directory+17 FAILURE_BUCKET_ID: AV_R_(null)_ExternalAnticheat!IsInSystem32Directory
이를 계기로 IOCTL 입력을 단순한 char*로 취급하지 않고, ANSI→Unicode 변환 길이와 출력 버퍼 범위, NULL 종료 위치를 명시적으로 관리하도록 수정했습니다. 이후 전달된 볼륨 경로와 조합된 System32 경로가 WinDbg 로그에 정상적으로 출력되고, 해당 기준으로 핸들 정책 검증을 이어갈 수 있었습니다.
5) 제어 경로를 종단 간 검증했습니다
PalGuard가 볼륨 정보를 확인하고, Palworld 시작을 기다린 뒤, 게임과 메인 보안 모듈의 상태를 순서대로 확인하는 흐름을 실행했습니다. 커널에서는 전달된 경로가 정책 기준값으로 구성되는지 확인했습니다.
notion image
PalGuard에서 볼륨 탐색, 게임 시작, 메인 보안 모듈 활성화 상태를 순서대로 확인한 기록
문제
구현한 전환
확인한 결과
커널에 고정된 볼륨 경로
PalGuard의 런타임 탐색과 IOCTL 전달
동적으로 구성된 System32 경로를 커널 로그에서 확인했습니다.
커널 로그에만 존재하던 상태
게임·보호 모듈 상태 조회 IOCTL
PalGuard에서 활성화 순서를 확인했습니다.
문자열 변환 중 접근 위반
WinDbg·PDB 기반 위치 추적과 길이·종료 처리 수정
경로 전달 이후 정책 검증 흐름을 계속 실행했습니다.
⚠️
검증 범위와 남은 과제. 이 단계의 성과는 테스트 환경에서 사용자–커널 제어 경로와 런타임 경로 전달을 연결한 것입니다. 다만 경로 문자열만으로 프로세스를 신뢰할 수는 없으며, 당시 코드에는 상태 조회 IOCTL 구분, 실패한 장치 핸들의 즉시 반환과 정리, 입력 길이·호출자 권한 검증을 보완해야 하는 부분이 남아 있습니다. 따라서 이를 운영 수준의 안전한 제어 채널로 과장하지 않았습니다.
결과. PalGuard를 단순 상태 표시 프로그램이 아니라 환경 탐색과 커널 제어를 담당하는 사용자 모드 제어면으로 확장했습니다. 이를 통해 고정된 볼륨값에 의존하던 커널 정책을 런타임 전달 구조로 바꾸고, 두 드라이버의 상태와 보호 정책을 하나의 실행 흐름으로 연결했습니다. 또한 IOCTL 구현 과정에서 발생한 BSOD를 WinDbg로 원인 지점까지 추적하며, 커널 통신에서는 기능 구현만큼 버퍼 경계와 상태 관리가 중요하다는 점을 확인했습니다.

RESULTS

구분
결과
위협 모델
외부 핵의 메모리 접근이 대상 프로세스 핸들과 권한에 의존한다는 점을 방어 기준으로 구체화
핵심 구현
ObRegisterCallbacks 사전 콜백으로 Palworld 대상 핸들 요청의 DesiredAccess 제어
정책 개선
전체 접근 차단에서 요청자·대상·위험 권한을 구분하는 정책으로 개선
통합
시작 감시 드라이버, 집중 보호 드라이버, PalGuard와 IOCTL 기반 활성화 구조 연결
검증
통제된 테스트 경로에서 드라이버 적용 후 위험 권한 요청이 제거되는 동작 확인
한계
  • 이미 열린 핸들, 커널 권한 공격자, 취약 드라이버 악용은 다루지 못했습니다.
  • 핸들 생성·복제를 등록했지만 당시 정책 분기는 생성 경로 중심이어서 복제 경로 보완이 필요합니다.
  • System32 경로 신뢰는 서명·토큰 검증이 아닌 경로 기반 휴리스틱입니다.
  • 일부 IOCTL 제어 코드와 오류 처리, 하드코딩된 경로는 수정이 필요합니다.
  • 테스트 서명 환경을 전제로 하며 우회 내성, 성능, 배포·업데이트·변조 방지 체계는 검증하지 않았습니다.
page icon

배운 점과 다음 단계

커널로 방어 지점을 옮기는 것만으로 문제가 해결되지는 않았습니다. 정상 프로세스까지 막았던 시행착오를 통해 방어 강도와 정상 동작 사이의 균형은 권한 단위 정책에서 결정된다는 점을 배웠습니다. 다음 단계에서는 경로 기반 신뢰를 서명·토큰 검증으로 교체하고, 핸들 복제 경로와 IOCTL 입력 검증을 보완하며, 정량 성능 측정과 배포·변조 방지 체계까지 검증할 계획입니다.

전체 기록과 원본