驅動開發:核心實現程序組合與反組合

2023-05-23 18:00:55

在筆者上一篇文章《驅動開發:核心MDL讀寫程序記憶體》簡單介紹瞭如何通過MDL對映的方式實現程序讀寫操作,本章將通過如上案例實現遠端程序反組合功能,此類功能也是ARK工具中最常見的功能之一,通常此類功能的實現分為兩部分,核心部分只負責讀寫位元組集,應用層部分則配合反組合引擎對位元組集進行解碼,此處我們將運用capstone引擎實現這個功能。

首先是實現驅動部分,驅動程式的實現是一成不變的,僅僅只是做一個讀寫功能即可,完整的程式碼如下所示;

// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
#include <ntifs.h>
#include <windef.h>

#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)

#define DEVICENAME L"\\Device\\ReadWriteDevice"
#define SYMBOLNAME L"\\??\\ReadWriteSymbolName"

typedef struct
{
	DWORD pid;       // 程序PID
	UINT64 address;  // 讀寫地址
	DWORD size;      // 讀寫長度
	BYTE* data;      // 讀寫資料集
}ProcessData;

// MDL讀取封裝
BOOLEAN ReadProcessMemory(ProcessData* ProcessData)
{
	BOOLEAN bRet = TRUE;
	PEPROCESS process = NULL;

	// 將PID轉為EProcess
	PsLookupProcessByProcessId(ProcessData->pid, &process);
	if (process == NULL)
	{
		return FALSE;
	}

	BYTE* GetProcessData = NULL;
	__try
	{
		// 分配堆空間 NonPagedPool 非分頁記憶體
		GetProcessData = ExAllocatePool(NonPagedPool, ProcessData->size);
	}
	__except (1)
	{
		return FALSE;
	}

	KAPC_STATE stack = { 0 };
	// 附加到程序
	KeStackAttachProcess(process, &stack);

	__try
	{
		// 檢查程序記憶體是否可讀取
		ProbeForRead(ProcessData->address, ProcessData->size, 1);

		// 完成拷貝
		RtlCopyMemory(GetProcessData, ProcessData->address, ProcessData->size);
	}
	__except (1)
	{
		bRet = FALSE;
	}

	// 關閉參照
	ObDereferenceObject(process);

	// 解除附加
	KeUnstackDetachProcess(&stack);

	// 拷貝資料
	RtlCopyMemory(ProcessData->data, GetProcessData, ProcessData->size);

	// 釋放堆
	ExFreePool(GetProcessData);
	return bRet;
}

// MDL寫入封裝
BOOLEAN WriteProcessMemory(ProcessData* ProcessData)
{
	BOOLEAN bRet = TRUE;
	PEPROCESS process = NULL;

	// 將PID轉為EProcess
	PsLookupProcessByProcessId(ProcessData->pid, &process);
	if (process == NULL)
	{
		return FALSE;
	}

	BYTE* GetProcessData = NULL;
	__try
	{
		// 分配堆
		GetProcessData = ExAllocatePool(NonPagedPool, ProcessData->size);
	}
	__except (1)
	{
		return FALSE;
	}

	// 迴圈寫出
	for (int i = 0; i < ProcessData->size; i++)
	{
		GetProcessData[i] = ProcessData->data[i];
	}

	KAPC_STATE stack = { 0 };

	// 附加程序
	KeStackAttachProcess(process, &stack);

	// 分配MDL物件
	PMDL mdl = IoAllocateMdl(ProcessData->address, ProcessData->size, 0, 0, NULL);
	if (mdl == NULL)
	{
		return FALSE;
	}

	MmBuildMdlForNonPagedPool(mdl);

	BYTE* ChangeProcessData = NULL;

	__try
	{
		// 鎖定地址
		ChangeProcessData = MmMapLockedPages(mdl, KernelMode);

		// 開始拷貝
		RtlCopyMemory(ChangeProcessData, GetProcessData, ProcessData->size);
	}
	__except (1)
	{
		bRet = FALSE;
		goto END;
	}

	// 結束釋放MDL關閉參照取消附加
END:
	IoFreeMdl(mdl);
	ExFreePool(GetProcessData);
	KeUnstackDetachProcess(&stack);
	ObDereferenceObject(process);

	return bRet;
}

