Reversing/Kernel

[ Window Device Driver ] 4. 프로세스 리스트 출력 : ZwQuerySystemInformation

목적

이번엔 커널레벨에서 프로세스 목록을 가져와 디버깅 메시지로 출력하는 동작을 수행해보려고 한다.

조건이 몇가지 있는데

  1. 오프셋 하드코딩 하지 말것
    • 프로세스별로 오프셋 n바이트 위치에 있는 값을 가져오지 않게
    • 특정할 수 있는 기준 오프셋을 항상 동일하게 뽑을수 있는 경우를 상정하기
  2. 호환성 준수할것
    • OS 버전에 따라 사용하는 구조체나 함수가 최대한 달라지지 않을 것
    • 다른 OS 커널 버전에서 사용하지 않는 구조의 사용은 피할 것
  3. 최대한 알려진 구조를 사용할 것
    • 공식 문서에 나와있는 구조체와 함수 사용

아래 웹사이트에서 커널 버전별로 문서화되지 않은 커널 구조들에 대해 찾아볼 수 있다.

https://www.vergiliusproject.com

 

Vergilius Project

Take a look into the depths of Windows kernels and reveal more than 60000 undocumented structures.

www.vergiliusproject.com

 

이를 참조하면 2번과 3번 조건의 만족이 좀 더 용이할 듯.

이러한 조건들을 만족하면서 프로세스 목록을 띄워보자.


프로세스 구조 - Windbg

프로세스 목록을 띄우려면 일단 프로세스가 메모리에 올라갔을 때 어떻게 보이는지 부터 알아놔야 어떤 값을 찾아올지를 결정해볼 수 있을 것 같다.

!process

  • 프로세스의 정보 조회하는 명령어
  • !process 0 0 으로 모든 프로세스 목록 조회 가능
0: kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS ffff958f0f6aa040
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad002  ObjectTable: ffffab0ab4e47e40  HandleCount: 2335.
    Image: System

PROCESS ffff958f0f6db080
    SessionId: none  Cid: 005c    Peb: 00000000  ParentCid: 0004
    DirBase: 012f2002  ObjectTable: ffffab0ab4e80cc0  HandleCount:   0.
    Image: Registry

PROCESS ffff958f11633080
    SessionId: none  Cid: 0150    Peb: 3d94db9000  ParentCid: 0004
    DirBase: 02b9f002  ObjectTable: ffffab0ab57a7180  HandleCount:  53.
    Image: smss.exe

PROCESS ffff958f116c3140
    SessionId: 0  Cid: 01a4    Peb: fdad173000  ParentCid: 019c
    DirBase: 234b0e002  ObjectTable: ffffab0ab8692b00  HandleCount: 496.
    Image: csrss.exe

PROCESS ffff958f12233080
    SessionId: 0  Cid: 01f0    Peb: ddd242c000  ParentCid: 019c
    DirBase: 22fa33002  ObjectTable: ffffab0ab8694480  HandleCount: 169.
    Image: wininit.exe
  • 프로세스 주소, PID, PEB, 부모 PID, 이미지, 오브젝트 테이블 등의 정보를 조회할 수 있다.
  • 이중에 Image 가 프로세스 이름이 들어있는 부분
  • 프로세스 주소를 입력하면 해당 프로세스 내 쓰레드 정보도 조회할 수 있다.
  • dt _EPROCESS 프로세스 주소 명령어를 입력해 프로세스 구조체를 조회할 수 있다.
