커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램
아래의 글을 쓰다 보니,
User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
; https://www.sysnet.pe.kr/2/0/12102
커널 메모리 값을 읽기 위해 windbg의 로컬 커널 디버그 모드로 진입해야 하고, 다시 이것을 위해 UEFI Secure Boot도 꺼야 하고 "bcdedit -debug on"를 실행해 재부팅까지 해야 하는 불편함이 있습니다. 그래서 이런 불편함을 덜기 위해 ^^ 그냥 커널 메모리를 읽고 쓸 수 있는 device driver를 만들었습니다.
단순히 메모리를 읽고 쓰는 거라 예전에 만들어 둔,
NT Legacy 드라이버를 이용하여 C#에서 Port 입출력
; https://www.sysnet.pe.kr/2/0/932
소스 코드와 별반 다르지 않게 구현할 수 있었는데요. 게다가
Visual Studio 2019 + WDK의 도움으로 다음과 같이 금방 만들 수 있었습니다.
...[생략]...
NTSTATUS MajorDeviceControl(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
PIO_STACK_LOCATION irpSp;
NTSTATUS ntStatus = STATUS_SUCCESS;
ULONG inBufLength; /* Input buffer length */
ULONG outBufLength; /* Output buffer length */
PVOID ioBuffer;
ULONG ptrSize = sizeof(void*);
irpSp = IoGetCurrentIrpStackLocation(pIrp);
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;
ioBuffer = pIrp->AssociatedIrp.SystemBuffer;
pIrp->IoStatus.Information = 0; /* Output Buffer Size */
PMEMORYIO_DEVICE_EXTENSION deviceExtension = (PMEMORYIO_DEVICE_EXTENSION)pDeviceObject->DeviceExtension;
switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_READ_MEMORY:
if (inBufLength != ptrSize)
{
ntStatus = STATUS_BUFFER_TOO_SMALL;
}
else
{
PUCHAR ptr = BytesToPtr(ioBuffer);
deviceExtension->Position = ptr;
/*
PUCHAR outBuffer = (PUCHAR)ioBuffer;
for (ULONG i = 0; i < outBufLength; i++)
{
outBuffer[i] = *(ptr + i);
}
*/
MM_COPY_ADDRESS address;
address.VirtualAddress = ptr;
SIZE_T numberOfBytesTransferred = 0;
MmCopyMemory(ioBuffer, address, outBufLength, MM_COPY_MEMORY_VIRTUAL, &numberOfBytesTransferred);
pIrp->IoStatus.Information = numberOfBytesTransferred;
ntStatus = STATUS_SUCCESS;
}
break;
...[생략]...
case IOCTL_WRITE_MEMORY:
if (inBufLength == 0)
{
ntStatus = STATUS_BUFFER_TOO_SMALL;
}
else
{
PUCHAR ptr = deviceExtension->Position;
/*
PUCHAR inBuffer = (PUCHAR)ioBuffer;
for (ULONG i = 0; i < inBufLength; i++)
{
*(ptr + i) = inBuffer[i];
}
*/
MM_COPY_ADDRESS address;
address.VirtualAddress = ioBuffer;
SIZE_T numberOfBytesTransferred = 0;
MmCopyMemory(ptr, address, inBufLength, MM_COPY_MEMORY_VIRTUAL, &numberOfBytesTransferred);
pIrp->IoStatus.Information = numberOfBytesTransferred;
ntStatus = STATUS_SUCCESS;
}
break;
default:
ntStatus = STATUS_UNSUCCESSFUL;
break;
}
pIrp->IoStatus.Status = ntStatus;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return ntStatus;
}
...[생략]...
테스트도 해봐야겠지요. ^^ 클라이언트는 C#으로 DeviceIoControl을 DllImport로 연결하면 다음과 같이 쉽게 구현할 수 있습니다.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Net;
namespace MemoryIOLib
{
class MemoryIOLib : IDisposable
{
const uint IOCTL_READ_MEMORY = 0x9c402410;
const uint IOCTL_WRITE_MEMORY = 0x9c402414;
const uint IOCTL_GETPOS_MEMORY = 0x9c402418;
const uint IOCTL_SETPOS_MEMORY = 0x9c40241c;
SafeFileHandle fileHandle;
public MemoryIOLib()
{
InitializeDevice();
}
public bool InitializeDevice()
{
Dispose();
fileHandle = Kernel32.CreateFile(@"\\.\KernelMemoryIO", NativeFileAccess.FILE_GENERIC_READ,
NativeFileShare.NONE, IntPtr.Zero, NativeFileMode.OPEN_EXISTING, NativeFileFlag.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);
if (fileHandle.IsInvalid == true)
{
return false;
}
return true;
}
public void Dispose()
{
if (fileHandle != null)
{
fileHandle.Close();
fileHandle = null;
}
}
public bool IsInitialized
{
get
{
return fileHandle != null && fileHandle.IsInvalid == false;
}
}
public IntPtr Position
{
get
{
if (this.IsInitialized == false)
{
return IntPtr.Zero;
}
byte[] addressBytes = new byte[IntPtr.Size];
int pBytesReturned;
if (Kernel32.DeviceIoControl(fileHandle, IOCTL_GETPOS_MEMORY,
null, 0, addressBytes, addressBytes.Length, out pBytesReturned, IntPtr.Zero) == true)
{
if (IntPtr.Size == 8)
{
return new IntPtr(BitConverter.ToInt64(addressBytes, 0));
}
return new IntPtr(BitConverter.ToInt32(addressBytes, 0));
}
return IntPtr.Zero;
}
set
{
if (this.IsInitialized == false)
{
return;
}
byte[] addressBytes = null;
if (IntPtr.Size == 8)
{
addressBytes = BitConverter.GetBytes(value.ToInt64());
}
else
{
addressBytes = BitConverter.GetBytes(value.ToInt32());
}
int pBytesReturned;
Kernel32.DeviceIoControl(fileHandle, IOCTL_SETPOS_MEMORY, addressBytes, addressBytes.Length, null, 9,
out pBytesReturned, IntPtr.Zero);
}
}
internal int WriteMemory(IntPtr ptr, byte[] buffer)
{
if (this.IsInitialized == false)
{
return 0;
}
this.Position = ptr;
int pBytesReturned;
if (Kernel32.DeviceIoControl(fileHandle, IOCTL_WRITE_MEMORY, buffer, buffer.Length,
null, 0, out pBytesReturned, IntPtr.Zero) == true)
{
return pBytesReturned;
}
return 0;
}
public int ReadMemory(IntPtr position, byte [] buffer)
{
if (this.IsInitialized == false)
{
return 0;
}
byte[] addressBytes = null;
if (IntPtr.Size == 8)
{
addressBytes = BitConverter.GetBytes(position.ToInt64());
}
else
{
addressBytes = BitConverter.GetBytes(position.ToInt32());
}
int pBytesReturned;
if (Kernel32.DeviceIoControl(fileHandle, IOCTL_READ_MEMORY, addressBytes, addressBytes.Length,
buffer, buffer.Length,
out pBytesReturned, IntPtr.Zero) == true)
{
return pBytesReturned;
}
return 0;
}
}
}
자, 그럼 다시 Process Explorer를 실행시키고,
Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회
; https://www.sysnet.pe.kr/2/0/12099
원하는 프로세스의 handle 목록 중에서 "Thread" 유형인 것을 찾아냅니다. 당연히 해당 핸들의
Object Address는 _ETHREAD 구조체의 주소일 것이고, 따라서 다음과 같이 코딩을 하면,
// Install-Package KernelStructOffset
using System;
using KernelStructOffset;
namespace MemoryIOLib
{
class Program
{
static void Main(string[] args)
{
using (KernelMemoryIO memoryIO = new KernelMemoryIO())
{
if (memoryIO.IsInitialized == false)
{
Console.WriteLine("Failed to open device");
return;
}
// 0xFFFF878274668080 from process explorer (Handles view pane: Ctrl+H)
// It must be a Handle of Thead type.
IntPtr ethreadPtr = new IntPtr(unchecked((long)0xFFFF878274668080));
{
IntPtr clientIdPtr = ethreadPtr + 0x648;
byte[] buffer = new byte[16];
if (memoryIO.ReadMemory(clientIdPtr, buffer) != buffer.Length)
{
Console.WriteLine("failed to read");
return;
}
long value = BitConverter.ToInt64(buffer, 0);
Console.WriteLine("PID: " + value + "(" + value.ToString("x") + ")");
value = BitConverter.ToInt64(buffer, 8);
Console.WriteLine("TID: " + value + "(" + value.ToString("x") + ")");
}
}
}
}
}
/* 출력 결과
PID: 63740(f8fc)
TID: 72284(11a5c)
*/
_ETHREAD 구조체에 보관된 _CLIENT_ID 타입의 Cid 구조체 값을 접근할 수 있습니다. 즉, 사용자 모드(Ring 3) 프로그램에서 원하는 커널 메모리의 내용을 자유롭게 확인할 수 있는 것입니다.
이 글의 전체 소스 코드는 github에 올려 두었습니다.
stjeong/KernelMemoryIO
; https://github.com/stjeong/KernelMemoryIO
이 프로그램을 사용할 때 주의할 점이 있다면, 절대로 이렇게 만든 device driver를 "서명"까지 해서 제품에 포함하거나 공식적으로 배포하면 안 됩니다. 커널 메모리를 자유롭게 읽을 수 있게 만드는 것은 시스템에 심각한 보안 위협을 초래할 수 있으며 더군다나 서명까지 해서 배포하면 전 세계의 수많은 해커들이 여러분들이 배포한 sys 파일을 기반으로 더욱 나쁜 해악을 끼칠 수 있도록 도와주게 되는 것입니다.
설마 소스 코드도 없이 몰래 구현한 sys의 기능을 누가 알까...라고 안심하는 분이 있다면, 과거 capcom에서 자사의 제품에 배포한 capcom.sys에 사용자 정의 코드를 실행할 수 있는 것을 넣어두었던 것이 역공학으로 널리 ^^; 알려져,
Double KO! Capcom's Street Fighter V installs hidden rootkit on PCs
; https://www.theregister.co.uk/2016/09/23/capcom_street_fighter_v/
tandasat/ExploitCapcom
; https://github.com/tandasat/ExploitCapcom
사용되었던 사례를 간과해서는 안 될 것입니다.
그러니까,,, KernelMemoryIO는 공부나 테스트 용도로만 사용해야 합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]