Windows内核基础知识-8-监听进程、线程和模块

Windows内核有一种强大的机制,可以在重大事件发送时得到通知,比如这里的进程、线程和模块加载通知。

本次采用链表+自动快速互斥体来实现内核的主要架构。

进程通知

只要在内核里面注册了进程通知那么创建进程就会反馈给内核里面。

//注册/销毁进程通知函数
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
 PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,//回调函数
 BOOLEAN                           Remove//False表示注册,TRUE表示销毁
);
PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx;

void PcreateProcessNotifyRoutineEx(
 PEPROCESS Process,//得到的进程EPROCESS结构体
 HANDLE ProcessId,//得到的进程句柄
 PPS_CREATE_NOTIFY_INFO CreateInfo//得到的进程信息,如果是销毁就是NULL,创建就是一个指针
)
{...}

注意:在用到上述回调函数的驱动必须在PE的PE映像头里设有IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY标志,可以通过vs中的linker添加命令行:/integritycheck

实现进程通知

创建一个驱动项目,名为SysMon,文件结构图如下:

AutoLock和FastMutex是用来封装一个快速互斥体方便和保护多线程访问同一内容。pch是预编译头SysMonCommon.h是给User和Kernel公用的结构体文件,SysMon是驱动主要逻辑的代码文件。

首先是pch.h和pch.cpp,这个就是一个预编译头用来加速编译速度,预编译头只编译一次,内部用二进制保存下来并用于后面的编译,这样可以显著的加快编译速度:(就可以把不会变的头文件直接加进去来提速,但是后面的每一个cpp文件都必须包含pch.h,而头文件不用,头文件可以直接用pch的内容)

//pch.h
#include<ntddk.h>
//pch.cpp
#include"pch.h"

然后是AutoLock和FastMutex,这个在前面Windows内核开发-6-内核机制 Kernel Mechanisms - Sna1lGo - 博客园 (cnblogs.com)有讲过,这里直接上代码了:

//FastMutex.h
#pragma once
class FastMutex {
public:
void Init();
void Lock();
void Unlock();

private:
FAST_MUTEX _mutex;
};

//FastMutex.cpp
#include"pch.h"
#include"FastMutex.h"
void FastMutex::Init()
{
ExInitializeFastMutex(&_mutex);
}
void FastMutex::Lock()
{
ExAcquireFastMutex(&_mutex);
}
void FastMutex::Unlock()
{
ExReleaseFastMutex(&_mutex);
}

//AutoLock.h
#pragma once
//封装成一个自动的互斥体
template<typename TLock>
struct AutoLock {
AutoLock(TLock& lock):_lock(lock){
_lock.Lock();
}
~AutoLock()
{
_lock.Unlock();
}

private:
TLock& _lock;
};

//AutoLock.cpp
#include"pch.h"
#include"AutoLock.h"

接着是公用的结构体文件: SysMonCommon.h:

这里我们采用一些正式开发比较常用的办法:

//添加枚举类来进行区别响应的事件,这个采用的是C++11的有范围枚举(scoped enum)特性
enum class ItemType : short{
None,
ProcessCreate,
ProcessExit
};

//公有的内容就可以设置为一个头结构体,后面的再继承它来扩充
struct ItemHeader{
ItemType Type;
USHORT Size;
LARGE_INTEGER Time;//系统的时间类
};

//添加具体的事件信息结构体,退出一个进程没啥好知道的,知道个退出的进程ID就行
struct ProcessExitInfo : ItemHeader{
ULONG ProcessId;
};

最后是SysMon.h:

//这个头文件主要用来实现驱动的主要逻辑代码,因为我们采用链表来存储所有的信息,所以链表也要加在这里面
//采用模板类来让所有的结构体都可以利用链表串联起来而防止编写很多重复的代码
template<typename T>
struct FullItem{
LIST_ENTRY entry;
ProcessExitInfo Data;
}


//再建立一个统领全局的全局变量结构体,来存储所有的信息
//包含了驱动程序的所有全局状态的数据结构体
struct Globals{
   LIST_ENTRY ItemsHead;//链表的头指针
   int ItemCount;//事件的个数
   FastMutex Mutex;//快速互斥体
}

DriverEntry例程

