登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 234|回复: 0

WindowsX86内核16.API调用流程和HookAPI

[复制链接]

152

主题

15

回帖

675

积分

管理员

积分
675
发表于 2024-12-17 22:02:44 | 显示全部楼层 |阅读模式
WindowsAPI调用流程
  • 不管调那个函数都可以,只要能进内核就行了,但是一般是不会调Ntdll里的,因为Ntdll里的参数要求和3环是不一样的
  • user32.dll里都是UI相关的API,Kernel.dll里是核心的功能,这两个是常用的,可以选择一个来分析
分析 ReadProcessMemory
IDA打开Kernel.dll,找到ReadProcessMemory
push    ebp
mov     ebp, esp
lea     eax, [ebp+nSize]
push    eax             ; NumberOfBytesRead
push    [ebp+nSize]     ; NumberOfBytesToRead
push    [ebp+lpBuffer]  ; Buffer
push    [ebp+lpBaseAddress] ; BaseAddress
push    [ebp+hProcess]  ; ProcessHandle
call    ds:NtReadVirtualMemory; 发现它转换完参数就调了Nt这个导出函数然后点击导出表,搜索这个函数,发现它是由ntdll.dll导入的,也就是这里啥也没干,全部交给ntdll了

    IDA打开ntdll.dll,搜索NtReadVirtualMemory函数,在导出表搜索
    其实老版本用的都是这个,新版本都改成zw开头的了也就是ZwReadVirtualMemory,但是在IDA导出表搜索的时候发现它其实两个函数指向的地址是同一个地址;为了兼容老版本.所以就分析NtReadVirtualMemory就行了
ZwReadVirtualMemory proc near                        
mov     eax, 0BAh         ;186 API编号     
mov     edx, 7FFE0300h    ;全局变量
call    edx
retn    14h
ZwReadVirtualMemory endp
    这个全局变量要么通过KiIntSystemCall,要么通过KiFastSystemCall 或者直接在WinDbg里跟函数也行, 也就是rdmsr 176得到地址,然后跟这个函数.在WinDbg里发现它最终调到内核里的KiFastCallEntry了,所以就分析这个就行了
分析KiFastCallEntry
_KiFastCallEntry proc near             
var_B  = byte ptr -0Bh
mov     ecx, 23h ; '#' 
push    30h ; '0'            把fs改成内核的,这样就可以访问kpcrb了
pop     fs ;                  fs本来指向的是3环的teb,改完就能访问kpcrb了
mov     ds, ecx ;
mov     es, ecx ;                        ds和es改成23
mov     ecx, large fs:40;   40就是kpcr里的TSS : +0x040 TSS:Ptr32 _KTSS
mov     esp, [ecx+4];       +4就是TSS里的Esp0: +0x004 Esp0:Uint4B;也就是 ESP=ESP0切换栈
push    23h ; '#'
push    edx ;保存环境
pushf   ;下面有个加法,怕影响标志寄存器,所以这里就pushf了

loc_40770A:
push    2
;这里+8就说明它在3环就没有+8,栈顶是EBP+返回值,真正的参数就在+8的位置
add     edx, 8;    edx是3环的栈,
popf
or      [esp+0Ch+var_B], 2
push    1Bh
push    dword ptr ds:0FFDF0304h
push    0
push    ebp
push    ebx
push    esi
push    edi ;保存环境

;因为汇编里不能用 lea eax,fs[0],所以它就把首地址放到1C的位置,它就可以mov获取了
mov     ebx, large fs:1Ch;      这里是拿结构体首地址 +0x01c SelfPcr : Ptr32 _KPCR
push    3Bh ; 