NTSTATUS DriverIrpCtl(PDEVICE_OBJECT device, PIRP pirp)
{
	PIO_STACK_LOCATION stack;
	stack = IoGetCurrentIrpStackLocation(pirp);
	ProcessData* ProcessData;

	switch (stack->MajorFunction)
	{

	case IRP_MJ_CREATE:
	{
		break;
	}

	case IRP_MJ_CLOSE:
	{
		break;
	}

	case IRP_MJ_DEVICE_CONTROL:
	{
		// 獲取應用層傳值
		ProcessData = pirp->AssociatedIrp.SystemBuffer;

		DbgPrint("程序ID: %d | 讀寫地址: %p | 讀寫長度: %d \n", ProcessData->pid, ProcessData->address, ProcessData->size);

		switch (stack->Parameters.DeviceIoControl.IoControlCode)
		{
		// 讀取函數
		case READ_PROCESS_CODE:
		{
			ReadProcessMemory(ProcessData);
			break;
		}
		// 寫入函數
		case WRITE_PROCESS_CODE:
		{
			WriteProcessMemory(ProcessData);
			break;
		}

		}

		pirp->IoStatus.Information = sizeof(ProcessData);
		break;
	}

	}

	pirp->IoStatus.Status = STATUS_SUCCESS;
	IoCompleteRequest(pirp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
	if (driver->DeviceObject)
	{
		UNICODE_STRING SymbolName;
		RtlInitUnicodeString(&SymbolName, SYMBOLNAME);

		// 刪除符號連結
		IoDeleteSymbolicLink(&SymbolName);
		IoDeleteDevice(driver->DeviceObject);
	}
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	NTSTATUS status = STATUS_SUCCESS;
	PDEVICE_OBJECT device = NULL;
	UNICODE_STRING DeviceName;

	DbgPrint("[LyShark] hello lyshark.com \n");

	// 初始化裝置名
	RtlInitUnicodeString(&DeviceName, DEVICENAME);

	// 建立裝置
	status = IoCreateDevice(Driver, sizeof(Driver->DriverExtension), &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &device);
	if (status == STATUS_SUCCESS)
	{
		UNICODE_STRING SymbolName;
		RtlInitUnicodeString(&SymbolName, SYMBOLNAME);

		// 建立符號連結
		status = IoCreateSymbolicLink(&SymbolName, &DeviceName);

		// 失敗則刪除裝置
		if (status != STATUS_SUCCESS)
		{
			IoDeleteDevice(device);
		}
	}

	// 派遣函數初始化
	Driver->MajorFunction[IRP_MJ_CREATE] = DriverIrpCtl;
	Driver->MajorFunction[IRP_MJ_CLOSE] = DriverIrpCtl;
	Driver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverIrpCtl;

	// 解除安裝驅動
	Driver->DriverUnload = UnDriver;

	return STATUS_SUCCESS;
}

上方的驅動程式很簡單關鍵部分已經做好了備註,此類驅動換湯不換藥沒啥難度,接下來才是本節課的重點,讓我們開始瞭解一下Capstone這款反組合引擎吧,Capstone是一個輕量級的多平臺、多架構的反組合框架。Capstone旨在成為安全社群中二進位制分析和反組合的終極反組合引擎,該引擎支援多種平臺的反組合,非常推薦使用。

這款反組合引擎如果你想要使用它則第一步就是呼叫cs_open()官方對其的解釋是開啟一個控制程式碼,這個開啟功能其中的引數如下所示;

  • 引數1:指定模式 CS_ARCH_X86 表示為Windows平臺
  • 引數2:執行位數 CS_MODE_32為32位元模式,CS_MODE_64為64位元
  • 引數3:開啟後儲存的控制程式碼&dasm_handle

第二步也是最重要的一步,呼叫cs_disasm()反組合函數,該函數的解釋如下所示;

  • 引數1:指定dasm_handle反組合控制程式碼
  • 引數2:指定你要反組合的資料集或者是一個緩衝區
  • 引數3:指定你要反組合的長度 64
  • 引數4:輸出的記憶體地址起始位置 0x401000
  • 引數5:預設填充為0
  • 引數6:用於輸出資料的一個指標

這兩個函數如果能搞明白,那麼如下反組合完整程式碼也就可以理解了。

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>
#include <inttypes.h>
#include <capstone/capstone.h>

#pragma comment(lib,"capstone64.lib")

#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)

typedef struct
{
	DWORD pid;
	UINT64 address;
	DWORD size;
	BYTE* data;
}ProcessData;

int main(int argc, char* argv[])
{
	// 連線到驅動
	HANDLE handle = CreateFileA("\\??\\ReadWriteSymbolName", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	ProcessData data;
	DWORD dwSize = 0;

	// 指定需要讀寫的程序
	data.pid = 6932;
	data.address = 0x401000;
	data.size = 64;

	// 讀取機器碼到BYTE位元組陣列
	data.data = new BYTE[data.size];
	DeviceIoControl(handle, READ_PROCESS_CODE, &data, sizeof(data), &data, sizeof(data), &dwSize, NULL);
	for (int i = 0; i < data.size; i++)
	{
		printf("0x%02X ", data.data[i]);
	}

	printf("\n");

	// 開始反組合
	csh dasm_handle;
	cs_insn *insn;
	size_t count;

	// 開啟控制程式碼
	if (cs_open(CS_ARCH_X86, CS_MODE_32, &dasm_handle) != CS_ERR_OK)
	{
		return 0;
	}

	// 反組合程式碼
	count = cs_disasm(dasm_handle, (unsigned char *)data.data, data.size, data.address, 0, &insn);

	if (count > 0)
	{
		size_t index;
		for (index = 0; index < count; index++)
		{
			/*
			for (int x = 0; x < insn[index].size; x++)
			{
				printf("機器碼: %d -> %02X \n", x, insn[index].bytes[x]);
			}
			*/

			printf("地址: 0x%"PRIx64" | 長度: %d 反組合: %s %s \n", insn[index].address, insn[index].size, insn[index].mnemonic, insn[index].op_str);
		}
		cs_free(insn, count);
	}
	cs_close(&dasm_handle);

	getchar();
	CloseHandle(handle);
	return 0;
}

通過驅動載入工具載入WinDDK.sys然後在執行本程式,你會看到正確的輸出結果,反組合當前位置處向下64位元組。

說完了反組合接著就需要講解如何對記憶體進行組合操作了,組合引擎這裡採用了XEDParse該引擎小巧簡潔,著名的x64dbg就是在運用本引擎進行組合替換的,本引擎的使用非常簡單,只需要向XEDParseAssemble()函數傳入一個規範的結構體即可完成轉換,完整程式碼如下所示。

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>

extern "C"
{
#include "D:/XEDParse/XEDParse.h"
#pragma comment(lib, "D:/XEDParse/XEDParse_x64.lib")
}

using namespace std;

#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)

typedef struct
{
	DWORD pid;
	UINT64 address;
	DWORD size;
	BYTE* data;
}ProcessData;

int main(int argc, char* argv[])
{
	// 連線到驅動
	HANDLE handle = CreateFileA("\\??\\ReadWriteSymbolName", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	ProcessData data;
	DWORD dwSize = 0;

	// 指定需要讀寫的程序
	data.pid = 6932;
	data.address = 0x401000;
	data.size = 0;

	XEDPARSE xed = { 0 };
	xed.x64 = FALSE;

	// 輸入一條組合指令並轉換
	scanf_s("%llx", &xed.cip);
	gets_s(xed.instr, XEDPARSE_MAXBUFSIZE);
	if (XEDPARSE_OK != XEDParseAssemble(&xed))
	{
		printf("指令錯誤: %s\n", xed.error);
	}

	// 生成堆
	data.data = new BYTE[xed.dest_size];

	// 設定長度
	data.size = xed.dest_size;

	for (size_t i = 0; i < xed.dest_size; i++)
	{
		// 替換到堆中
		printf("%02X ", xed.dest[i]);
		data.data[i] = xed.dest[i];
	}

	// 呼叫控制器,寫入到遠端記憶體
	DeviceIoControl(handle, WRITE_PROCESS_CODE, &data, sizeof(data), &data, sizeof(data), &dwSize, NULL);

	printf("[LyShark] 指令集已替換. \n");
	getchar();
	CloseHandle(handle);
	return 0;
}

通過驅動載入工具載入WinDDK.sys然後在執行本程式,你會看到正確的輸出結果,可開啟反核心工具驗證是否改寫成功。

開啟反核心工具,並切換到觀察是否寫入了一條mov eax,1的指令集機器碼,如下圖已經完美寫入。