DriverEntry主要处理的就是建立设备对象,绑定符号链接,然后符号链接可以给User用,Device给Kernel用,再绑定IRP派遣函数,然后注册响应通知。

//这里有一些函数可以先添加申明,代码逻辑后面再讲
DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
auto status = STATUS_SUCCESS;
InitializeListHead(&g_Globals.ItemHead);//初始化链表
g_Globals.Mutex.Init(); //初始化互斥体
//建立设备对象和符号链接
PDEVICE_OBJECT DeviceObject = NULL;
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\sysmon");
bool symLinkCreate = FALSE;
do {
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\sysmon");
status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to create device Error:(0x%08X)",status));
break;
}
DeviceObject->Flags |= DO_DIRECT_IO;//直接IO
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to create SymbolcLink Error:(0x%08X)\n",status));
break;
}
symLinkCreate = TRUE;
//注册进程提醒函数
status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to register process callback (0x%08X)\n",status));
break;
}
if (!NT_SUCCESS(status))
{
if (symLinkCreate)
IoDeleteSymbolicLink(&symLinkName);
if (DeviceObject)
IoDeleteDevice(DeviceObject);
}
DriverObject->DriverUnload = SysMonUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = SysMonCreateClose;
DriverObject->MajorFunction[IRP_MJ_READ] = SysMonRead;

return status;
}

处理进程退出通知

前面讲到注册进程通知函数里面有一个回调函数,这个函数就是用来得到进程响应的信息,不管是进程退出还是创建都可以

//前面在注册进程提醒函数的时候有用到这条代码,所以我们需要完善的就是这个回调函数就行:
// status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
//前面进程通知的时候有讲函数原型,所以这里直接贴代码:
//PushItem是一个后续会完善的一个函数,用来将内容添加到链表里
void OnProcessNotify(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);
//如果进程被销毁CreateInfo这个参数为NULL
if (CreateInfo)
{
//进程创建事件获取内容
}
else
{
//进程退出

//保存退出的进程的ID和事件的公用头部,ProcessExitInfo是封装的专门针对退出进程保存的信息结构体,DRIVER_TAG是分配的内存的标签位。
auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool, sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("when process exiting,failed to allocation\n"));
return;
}
//分配成功就开始收集信息
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);//获取进程时间
item.Type = ItemType::ProcessExit;//设置捕获的进行信息类型为枚举类的退出进程
item.ProcessId = HandleToULong(ProcessId);//把句柄转换为ulong类型(其实是一个)
item.Size = sizeof(ProcessExitInfo);
PushItem(&info->Entry);//将该数据添加到链表尾部
}
}

处理进程创建通知

这个其实有了前面的经验就知道了,只需要在进程响应回调函数里面的if语句中再添加代码就好了:

void OnProcessNotify(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);
//如果进程被销毁CreateInfo这个参数为NULL
if (CreateInfo)
{
//进程创建事件获取内容

USHORT allocSize = sizeof(FullItem<ProcessCreateInfo>);
USHORT commandLineSize = 0;
if (CreateInfo->CommandLine)//如果有命令行输入
{
commandLineSize = CreateInfo->CommandLine->Length;
allocSize += commandLineSize;//要分配的内存大小
}
       //分配进程创建结构体大小
auto info = (FullItem<ProcessCreateInfo>*)ExAllocatePoolWithTag(PagedPool, allocSize, DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("SysMon: When process is creating,failed to allocate memory"));
return;
}
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);
item.Type = ItemType::ProcessCreate;
item.Size = allocSize;
item.ProcessId = HandleToULong(ProcessId);
item.ParentProcessId = HandleToULong(CreateInfo->ParentProcessId);

if (commandLineSize > 0)
{
::memcpy((UCHAR*)&item+sizeof(item),CreateInfo->CommandLine->Buffer,commandLineSize);//把命令行的内容复制到开辟的内存空间后面
item.CommandLineLength = commandLineSize / sizeof(WCHAR);//以wchar为单位
item.CommandLineOffset = sizeof(item);//从多久开始偏移是命令字符串的首地址
}
else
{
item.CommandLineLength = 0;
item.CommandLineOffset = 0;
}
PushItem(&info->Entry);
}
else
{
//进程退出

//保存退出的进程的ID和事件的公用头部,ProcessExitInfo是封装的专门针对退出进程保存的信息结构体,DRIVER_TAG是分配的内存的标签位。
auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool, sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("when process exiting,failed to allocation\n"));
return;
}
//分配成功就开始收集信息
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);//获取进程时间
item.Type = ItemType::ProcessExit;//设置捕获的进行信息类型为枚举类的退出进程
item.ProcessId = HandleToULong(ProcessId);//把句柄转换为ulong类型(其实是一个)
item.Size = sizeof(ProcessExitInfo);
PushItem(&info->Entry);//将该数据添加到链表尾部
}
}

