很久没来论坛了,今天来跟大家一起讨论一下关于检测内核中inline hook IAT hook方面的知识
我们平时常用的工具中xuetr和kd都提供了这个功能,比较方便地可以让我们看出来是谁在我们的电脑中做了什么手脚
不过现在有很多软件或者游戏不希望我们看到它们做的种种坏事,纷纷封杀掉这些工具,
更有甚者检测到这类工具启动立马给我来个蓝脸或者重启
这也太霸道了,这到底成了谁的电脑了?
于是本着维护对于拥有无可争辩所有权的个人电脑的完全的信息知情权的目的,我山寨了一下检测内核inline / IAT hook功能
说完废话,开进主题
网上没有搜索到详细的检测方法,可能是大家觉得比较没啥技术含量没必要写出来吧
说白了其实真的挺简单,就是将内核中待检测的数据拷贝到RING3层,RING3层做一份原文件的副本镜像映射,然后做各种处理,最后比较一下
在讨论具体代码之前先用文字介绍一下我的检测流程,便于大家理解<具体PE知识就不做科普了>:
因为大部分实际执行代码在
驱动
中存在于.text和PAGE两个节区,所以我用RING3层指定,由
驱动
部分拷贝回这两个节区;
为了减少
驱动
中的处理,由RING3层计算出需要拷贝的起始终结地址范围<包含对应的节区>,这样
驱动
要做的仅仅是读取指定地址范围的数据;
本来上面这两步通过RING3层一个ZwQuerySystemInformation得到内核
驱动
的加载基址之后,就可以定位到要拷贝的节区的地址信息,
但是我是通过
驱动
层读取PsLoadedModuleList链来得到的模块信息,具体原因后面再解释
这样我们就得到了待检测的数据<节区>.
然后我们需要映射一份原
驱动
文件的映像,以作为对比的源.
因为已经加载的
驱动
已经被重定向过,所以我们映射的
驱动
镜像也需要重定向修复一下;
另外要做的一个修复就是对于输入表的处理,这里简单说下.
我们知道输入表结构中的OriginalFirstThunk和FirstThunk在文件未被装载映射时是指向导入的函数名称的<或者序号>,
而当它被装载完之后PE装载器会重写FirstThunk部分,将之改写为导入的函数的真实地址.
这样问题就出来了:如果输入表FirstThunk位于.text节区或者PAGE节区,我们就需要对它就行相应的处理,否则在进行比对时会出现错误.因为
驱动
中的输入表FirstThunk数据已经被改写而我们映射的文件的FirstThunk还是原始值
我是这样处理的,在检测inline hook之前首先检测IAT hook,检测完之后将
驱动
拷贝回的数据中的FirstThunk和我们映射的文件的FirstThunk部分都清空,
<如果FirstThunk位于我们正在检测的节区当中的话>
这样就不会对我们接下去的处理产生影响了.
所以接续上面的流程应当是重定向修复->IAT hook 检测->IAT修复->inline hook检测
下面我们具体看一下代码:
<忘了说一点,因为考虑到检测内核hook及应用层hook,一些结构及函数是共用的,这里只介绍内核hook>
代码:
function CheckKernelInlineHook(parameter:Pointer):DWORD;stdcall;
var
hDevice : THandle;
i : DWORD;
pModuleListBuf : Pointer;
dwBufSize : DWORD;
dw : DWORD;
tempStrStr : string;
begin
Result:=0;
// 打开驱动层的设备对象
hDevice:=OpenDevice(DEVICE_SYMBOLIC_NAMR);
if hDevice = 0 then Exit;
i:=0;
pModuleListBuf:=nil;
// 获得模块信息列表
while True do
begin
dwBufSize:=(200 + i*5) * SizeOf(TLdrDataTableEntry);
pModuleListBuf:=AllocMem(dwBufSize);
if pModuleListBuf = nil then
begin
CloseHandle(hDevice);
Exit;
end;
if not DeviceIoControl(hDevice, IOCTL_GET_MODULE_LIST_INFO, nil, 0, pModuleListBuf, dwBufSize, dw, nil) then
begin
if GetLastError = ERROR_INSUFFICIENT_BUFFER then
begin
Inc(i);
FreeMem(pModuleListBuf);
Continue;
end
else
begin
FreeMem(pModuleListBuf);
CloseHandle(hDevice);
Exit;
end;
end;
Break;
end;
CloseHandle(hDevice);
// 得到模块数量
dw:=dw div SizeOf(TLdrDataTableEntry);
// 扫描以下指定驱动模块
FindDestModule(pModuleListBuf, 'ntoskrnl.exe', dw);
FindDestModule(pModuleListBuf, 'win32k.sys', dw);
FindDestModule(pModuleListBuf, 'hal.dll', dw);
FindDestModule(pModuleListBuf, 'kdcom.dll', dw);
FindDestModule(pModuleListBuf, 'partmgr.sys', dw);
FindDestModule(pModuleListBuf, 'ndis.sys', dw);
//FindDestModule(pModuleListBuf, 'tcpip.sys', dw);
FreeMem(pModuleListBuf);
// 界面回显
tempStr:='All done';
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@tempStr[1]), 0);
Result:=1;
end;
上面的函数过程与
驱动
进行通讯,得到PsLoadedModuleList链上的模块信息,然后调用函数FindDestModule开始检测指定模块
这里使用PsLoadedModuleList链上的模块信息而不是使用更简单便捷的ZwQuerySystemInformation是因为模块名称的缘故
对于ntoskrnl.exe和hal.dll,系统会根据硬件来加载不同版本的文件,用ZwQuerySystemInformation查询到的是真正加载的模块名称:
而在PsLoadedModuleList中记录了两种名称:
这样我们就能在程序中使用更为通用的名称来指定检索的模块,而使用FullName来定位具体的模块,避免硬性指定名称.
篇幅原因就不贴读取PsLoadedModuleList的过程了,网上很容易能搜索出来
读取出来的数据我使用下面的结构进行记录:
代码:
_LDR_DATA_TABLE_ENTRY = packed record
InLoadOrderLinks : TListEntry;
InMemoryOrderLinks : TListEntry;
InInitializationOrderLinks: TListEntry;
DllBase : Pointer;
EntryPoint : Pointer;
SizeOfImage : DWORD;
FullDllName : UNICODE_STRING;
BaseDllName : UNICODE_STRING;
Flags : DWORD;
LoadCount : Word;
TlsIndex : Word;
//HashLinks : TListEntry;
SectionPointer : Pointer;
CheckSum : DWORD;
//TimeDateStamp : DWORD;
LoadedImports : Pointer;
EntryPointActivationContext : Pointer;
PatchInformation : Pointer;
//自定义
FullName : array [0..255] of Char;
BaseName : array [0..63] of Char;
end;
TLDR_DATA_TABLE_ENTRY = _LDR_DATA_TABLE_ENTRY;
PLDR_DATA_TABLE_ENTRY = ^_LDR_DATA_TABLE_ENTRY;
TLdrDataTableEntry = _LDR_DATA_TABLE_ENTRY;
PLdrDataTableEntry = ^_LDR_DATA_TABLE_ENTRY;
其中的FullName和BaseName是我追加定义的,用来记录两种名称
继续
代码:
function FindDestModule(pModuleListBuf:Pointer; DestModuleName:string; ModuleCount:DWORD):Boolean;
var
pTableEntry : PLdrDataTableEntry;
i : DWORD;
scanInfo : TScanHookInfo;
ShortName : string;
SystemPath : string;
FullPath : string;
begin
Result:=False;
// 得到系统路径,确保以路径分隔符结尾
SystemPath:=IncludeTrailingBackslash(GetSystemPath());
// 遍历由驱动传回的模块列表,找到指定basename的项
pTableEntry:=PLdrDataTableEntry(pModuleListBuf);
for i := 0 to ModuleCount - 1 do
begin
if SameText(string(pTableEntry.BaseName), DestModuleName) then
begin
// 找到指定的BaseName之后跟系统路径组合一下
ShortName:=ExtractFileName(string(pTableEntry.FullName));
FullPath:=SystemPath + ShortName;
Break;
end;
Inc(pTableEntry);
end;
// 如果文件不在系统路径下,可能在drivers路径下,如果不在这两个路径中就忽略掉
if not FileExists(FullPath) then FullPath:=SystemPath + 'drivers\' + ShortName;
if not FileExists(FullPath) then Exit;
// 填充主要结构
InitModuleInfo(@scanInfo, ShortName, FullPath, DWORD(pTableEntry.DllBase), KernelMode);
// 正式开始检测
Result:=CheckInlineHook(@scanInfo);
end;
这个函数主要进行了模块的定位.
其中使用了自定义结构TScanHookInfo,用以记录扫描的信息
代码:
type
TScanMode = (KernelMode, UserMode); //扫描内核还是用户空间
THookType = (InlineHook, IATHook); //Hook 类型
type //扫描到的hook 返回结果
THookBytes = packed record
OriData : array [0..49] of Byte; //原始数据
CurData : array [0..49] of Byte; //当前数据
Len : DWORD; //长度
Address : DWORD; //起始地址
HookType : THookType; //Hook 类型
end;
PHookBytes = ^THookBytes;
type
TScanHookInfo = packed record //包含当前扫描的模块信息
ModuleName : array [0..63] of Char; //模块名称
ModulePath : array [0..MAX_PATH - 1] of Char; //模块路径
SectionName : array [0..15] of Char; //节区名称
ModuleBase : DWORD; //模块的加载基址
MapModuleBase : DWORD; //模块的映射基址
Mode : TScanMode; //当前扫描模式
ProcessId : DWORD; //用户模式下有效
SectionHeader : PImageSectionHeader; //当前扫描的节区
SourceAddress : DWORD; //进行对比时的地址A
DestAddress : DWORD; //进行对比时的地址B
CompareSize : DWORD; //对比大小
HookData : THookBytes;
end;
PScanHookInfo = ^TScanHookInfo;
过程InitModuleInfo用来初始化主结构
代码:
procedure InitModuleInfo(scanInfo:PScanHookInfo; ModuleName:string; ModulePath:string; ModuleBase:DWORD; Mode:TScanMode; ProcId:DWORD = $FFFFFFFF);
begin
ZeroMemory(scanInfo, SizeOf(TScanHookInfo));
MoveMemory(@scanInfo.ModuleName, PChar(ModuleName), Length(ModuleName)*sizeof(Char));
MoveMemory(@scanInfo.ModulePath, PChar(ModulePath), Length(ModulePath)*sizeof(Char));
scanInfo.ModuleBase:=ModuleBase;
scanInfo.Mode:=Mode;
if ProcId <> $FFFFFFFF then
scanInfo.ProcessId:=ProcId;
end;
其中ProcId只在扫描RING3层时使用
继续,包装函数就不再继续展开了,都是比较简单的:
代码:
function CheckInlineHook(scanInfo:PScanHookInfo):Boolean;
var
MapModuleBase :DWORD;
tempStr :string;
tempFile :string;
label lbExit1;
begin
Result:=False;
// 用户界面回显
tempStr:=Format('[%s]准备初始化',[string(scanInfo.ModuleName)]);
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@tempStr[1]), 0);
// 在这里,我将原文件拷贝到了临时目录再进行映射
tempFile:=CopyFileToTempDirectory(scanInfo.ModulePath);
if not FileExists(tempFile) then Exit;
// 映射模块
MapModuleBase:=DWORD(MapFileWriteAble(tempFile));
if MapModuleBase = 0 then
begin
tempStr:=Format('[%s]映射模块发生错误',[string(scanInfo.ModuleName)]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@tempStr[1]), 0);
Exit;
end;
// 用户界面回显
tempStr:=Format('[%s]初始化完毕,重定向数据...',[string(scanInfo.ModuleName)]);
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@tempStr[1]), 0);
// 重定向模块
if not RelocSpecialModule(MapModuleBase, scanInfo.ModuleBase) then
begin
tempStr:=Format('[%s]重定向模块时发生错误',[string(scanInfo.ModuleName)]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@tempStr[1]), 0);
goto lbExit1;
end;
// 填充结构
scanInfo.MapModuleBase :=MapModuleBase;
// 首先检测IATHook
case scanInfo.Mode of
KernelMode:
begin
CheckSpecialModuleIATHookKernel(scanInfo);
end;
UserMode:
begin
//CheckSpecialModuleIATHookUser(scanInfo);
end;
end;
// 扫描.text PAGE节区
ScanSection('.text', scaninfo);
ScanSection('PAGE', scaninfo);
Result:=True;
lbExit1:
if MapModuleBase <> 0 then UnMapFile(PBYTE(MapModuleBase));
DeleteFile(tempFile);
end;
重定向也就是将PE结构中重定向目录表中的所有条目进行修复,遍历一下即可,
因为我们要使我们映射的模块看起来就像是加载到了内核中一样,所以我们利用内核的加载基址来修复重定向条目
代码:
function RelocSpecialModule(ModuleBase:DWORD; NewBase:DWORD; IsMapped:Boolean = True):Boolean;
var
reloc :PImageBaseReloc;
relocSize :DWORD;
RvaBase :DWORD;
relocNum :DWORD;
i :DWORD;
DefaultBase :DWORD;
w :Word;
dw :DWORD;
va :DWORD;
begin
Result:=False;
if ModuleBase = 0 then Exit;
if NewBase = 0 then Exit;
Result:=True;
// 取得重定向目录表,如果不存在,直接返回
reloc:=GetModuleBaseRelocDirectory(ModuleBase, IsMapped);
if reloc = nil then Exit;
// 取模块默认装载基址
DefaultBase:=GetModuleDefaultBase(ModuleBase);
// 重定向总大小
relocSize:=GetModuleOptionalHeader(ModuleBase).DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
// 遍历所有重定向条目
while relocSize > 0 do
begin
relocSize:=relocSize - reloc.SizeOfBlock;
RvaBase:=reloc.VirtualAddress;
relocNum:=(reloc.SizeOfBlock - SizeOf(TImageBaseReloc)) div 2;
if relocNum = 0 then Break;
for i := 0 to relocNum - 1 do
begin
w:=PWORD(Cardinal(reloc)+sizeof(TImageBaseReloc)+i*sizeof(WORD))^;
if (w and $F000) shr 12 <> 3 then Continue;
dw:=RvaBase + (w and $FFF); //RVA
if dw = 0 then Continue;
if IsMapped = True then dw:=GetModuleRawFromRVA(ModuleBase, dw) - ModuleBase;
va:=dw + ModuleBase; //va
dw:=PDWORD(va)^ - DefaultBase + NewBase; //重定向修正后的值
PDWORD(va)^:=dw; //写入新值
end;
reloc:=PIMAGEBASERELOC(Cardinal(reloc)+reloc.SizeOfBlock);
if reloc.VirtualAddress = 0 then Break;
if reloc.SizeOfBlock = 0 then Break;
end;
Result:=True;
end;
具体就不多解释了,看雪的加密解密第三版中详细介绍了这些内容
重定向处理完之后需要首先检测IAT hook,检测IAT hook比较简单,我们只需要解析映射的PE文件的导入表定位到相应的偏移地址,
然后使用模块的内核基址加上这个偏移地址就能直接读取出导入表的当前值,然后使用MmGetSystemRoutineAddress得到真实值,
两个值比较一下就OK了,当然如果导入表中的不是hal或ntoskrnl中的函数,需要自己根据模块及函数名称在
驱动
中解析PE,
遍历一下对应模块的导出表内容,得到真实的地址,再进行比较
<上面这步需要
驱动
配合>
这部分就不列出来了,网上搜索也可以得到
检测完IAT hook开始扫描.text和PAGE两个节区
代码:
function ScanSection(SectionName:string; scanInfo:PScanHookInfo):Boolean;
var
pSection :PImageSectionHeader;
begin
Result:=False;
if SectionName = '' then Exit;
if scanInfo = nil then Exit;
// 获取指定节区信息
pSection:=GetSpecialSectionInformation(scanInfo.MapModuleBase, PAnsiChar(AnsiString(SectionName)));
if pSection <> nil then
begin
// 填充结构
ZeroMemory(@scanInfo.SectionName, Length(scanInfo.SectionName)*sizeof(Char));
MoveMemory(@scanInfo.SectionName, PChar(SectionName), Length(SectionName)*sizeof(Char));
scanInfo.SectionHeader:=pSection;
CheckInlineHook_Do(scanInfo);
end;
Result:=True;
end;
代码:
function CheckInlineHook_Do(scanInfo:PScanHookInfo):Boolean;
var
hDevice :THandle;
temp :string;
dw :DWORD;
BytesReturn :DWORD;
MemRange :TMemoryAddressRange;
MemRangeProc:TMemoryAddressRangeProcess;
pSectionBuf :Pointer;
bl :Boolean;
ret :Boolean;
label lbExit1;
begin
Result:=False;
bl:=False;
ret:=False;
if scanInfo = nil then Exit;
// 界面回显
temp:=Format('[%s] 目标节区: <%s>',[string(scanInfo.ModuleName), string(scanInfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@temp[1]), 0);
// 打开驱动设备
hDevice:=OpenDevice(DEVICE_SYMBOLIC_NAMR);
if hDevice = 0 then Exit;
// 准备与驱动进行通讯 分配内存 因为要读取整个节区,这里分配内存大小是节区大小
dw:=scaninfo.SectionHeader.SizeOfRawData;
pSectionBuf:=AllocMem(dw);
if pSectionBuf = nil then
begin
Closehandle(hDevice);
Exit;
end;
// 与驱动通讯,读取数据
case scanInfo.Mode of
KernelMode:
begin
// 要读取的起始地址,及长度. 起始地址为模块的内核基址+要扫描的节区的偏移地址
MemRange.BeginAddress:=scaninfo.ModuleBase + scaninfo.SectionHeader.VirtualAddress;
MemRange.Length:=scaninfo.SectionHeader.SizeOfRawData;
ret:=DeviceIoControl(hDevice, IOCTL_READ_MEMORY_RANGE, @MemRange, SizeOf(TMemoryAddressRange), pSectionBuf, dw, BytesReturn, nil);
end;
UserMode:
begin
MemRangeProc.ProcessId:=scaninfo.ProcessId;
MemRangeProc.BeginAddress:=scaninfo.ModuleBase + scaninfo.SectionHeader.VirtualAddress;
MemRangeProc.Length:=scaninfo.SectionHeader.SizeOfRawData;
ret:=DeviceIoControl(hDevice, IOCTL_READ_MEMORY_RANGE_PROCESS, @MemRangeProc, SizeOf(TMemoryAddressRangeProcess), pSectionBuf, dw, BytesReturn, nil);
end;
end;
// 如果通讯失败 通知用户界面 并停止扫描
if not ret then
begin
temp:=Format('[%s]与驱动通讯失败,在获取节区 <%s>',[string(scaninfo.ModuleName), string(scaninfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@temp[1]), 0);
goto lbExit1;
end;
// 验证返回的数据长度
if dw <> BytesReturn then
begin
temp:=Format('[%s]读取到的节区数据与节区大小不相符 <%s>',[string(scaninfo.ModuleName), string(scaninfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@temp[1]), 0);
end;
temp:=Format('[%s]与驱动通讯完毕, 准备扫描节区 <%s>',[string(scaninfo.ModuleName), string(scaninfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@temp[1]), 0);
// 为最后的扫描填充数据
// SourceAddress:我们映射的文件中当前扫描节区的地址
// DestAddress:驱动传回的具体数据
// CompareSize:节区大小
scanInfo.SourceAddress:=GetModuleRawFromRVA(scanInfo.MapModuleBase, scanInfo.SectionHeader.VirtualAddress);
scanInfo.DestAddress:=Cardinal(pSectionBuf);
scanInfo.CompareSize:=BytesReturn;
// 修正模块IAT, 防止产生误导
if scanInfo.Mode = KernelMode then
begin
FixSpecialModuleIATKernel(scanInfo);
end
else
begin
FixSpecialModuleIATUser(scanInfo);
end;
// 比较两段内存数据
if not CompareSection(scanInfo) then
begin
temp:=Format('[%s - %s]扫描发生异常, 终止',[string(scanInfo.ModuleName), string(scanInfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@temp[1]), 0);
goto lbExit1;
end;
// 扫描完成
temp:=Format('[%s]节区扫描完毕 <%s>',[string(scanInfo.ModuleName), string(scanInfo.SectionName)]);
SendMessage(MainForm.Handle, MSG_THREAD_STATUS, Integer(@temp[1]), 0);
bl:=True;
lbExit1:
if hDevice <> 0 then CloseHandle(hDevice);
if pSectionBuf <> nil then FreeMem(pSectionBuf);
Result:=bl;
end;
驱动
中读取指定范围的数据,这个很简单了,三五行代码就解决了
代码:
c = 0;
for ( i = (ULONG)BeginAddress; i < (ULONG)BeginAddress + Length; i = i + sizeof(ULONG), j = j + sizeof(ULONG) )
{
if ( MmIsAddressValidEx((PVOID)i) != VCS_INVALID )
RtlMoveMemory( (PVOID)((ULONG)(*pBuf) + j), (PVOID)i, sizeof(ULONG) );
else
RtlMoveMemory( (PVOID)((ULONG)(*pBuf) + j), &c, sizeof(ULONG) ); // 无效数据填充0
}
MmIsAddressValidEx是MmIsAddressValid的扩展版本,是一个很好的用来取代MmIsAddressValid的函数,可以在这里找到
http://bbs.pediy.com/showthread.php?t=109819
我们知道如果一个
驱动
中某个页面从没有被访问的时候系统可能不会对这个页面进行映射,
而我们在读取一块内存的时候通常都使用MmIsAddressValid来首先验证一下地址的有效性,
这样问题就来了,当使用MmIsAddressValid对一块没有被映射的有效内存进行判断的时候MmIsAddressValid会FALSE,
而实际上这内存是有效的,只不过还没有被映射而已,这样就会造成误判了
而使用MmIsAddressValidEx则不需要有这个顾虑,只要返回的不是VCS_INVALID,都是有效的内存地址,可以直接访问
驱动
传回数据之后我们需要对IAT的FirstThunk进行一下修复<也就是直接清零了>:
代码:
function FixSpecialModuleIATKernel(scanInfo:PScanHookInfo):Boolean;
var
pImport :PImageImportDescriptor;
ThunkVA :DWORD;
RVA :DWORD;
begin
Result:=False;
if scaninfo = nil then Exit;
Result:=True;
// 得到输入表
pImport:=GetModuleImportDescriptor(scanInfo.MapModuleBase);
// 没有输入表直接返回
if pImport = nil then Exit;
// 如果first thunk不在当前扫描的区段中不必进行IAT修正
if ( pImport.FirstThunk < scanInfo.SectionHeader.VirtualAddress ) or
( pImport.FirstThunk > scaninfo.SectionHeader.VirtualAddress + scaninfo.SectionHeader.SizeOfRawData ) then Exit;
// 遍历输入表
while (pImport.OriginalFirstThunk <> 0) or (pImport.FirstThunk <> 0) do
begin
RVA:=pimport.FirstThunk;
// 取得首个thunk VA
ThunkVA:=GetModuleRawFromRVA(scaninfo.MapModuleBase, RVA);
RVA:=RVA - scanInfo.SectionHeader.VirtualAddress;
// 遍历thunk
while PDWORD(ThunkVA)^ <> 0 do
begin
PDWORD(ThunkVA)^:=0;
// DestAddress就是驱动返回的数据块的首地址+RVA得到要清零的地址
PDWORD(scanInfo.DestAddress + RVA)^:=0;
Inc(ThunkVa, sizeof(DWORD));
Inc(RVA, sizeof(DWORD));
end;
Inc(pImport);
end;
Result:=True;
end;
注意这里对IAT的修复,需要对我们映射的原文件和
驱动
返回的数据块同时进行
最后的扫描函数
代码:
function CompareSection(scanInfo:PScanHookInfo):Boolean;
var
dwCompareSize :DWORD;
i :DWORD;
dwSourceAddr :DWORD;
dwDestAddr :DWORD;
dw :DWORD;
bl :Boolean;
tempStr :string;
begin
bl:=False;
Result:=False;
if scanInfo = nil then Exit;
dwCompareSize :=scanInfo.dwCompareSize;
dwSourceAddr :=scanInfo.SourceAddress;
dwDestAddr :=scanInfo.DestAddress;
while dwCompareSize > 0 do
begin
dw:=RtlCompareMemory(Pointer(dwSourceAddr), Pointer(dwDestAddr), dwCompareSize);
// 返回相同的字节数 也就是定位到了首个不同字节的位置
if dw = dwCompareSize then
begin
bl:=True;
Break;
end;
dwCompareSize:=dwCompareSize - dw;
// 过滤
if dwCompareSize > SizeOf(DWORD) then
begin
if PDWORD(dwSourceAddr + dw)^ = 0 then
begin
dwSourceAddr:=dwSourceAddr + dw + SizeOf(DWORD);
dwDestAddr:=dwDestAddr + dw + SizeOf(DWORD);
dwCompareSize:=dwCompareSize - SizeOf(DWORD);
Continue;
end;
end
else
begin
bl:=True;
Break;
end;
i:=0;
ZeroMemory(@scanInfo.HookData, SizeOf(THookBytes));
scanInfo.HookData.Address:=scanInfo.ModuleBase + scanInfo.SectionHeader.VirtualAddress + scanInfo.SectionHeader.SizeOfRawData - dwCompareSize;
// 确定不相同的字节个数 用了很土的方法
while True do
begin
if PBYTE(dwSourceAddr + dw + i)^ <> PBYTE(dwDestAddr + dw + i)^ then
begin
scanInfo.HookData.OriData[i]:=PBYTE(dwSourceAddr + dw + i)^;
scanInfo.HookData.CurData[i]:=PBYTE(dwDestAddr + dw + i)^;
scanInfo.HookData.Len:=i+1;
end
else
if PBYTE(dwSourceAddr + dw + i + 1)^ <> PBYTE(dwDestAddr + dw + i + 1)^ then //向下验证一下
begin
scanInfo.HookData.OriData[i]:=PBYTE(dwSourceAddr + dw + i)^;
scanInfo.HookData.CurData[i]:=PBYTE(dwDestAddr + dw + i)^;
scanInfo.HookData.OriData[i + 1]:=PBYTE(dwSourceAddr + dw + i + 1)^;
scanInfo.HookData.CurData[i + 1]:=PBYTE(dwDestAddr + dw + i + 1)^;
scanInfo.HookData.Len:=i + 1 + 1;
end
else
if PBYTE(dwSourceAddr + dw + i + 2)^ <> PBYTE(dwDestAddr + dw + i + 2)^ then
begin
scanInfo.HookData.OriData[i]:=PBYTE(dwSourceAddr + dw + i)^;
scanInfo.HookData.CurData[i]:=PBYTE(dwDestAddr + dw + i)^;
scanInfo.HookData.OriData[i + 1]:=PBYTE(dwSourceAddr + dw + i + 1)^;
scanInfo.HookData.CurData[i + 1]:=PBYTE(dwDestAddr + dw + i + 1)^;
scanInfo.HookData.OriData[i + 2]:=PBYTE(dwSourceAddr + dw + i + 2)^;
scanInfo.HookData.CurData[i + 2]:=PBYTE(dwDestAddr + dw + i + 2)^;
scanInfo.HookData.Len:=i + 1 + 2;
end
else Break;
inc(i);
if i >= 50 then
begin
tempStr:=Format('[%s]出现了大面积的错误, 停止扫描. Address : 0x%.8x',[string(scanInfo.ModuleName), scanInfo.HookData.Address]);
SendMessage(MainForm.Handle, MSG_THREAD_ERROR, Integer(@tempStr[1]), 0);
Exit;
end;
end;
scanInfo.HookData.HookType:=InlineHook;
SendMessage(MainForm.Handle, MSG_THREAD_DATA, Integer(scanInfo), 0); //更新主界面
// 更新地址 继续比较
dwSourceAddr:=dwSourceAddr + scanInfo.HookData.Len + dw;
dwDestAddr:=dwDestAddr + scanInfo.HookData.Len + dw;
dwCompareSize:=dwCompareSize - scanInfo.HookData.Len;
end;
Result:=bl;
end;
到了这个过程中所有数据已经准备好,直接比较就行了
RtlCompareMemory是ntdll中没有文档化的一个函数<在WDK中被文档化>,它比较两段内存返回首个不相同的字节位置
如果出现了不同,就向下验证一下,以将连续不同的一段内存编排到一起返回给界面
期间省略的很多与PE相关的处理过程可以在这里找到:
http://bbs.pediy.com/showthread.php?t=140422
到了这里就已经能扫描出指定模块中的inline hook了,像这样:
跟xuetr对比一下:
因为我们没有指定要扫描peauth.sys,所以比xuetr少了两个结果
xuetr是能够显示出被hook的函数名称的,对于这个信息可以遍历一下模块的导出表做一张表来得到,不过在这里我使用了符号进行匹配
具体方法就是枚举出正在扫描的模块的所有符号信息,保存到一个表中,
因为符号记载了一个符号的名称信息以及它的地址信息,所以我们只要根据hook位置找到与之相等或者最相近的符号,就可以了
比如我们检测到NtCreateFile中存在一个inline hook 位置为 0x1100,而与之最相近的符号位于0x1000,名称为NtCreateFile,
我们就能确定依此断定被hook函数为NtCreateFile了
另外关于对这些符号表的管理,我使用了一个总的TList类来保存每一个模块的符号表信息,每一个模块的符号表再用一个TList类来保存
同时扩展出一个ModuleName域用来记载符号表的模块名称信息,防止重复枚举
对于符号表的枚举以及定位我会再找时间总结,今天就写这么多吧,放两张最终的效果图
检测HS所做hook: