3.6 Windows驱动开发:内核进程汇编与反汇编
在笔者上一篇文章《内核MDL读写进程内存》
简单介绍了如何通过MDL映射的方式实现进程读写操作,本章将通过如上案例实现远程进程反汇编功能,此类功能也是ARK工具中最常见的功能之一,通常此类功能的实现分为两部分,内核部分只负责读写字节集,应用层部分则配合反汇编引擎对字节集进行解码,此处我们将运用capstone
引擎实现这个功能。
首先是实现驱动部分,驱动程序的实现是一成不变的,仅仅只是做一个读写功能即可,完整的代码如下所示;
#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
这款反汇编引擎吧!
3.6.1 内存反汇编的应用
Capstone 是一款轻量级、多平台、多架构的反汇编引擎,旨在成为二进制分析和反汇编的终极工具。它支持多种平台和架构的反汇编,包括x86、ARM、MIPS等,并且可以轻松地集成到各种二进制分析工具中。Capstone的主要优点是它易于使用和快速的反汇编速度,而且由于其开源和活跃的社区支持,可以很容易地更新和维护。因此,Capstone被广泛用于二进制分析、安全研究和反汇编工作中。
- 反汇编引擎GitHub地址:https://github.com/capstone-engine
这款反汇编引擎如果你想要使用它,则第一步就是调用cs_open()
打开一个句柄,这个打开功能的函数原型如下所示;
cs_err cs_open(
cs_arch arch,
cs_mode mode,
csh *handle
);
- 参数 arch:指定架构类型,例如
CS_ARCH_X86
表示为 x86 架构。 - 参数 mode:指定模式,例如
CS_MODE_32
表示为 32 位模式。 - 参数 handle:打开的句柄,用于后续对引擎的调用。由于其是传递指针的方式,因此需要先分配好该指针的内存。函数执行成功后,该句柄将被填充,可以用于后续的反汇编操作。
函数cs_open()
是Capstone
反汇编引擎提供的,它用于初始化Capstone
库并打开一个句柄,以便进行后续的反汇编操作。该函数有三个参数,分别是架构类型、执行模式和指向句柄的指针。
具体地说,第一个参数CS_ARCH_X86
指定了反汇编的架构类型,这里表示为Windows平台;第二个参数CS_MODE_32
或CS_MODE_64
则指定了反汇编的执行模式,即32位模式或64位模式;第三个参数则是指向一个Capstone
库句柄的指针,通过该指针可以进行后续的反汇编操作。
打开句柄后,我们可以使用其他的Capstone
函数进行反汇编操作,比如cs_disasm()
函数用于对二进制代码进行反汇编,反汇编后的结果可以用于分析和理解程序的行为。最后,我们还需要使用cs_close()
函数关闭打开的句柄以释放资源。
第二步也是最重要的一步,调用cs_disasm()
反汇编函数,函数返回实际反汇编的指令数,或者如果发生错误,则返回0。该函数的原型如下所示;
size_t cs_disasm(
csh handle,
const uint8_t *code,
size_t code_size,
uint64_t address,
size_t count,
cs_insn *insn
);
其中各参数的含义为:
- 参数 handle:要使用的Capstone引擎的句柄,指定dasm_handle反汇编句柄
- 参数 code:要反汇编的二进制代码的指针,定你要反汇编的数据集或者是一个缓冲区
- 参数 code_size:要反汇编的二进制代码的大小(以字节为单位),指定你要反汇编的长度64
- 参数 address:要反汇编的二进制代码在内存中的地址(用于计算跳转目标地址),输出的内存地址起始位置 0x401000
- 参数 count:要反汇编的指令数量限制。如果设置为0,则表示没有数量限制,将会反汇编所有有效的指令
- 参数 insn:用于存储反汇编结果的结构体数组。它是一个输出参数,由调用者分配内存。用于输出数据的一个指针
如上所示的cs_open()
以及cs_disasm()
两个函数如果能搞明白,那么反汇编完整代码即可写出来了,根据如下流程实现;
- 创建一个句柄
handle
,用于连接到驱动程序。 - 定义
ProcessData
结构体,包含需要读取的进程 ID、起始地址、读取的字节数以及存储读取结果的BYTE
数组。 - 使用
DeviceIoControl()
函数从指定进程读取机器码,将结果存储到data
结构体的data
字段中。 - 使用
cs_open()
函数打开Capstone
引擎的句柄dasm_handle
,指定了架构为x86
平台,模式为32
位。 - 使用
cs_disasm()
函数将data
结构体中的机器码进行反汇编,将结果存储到insn
数组中,同时返回反汇编指令的数量 count。 - 循环遍历
insn
数组,将每个反汇编指令的地址、长度、助记符和操作数打印出来。 - 使用
cs_free()
函数释放insn
数组占用的内存。 - 使用
cs_close()
函数关闭Capstone
引擎的句柄 dasm_handle。 - 关闭连接到驱动程序的句柄
handle
。
根据如上实现流程,我们可以写出如下代码片段;
#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
字节。
3.6.2 内存汇编的应用
实现了反汇编接着就需要讲解如何对内存进行汇编操作,汇编引擎这里采用了XEDParse
该引擎小巧简洁,著名的x64dbg
就是在运用本引擎进行汇编替换的,XEDParse 是一个开源的汇编引擎,用于将汇编代码转换为二进制指令。它基于Intel
的XED库,并提供了一些易于使用的接口。
- 汇编引擎GitHub地址:https://github.com/x64dbg/XEDParse
一般而言,再进行汇编转换之前需要做如下几个步骤的工作;
1.定义xed_state_t
结构体,该结构体包含有关目标平台的信息,例如处理器架构和指令集。可以使用xed_state_zero()
函数来初始化该结构体。
xed_state_t state;
xed_state_zero(&state);
state.mmode = XED_MACHINE_MODE_LONG_64;
state.stack_addr_width = XED_ADDRESS_WIDTH_64b;
2.定义xed_error_enum_t
类型的变量来接收转换过程中可能出现的错误信息。
xed_error_enum_t error = XED_ERROR_NONE;
3.定义xed_encoder_request_t
结构体,该结构体包含要转换的汇编指令的信息,例如操作码和操作数。
xed_encoder_request_t request;
xed_encoder_request_zero_set_mode(&request, &state);
request.iclass = XED_ICLASS_MOV;
request.operand_order[0] = 0;
request.operand_order[1] = 1;
request.operands[0].name = XED_REG_RAX;
request.operands[1].name = XED_REG_RBX;
4.使用XEDParseAssemble()
函数将汇编代码转换为二进制指令,并将结果存储在xed_uint8_t
类型的数组中。此函数返回转换后的指令长度。
xed_uint8_t binary[15];
xed_uint_t length = XEDParseAssemble(&request, binary, sizeof(binary), &error);
if (error != XED_ERROR_NONE) {
// handle error
}
5.使用转换后的二进制指令进行后续操作。
typedef int (*func_t)(void);
func_t func = (func_t)binary;
int result = func();
在本次转换流程中我们只需要向XEDParseAssemble()
函数传入一个规范的结构体即可完成转换,通过向XEDPARSE
结构传入需要转换的指令,并自动转换为机器码放入到data.data
堆中,实现核心代码如下所示;
#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
的指令集机器码,如下图已经完美写入。
3.6 Windows驱动开发:内核进程汇编与反汇编的更多相关文章
- Windows驱动开发(中间层)
Windows驱动开发 一.前言 依据<Windows内核安全与驱动开发>及MSDN等网络质料进行学习开发. 二.初步环境 1.下载安装WDK7.1.0(WinDDK\7600.16385 ...
- windows驱动开发推荐书籍
[作者] 猪头三 个人网站 :http://www.x86asm.com/ [序言] 很多人都对驱动开发有兴趣,但往往找不到正确的学习方式.当然这跟驱动开发的本土化资料少有关系.大多学的驱动开发资料都 ...
- C++第三十三篇 -- 研究一下Windows驱动开发(一)内部构造介绍
因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚.因此想看一下驱动开发.查了很多资料,看到有人推荐Windows驱动 ...
- [Windows驱动开发](一)序言
笔者学习驱动编程是从两本书入门的.它们分别是<寒江独钓——内核安全编程>和<Windows驱动开发技术详解>.两本书分别从不同的角度介绍了驱动程序的制作方法. 在我理解,驱动程 ...
- Windows 驱动开发 - 5
上篇<Windows 驱动开发 - 4>我们已经完毕了硬件准备. 可是我们还没有详细的数据操作,比如接收读写操作. 在WDF中进行此类操作前须要进行设备的IO控制,已保持数据的完整性. 我 ...
- windows 驱动开发入门——驱动中的数据结构
最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书--<独钓寒江 windows安全编程> 和 <windows驱动 ...
- Windows驱动——读书笔记《Windows驱动开发技术详解》
=================================版权声明================================= 版权声明:原创文章 谢绝转载 请通过右侧公告中的“联系邮 ...
- Windows 驱动开发 - 7
在<Windows 驱动开发 - 5>我们所说的读写操作在本篇实现. 在WDF中实现此功能主要为:EvtIoRead和EvtIoWrite. 首先,在EvtDeviceAdd设置以上两个回 ...
- Windows 驱动开发 - 8
最后的一点开发工作:跟踪驱动. 一.驱动跟踪 1. 包括TMH头文件 #include "step5.tmh" 2. 初始化跟踪 在DriverEntry中初始化. WPP_INI ...
- Windows驱动开发-IRP的完成例程
<Windows驱动开发技术详解 >331页, 在将IRP发送给底层驱动或其他驱动之前,可以对IRP设置一个完成例程,一旦底层驱动将IRP完成后,IRP完成例程立刻被处罚,通过设置完成例程 ...
随机推荐
- VWAP 订单的最佳执行方法:随机控制法
更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流. 引言:相关研究 在当今的投资领域,算法交易正迅速成为客户获取和清算股票头寸的首选方法. 通常,被委托者会 ...
- UVA - 10935:Throwing cards away I (简单模拟)
题目大意 桌上有一叠牌,自上而下编号为1~n.若桌上牌数大于1张,那么丢弃一张顶部牌后,再将现在的顶部牌移到最后.要求给出模拟过程和最终剩余的牌号 思路分析 典型队列模拟,丢弃即出队,移到最后即入队, ...
- Round A 2021 - Kick Start 2021
比赛链接:https://codingcompetitions.withgoogle.com/kickstart/round/0000000000436140 K-Goodness String (5 ...
- java对excle操作:下载、上传以及上传中错误数据动态生成excle给用户下载
工作中经常遇到excle文件的上传下载,这里就总结一下相关的操作,尤其是最后一个方法"上传excle文件校验数据格式,挑出格式错误的数据"网上没有找到相关的例子,自己组合改写了一下 ...
- Java面试——VUE2&VUE3概览
一.VUE2.0 1.对于MVVM的理解 MVVM 是 Model-View-ViewModel 的缩写. Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑: View 代表U ...
- Threejs实现一个园区
一.实现方案 单独贴代码可能容易混乱,所以这里只讲实现思路,代码放在最后汇总了下. 想要实现一个简单的工业园区.主要包含的内容是一个大楼.左右两片停车位.四条道路以及多个可在道路上随机移动的车辆.遇到 ...
- Vue2知识点简要
一.双向绑定原理 Vue2采用的是观察者-发布订阅模式,利用Object.defineProperty实现对数据已定义属性的监控(定义观察者模式), 编译DOM时解析v-model等属性以及对inpu ...
- Nacos源码 (7) Nacos与Spring
SpringCloud工程可以使用Nacos作为注册中心和配置中心,配置和使用非常简单,本文将简单介绍使用方式,并分析其实现方式. SpringCloud工程集成Nacos SpringCloud工程 ...
- SpringBoot01:HelloWorld!
回顾Spring Spring是一个开源框架,2003年兴起的一个轻量级的Java开发框架. Spring是为了解决企业级应用开发的复杂性而创建的,简化开发. Spring是怎样简化Java开发的呢? ...
- 【Mysql系列】(一)MySQL语句执行流程
首发博客地址 首发博客地址 系列文章地址 参考文章 MySQL 逻辑架构 连接器 连接命令一般是这么写的 mysql -h$ip -P$port -u$user -p 那么 什么是连接器? MySQL ...