将数据提供给用户模式User

这里就需要设计到IRP派遣函数了。派遣函数前面有讲过,主要就是用作User和Kernel的交互,可以比作Windows的消息处理机制,User读取Kernel的Device中的内容需要Read,然后这个Read通过派遣函数分发到了Kernel里面,Kernel里面。IRP比较复杂,可以暂时理解为一个桥梁,将User下的API和Kernel下的函数一一对应,比如说CreateFile通过IRP对应到了Kernel的TestCreate函数。

NTSTATUS SysMonRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
UNREFERENCED_PARAMETER(pDevObj);
auto stack = IoGetCurrentIrpStackLocation(pIrp);
auto len = stack->Parameters.Read.Length;//获取User的读取缓冲区大小
auto status = STATUS_SUCCESS;
auto count = 0;
NT_ASSERT(pIrp->MdlAddress);//MdlAddress表示使用了直接I/O

auto buffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);//获取直接I/O对应的内存空间缓冲区
if (!buffer)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
//访问链表头,获取数据返回给User,获得内容后就直接删除
AutoLock<FastMutex> lock(g_Globals.Mutex);
while (TRUE)
{
if (IsListEmpty(&g_Globals.ItemHead))//如果链表为空就退出循环,当然检测ItemCount也是可以的
{
break;//退出循环
}
auto entry = RemoveHeadList(&g_Globals.ItemHead);
auto info = CONTAINING_RECORD(entry,FullItem<ItemHeader>, Entry);//返回首地址
auto size = info->Data.Size;
if (len < size)
{
//剩下的BUFFER不够了
//又放回去
InsertHeadList(&g_Globals.ItemHead, entry);
break;
}
g_Globals.ItemCount--;
::memcpy(buffer, &info->Data, size);
len -= size;
buffer += size;
count += size;

//释放内存
ExFreePool(info);
}
}
//完成此次
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = count;
IoCompleteRequest(pIrp, 0);
return status;
}

//Create和Close没啥用,因为它们只要能够让这个完整执行就行了,而一个IRP完整执行通常都会有一下的三条语句
NTSTATUS SysMonCreateClose(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
UNREFERENCED_PARAMETER(pDevObj);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, 0);
return 0;
}

然后还有比较重要的User代码:

(主要的代码逻辑就是:接受内核传递的信息,然后输出出来)

#include<iostream>
#include<Windows.h>
#include"../SysMon/SysMonCommon.h"
using namespace std;

int Error(const char* Msg)
{
cout << Msg << endl;
return 0;
}
void DisplayTime(const LARGE_INTEGER& time)
{
SYSTEMTIME st;
::FileTimeToSystemTime((FILETIME*)&time, &st);
printf("%02d:%02d:%02d.%03d: ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
}
void DisplayInfo(BYTE* buffer, DWORD size)
{
auto count = size;//读取的总数
while (count > 0)
{
//利用枚举变量来区分,分开输出
auto header = (ItemHeader*)buffer;
switch (header->Type)
{
case ItemType::ProcessCreate:
{
DisplayTime(header->Time);
auto info = (ProcessCreateInfo*)buffer;
std::wstring commandline((WCHAR*)(buffer + info->CommandLineOffset), info->CommandLineLength);
printf("Process %d created.Command line:%ws\n", info->ProcessId, commandline.c_str());
break;
}
case ItemType::ProcessExit:
{
DisplayTime(header->Time);
auto info = (ProcessExitInfo*)buffer;
printf("Process %d Exited\n", info->ProcessId);
break;
}
case ItemType::ThreadCreate:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Create in process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ThreadExit:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Exit from process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ImageLoad:
{
DisplayTime(header->Time);
auto info = (ImageLoadInfo*)buffer;
printf("Image loaded into process %d at address 0x%p (%ws)\n", info->ProcessId, info->LoadAddress, info->ImageFileName);
break;
}
default:
break;
}
buffer += header->Size;
count += header->Size;
}
}
int main()
{
   //通过符号链接来读取文件
auto hFile = ::CreateFile(L"\\\\.\\sysmon", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return Error("Failed to open File");
}
BYTE buffer[1 << 16];//左移16位,64KB的BUFFER
while (1)
{
DWORD bytes;
if (!::ReadFile(hFile, buffer, sizeof(buffer), &bytes, nullptr))
Error("Failed to read File");
if (bytes != 0)
DisplayInfo(buffer, bytes);

::Sleep(2000);
}
system("pause");
}

线程通知

和前面一样,线程通知也是有注册线程通知信息的API,可以仿造着进程通知的方式来写,但是有一点不一样:

//sysMon.cpp中添加到进程注册后面的代码
//注册线程提醒函数
status = PsSetCreateThreadNotifyRoutine(OnThreadNotiry);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to register thread callback (0x%08X)\n", status));
break;
}

