自己动手,制作inline hook扫描工具

Windows API Hook实战:通过Inline Hook绕过微信单实例限制 互斥体是Windows操作系统中用于实现进程间同步的核心内核对象,其核心原理是通过命名实现全局唯一性,确保关键资源在同一时刻只能被一个进程访问。API拦截技术,特别是Inline Hook,通过修改目标函数在内存中的指令,能够在不修改原始程序文件的前提下,精准控制其行为。这项技术在软件调试、安全研究和功能扩展等领域具有重要价值。在应用场景上,它可以用于分析程序行为、实现热修复或绕过特定限制。本文以微信客户端的单实例检测机制为例,深入探讨了如何通过Hook CreateMutexW函数,并结合Detours库 阅读详情
很久没来论坛了,今天来跟大家一起讨论一下关于检测内核中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查询到的是真正加载的模块名称:
点击图片以查看大图图片名称:	01.jpg查看次数:	2059文件大小:	30.2 KB文件 ID :	65072
而在PsLoadedModuleList中记录了两种名称:
点击图片以查看大图图片名称:	02.jpg查看次数:	2062文件大小:	38.7 KB文件 ID :	65073
这样我们就能在程序中使用更为通用的名称来指定检索的模块,而使用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了,像这样:
点击图片以查看大图图片名称:	03.jpg查看次数:	34文件大小:	86.1 KB文件 ID :	65074
跟xuetr对比一下:
点击图片以查看大图图片名称:	04.jpg查看次数:	2064文件大小:	81.5 KB文件 ID :	65075
因为我们没有指定要扫描peauth.sys,所以比xuetr少了两个结果
xuetr是能够显示出被hook的函数名称的,对于这个信息可以遍历一下模块的导出表做一张表来得到,不过在这里我使用了符号进行匹配


具体方法就是枚举出正在扫描的模块的所有符号信息,保存到一个表中,
因为符号记载了一个符号的名称信息以及它的地址信息,所以我们只要根据hook位置找到与之相等或者最相近的符号,就可以了
比如我们检测到NtCreateFile中存在一个inline hook 位置为 0x1100,而与之最相近的符号位于0x1000,名称为NtCreateFile,
我们就能确定依此断定被hook函数为NtCreateFile了


另外关于对这些符号表的管理,我使用了一个总的TList类来保存每一个模块的符号表信息,每一个模块的符号表再用一个TList类来保存
同时扩展出一个ModuleName域用来记载符号表的模块名称信息,防止重复枚举

对于符号表的枚举以及定位我会再找时间总结,今天就写这么多吧,放两张最终的效果图
点击图片以查看大图图片名称:	05.jpg查看次数:	34文件大小:	151.5 KB文件 ID :	65076

检测HS所做hook:
点击图片以查看大图图片名称:	06.jpg查看次数:	30文件大小:	268.2 KB文件 ID :	65077
[windows内核]分析MmIsAddressValid 如果有过驱动编程经验的人一定会了解这个函数MmIsAddressValid(),这个函数的作用简单来说就是验证一个地址可否使用,因为在驱动编程里稍有不慎就会导致BSOD,会在读取和写入数据的时候都要非常谨慎,做好充足的判断。 这是MSDN中的解释 https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-mmisaddressvalid If no page fault would occur from r 阅读详情

相关推荐

ProbeForRead/Write和MmIsAddressValid

ProbeForRead/Write 在开发驱动模块时,有时候需要处理来自用户态进程发送的IOCTL请求。在解析参数的时候,如果有用户地址空间的内存,要检测是否可以读(ProbeForRead)写(ProbeForWrite)。 看一下这两个函数的具体实现: ProbeForRead函数就是简单的检查了一下对齐粒度,然后看这块地址是否属于用户层**(地址末尾要小于MmUserProbeAddress)** 3: kd> dd MmUserProbeAddress 849ac850 7fff0000

qq_42814021的博客 1550

漏洞预防

内核驱动漏洞原因和七大忠告 不要使用MMIsAddressValid函数,这个函数对于校验内存结果是UNreliable的 首先,他只能判断一个字节地址的有效性 比如:​​​​​​ if(MmIsAdressValid(p1)){ memcmp(p1,p2,len); } 攻击者只需要传递第一个字节在有效页,而第二个字节在无效页的内存就会导致系统崩溃。比如0x7000是有效也 0x8000是无效也 传入0x7fff 其次,MmIsAddressValid对于 pageout的页面不能准..

m0_55875295的博客 433

Inline Hook检测模块到驱动

 by sudami由于项目需要,驱动中要做所有的事情,所以我负责的一个小小的模块自然要全部在驱动中实现;    小小的模块中一个小小的Inline Hook检测部分当然也不例外. 不过是要把某个函数调用链下所有调用过的函数都扫一遍,看是否存在恶意程序的干扰,自然用个深度递归就搞定,可是移植到驱动中,扫描是没问题,就怕那些变态的HOOK恢复不过来,还好,经过一小段时间的算法改进,差不多实现