+0x440 UniqueProcessId  : 0x00000000`00000004 Void
+0x5a8 ImageFileName    : [15]  "System"

뽑아올 내용은 UniqueProcessId 와 ImageFileName.

구조체상의 저 두가지 내용을 다른 함수나 구조체에 명시된(혹은 반환하는) 오프셋을 통해 동적으로 접근 가능한 방법을 찾아야한다.


ZwQuerySystemInformation - 예제코드

//프로세스 목록 가져오는 함수 - ZwQuerySystemInformation 함수 
void getProcessList() {
	NTSTATUS retStatus = STATUS_SUCCESS;
	ULONG BufferSize, BackwardPID = 0;

	//전달받을 데이터 크기 구함
	retStatus = ZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation, NULL, 0, &BufferSize);
	DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== Sysinfo Class : %d ====== \n", (SYSTEM_INFORMATION_CLASS)SystemProcessInformation);
	DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== BufferSize : %ld ====== \n", BufferSize);
	DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== NT RETURN : %x ====== \n", retStatus);
	//반환값 체크 
	if (retStatus == STATUS_INFO_LENGTH_MISMATCH) {
		dmsg("Get Buffer Size Complete!");
		if (BufferSize) {
			//커널 메모리 동적할당 
			PVOID PoolMem = ExAllocatePoolWithTag(PagedPool, BufferSize, POOL_TAG);
			dmsg("Pool Memory Complete !");
			if (PoolMem) {
				//데이터 전달받음  
				retStatus = ZwQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation, PoolMem, BufferSize, &BufferSize);
				if (NT_SUCCESS(retStatus))				{
					dmsg("Get Process Info Complete!");
					//할당한 메모리 공간 SYSTEM_PROCESS_INFORMATION 구조체에 대입 
					SYSTEM_PROCINFO *ProcessEntry = (SYSTEM_PROCINFO*)PoolMem;
					do {
						ULONG TargetPID = 0;
						if (ProcessEntry->ImageName.Length) {
							TargetPID = (ULONG)ProcessEntry->UniqueProcessId;
							DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== DRIVER MESSAGE : PNAME - %ws | PID - %lx ====== \n",(ProcessEntry->ImageName.Buffer), TargetPID);
						}
						
						ProcessEntry = (SYSTEM_PROCINFO *)((BYTE*)ProcessEntry + ProcessEntry->NextEntryOffset);
						BackwardPID = TargetPID;
					} while (ProcessEntry->NextEntryOffset);
				}
			}
		}
	}
}

윈도우 8 이전까지 사용하던 함수로, 지정된 시스템 정보를 찾아 출력하는 역할을 한다.

NTSTATUS WINAPI ZwQuerySystemInformation(
  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,
  _Inout_   PVOID                    SystemInformation,
  _In_      ULONG                    SystemInformationLength,
  _Out_opt_ PULONG                   ReturnLength
);

해당 함수의 SystemInformationClass 인자로 5가 넘어올 경우 SYSTEM_PROCESS_INFORMATION 을 조회하며 프로세스 목록을 구조체 형식의 버퍼로 전달한다.

위 예제코드가 어떤 원리로 프로세스 목록을 가져오는지를 분석해보자.

if (ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &bufferSize) == STATUS_INFO_LENGTH_MISMATCH) {
  • ZwQuerySystemInformation 함수를 호출하지만 두번째, 세번째 인자가 각각 NULL과 0임
  • 네 번째 인자인 ReturnLength 는 선택적으로 전달됨
    • 요청된 내용에 대해 함수가 실제로 데이터를 write할 영역의 크기를 가지고 있다.
    • 해당 값이 세번째 인자인 SystemInformationLength 보다 작거나 같으면 복사가 가능하다는 의미 → 두번째 인자로 전달된 SystemInformation 에 정보를 복사한다.
    • 해당 값이 세번째 인자보다 크면 복사가 불가능하다는 의미 → NTSTATUS 에러코드를 반환하고, ReturnLength 인자에 실제 요청된 데이터의 크기를 복사한다.
  • 즉, 해당 코드는 실제 복사할 데이터의 크기를 모르는 상태에서 이를 얻기위해 실행된다.
  • 결과값이 STATUS_INFO_LENGTH_MISMATCH 인 경우라면 ReturnLength 인자에 실제 데이터의 크기가 복사되었을 것.
if (bufferSize) {
        PVOID memory = ExAllocatePoolWithTag(PagedPool, bufferSize, POOL_TAG);
  • 그렇게 buffersize가 구해진 경우에 ExAllocatePoolWithTag() 함수를 실행함
    • 커널 메모리를 동적으로 할당한다.
    • PagedPool - 할당될 메모리의 pool 유형
    • 위에서 구한 데이터의 크기만큼 메모리를 동적으로 할당한다.
    • 성공적으로 종료되면 할당된 메모리의 주소를 반환함.
if (memory) {
  ntstatus = ZwQuerySystemInformation(SystemProcessInformation, memory, bufferSize, &bufferSize);
    if (NT_SUCCESS(ntstatus)) {
      PSYSTEM_PROCESSES processEntry = memory;
  • 주소가 할당된 경우 ZwQuerySystemInformation() 함수를 다시 한번 호출
    • 세번째 인자와 네번째 인자에 같은값이 전달되었으므로 데이터 복사가 진행 될 것.
    • 동적으로 할당된 메모리 영역에 데이터를 복사한다.
  • 복사 성공한 경우 memory의 포인터를 ProcessEntry로 복사한다.
    • SYSTEM_PROCESS_INFORMATION 구조체처럼 사용할 수 있음.
//할당한 메모리 공간 SYSTEM_PROCESS_INFORMATION 구조체에 대입 
SYSTEM_PROCINFO *ProcessEntry = (SYSTEM_PROCINFO*)PoolMem;
do {
    ULONG TargetPID = 0;
    if (ProcessEntry->ImageName.Length) {
        TargetPID = (ULONG)ProcessEntry->UniqueProcessId;
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== DRIVER MESSAGE : PNAME - %ws | PID - %lx ====== \n",(ProcessEntry->ImageName.Buffer), TargetPID);
    }

    ProcessEntry = (SYSTEM_PROCINFO *)((BYTE*)ProcessEntry + ProcessEntry->NextEntryOffset);
    BackwardPID = TargetPID;
} while (ProcessEntry->NextEntryOffset);
  • 반복문을 돌면서 복사한 데이터를 ProcessEntry를 통해 구조체처럼 사용함
    • 유효한 ProcessName을 가진 경우
      • RtlStringCbPrintfA() 함수 실행 - 문자열 구성
      • 구조체를 참조해 ProcessName과 ProcessId를 가져와 문자열 구성하여 String에 저장
    • 문자열 구성 성공한 경우
      • RtlStringCbLengthA() 함수 실행 - 문자열 길이 구함
      • 구성한 string 문자열의 길이를 결정해 length 변수에 저장
      • strlen과 동일한 역할 수행
    • 문자열 길이 구한 경우
      • ZwWriteFile() 함수 실행 - 파일에 문자열 쓰기 수행
      • 파일 핸들, 상태블록, 문자열, 길이가 인자로 전달
      • 지정 파일에 문자열 쓰기 작업을 수행한다.
  • processEntry->NextEntryDelta 멤버에는 다음 systemprocess 주소의 오프셋이 담겨있다.
    • 해당 값을 참조하여 다음 프로세스 구조체 시작위치의 오프셋을 구한 후 연산하여 다음 프로세스에 접근한다.
    • 해당 값이 존재하지 않는다면 반복문을 종료한다.
ExFreePoolWithTag(memory, POOL_TAG);
  • ExFreePoolWithTag() 함수는 동적할당한 메모리를 할당 해제한다.

정리하면, 예제코드는 ZwQuerySystemInformation() 함수를 통해 프로세스 목록을 가져오고, 각 프로세스 구조체에 들어있는 프로세스 id와 이름을 알아낸 다음 , 다음순서 프로세스 구조체로 연속해 넘어가는 형태로 동작하여 전체 프로세스의 정보를 얻어낸다.


구현 방법

  • NtQuerySystemInformation 함수를 사용하면 예제코드와 동일하게 동작을 구현 가능하다.
    • 뒤에 설명
  • Process Hide 기법들이 이런 프로세스 목록을 조회하는 함수를 후킹하거나 구조체를 조작해 프로세스를 숨기곤 하므로, 이런 기법들 위주로 찾으면 여러 함수나 구조체를 찾아볼 수 있을것같다.
    • PsGetCurrentProcess() 매크로 함수를 사용하여 현재 쓰레드의 프로세스 목록을 가져와 EPROCESS 포인터를 획득할 수 있다.
      • EPROCESS와 그 안에 들어있는 KPROCESS 포인터는 커널 빌드에 따라 구성 형태가 바뀔 수 있어 오프셋을 바로 사용하는 방법은 바람직하지 않다.
      • 그러나 해당 구조체들의 특정 오프셋에 바로 접근하는 함수를 사용할 수만 있으면 기준점을 잡아 충분히 사용할 수 있다. 커널 빌드가 바뀌어 포인터 오프셋이 바뀌면 해당 함수들이 접근하는 오프셋도 바뀔 것이기 때문.
  • ntOSKernel
    • 현재 버전 OS 커널빌드에서 사용하는 구조체나 함수등은 system32/ntoskrnl.exe에 선언되어있고, 해당 라이브러리를 분석하면 어떤 키워드를 사용할 수 있는지를 알 수 있다.
      • 디버거를 통해 함수 구현부를 자세히 분석하면,
        • 내부적으로 호출하는 함수나 특정 오프셋에 접근하는 형태등을 알 수 있고,
        • 오브젝트나 구조체 내부 오프셋을 타고 넘어가면서 어떤 값들이 참조되고 인자로 넘어가는지의 연속적인 흐름을 알 수 있다.
        • 따라서, 어떤 함수를 사용하고, 이 함수를 통해 얻은 결과물의 어떤 부분을 어떻게 참조하여 어떤 값을 얻어낼 수 있는지를 미리 정해 어떤 함수를 사용할지 결정할 수 있게 된다.
  • 헤더선언때문에 발생하는 문제가 있다. ntddk와 winternl 은 같은 이름을 가진 선언들을 꽤 많이 공유하고 있어서, 두 헤더를 같이 사용하면 재정의 오류에 더불어 좀 많은 에러들이 발생한다.
    • 따라서, 현재 winternl 에서 필요로 하는 _SYSTEM_INFORMATION_CLASS 구조체와 _SYSTEM_PROCESS_INFORMATION 구조체는 직접 선언해서 쓴다.
    • NtQuerySystemInformation 함수는 ntkerel(ntoskrnl.exe) 에 포함된 export 함수이므로 함수 포인터만 선언하고 원형은 직접 불러와서 사용하자.
      • 유저모드에서는 GetModuleHandle() 이나 GetProcAddress() 함수를 통해 이런 형태의 동작을 구현할 수 있다.
      • MmGetSystemRoutineAddress() 함수를 사용해 GetProcAddress()와 동일한 동작을 구현할 수 있다.
      • GetProcAddress() 와는 살짝 다른게, 특정 DLL을 지정하지 않고 이름만 가지고 NtosKrnl에서 export 하는 함수를 바로 가져온다.
      RtlInitUnicodeString(&NtQuerySystemInformationName, L"NtQuerySystemInformation");
      NtQuerySystemInformation = (NtQuerySystemInformation_t) MmGetSystemRoutineAddress(&NtQuerySystemInformationName);

정리 - 구현

  1. ZwQuerySystemInformation 함수 이용
    • ntOSKernel.exe 의 Export 함수 이용
      • 함수 포인터를 MmGetSystemRoutineAddress() 실행하여 가져옴
    • 데이터 길이 계산
    • 실제 데이터 받아옴
  2. SYSTEM_PROCESS_INFORMATION 구조 이용
    • ImageNameProcessId 갖고 있음
    • NextEntryOffset 에 다음 프로세스까지의 오프셋 저장되어 있음
      • 해당 오프셋 가져와 현재 프로세스의 주소값과 더하면 다음 프로세스의 주소값 나옴
      • 연달아서 다음 프로세스 참조하는 형식으로 진행
  3. IOCTL 통한 호출
    • 유저모드 프로세스에서 드라이버로 IOCTL 코드 전송하여 동작 트리거

실습

  • 코드에서 가지고 온 주소값이 실제 함수의 주소인지 확인
    • 서로 일치하는것 확인된다.
  • NtQuerySystemInformation 함수가 정상작동하지 않는걸로 확인되어 값이 바뀔 BufferSize와 ntstatus를 확인
    • BufferSize 는 0으로, 함수가 정상 실행되지 않은것으로 보임
    • NT Return 이 0xc0000005 → STATUS_ACCESS_VIOLATION 오류.
      • Nt 접두사가 붙은 함수들은 인자들이 유저모드 메모리 공간에서 넘어오는지를 검증하고, 그렇지 않으면 에러를 뱉어낸다.
      • 즉, 커널모드에서 함수를 쓸때는 Nt~ 계열이 아닌 Zw~ 계열 함수를 사용해야 하는 것.
      • 최종적으로 ZwQuerySystemInformation() 함수를 사용하는 방식으로 변경.
  • ProcessEntry->ImageName 은 구조체 형식을 가지고 있고, .Buffer 멤버에 접근해야 WHAR 자료형 Imagename 텍스트를 가져올 수 있다.
DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "===== DRIVER MESSAGE : PNAME - %ws | PID - %ld ====== \n",(ProcessEntry->ImageName.Buffer), (long int)ProcessEntry->UniqueProcessId);

실습 완료


Github

https://github.com/synod2/Kernel_Hooking/tree/main/Window%20Device%20Driver/MyDriver3

 

GitHub - synod2/Kernel_Hooking: windows Kernel Hooking

windows Kernel Hooking. Contribute to synod2/Kernel_Hooking development by creating an account on GitHub.

github.com

 


참고 문서

https://docs.microsoft.com/ko-kr/windows-hardware/drivers/kernel/windows-kernel-mode-process-and-thread-manager

 

Windows Kernel-Mode Process and Thread Manager - Windows drivers

Windows Kernel-Mode Process and Thread Manager

docs.microsoft.com

https://www.vergiliusproject.com

 

Vergilius Project

Take a look into the depths of Windows kernels and reveal more than 60000 undocumented structures.

www.vergiliusproject.com

https://github.com/danielkrupinski/KernelProcessList

 

GitHub - danielkrupinski/KernelProcessList: Example Windows Kernel-mode Driver which enumerates running processes.

Example Windows Kernel-mode Driver which enumerates running processes. - GitHub - danielkrupinski/KernelProcessList: Example Windows Kernel-mode Driver which enumerates running processes.

github.com

https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/process.htm?ts=0,1000

 

SYSTEM_PROCESS_INFORMATION

Geoff Chappell, Software Analyst

www.geoffchappell.com

https://t0rchwo0d.github.io/windows/Windows-Anti-Reversing-Technique-Hide-Process/ - Process Hide

 

Windows - Anti-Reversing Technique: Hide Process

Anti-Reversing Technique: Hide Process0x00_Description별도의 Driver를 로드하는 프로그램 분석을 진행하다 보면 프로세스가 TaskManager에서 탐색되지 않는 경우가 존재한다. 이 기법을 DKOM(Direct Kernel Object Manipula

t0rchwo0d.github.io

http://alter.org.ua/docs/nt_kernel/procaddr/ - Using NT KernlMode Export

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmgetsystemroutineaddress - MmGetSystemRoutineAddress

 

MmGetSystemRoutineAddress function (wdm.h) - Windows drivers

The MmGetSystemRoutineAddress routine returns a pointer to a function specified by SystemRoutineName.

docs.microsoft.com

 

오 쓰고보니 100번째 글