可以看到这里的API:PsSetCreateThreadNotifyRoutine有一点点不一样

NTSTATUS PsSetCreateThreadNotifyRoutine(
 PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
PCREATE_THREAD_NOTIFY_ROUTINE PcreateThreadNotifyRoutine;

void PcreateThreadNotifyRoutine(
 HANDLE ProcessId,
 HANDLE ThreadId,
 BOOLEAN Create
)
{...}

这里的函数是通过回调函数的Create标志位来判断是创建还是销毁。

前面的可以套用进程通知,但是有一些结构体需要扩充,比如说,SysMonCommand.h里面的内容:

//事件的类型

enum class ItemType : short {
None,
ProcessCreate,
ProcessExit,
ThreadCreate,
ThreadExit,
};

//线程的信息结构体
struct ThreadCreateExitInfo : ItemHeader {
ULONG ThreadId;//线程ID
ULONG ProcessID;//线程对应的进程ID

};

还有User的Switch语句,也要依据类型来不同的输出:

        case ItemType::ThreadCreate:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Create in process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ThreadExit:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Exit from process %d\n", info->ThreadId, info->ProcessID);
break;
}

就依葫芦画瓢基本上可以解决掉。

模块载入通知

模块也和进程、线程加载差不多:(但是改API没有卸载的响应,这个暂时不清楚,我也没有尝试,有兴趣的可以试一下)