;+0x120 PrcbData: _KPRCB;然后在+4,就是KPRCB里的
;+0x004 CurrentThread : Ptr32_KTHREAD了
mov     esi, [ebx+124h];   获取当前线程CurrentThread
push    dword ptr [ebx]
mov     dword ptr [ebx], 0FFFFFFFFh;    然后这里取内容得到了异常链表,再把它赋值为-1
;+0x018 InitialStack     : Ptr32 Void
mov     ebp, [esi+18h];   当前线程结构体+18的位置,也就是拿栈
push    1
sub     esp, 48h
sub     ebp, 29Ch
;+0x140 PreviousMode     : Char
mov     byte ptr [esi+140h], 1;    这是获取之前的模式,记一下是从3环调的还是从0环调的
cmp     ebp, esp
jnz     loc_4076C8
and     dword ptr [ebp+2Ch], 0
test    byte ptr [esi+2Ch], 0FFh
mov     [esi+134h], ebp
jnz     Dr_FastCallDrSave
loc_40776A:                                
mov     ebx, [ebp+60h]
mov     edi, [ebp+68h]
mov     [ebp+0Ch], edx
mov     dword ptr [ebp+8], 0BADB0D00h
mov     [ebp+0], ebx
mov     [ebp+4], edi
sti  ;这里的代码都不是很重要
loc_407781:                            
mov     edi, eax ;eax给edi
shr     edi, 8;    右移8位
and     edi, 30h;  再and30
;1111 1111 1111 1111右移八位=>1111 1111
;再&30 也就是 1100  => 1111 11 00 0000 0000 => 11 00 0000 0000
;&30就是取出11这两位,正常情况下 +0000 就是SSDT;要不就是10了+10就是ShadowSSDT
mov     ecx, edi
;KTHREAD 里的+0x0e0 ServiceTable: Ptr32 Void;这就是SSDT表,
;也就是这个表它是从ethread拿的,说明它在创建线程的时候就要把表地址放到这里了
;但是这个地址还要加上edi,从这里可以说明这个表有两个.两个表就通过上面的and结果来区分
;一个叫SSDT这个表放NTdll的所有函数,另外一个叫ShadowSSDT(这里放的都是UI的API)
 add     edi, [esi+0E0h];做了一个加法 esi此时是ethread+e0 就是ServiceTable
;如果调的是UI的API它就拿服务表的首地址再+16,否则就+0,+8的位置刚好是数量
mov     ebx, eax
             and     eax, 0FFFh ;与掉低12位,说明API编号只有12位有效,高位都丢弃了
cmp     eax, [edi+8];然后和edi+8比较,说明edi+8放的是函数的数量
jnb     _KiBBTUnexpectedRange
cmp     ecx, 10h
jnz     short loc_4077C0
mov     ecx, ds:0FFDFF018h
xor     ebx, ebx;这里是检查

loc_4077AE:                         
or      ebx, [ecx+0F70h]
 jz      short loc_4077C0
