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. TCP通信简单梳理

    一.什么是TCP协议 TCP协议是一种面向连接的可靠的通信协议,最重要的两个特点:连接.可靠. 二.TCP是如何进行通信的 TCP通过三次握手建立连接后客户端服务端的内核都分别开辟资源,这时候开始进行 ...

  2. Unsupported major.minor version 52.0解决办法【转】

    1.首先解释一下报错原因: stanford parser和jdk版本对应关系 J2SE8=52, J2SE7=51, J2SE6.0=50, J2SE5.0=49, JDK1.4=48, JDK1. ...

  3. 数据结构和算法学习笔记十五:多路查找树(B树)

    一.概念 1.多路查找树(multi-way search tree):所谓多路,即是指每个节点中存储的数据可以是多个,每个节点的子节点数也可以多于两个.使用多路查找树的意义在于有效降低树的深度,从而 ...

  4. 记一次 GitLab 的迁移过程

    目录 1. 迁移背景 2. GitLab 整体架构介绍 3. GitLab 安装 配置选择 安装方式选择 安装的网络区域 安装 GitLab GitLab 常用命令 配置管理员账号密码 4. 配置 G ...

  5. JAVA数组的基础入门>从零开始学java系列

    目录 JAVA数组的基础入门 什么是数组,什么情况下使用数组 数组的创建方式 获取数组的数据 数组的内存模型 为什么数组查询修改快,而增删慢? 查询快的原因 增删慢的原因 数组的两种遍历方式以及区别 ...

  6. 针对不同场景的Python合并多个Excel方法

    大家好,我是辰哥~ 在辰哥看来,技术能够减少繁琐工作带来的枯燥,技术+实际=方便.最近辰哥也是在弄excel文件的时候发现手动去整理有点繁琐枯燥,想着技术可以代替我去处理这部分繁琐的工作那何乐而不为呢 ...

  7. [GXYCTF2019]Ping Ping Ping(ping命令执行绕过Waf)

    记一道ping注入的题.过滤了很多字符. 分析 简单的测了一下,很容易就拿到了flag.php和index.php. 但是存在waf无法直接查看.直接?ip=127.0.0.1|cat flag.ph ...

  8. 腾讯云TDSQL PostgreSQL版 -最佳实践 |优化 SQL 语句

    查看是否为分布键查询 postgres=# explain select * from tbase_1 where f1=1; QUERY PLAN ------------------------- ...

  9. 【javaFX学习】(二) 面板手册

    移至http://blog.csdn.net/qq_37837828/article/details/78732591 更新 找了好几个资料,没找到自己想要的,自己整理下吧,方便以后用的时候挑选,边学 ...

  10. 使用AVPro Video在Unity中播放开场视频(CG)笔记

    游戏中的开场CG(播放视频),采用的插件为AVPro Video1.x(和W的版本一致),Unity版本为2018.4.0f1 Asset Store:AVPro Video - Core Andro ...