📓

원본 기록 — IOCTL·드라이버 통합과 최종 테스트 (2024 당시)

 
 
 

심볼릭 링크 사용 이유

Windows의 커널 드라이버에서는 심볼릭 링크를 통해 사용자 모드 애플리케이션이 커널 모드 드라이버에 쉽게 접근할 수 있게 준다.

예를 들어:

  • 실제 디바이스 경로가 \Device\MyDriverDevice라면, 이 경로는 커널 내부에서만 사용될 수 있다.
  • 하지만 이 경로에 심볼릭 링크 \\.\MyDriver를 설정하면, 사용자 모드 애플리케이션은 \\.\MyDriver라는 경로를 사용해 커널 드라이버와 통신할 수 있게 된다.
이것이 심볼릭 링크의 역할이다. 즉, 사용자 모드커널 모드 사이의 경로를 연결하는 가상 링크라고 보면 된다.
 
드라이버에서 다음과 같이 심볼릭 링크를 생성하면
if (NT_SUCCESS(status)) { // 심볼릭 링크 생성 status = IoCreateSymbolicLink(&symbolicLink, &deviceName); }
 
유저 모드 프로세스에서
HANDLE hDevice = CreateFileW(L"\\\\.\\ExternalStartDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
다음과 같이 심볼릭 링크를 통해 핸들을 요청 할 수 있다. 물론 이 핸들을 통해 IOCTL을 하는 것
 
 

디바이스 객체가 필요한 이유

DeviceIoControl 또는 ioctl 을 통해 사용자 모드 애플리케이션과 커널 드라이버가 통신하려면 디바이스 객체를 반드시 생성해야 한다.

이유:

  • ioctl(I/O control)은 사용자 모드 프로그램이 커널 드라이버에 명령을 전달하는 표준적인 방법입니다. 이를 통해 다양한 I/O 요청을 처리할 수 있습니다.
  • DeviceIoControl 또는 ioctl 호출을 통해 커널 드라이버가 I/O 요청을 처리하려면, 그 요청을 받을 디바이스 객체가 필요합니다.
  • 이 디바이스 객체가 없으면, 드라이버는 I/O 요청을 받을 수 없으며, ioctl을 사용할 수 없습니다.

절차:

  1. *IoCreateDevice*로 디바이스 객체를 생성해야 합니다. 이 객체가 있어야 커널 드라이버가 사용자 모드 애플리케이션의 요청을 받을 수 있습니다.
  1. 그 후, 심볼릭 링크를 통해 사용자 모드 애플리케이션이 해당 디바이스 객체에 접근할 수 있도록 해야 합니다. 이 심볼릭 링크를 사용해 애플리케이션은 \\.\MyDevice 같은 경로로 커널 드라이버에 연결할 수 있습니다.
  1. IRP_MJ_DEVICE_CONTROL 핸들러를 구현하여, 사용자 모드 애플리케이션에서 전달된 IOCTL 요청을 처리합니다.

결론:

IOCTL을 사용하여 사용자 모드 애플리케이션과 커널 드라이버 간에 데이터를 주고받으려면, 디바이스 객체를 생성하는 것이 필수적이라는 말
 
즉, 여태까지는 그냥 후킹만 만들고 DGBprint만 사용하고 이러다 보니깐 상관이 없었는데 이번에는 진짜 객체가 필요했던 것
 
좀더 이해가 쉽게 말하자면, IOCTL 명령이 드라이버가 제어하는 특정 디바이스에 대한 요청을 전달하기 때문에 그렇다. 드라이버 객체는 드라이버 자체를 나타내며, 디바이스 객체는 드라이버가 관리하는 실제 또는 가상 장치를 나타낸다. 따라서 받을 디바이스를 만들기 위해 객체를 생성해야함!
 
드라이버에서 다음과 같은 코드로 객체를 생성 할 수있다.
NTSTATUS status = IoCreateDevice( DriverObject, // 드라이버 오브젝트 0, // 장치 확장 크기 (없음) &deviceName, // 장치 이름 FILE_DEVICE_UNKNOWN, // 장치 타입 0, // 특성 없음 FALSE, // 익스클루시브 사용 (FALSE = 공유 가능) &DeviceObject // 생성된 장치 객체 반환 );
 
 
 
 

BLUEPRIINT

 
 
notion image
ExternalStart.sys ( 원래이름은 ExternalAnticheatStart.sys)
External.sys ( 원래이름은 ExternalAnticheat.sys)
BLUEPRINT
 
 
 
 
 

첫번째 부분 구현 성공

notion image
이부분 구현 성공
 
PalGuard가 IRP에서 대답으로 TRUE 형의 boolean을 받을때 까지 1초 단위로 대기
notion image
 
PalGuard.exe
BOOL WaitforPALSTART () { DWORD bytesReturned = NULL; HANDLE hDevice = CreateFileW(L"\\\\.\\TestDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) { printf("[ERROR : PalGuard] 보안 모듈 탐색 실패 Palworld를 종료합니다. \n"); // 커널 드라이버로 게임 종료 코드를 보냄 } BOOL PALSTART = FALSE; BOOL result = DeviceIoControl(hDevice, IOCTL_RECEIVE_STARTPAL, NULL, 0, &PALSTART, sizeof(PALSTART), &bytesReturned, NULL); if (result) { if (PALSTART) { printf("[INF : PalGuard] PAL 시작 확인...\n"); return TRUE; } else { return FALSE; } } else { printf("[ERROR : PalGuard] 시스템 정보 확인 실패 에러코드 : 0x0001\n"); return 1; } CloseHandle(hDevice); return FALSE; }
 
while (!WaitforPALSTART()) { printf("[INF : PalGuard] Pal 시작을 기다리는 중...\n"); Sleep(5000); } printf("와 발견띠\n");
그리고 이제 WaitforPALSTART 함수로 5초마다 꾸준히 IRP를 커널 드라이버에 보내 확인한다.
 
 
ExternalStart.sys ( 원래이름은 ExternalAnticheatStart.sys)
if (controlCode == IOCTL_RECEIVE_STARTPAL) { // 유저 모드로부터 문자열을 받음 if (stack->Parameters.DeviceIoControl.OutputBufferLength >= sizeof(BOOLEAN)) { RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, &PALSTART, sizeof(BOOLEAN)); // 유저 모드로 전송 Irp->IoStatus.Information = sizeof(BOOLEAN); Irp->IoStatus.Status = STATUS_SUCCESS; } else { Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; // 버퍼가 작으면 오류 } }
 
IOCTL_RECEIVE_STARTPAL 와 같은 코드일때 Boolean 형의 전역 변수인 PALSTART를 보낸다.
PALSTART 는 알다시피 그냥 커널 드라이브에서 프로세스 시작때 콜백함수를 이용해서 알 수 있다.
 
 
 
 

막간을 이용한 윈도우 어휘 정리

 
윈도우를 하다보면 형식이 정말 여러개임을 알 수 있다. DWORD ,QWORD ,WORD 이런것은 쉽게 알 수 있지만 나머지는 좀 어려울 수 도 있다. 일단, 문자열 종류에는 WIDE 와 ANSI 가 대표적으로 있다. WIDE는 유니코드 문자열 ANSI는 아스키코드 문자열이다. 따라서 여러 함수뒤에 W 가 붙으면 유니코드 문자열 A 가 붙으면 아스키코드 문자열을 사용하는 버전이다. ( 참고로 EX 는 익스텐션으로 기능이 확장된 거임 )
 
UNICODE_STRING → 유니코드 문자열을 나타내는데 이건 여러가지 정보가 같이 들어가있는 구조체임
가장 많이 사용한다.
 
PUNICODE_STRING → 여기서 추가적인 정보가 나오는데 P 가 붙으면 포인터 라는 뜻임, 따라서 이건 UNICODE_STRING 의 포인터라는 뜻
 
CHAR* → 우리가 아는 일반 아스키 문자열 WCHAR* → 이제 감이 올거다. 어 W 가 붙었네? 이거 유니코드 문자열이구나~
 
PCWSTR 그럼 얘는 뭘까?
  • P: Pointer (포인터)
  • C: Constant (읽기 전용)
  • W: Wide character (2바이트 유니코드 문자)
  • STR: String (문자열)
 
이다. 참고로 STR 과 CHAR의 차이점은 없는 것 같다. C++의 힙을 사용하는 객체 스트링을 커널에서 사용할 이유가 없으니깐
 
 
 
 
 
 
 
 

크래시가 발생했다.

 
BSOD( blue screen of death )가 발생했을때 windbg 에서 !analyze -v 명령어를 사용하고 조금 기다리면 다음과 같이 어디에서 문제가 발생했는지 상세히 알려준다.
 
심지어 코드 원형 까지 알려준다. 어케 가능한거지?
알고보니 windbg에서 로컬 컴퓨터에 있는 심볼을 가져와준단다.
어쨋든 로그는 다음과 같다.
FAULTING_SOURCE_LINE: C:\repos\ExternalAnticheat\ExternalAnticheat\Driver.c FAULTING_SOURCE_FILE: C:\repos\ExternalAnticheat\ExternalAnticheat\Driver.c FAULTING_SOURCE_LINE_NUMBER: 122 FAULTING_SOURCE_CODE: 118: 119: 120: 121: BOOLEAN IsInSystem32Directory(PUNICODE_STRING processName) { > 122: if (RtlPrefixUnicodeString(&SYSTEM32_PATH, processName, TRUE)) { 123: return TRUE; 124: } 125: return FALSE; 126: } 127: SYMBOL_NAME: ExternalAnticheat!IsInSystem32Directory+17 MODULE_NAME: ExternalAnticheat IMAGE_NAME: ExternalAnticheat.sys STACK_COMMAND: .process /r /p 0xffffb70547142280; .thread 0xffffb70547330080 ; kb BUCKET_ID_FUNC_OFFSET: 17 FAILURE_BUCKET_ID: AV_R_(null)_ExternalAnticheat!IsInSystem32Directory OS_VERSION: 10.0.19041.1 BUILDLAB_STR: vb_release OSPLATFORM_TYPE: x64 OSNAME: Windows 10 FAILURE_ID_HASH: {92bcb921-c30f-5b69-2128-85b786132cd0} Followup: MachineOwner ---------
 
 
 
 
 
 
 
 

추가적인 부분 구현 성공

 
notion image
PAlGUARD.exe ⇒ ExternalDriver.sys 로 보내기도 성공
 
 
if (controlCode == IOCTL_SEND_CDRIVER_NAME) { // 유저 모드로부터 문자열을 받음 char* Cdriver = (char*)Irp->AssociatedIrp.SystemBuffer; DbgPrint("[INF : DRIVER] C드라이브 NT Device Name : %s\n", Cdriver); WCHAR unicodeString[256]; ULONG unicodeStringLength = 0; RtlMultiByteToUnicodeN( unicodeString, // 출력 유니코드 버퍼 sizeof(unicodeString), // 출력 버퍼 크기 (바이트 단위) &unicodeStringLength, // 변환된 유니코드 문자열 크기 Cdriver, // 입력 ANSI 문자열 (ULONG)strlen(Cdriver) // 입력 문자열 길이 ); WCHAR system32path[PATHNUM] = L"Windows\\System32\0"; // 변환된 유니코드 문자열 끝에 널 종료 문자 추가 if (unicodeStringLength < 256) { int B = unicodeStringLength / sizeof(WCHAR); unicodeString[B] = L'\\'; B++; int A = 0; while (A < PATHNUM) { unicodeString[B] = system32path[A++]; B++; } } else { unicodeString[255] = L'\0'; // 문자열이 꽉 차 있는 경우 마지막에 널 종료 문자 추가 } DbgPrint("[INF : DRIVER] C드라이브 NT Device Name : %ls\n", unicodeString); SetSystem32Path(unicodeString); BOOLEAN FLAG_ = TRUE; if (stack->Parameters.DeviceIoControl.OutputBufferLength >= sizeof(BOOLEAN)) { RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, &FLAG_, sizeof(BOOLEAN)); Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = sizeof(BOOLEAN); } else { Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; // 버퍼가 작으면 오류 }
 
볼륨에 대한 정보를 받는 코드이다. 받고
WCHAR system32path[PATHNUM] = L"Windows\\System32\0"; 으로 뒤에 이어서 경로를 붙인다.
 
RtlMultiByteToUnicodeN 함수는 버퍼로 온 char* 형 문자를 unicodestring 형태로 바꾸어준다.
이후 SetSystem32Path(unicodeString); 으로 경로를 해당해준다.
 
 
 
 

최종 구현 성공

 
notion image
 
최종적으로 완성했다. 내가 직접 만든 핸들 컨트롤러.exe에 대해 성공적으로 handle을 접근 거부 하였다.
 
 
간이 테스트를 해본 결과
notion image
 
원래는 다음과 같이 17 → 999 로 해당 메모리를 wpm 공격을 성공할 수 있었다. 자 , 그럼 이제 드라이버를 올린 뒤 해보자
 
notion image
 
 
완벽히 막아내었다.
 
 
 
 
 
 
 

PalGuard.exe 의 역할

 
notion image
 
일단, 구현 이유는 뱅가드 처럼 유저모드에서의 인터페이스가 있었으면 좋겠다 싶어서 만들었다. 유저모드에서 인터페이스가 있으면 유저와의 통신도 가능 하기 때문.
그리고, 무엇 보다고 IOCTL로 프로세스와 드라이버간의 통신을 구현하고 싶었다.
성공적으로 구현을 완성해서 다행이다.