push    edx
push    eax
call    ds:_KeGdiFlushUserBatch
pop     eax
pop     edx;这里是检查
loc_4077C0:                        
inc     dword ptr ds:0FFDFF638h
mov     esi, edx
mov     ebx, [edi+0Ch];从第12的位置取出参数
xor     ecx, ecx ;ecx清0
mov     cl, [eax+ebx];然后直接拿出来用了
mov     edi, [edi];edi是函数表
mov     ebx, [edi+eax*4];查出函数地址
sub     esp, ecx ;抬栈
shr     ecx, 2 ;除4
mov     edi, esp
cmp     esi, ds:_MmUserProbeAddress ;检查栈顶是不是有效的
jnb     loc_407990
loc_4077E8:                         
rep movsd ;开始拷贝参数
call    ebx ;然后就直接调用了
loc_4077EC:
mov     esp, ebp 
loc_4077EE:                        
;调完就返回了     
mov     ecx, ds:0FFDFF124h
mov     edx, [ebp+3Ch]
mov     [ecx+134h], edx
_KiFastCallEntry endp ;这个函数其实没有完,
它会走到_KiServiceExit里,然后在那里恢复环境,在返回
  • UI的内核实现代码不在ntoskrnl.exe里, 它是重新封装了一个模块叫Win32k.sys这里放UI函数的实现代码
  • win32k不是所有进程都有的.因为Windows上也有没有界面的软件,所以它做了两个表SSDT和ShadowSSDT
    • 也就是说当一个进程是控制台的时候它就只用SSDT表;
    • 当它是一个Win32应用程序的时候这两个表都存在
  • 获取win32k模块 可以遍历所有进程,找一个带窗口的进程,最直接的就是找桌面进程或者登录界面;explorer 切换到这个进程 .process /p /i 822e7020
    然后再lm 就可以看到有一个win32k了:bf800000 bf9c2800 win32k (deferred)
    切换到这个进程后查看它的eprocess里面的ethread表,它就会有两个
  • 查看SSDT表
  • 在WinDbg里查看KiFastCallEntry函数,然后设置一个断点,断下来就查看edi这个地址的值 可以断在判断成功的地址比如: 804df791 8bd8 mov ebx,eax;这个地址就可以 然后就查看edi就行了,edi指向的地址是一个结构体 此时eip = 8055b1e0 kd> dd 8055b1e0 8055b1e0 804e36a8 00000000 0000011c 80511088 上面分析已经知道了+8的地址是函数数量,也就是0000011c 第一个地址指向的是一个函数指针表;可以使用dds 804e36a8,它就会把每个成员符号解析一下. kd> dds 804e36a8 804e36a8 80590df5 nt!NtAcceptConnectPort 804e36ac 8057a0f1 nt!NtAccessCheck .......省略 可以这里放的确实是所有API的实现 第四个成员也是一个表,放的是参数的大小,可以通过 db 80511088查看
  • 如果eip+16的位置有表,就说明它是个Win32的程序,如果没有表,就是控制台程序. 而它+16的地址就是8055b1f0,正好有数据 8055b1f0 bf999b80 00000000 0000029b bf99a890 和上面查看的流程一样 kd> dds bf999b80 bf999b80 bf935f7e win32k!NtGdiAbortDoc bf999b84 bf947b29 win32k!NtGdiAbortPath ;这里确实是Win32k的API
  • 不在函数里下断点的查看流程
  • 直接遍历所有进程,然后随便找一个进程,打开它的ETHREAD 也可以先看eprocess;直接dt _eprocess 864ac578;然后就可以在结构体里找到线程了, 或者直接kd>!process 864ac578 7 ;这样就直接可以看到它的线程了 然后随便找一个线程 kd> dt _kthread 8223b648 就能找到它的服务表了,在e0的位置+0x0e0 ServiceTable: 0x8055b220 Void 然后dd查看它,kd>dd 0x8055b220 kd> dd 0x8055b220A ReadVirtual: 8055b220 not properly sign extended 8055b220 804e36a8 00000000 0000011c 80511088 8055b230 00000000 00000000 00000000 00000000 可以看到它是一个控制台程序,没有第二个表
  • HookAPI
    • HookApi就有很多方案了
    • hook msr
    • inline hook KiFastCallEntry;在这个函数里找一个位置改jmp,可以在call ebx这里改jmp,因为这里环境准备好了,Erie参数也在栈顶,而且判断 ebx就知道他要调哪个函数了;有时候hook可能并不只为了拦截,如果是行为监控的话,就可以call ebx之后改jmp,只为了获取它调了什么,打个日志
    • hook修改SSDT表,或者ShadowSSDT表 改SSDT或者ShadowSSDT表是最稳定的,改表兼容性更好,只是麻烦一点.
  • 这三种HOOK也有缺点,比如内核里调NT是拦截不到的,因为改的是表,而它不走表,除非它调zw
  • 做Hook首先要搞清楚的是如何拿到这两个表SSDT和ShadowSSDT
  • 这两个表是全局变量,可以看一下WRK,看它是怎么做的 表在ethread里,所以应该看kthread; 看它在kthread什么时候填的 kthread里有个服务表,就是ServiceTable,可以参考引用,看谁填了这个表 这样就找到了这两个表 DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES]; DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES]; 因为它是两个全局变量,所以它正好就是隔16 如果是控制台,它就把KeServiceDescriptorTable的地址给ethread 如果是Win32的就是给完一个,再+16给第二个
  • 它的结构体就是 typedef struct _KSERVICE_TABLE_DESCRIPTOR { PULONG_PTR Base; PULONG Count; ULONG Limit; PUCHAR Number; } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
  • 要拿到这两个表的话很简单,在老版本里这个表KeServiceDescriptorTable它直接导出了 所以要拿到这个表的地址直接extern就行了 extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; 而KeServiceDescriptorTableShadow表没有导出,但是它在内存中是连续的,所以直接+16就是它了 有了这两个表的地址就可以hook了
  • 但是从Win7开始它就不导出了这个表了,Win7拿的话就得从kthread里拿了 从kthread里拿得话要先搞定偏移,第二个问题就是不一定能拿到Shadow表.因为ethread里不一定有 所以从etherad里拿也是不完整.但是可以强制切进程到桌面,然后再去拿这个表就可以了,所以难点就是偏移如何解决,要兼容所有版本的话写switch也可以,但是太麻烦了,其实可以从KiFastCallEntry里拿;因为它里面肯定会用这个偏移的也就是 add edi, [esi+0E0h]这行代码,那么就可以扫特征码了 这个函数地址是很好拿的,可以通过msr拿,就是rdmsr 176,拿到地址后就扫特征码,拿出偏移. 拿这个表的时候要把进程切到一个Win32的程序上,这样的话这两个表都能拿到了 注意要拿ethread就得先拿eprocess,遍历进程的话又有偏移问题了,不过这个遍历进程线程得话它有未公开函数来用;可以用PsLookupthreadByThread();给个线程ID就返回ethread,要拿线程ID还是不难的. 而Ps这些函数都是导出的,所以拿地址就很方便
  • 拿线程的话拿谁的线程ID也很关键,都是拿Win32程序的,没有的话也可以自己写一个.
  • 获取表地址代码示例
    typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG_PTR Base;
    PULONG Count;
    ULONG Limit;
    PUCHAR Number;
    } KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
    
    //SSDT表
    PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
    //ShadowSSDT表
    PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;
    
    //声明函数
    NTSTATUS __stdcall PsLookupThreadByThreadId(
    __in HANDLE ThreadId,
        __deref_out PETHREAD* Thread
        );
    /*驱动卸载函数 clean_up*/
    VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
    {
        DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);
    
    }
    /*1.驱动入口函数*/
    NTSTATUS DriverEntry(
    __in struct _DRIVER_OBJECT* DriverObject,
    __in PUNICODE_STRING  RegistryPath)
    {
        UNREFERENCED_PARAMETER(RegistryPath);
    
        DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
        DriverObject->DriverUnload = Unload;
    
        PETHREAD Thread = NULL;
        //通过线程ID获取EThread;284是桌面的一个线程ID
        NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
        DbgPrint("[WANG] Thread:%p\n", Thread);
        if (!NT_SUCCESS(Status)) {
            return STATUS_SUCCESS;
        }
    
        //成功了以后就可以开始拿表了 就是ethread + 偏移
        KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
        //Shadow表的话就拿指针+1就可以了
        KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;
    
        DbgPrint("[WANG] KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
        //Shadow表直接+1就行了,因为它是结构体指针
        DbgPrint("[WANG] KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);
    
        //ethread成功获取了的话对象的引用计数要--
        if (Thread) {
            ObfDereferenceObject(Thread);
        }
        return STATUS_SUCCESS;
    }

    • 接下来就可以HookAPI了
  • 可以选择Hook:OpenProcess,模拟读写进程内存做保护的情况,因为HookAPi的话太多了,读啊写啊的,但是不管
  • 读还是写都要先打开获取句柄,但是不知道OpenProcess的API编号是多少,可以通过Ntdll导出函数来查看
  • Ntdll里有个ZwOpenProcess,它里面就写了API编号,是7Ah.得到API编号后就可以对它进行Hook了
  • Hook的话要知道它的参数,可以通过WRK查一下就知道了
  • 这样就可以自己写一个伪造的了,然后把SSDT对应的表项改为自己的就可以了
    • 代码示例
      #include <ntddk.h>
      
      typedef struct _KSERVICE_TABLE_DESCRIPTOR {
      PULONG_PTR Base;
      PULONG Count;
      ULONG Limit;
      PUCHAR Number;
      } KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
      
      //SSDT表
      PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
      //ShadowSSDT表
      PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;
      
      //声明函数
      NTSTATUS __stdcall PsLookupThreadByThreadId(
      __in HANDLE ThreadId,
          __deref_out PETHREAD* Thread
          );
      
      //要保存一下旧的OpenProcess函数
      NTSTATUS (*g_OldNtOpenProcess)(
      __out PHANDLE ProcessHandle,
      __in ACCESS_MASK DesiredAccess,
      __in POBJECT_ATTRIBUTES ObjectAttributes,
      __in_opt PCLIENT_ID ClientId);
      
      LONG g_RefCount = 0;//引用计数
      
      //自己伪造的OpenProcess函数
      NTSTATUS FakeNtOpenProcess(
      __out PHANDLE ProcessHandle, //句柄
      __in ACCESS_MASK DesiredAccess,
      __in POBJECT_ATTRIBUTES ObjectAttributes,//路径在这里
      __in_opt PCLIENT_ID ClientId //进程ID在这里写了
      ) 
      {
          InterlockedIncrement(&g_RefCount);//引用计数++,为了避免多线程同步问题,用延迟锁来++
          //拦截的话可以不返回失败,返回成功,让对方找不到bug
      
          //判定一下参数
          if (ClientId != NULL) {
              //当前ID不等于指定的线程ID就说明不是自己操作
              if (PsGetCurrentProcessId() != (HANDLE)1984) {
                  //并且这个参数PID也等于指定的线程ID
                  if (ClientId->UniqueProcess == (HANDLE)1984) { 
                      //这就说明是别的进程在操作了
                      return  STATUS_INVALID_BUFFER_SIZE; //给它随便返回一个错误码
                  }
              }
              //这样就对指定进程做了个保护,就是只要不是自己操作,就返回错误码,是自己操作就正常调旧的
      
              DbgPrint("FakeNtOpenProcess UniqueProcess %d\n", ClientId->UniqueProcess);
          }
          else {
              //打印一下日志就行了
              DbgPrint("FakeNtOpenProcess %wZ\n", ObjectAttributes->ObjectName);
          }
      
      
          //做监控的话就记录一下,然后给它调旧的就行了
          NTSTATUS Status  = g_OldNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);;
      
          InterlockedDecrement(&g_RefCount);//返回之前--
          return Status;
      };
      
      
      //开启写保护
      void EnableWP() {
          //获取CR0寄存器.
          ULONG_PTR cr0 = __readcr0();
          //把第16位置为1
          cr0 |= 0x10000;
          //在写回去
          __writecr0(cr0);
      }
      
      //关闭写保护
      void DisableWP() {
          //获取CR0寄存器.
          ULONG_PTR cr0 = __readcr0();
          //把第16位清0
          cr0 &= ~0x10000;
          //在写回去
          __writecr0(cr0);
      }
      
      /*驱动卸载函数 clean_up*/
      VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
      {
      
          //卸载的时候把他还原回去
          DisableWP();
          //再把它改掉
          KeServiceDescriptorTable->Base[0x7a] = (ULONG_PTR)g_OldNtOpenProcess;
          EnableWP();
      
          DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);
          while (g_RefCount != 0); //引用计数不等于0就不卸载
      }
      
      
      /*1.驱动入口函数*/
      NTSTATUS DriverEntry(
      __in struct _DRIVER_OBJECT* DriverObject,
      __in PUNICODE_STRING  RegistryPath)
      {
          UNREFERENCED_PARAMETER(RegistryPath);
      
          DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
          DriverObject->DriverUnload = Unload;
      
          //PETHREAD Thread = NULL;
          ////通过线程ID获取EThread
          //NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
          //DbgPrint("[WANG] Thread:%p\n", Thread);
          //if (!NT_SUCCESS(Status)) {
          //    return STATUS_SUCCESS;
          //}
      
          //因为没用到Shadow表,所以可以拿自己的
          PETHREAD Thread = PsGetCurrentThread();
      
          //成功了以后就可以开始拿表了 就是ethread + 偏移
          KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
          //Shadow表的话就拿指针+1就可以了
          KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;
      
          DbgPrint("[WANG] KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
          //Shadow表直接+1就行了,因为它是结构体指针
          DbgPrint("[WANG] KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);
          //修改SSDT表第7A项,就是OpenProcess先保存一下,要强转成函数指针
          g_OldNtOpenProcess =  (NTSTATUS(*)(__out PHANDLE ,__in ACCESS_MASK , 
          __in POBJECT_ATTRIBUTES ,__in_opt PCLIENT_ID))
          KeServiceDescriptorTable->Base[0x7a]; 
      
          //因为SSDT表的内存地址是不可写的,所以要关闭写保护.然后在修改
          DisableWP();
          //再把它改掉
          KeServiceDescriptorTable->Base[0x7a] = (ULONG_PTR)FakeNtOpenProcess;
          EnableWP();
      
          //ethread成功获取了的话对象的引用计数要--
          //if (Thread) {
          //    ObfDereferenceObject(Thread);
          //}
          return STATUS_SUCCESS;
      }
      
          //这段代码在卸载的时候还是有问题的,考虑不够.
      这些都是在做保护,如果病毒作者他HOOK了API的话,ARK工具就需要检测了 可以先遍历SSDT表,要知道那个API被HOOK了的话,就要判断地址了 只要判断API地址是不是在Nt范围内,如果在NtKernel范围内就没有被HOOK,因为要Hook的话肯定要把自己的函数地址写在这里,所以它的地址肯定不是在主模块. 但是它完全可以在NtKernel里找一个空的地址,然后jmp过去,这样的话ARK就检测不出来了 更深层次的检测应该是对比文件,就是重载内核.重载内核可以得到一个干净的SSDT表,然后和它做对比. 说白了检测APIHook就是扫描这个SSDT表. 检测出来就要恢复了.
      ● ARK工具遍历SSDT表显示函数名的方法 不同版本表是不一样的,所以显示出函数名是个难点 1.最老的办法就是写死,写一个数组,什么版本用什么数组. 2.遍历Ntdll的导出表.Ntdll里肯定有个函数进内核,而且它也写了函数编号,所以就可以遍历它的导出表,找到每 个函数对应的编号,然后编号和SSDT表的下标对比,这样就知道函数名称了,但是这种方法有缺点,就是不全,比如 SSDT表里的有些函数它是非公开的,那么在Ntdll里就没有导出.这样的话拿函数名就不全了 3.解析PDB文件,根据系统版本自动从Windows服务器把PDB下载下来,只下载NtKernel和Win32k的PDB 下载下来以后就可以解析PDB里的符号信息,把RVA传递进去就能查出它的函数名了 微软提供了API可以自动下载,下载完还需要解析,微软也做好库了,在VS里有个完整的工具能解析所有符号. 路径就是Microsoft Visual Studio\2019\Community\DIA SDK\Samples 它有个示例程序DIA2Dump,直接打开就可以查看了 要运行的话需要把dll注册一下,要不然就自己加载dll
      ● 绕过ARK工具检测是否被Hook的方法 1备份原SSDT表 2目标进程的ETHREAD.ServiceTable = 备份的SSDT表 也就是改表的地址,不改表项了. ● ZwQuerySystemInformation可以获取所有的系统信息....不管是进程,线程,模块啥都能获取到




您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-3-14 16:15 , Processed in 0.100057 second(s), 21 queries , Yac On.

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表