1486

MmIsAddressValid

ULONG readmemory(IN ULONG windowh,IN ULONG BaseAdd,IN ULONG BufferSize) {   //参数一:窗口句柄 参数二:要读取的地址 参数三:读取的大小   HANDLE ProcessId=0;   ProcessId =(HANDLE) RealNtUserQueryWindow(windowh, 0);//窗口句柄转化为进

640

微信消息接收Hook实战:逆向分析与Inline Hook技术详解

在Windows平台软件安全与逆向工程领域,Hook技术是深入理解程序内部运行机制的核心手段之一。其原理是通过修改目标进程的代码或数据流,实现对特定函数或系统调用的拦截与监控,从而在不修改源代码的情况下,扩展或分析软件功能。这项技术在软件调试、安全分析、行为监控等场景中具有重要价值。本文聚焦于即时通讯软件的消息处理流程,通过逆向分析定位关键函数,并运用Inline Hook技术实现实时捕获。项目涉及动态链接库注入、内存操作与多线程同步等底层知识点,为理解RPC通信、消息队列及客户端安全提供了经典案例。

weixin_29324401的博客 847

Android安全攻防:基于Inline Hook的Frida检测与绕过实战

在移动应用安全领域,动态插桩技术是进行运行时分析和漏洞挖掘的重要手段。其核心原理是通过注入代码或修改内存指令,实现对目标进程行为的监控与干预。这项技术在安全评估、逆向工程和自动化测试中具有极高的技术价值,尤其在Android平台,被广泛应用于应用行为分析、协议解密和漏洞验证等场景。然而,随着安全对抗升级,针对动态插桩工具的检测与防护机制也日益增强。本文聚焦于基于Inline Hook的实战攻防,深入探讨如何通过直接修改内存指令来检测Frida等工具的注入痕迹,并详细解析相应的绕过策略。文章将结合ARM/AR

weixin_34082854的博客 351

更安全的MmIsAddressValid

代码来着开源工具 CheatEngine 在初始化是先调用InitMemSafe() 然后就可以使用IsAddressSafe来代替MmIsAddressValid了 int PTESize; UINT_PTR PAGE_SIZE_LARGE; UINT_PTR MAX_PDE_POS; UINT_PTR MAX_PTE_POS; struct PTEStruct { un

zhuhuibeishadiao 4495

VEH Hook vs. 传统Inline Hook:在游戏反作弊对抗中如何选择与隐藏?

本文深入对比了VEH Hook与传统Inline Hook在游戏反作弊对抗中的应用与隐蔽性优化。VEH Hook利用硬件断点实现零内存修改,适合高强度检测环境;而Inline Hook虽易被检测,但通过多级跳转和代码洞穴技术仍可提升隐蔽性。文章还提供了混合架构实践建议,帮助开发者在不同场景下做出最优选择。

weixin_30632883的博客 227

HOOK技术的简单使用

APIHOOK技术是逆向工程中的核心方法,通过拦截和修改API调用实现程序行为控制。本文系统阐述了APIHOOK的三大技术层次(用户层、内核层、混合层)及其应用场景,包括功能扩展、行为监控和逆向修改。重点解析了IATHOOK、InlineHOOK等实现原理,并推荐了可视化DLL劫持工具。通过注册验证案例演示了从目标分析到DLL配置的完整流程,同时探讨了反HOOK对抗和法律边界问题。该技术能显著提升逆向效率,但需在合法范围内使用。

vbnetcx的博客 3万+

从RCTF 2017 MyDriver2看Windows驱动中的Inline Hook实战与逆向分析

本文深入分析了RCTF 2017 MyDriver2题目中的Windows驱动逆向技术,重点探讨了Inline Hook的实现与破解方法。通过详细的逆向过程展示,包括动态代码解密、交叉引用追踪和内核API分析,帮助读者掌握驱动逆向的核心技巧。文章还提供了完整的Python解密代码实现,适合逆向工程爱好者学习参考。

weixin_28716443的博客 955

Frida高阶内存修改:权限操控、Inline Hook与防检测实战

内存操作是软件动态分析的核心技术,它允许在程序运行时直接读写和修改其内存数据,从而改变程序行为。其原理基于操作系统对进程虚拟内存空间的管理,通过系统API动态调整内存页的读写执行权限。这项技术的价值在于为安全研究、漏洞挖掘、软件调试和功能分析提供了底层干预能力,广泛应用于移动应用安全评估、游戏逆向和恶意软件分析等场景。本文聚焦于Frida框架下的高阶内存修改技巧,深入探讨如何通过内存权限的动态操控、异步内存扫描、Inline Hook代码流劫持等实战方法,实现对加固目标的安全、隐蔽修改,并系统性地应对反调试

weixin_29325007的博客 273

基于Inline Hook的PC微信机器人:消息拦截与自动回复实战

Hook技术是Windows平台实现进程内功能扩展的经典手段,通过改写目标函数入口跳转,实现对消息流量的透明拦截。Inline Hook因其可挂接任意函数和偏移点,成为PC客户端自动化场景中的主流方案。在即时通讯领域,基于Inline Hook的微信机器人能够将微信PC客户端转变为可控的消息网关,在不依赖服务器和扫码协议的情况下,实现消息读取、解析与自动回复。通过MinHook库完成指令搬移,配合特征码定位消息处理函数,并以消息队列解耦回调与业务逻辑,可构建稳定高效的自动应答服务。该方案广泛应用于客服辅助、

weixin_28716443的博客 232

【Frida Android】实战篇17:Frida检测与绕过——基于inline hook的攻防实战

在Frida的动态调试中,是Native层钩子(如)实现的核心机制。其本质是通过修改目标函数的开头指令指令替换:Frida会在目标函数(如系统库libc.so中的strstrstrcmp)的开头,替换前N字节指令为跳转指令;执行流劫持:当目标函数被调用时,跳转指令会将执行流程导向Frida的钩子函数(onEnteronLeave),从而实现参数监控、修改或逻辑篡改;原函数恢复:钩子函数执行完成后,Frida通过“跳板(Trampoline)”恢复被替换的原始指令,并跳回原函数继续执行剩余逻辑。

水滴石穿 1338

基于CallStack的反Rootkit HOOK检测

Anti-Rootkit目前扫描Hook的方法主要有以下几种:   1.对抗inline -hook ,IAT/EAT Hook   Anti-Rootkit使用读取磁盘上系统文件并将之进行map\重定位后,同内存中的代码进行对比的方法来检测inline hook(或EAT/IAT HOOK,后同),类似的工具例如Rootkit Unhooker, gmer, Icesword等等。

1554

深入解析Ring3环境下Inline Hook绕过技术与Delphi实战

htmltable {th, td {th {pre {简介:在Windows系统中,Ring3即用户模式,是应用程序运行的常规环境,而Inline Hook作为一种常见的调试与监控手段,通过修改函数入口指令实现控制流劫持,广泛应用于恶意软件分析、逆向工程等领域。本文深入探讨在Ring3下如何有效绕过Inline Hook机制,并结合提供的Delphi源码进行技术剖析。

weixin_35019679的博客 1185

Windows API HOOK技术实战:从Inline Hook原理到DLL注入实现

HOOK(钩子)技术是软件安全与系统编程中的核心概念,它通过在程序执行路径中插入自定义代码,实现对函数调用的监控、拦截与修改。其基本原理是改变CPU的执行流,常见实现方式包括IAT HOOK、Inline Hook等。这项技术的核心价值在于提供了对软件运行时行为的深度洞察与控制能力,广泛应用于安全防护、行为分析、调试监控等场景。本文聚焦于Inline Hook这一底层实现方案,它通过直接修改目标函数内存中的机器指令(如写入JMP跳转),具有通用性强、效果直接的特点。文中结合信息安全工程实践,详细拆解了在Wi

weixin_29324877的博客 316

hook api win10失效_ScyllaHide的Hook原理

Hook是通过修改程序代码或数据,达到改变程序逻辑的目的。Hook种类很多,ScyllaHide主要使用的是inline hook,也就是在运行的流程中插入跳转指令(call/jmp)来抢夺程序运行流程的一个方法。Hook根据系统版本(win7、win8、win10等)、cpu架构(x86、x64)、子系统类型(是否是wow64进程),实现由细微差别。本节只讨论win7、x64下32位进程(即wo...

weixin_42471590的博客 989

C#实现通用程序补丁工具:PE静态补丁到Inline Hook

通过修改程序机器码改变运行行为是底层开发中一种重要的调试与修复手段。在Windows平台,理解PE文件结构是静态补丁的基础,而特征码扫描能摆脱固定地址的局限,结合Sunday算法可显著提升匹配效率。对于运行中的进程,借助Win32 API实现内存写入与Inline Hook,则能在不重启程序的情况下动态调整逻辑。这些技术在工业上位机开发中尤为实用,例如应对厂商SDK崩溃、老驱动兼容等问题,甚至可将扫码枪的轮询接口封装为事件驱动,而无需依赖原始DLL修改。然而,内存操作涉及多线程与架构差异,必须关注稳定性及自

weixin_31412781的博客 329
上一篇: 另类挂钩 RING3数据包监视
下一篇: 城里城外看SSDT
M-先生
博客等级 码龄17年 60粉丝 6原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值