注意事项 : 我用的OD是 自己有保护的,经过内核重载 CE就能打开OD了 没重载之前呢 是看不到OD进程的
不要跟360HOOK了一个点 要不就会蓝屏 , 加载驱动前一定要检查 有没有被HOOK NtCreateFile 函数否则就会加载失败的
重点注意QQ 它的驱动HOOK了不少函数而且是重启不会取消的HOOK 解决办法就是把它的驱动删除了 重启 就OK
比如打开了 CE 一定要先把CE关掉以后 才能卸载重载驱动 要不就会蓝屏
首先呢我们要学的是一个思想,过程代码就放在哪跟着代码一步一步看最后作者的意思是什么就会明白.也能自己调用但是如果分析的内容变了我们如何操作.假如换个系统 有好多东西要变,一个微小的改变就会造成致命的错误.
所以要学的是思路.百变不离其宗,如果我们明白细节了 我们就可以以不变应万变.(废话就说到这里了)
1.应用层要进入内核层要通过一个函数来进行KiFastCallEntry()函数 我们要定位到这个函数就要用到栈回溯(其中一个方法);
就是如果应用层进入KiFastCallEntry()函数 在这个函数里就要CALL SSDT里的函数才行.
每次call 函数后 就会把CALL下面一个地址保存到堆栈里 然后更据函数调用约定 每次调用CALL就会在函数头部
mov edi,edi push ebp mov ebp,esp 所有到函数里后我们去堆栈里获取ebp加4的地址 就是我们要转跳的
KiFastCallEntry 这个函数里 然后更加搜索特征码找到我们要HOOK的点进行HOOK就行了
2__declspec(naked) 这是一个函数调用约定 目的是 不让函数开头 mov edi,edi push ebp mov ebp,esp 这3句
这要放到我们要JMP转跳的那个函数上边, 本来函数调用要有 mov edi,edi push ebp mov ebp,esp 到结束的时候会 pop ebp
但是我们没有执行到retn 语句 不会pop 为了 堆栈平衡 我们要在函数头加上 __declspec(naked)
3.HOOK点 由于360 也HOOK了那个点 相同的位置会蓝屏 我们就分析 直接从上边的一个5字节地址开始
进入自己的函数 然后把中间缺少的代码 复制到我们自己的函数里 然后转跳到360HOOK的下面的地址
这样就绕过360了.我们也得到了我们想要得到的数据 及实现的功能
JMP哪的 是有5个字节的代码的 我们可以从一个没有被HOOK过也从来没打开过360的系统里用windbg来获取相关的
反汇编代码
4经过分析发现edx正是一个SSDT的函数地址
所有我们判断如果进入SSDT的函数的进程使我们的工具就 把堆栈里的edx值修改成我们新内核的SSDT函数对应的地址
这样我们的工具就通行无阻了 ; edx在堆栈中如何找到呢 看下图 esp(当前栈顶位置)加0x18的位置就是我们要修改的值edx的位置
内核重载:
1.把ntkrnlpa.exe从磁盘中 加载到内存
2.内核基地址重定位
3 SSDT重定位 .还有一个动态获取已加载驱动ntkrnlpa.exe的首地址
4. 先自己调用一个SSDT函数 获得KiFastCallEntry 的函数地址 然后HOOK
获取 调用SSDT程序的进程名字 判断是不是我们工具的名字 是 转入新内核 不是走旧内核;
代码如下:
#pragma once //只编译一次
#ifdef __cplusplus
extern"C"
{
#endif
#include "ntddk.h"
#include "ntimage.h"
#ifdef __cplusplus
}
#endif //如果是c++代码用c的方式链接{它}
#define _max(a,b) a>b?a:b
//这是一个SSDT表的结构类型先声明一下
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()
extern "C"{
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
}
//遍历驱动名用的一个结构
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
union {
LIST_ENTRY HashLinks;
struct {
PVOID SectionPointer;
ULONG CheckSum;
};
};
union {
struct {
ULONG TimeDateStamp;
};
struct {
PVOID LoadedImports;
};
};
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
//全局变量
PVOID sizeof_image; //映射到内存的地址指针
ULONG OrigImage; //原来的内核基地址
ServiceDescriptorTableEntry_t * pNewSSDT;//新SSDT地址
ULONG g_ntcreatefile; //保存原始的SSDT
ULONG g_fastcall_hookpointer; //保存要HOOK的地址
ULONG g_goto_origfunc; //判断后要返回的地址
//恢复内存保护
void PageProtectOn()
{
__asm
{
mov eax, cr0
or eax, 10000h
mov cr0, eax
sti
}
}
//去掉内存保护
void PageProtectOff()
{
__asm
{
cli
mov eax, cr0
and eax, not 10000h
mov cr0, eax
}
}
//****************************************判断HOOK部分的******************************************
//声明函数指针
typedef NTSTATUS(*NTCREATEFILE) (
__out PHANDLE FileHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__out PIO_STATUS_BLOCK IoStatusBlock,
__in_opt PLARGE_INTEGER AllocationSize,
__in ULONG FileAttributes,
__in ULONG ShareAccess,
__in ULONG CreateDisposition,
__in ULONG CreateOptions,
__in_bcount_opt(EaLength) PVOID EaBuffer,
__in ULONG EaLength
); //声明
//特征码搜寻
ULONG SearchHookPointer(ULONG StartAddress)
{
ULONG u_index;
UCHAR *p = (UCHAR*)StartAddress;
for (u_index = 0; u_index < 200; u_index++)
{
if (
*p == 0x33 &&
*(p + 1) == 0xC9 &&
*(p + 2) == 0x8B &&
*(p + 3) == 0x57 &&
*(p + 4) == 0x0C)
{
return (ULONG)p;
}
p--;
}
return 0;
}
//进程判断
ULONG FilterKiFastCallEntry()
{
ULONG l_deviation;//偏移
ULONG l_base_address;// 基地址
ULONG l_current_address;//当前地址
__asm
{
mov l_deviation, eax
mov l_base_address, edi
mov l_current_address, edx
}
//判断是不是要进入SSDT中的函数
if (l_base_address == (ULONG)KeServiceDescriptorTable.ServiceTableBase)
{
if (strstr((char*)PsGetCurrentProcess() + 0x16c, "FishcOD.exe") || strstr((char*)PsGetCurrentProcess() + 0x16c, "cheatengine") != 0)
{
//返回新重载的SSDT里
return pNewSSDT->ServiceTableBase[l_deviation];
}
}
//返回原来的地址
return l_current_address;
}
//判断是否是我们通行的进程
__declspec(naked)
void NewKiFastCallEntry()
{
__asm
{
xor ecx, ecx
mov edx, dword ptr[edi + 0Ch]
mov edi, dword ptr[edi]
mov cl, byte ptr[eax + edx]
mov edx, dword ptr[edi + eax * 4]
sub esp, ecx
shr ecx, 2
pushad
pushfd
call FilterKiFastCallEntry
mov[esp + 0x18], eax //换的是栈里边的值
popfd
popad
jmp g_goto_origfunc
}
}
//还原HOOK KiFastCallEntry
void UnHookKiFastCallEntry()
{
UCHAR str_origfuncode[5] = { 0x33, 0xC9, 0x8B, 0x57, 0x0C };
if (g_fastcall_hookpointer == 0)
{
return;
}
PageProtectOff();
RtlCopyMemory((PVOID)g_fastcall_hookpointer, str_origfuncode, 5);
PageProtectOn();
}
//替换转跳指令
void HookKiFastCallEntry(ULONG HookPointer)
{
ULONG u_temp;
UCHAR str_jmp_code[5];
str_jmp_code[0] = 0xE9;
u_temp = (ULONG)NewKiFastCallEntry - HookPointer - 5;
*(ULONG*)&str_jmp_code[1] = u_temp;
PageProtectOff();
RtlCopyMemory((PVOID)HookPointer, str_jmp_code, 5);
PageProtectOn();
}
//我们呢自己的NtCreateFile
NTSTATUS NewNtCreateFile(
__out PHANDLE FileHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__out PIO_STATUS_BLOCK IoStatusBlock,
__in_opt PLARGE_INTEGER AllocationSize,
__in ULONG FileAttributes,
__in ULONG ShareAccess,
__in ULONG CreateDisposition,
__in ULONG CreateOptions,
__in_bcount_opt(EaLength) PVOID EaBuffer,
__in ULONG EaLength
)
{
ULONG u_call_retaddr;
__asm{
pushad
mov eax, [ebp + 0x4]
mov u_call_retaddr, eax
popad
}
g_fastcall_hookpointer = SearchHookPointer(u_call_retaddr);
if (g_fastcall_hookpointer == 0)
{
KdPrint(("search failed."));
}
else{
KdPrint(("search success."));
}
g_goto_origfunc = g_fastcall_hookpointer + 0x12;
HookKiFastCallEntry(g_fastcall_hookpointer);
//还原SSDT
PageProtectOff();
KeServiceDescriptorTable.ServiceTableBase[66] = (unsigned int)g_ntcreatefile;
PageProtectOn();
return ((NTCREATEFILE)g_ntcreatefile)(
FileHandle, \
DesiredAccess, \
ObjectAttributes, \
IoStatusBlock, \
AllocationSize, \
FileAttributes, \
ShareAccess, \
CreateDisposition, \
CreateOptions, \
EaBuffer, \
EaLength);
}
//HOOKNtCreateFile
void SearchKiFastCallEntry()
{
HANDLE hFile;
NTSTATUS Status;
OBJECT_ATTRIBUTES ObjAttr;
UNICODE_STRING usFileName;
IO_STATUS_BLOCK IoStatusBlock;
RtlInitUnicodeString(&usFileName, L"\\??\\C:\\Windows\\System32\\ntkrnlpa.exe");
InitializeObjectAttributes(\
&ObjAttr, \
&usFileName, \
OBJ_CASE_INSENSITIVE, \
NULL, \
NULL);
g_ntcreatefile = KeServiceDescriptorTable.ServiceTableBase[66];
PageProtectOff();
KeServiceDescriptorTable.ServiceTableBase[66] = (unsigned int)NewNtCreateFile;
PageProtectOn();
Status = ZwCreateFile(\
&hFile, \
FILE_ALL_ACCESS, \
&ObjAttr, \
&IoStatusBlock, \
NULL, \
FILE_ATTRIBUTE_NORMAL, \
FILE_SHARE_READ, \
FILE_OPEN, \
FILE_NON_DIRECTORY_FILE, \
NULL, \
0);
if (NT_SUCCESS(Status))
{
ZwClose(hFile);
}
}
//************************************重定位部分********************************************************************
//SSDT重定位
VOID RelocationSSDT(PVOID NewAddress, ULONG RawAddress)
{
ULONG RelativeOffset; //相对的偏移量
RelativeOffset = (ULONG)NewAddress - RawAddress;//相对的偏移地址
pNewSSDT = (ServiceDescriptorTableEntry_t*)((ULONG)&KeServiceDescriptorTable + RelativeOffset); //新SSDT地址
if (!MmIsAddressValid(pNewSSDT))
{
KdPrint(("pNewSsdt is Error"));
return;
}
pNewSSDT->NumberOfServices = KeServiceDescriptorTable.NumberOfServices; //拷贝SSDT函数数量
ULONG uDeviation; //函数偏移
uDeviation = (ULONG)KeServiceDescriptorTable.ServiceTableBase - RawAddress;//Relative Virtual Address 相对于基址
pNewSSDT->ServiceTableBase = (unsigned int*)((ULONG)NewAddress + uDeviation); //新地址加相对偏移地址(旧SSDT-旧基址)
if (!MmIsAddressValid(pNewSSDT->ServiceTableBase))
{
KdPrint(("pNewSSDT->ServiceTableBase is Error"));
return;
}
//遍历修改SSDTServiceTableBase数组的值
int i;
for (i = 0; i<pNewSSDT->NumberOfServices; i++)
{
pNewSSDT->ServiceTableBase[i] += RelativeOffset;
}
KdPrint(("success RelocationSSDT"));
}
//基地址重定位
void BaseRelocation(PVOID pNewImage)
{
ULONG i; //for循环变量
ULONG uRelocTableSize; //存放数据块中的数据总个数
ULONG OriginalImageBase; //内存文件的首装入地址
ULONG Type; //16位数据高4位
ULONG *uRelocAddress; //指向需要修改内容的地址
PIMAGE_DOS_HEADER pImageDosHeader; //DOS头
PIMAGE_NT_HEADERS pImageNtHeader; //NT头
IMAGE_DATA_DIRECTORY ImageDataDirectory; //数据表
IMAGE_BASE_RELOCATION *pImageBaseRelocation;//重定位表
//将新内核地址作为一个PE文件头,依次向下,目的是寻找重定位表结构
pImageDosHeader = (PIMAGE_DOS_HEADER)pNewImage;
//定位到IMAGE_NT_HEADER
pImageNtHeader = (PIMAGE_NT_HEADERS)((ULONG)pNewImage + pImageDosHeader->e_lfanew);
//获取内核文件的imagebase,以便后面做偏移修改。
OriginalImageBase = pImageNtHeader->OptionalHeader.ImageBase;
//定位到数据目录
ImageDataDirectory = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
//定位到重定位表结构
pImageBaseRelocation = (PIMAGE_BASE_RELOCATION)(ImageDataDirectory.VirtualAddress + (ULONG)pNewImage);
if (pImageBaseRelocation == NULL)
{
return;
}
//修改数据*************************
while (pImageBaseRelocation->SizeOfBlock)
{ //得到需要更改数据的个数
uRelocTableSize = (pImageBaseRelocation->SizeOfBlock - 8) / 2;
//循环遍历
for (i = 0; i<uRelocTableSize; i++)
{//判断高4位是否等于3
Type = pImageBaseRelocation->TypeOffset[i] >> 12;
if (Type == IMAGE_REL_BASED_HIGHLOW)
{
//让指针指向要修改的数据
uRelocAddress = (ULONG *)((ULONG)(pImageBaseRelocation->TypeOffset[i] & 0xfff) + pImageBaseRelocation->VirtualAddress + (ULONG)pNewImage);
//修改重定位数据,原数据加上一个偏移
*uRelocAddress = (*uRelocAddress + (OrigImage - OriginalImageBase));
}
}
//把指针移到下一个快,如果->SizeOfBlock为空了,表示没有块了退出循环
pImageBaseRelocation = (IMAGE_BASE_RELOCATION *)((ULONG)pImageBaseRelocation + pImageBaseRelocation->SizeOfBlock);
}
KdPrint(("基址重定位完毕"));
}
//重载内核
VOID ReloadKernel() //重载内核
{
//创建文件*******
HANDLE hfile; //接受句柄
NTSTATUS status; //状态
IO_STATUS_BLOCK io_status_block; //接受状态结构
OBJECT_ATTRIBUTES object_attributes; //句柄属性
UNICODE_STRING path_name;
RtlInitUnicodeString(&path_name, L"\\??\\C:\\Windows\\System32\\ntkrnlpa.exe");
//初始化对象属性
InitializeObjectAttributes(&object_attributes, //对象属性变量 POBJECT_ATTRIBUTES OUT
&path_name, //文件名 PUNICODE_STRING
OBJ_CASE_INSENSITIVE, //表示不区分大小写
NULL, //NULL
NULL); //NULL
//创建文件
status = ZwCreateFile(
&hfile, //返回的句柄 OUT PHANDLE
FILE_ALL_ACCESS, //访问权限->所有权限
&object_attributes, //POBJECT_ATTRIBUTES 该结构包含要打开的文件名
&io_status_block, //PIO_STATUS_BLOCK 返回结果状态 OUT
0, //初始分配大小,0是动态分配
FILE_ATTRIBUTE_NORMAL, //文件属性 一般为<-或者0;
FILE_SHARE_READ, //指定共享方式一般<- 或者0;
FILE_OPEN, //这个参数指定要对文件干嘛
FILE_NON_DIRECTORY_FILE, //指定控制打开操作和句柄使用的附加标志位
NULL, //指向可选的扩展属性区
0); //扩展属性区的长度
if (!NT_SUCCESS(status))
{
KdPrint(("ZwCreateFile Failed!"));
return;
}
//读取DOS头******
IMAGE_DOS_HEADER image_dos_header;//dos头结构
LARGE_INTEGER large_integer;//记录偏移
large_integer.QuadPart = 0;
status = ZwReadFile(hfile, //ZwCreateFile成功后得到的句柄
NULL, //一个事件 NULL
NULL, //回调例程。NULL
NULL, //NULL
&io_status_block, //PIO_STATUS_BLOCK 返回结果状态 OUT ,同上
&image_dos_header, //存放读取数据的缓冲区 OUT PVOID
sizeof(IMAGE_DOS_HEADER), //试图读取文件的长度
&large_integer, //要读取数据相对文件的偏移量PLARGE_INTEGER
0); //NULL
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageDosHeader Failed!"));
ZwClose(hfile);
return;
}
//读取NT头*******
IMAGE_NT_HEADERS image_nt_header;//NT头
large_integer.QuadPart = image_dos_header.e_lfanew; //PE头偏移
status = ZwReadFile(hfile, //ZwCreateFile成功后得到的句柄
NULL, //一个事件 NULL
NULL, //回调例程。NULL
NULL, //NULL
&io_status_block, //PIO_STATUS_BLOCK 返回结果状态 OUT ,同上
&image_nt_header, //存放读取数据的缓冲区 OUT PVOID
sizeof(IMAGE_NT_HEADERS), //试图读取文件的长度
&large_integer, //要读取数据相对文件的偏移量PLARGE_INTEGER
0); //NULL
if (!NT_SUCCESS(status))
{
KdPrint(("Read image_nt_header Failed!"));
ZwClose(hfile);
return;
}
//读取区块*****
IMAGE_SECTION_HEADER * p_image_section_header;//指向多个区块结构
//分配所有模块总大小
p_image_section_header = (IMAGE_SECTION_HEADER*)ExAllocatePool(NonPagedPool, //NonPagedPool 从非分页内存池中分配内存
sizeof(IMAGE_SECTION_HEADER)*image_nt_header.FileHeader.NumberOfSections);
//读
large_integer.QuadPart += sizeof(IMAGE_NT_HEADERS); //区块偏移
status = ZwReadFile(hfile, //ZwCreateFile成功后得到的句柄
NULL, //一个事件 NULL
NULL, //回调例程。NULL
NULL, //NULL
&io_status_block, //PIO_STATUS_BLOCK 返回结果状态 OUT ,同上
p_image_section_header, //存放读取数据的缓冲区 OUT PVOID
sizeof(IMAGE_SECTION_HEADER)*image_nt_header.FileHeader.NumberOfSections, //试图读取文件的长度
&large_integer, //要读取数据相对文件的偏移量PLARGE_INTEGER
0); //NULL
if (!NT_SUCCESS(status))
{
KdPrint(("Read p_image_section_header Failed!"));
ExFreePool(p_image_section_header);
ZwClose(hfile);
return;
}
//复制数据**********
//PVOID sizeof_image;//定义成全局变量 因为卸载的时候要释放掉内存
sizeof_image = ExAllocatePool(NonPagedPool, image_nt_header.OptionalHeader.SizeOfImage);//NonPagedPool 从非分页内存池中分配内存
if (sizeof_image == 0)
{
KdPrint(("sizeof_image ExAllocatePool Failed!"));
ExFreePool(sizeof_image);
ExFreePool(p_image_section_header); //释放内存
ZwClose(hfile);
return;
}
//初始化下内存
memset(sizeof_image, 0, image_nt_header.OptionalHeader.SizeOfImage);
RtlCopyMemory(sizeof_image, &image_dos_header, sizeof(IMAGE_DOS_HEADER)); //dos头
RtlCopyMemory((PVOID)((ULONG)sizeof_image + image_dos_header.e_lfanew),
&image_nt_header, sizeof(IMAGE_NT_HEADERS)); //nt头
RtlCopyMemory((PVOID)((ULONG)sizeof_image + image_dos_header.e_lfanew + sizeof(IMAGE_NT_HEADERS)), //区块
p_image_section_header, sizeof(IMAGE_SECTION_HEADER)*image_nt_header.FileHeader.NumberOfSections);//计算区块总大小
ULONG sizeof_raw_data;
for (ULONG i = 0; i<image_nt_header.FileHeader.NumberOfSections; i++)
{
sizeof_raw_data = _max(p_image_section_header[i].Misc.VirtualSize, p_image_section_header[i].SizeOfRawData);
large_integer.QuadPart = p_image_section_header[i].PointerToRawData;
//读
status = ZwReadFile(hfile,
NULL,
NULL,
NULL,
&io_status_block,
(PVOID)((ULONG)sizeof_image + p_image_section_header[i].VirtualAddress),
sizeof_raw_data,
&large_integer,
0);
if (!NT_SUCCESS(status))
{
KdPrint(("循环区块出错[%s]%x\n",
p_image_section_header[i].Name,
(ULONG)sizeof_image + p_image_section_header[i].VirtualAddress));
ExFreePool(sizeof_image);
ExFreePool(p_image_section_header); //释放内存
ZwClose(hfile);
return;
}
}
BaseRelocation(sizeof_image);
KdPrint(("重载内存成功"));
ExFreePool(p_image_section_header);
ZwClose(hfile);
return;
}
//动态获取基地址
PVOID SearchDriver(PDRIVER_OBJECT pDriverObject, wchar_t *strDriverName)
{
LDR_DATA_TABLE_ENTRY *pDataTableEntry, *pTempDataTableEntry;
PLIST_ENTRY pList; //下一个节点
UNICODE_STRING usModuleName; // 存放字符串
RtlInitUnicodeString(&usModuleName, strDriverName);
pDataTableEntry = (LDR_DATA_TABLE_ENTRY*)pDriverObject->DriverSection;
if (!pDataTableEntry)
{
return 0;
}
pList = pDataTableEntry->InLoadOrderLinks.Flink;
while (pList != &pDataTableEntry->InLoadOrderLinks)
{
pTempDataTableEntry = (LDR_DATA_TABLE_ENTRY *)pList;
if (0 == RtlCompareUnicodeString(&pTempDataTableEntry->BaseDllName, &usModuleName, FALSE))
{
return pTempDataTableEntry->DllBase;
}
pList = pList->Flink;
}
KdPrint(("获取ntkrnlpa.exe基地址失败"));
return 0;
}
//***************************************************************************
//卸载函数
VOID MyDriverUnload(IN PDRIVER_OBJECT pDriverObject)
{
KdPrint(("成功进入卸载函数"));
if (sizeof_image != 0)
{
KdPrint(("sizeof_image内存释放"));
ExFreePool(sizeof_image);//释放内存
}
UnHookKiFastCallEntry();
KdPrint(("全部卸载完成"));
}
//驱动入口
extern"C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("加载驱动成功"));
pDriverObject->DriverUnload = MyDriverUnload;//卸载函数
//获取基地址
OrigImage = (ULONG)SearchDriver(pDriverObject, L"ntoskrnl.exe");
if (!OrigImage)
{
KdPrint(("获取ntoskrnl.exe失败"));
return 0;
}
KdPrint(("ntoskrnl.exe驱动模块首地址=%x", OrigImage));
//内核重载
ReloadKernel();
RelocationSSDT(sizeof_image, OrigImage);
KdPrint(("加载内存模块首地址=%x", sizeof_image));
//HOOK KiFastCallEntry;
SearchKiFastCallEntry();
return STATUS_SUCCESS;
}
复制代码
0则评论给“[驱动开发]重载内核(一)”