NTSTATUS PsSetLoadImageNotifyRoutine(
 PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
PLOAD_IMAGE_NOTIFY_ROUTINE PloadImageNotifyRoutine;

void PloadImageNotifyRoutine(
 PUNICODE_STRING FullImageName,
 HANDLE ProcessId,
 PIMAGE_INFO ImageInfo
)
{...}

通过这些API可以注册内核响应模块加载,但是和前面一样也需要注意结构体的信息。

总结

内核有很多强大的机制,这里介绍了进程、线程和模块的创建销毁的响应。

所以代码的合集:

https://github.com/skrandy/SysMon

Windows内核基础知识-8-监听进程、线程和模块的更多相关文章

  1. Windows内核基础知识-1-段寄存器

    Windows内核基础知识-1-段寄存器 学过汇编的应该都知道段寄存器,在Windows里段寄存器有很多,之前可能只接触了ds数据段,cs 代码段这种,今天这个博客就介绍Windows一些比较常用的段 ...

  2. Windows内核基础知识-2-段描述符

    Windows内核基础知识-2-段描述符 比如: ES 002B 0(FFFFFFFF) 意思就是es段寄存器,段选择子/段选择符 为002B, 起始地址base为0, 限制范围Limit地址最大能寻 ...

  3. Windows内核基础知识-5-调用门(32-Bit Call Gate)

    Windows内核基础知识-5-调用门(32-Bit Call Gate) 调用门有一个关键的作用,就是用来提权.调用门其实就是一个段. 调用门: 这是段描述符的结构体,里面的s字段用来标记是代码段还 ...

  4. shell基础知识---与监听服务器长连接端口状态

    从未写过脚本我的最近接了俩脚本的需求,就在这分享一下我的我学到基础知识主要就四部分内容 一.变量 变量的定义 string='字符串' string="字符串" num=808st ...

  5. IOS高级开发~开机启动&无限后台运行&监听进程

    一般来说, IOS很少给App后台运行的权限. 仅有的方式就是 VoIP. IOS少有的为VoIP应用提供了后台socket连接,定期唤醒并且随开机启动的权限.而这些就是IOS上实现VoIP App的 ...

  6. IOS开发~开机启动&无限后台运行&监听进程

    非越狱情况下实现: 开机启动:App安装到IOS设备设备之后,无论App是否开启过,只要IOS设备重启,App就会随之启动: 无限后台运行:应用进入后台状态,可以无限后台运行,不被系统kill: 监听 ...

  7. windows内核基础与异常处理

    前两日碰到了用异常处理来做加密的re题目 所以系统学习一下windows内核相关 windows内核基础 权限级别 内核层:R0 零环 核心态工作区域 大多数驱动程序 应用层:R3 用户态工作区域 只 ...

  8. Windows Phone 8 获取与监听网络连接状态(转)

    原文地址:http://www.cnblogs.com/sonic1abc/archive/2013/04/02/2995196.html 现在的只能手机对网络的依赖程度都很高,尤其是新闻.微博.音乐 ...

  9. Linux 动态监听进程shell

    背景 前几天在研究线程的时候,看到一句话说java里的线程Thread.run都会在Linux中fork一个的轻量级进程,于是就想验证一下(笔者的机器是Linux的).当时用top命令的时候,进程总是 ...

随机推荐

  1. Python基础之实现界面和代码分离

    第一步:用QT Designer画一个TreeWidget,存为treeview4.ui,这个处理前面TreeWidget那一节讲过,这里不细讲 treeview4.py # -*- coding: ...

  2. Appium使用inspactor开始session报"Could not connect to server; are you sure it's running?"

    appium在使用inspactor start session时提示:Could not connect to server; are you sure it's running?如下图 解决方案为 ...

  3. DC-1 靶机渗透测试

    DC-1靶机渗透测试 对着镜子大喊三声"太菜了""太菜了""太菜了" DC系列靶机的第一篇.边学习边日靶机边进步,摸爬滚打着前行. 内容不只 ...

  4. 八数码难题之 A* 算法

    人生第一个A*算法-好激动-- 八数码难题--又称八数码水题,首先要理解一些东西: 1.状态可以转化成整数,比如状态: 1 2 3 4 5 6 7 8 0 可以转化成:123456780这个整数 2. ...

  5. 使用Magicodes.IE快速导出Excel

    前言 总是有很多朋友咨询Magicodes.IE如何基于ASP.NET Core导出Excel,出于从框架的体验和易用性的角度,决定对Excel的导出进行独立封装,以便于大家更易于使用,开箱即用. 注 ...

  6. getRawX、getRawY与getX、getY、getScrollX、getScrollY,getTop,getLeft区别

    这篇文章写的不错,Mark一下 http://blog.csdn.net/sinat_29912455/article/details/51073537

  7. 卷向字节码-Java异常到底是怎么被处理的?

    你好呀,我是why,你也可以叫我歪歪. 比如下面这位读者: 他是看了我<神了!异常信息突然就没了?>这篇文章后产生的疑问. 既然是看了我的文章带来的进一步思考,恰巧呢,我又刚好知道. 虽然 ...

  8. 方法对了,你做1年Android开发能顶别人做10年

    前几天后台有读者问我这样的问题.他在一家互联网公司工作3年了,每天都很忙,事情又多又杂. 本想着学习多一些东西也不是坏事,可到头来一无所获,什么都没学会,满腔的热情也被消磨得差不多. 三天两头动辞职的 ...

  9. 神奇的 SQL 之别样的写法 → 行行比较

    开心一刻 昨晚我和我爸聊天 我:"爸,你怎么把烟戒了,也不出去喝酒了,是因为我妈不让,还是自己醒悟,开始爱惜自己啦?" 爸:"儿子啊,你说的都不对,是彩礼又涨价了.&qu ...

  10. 如何在idea中配置Tomcat服务器

    .IDEA 中动态 web 工程的操作         a)IDEA 中如何创建动态 web 工程        1.创建一个新模块: 2.选择你要创建什么类型的模块 3.输入你的模块名,点击[Fin ...