登录  | 立即注册

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

查看: 25|回复: 0

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

[复制链接]

71

主题

2

回帖

147

积分

管理员

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

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

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

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

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

  77. loc_4077AE:                        
  78. or      ebx, [ecx+0F70h]
  79. jz      short loc_4077C0
  80. push    edx
  81. push    eax
  82. call    ds:_KeGdiFlushUserBatch
  83. pop     eax
  84. pop     edx;这里是检查
  85. loc_4077C0:                        
  86. inc     dword ptr ds:0FFDFF638h
  87. mov     esi, edx
  88. mov     ebx, [edi+0Ch];从第12的位置取出参数
  89. xor     ecx, ecx ;ecx清0
  90. mov     cl, [eax+ebx];然后直接拿出来用了
  91. mov     edi, [edi];edi是函数表
  92. mov     ebx, [edi+eax*4];查出函数地址
  93. sub     esp, ecx ;抬栈
  94. shr     ecx, 2 ;除4
  95. mov     edi, esp
  96. cmp     esi, ds:_MmUserProbeAddress ;检查栈顶是不是有效的
  97. jnb     loc_407990
  98. loc_4077E8:                        
  99. rep movsd ;开始拷贝参数
  100. call    ebx ;然后就直接调用了
  101. loc_4077EC:
  102. mov     esp, ebp
  103. loc_4077EE:                        
  104. ;调完就返回了     
  105. mov     ecx, ds:0FFDFF124h
  106. mov     edx, [ebp+3Ch]
  107. mov     [ecx+134h], edx
  108. _KiFastCallEntry endp ;这个函数其实没有完,
  109. 它会走到_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程序的,没有的话也可以自己写一个.
  • 获取表地址代码示例
    1. typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    2. PULONG_PTR Base;
    3. PULONG Count;
    4. ULONG Limit;
    5. PUCHAR Number;
    6. } KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;

    7. //SSDT表
    8. PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
    9. //ShadowSSDT表
    10. PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;

    11. //声明函数
    12. NTSTATUS __stdcall PsLookupThreadByThreadId(
    13. __in HANDLE ThreadId,
    14.     __deref_out PETHREAD* Thread
    15.     );
    16. /*驱动卸载函数 clean_up*/
    17. VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
    18. {
    19.     DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);

    20. }
    21. /*1.驱动入口函数*/
    22. NTSTATUS DriverEntry(
    23. __in struct _DRIVER_OBJECT* DriverObject,
    24. __in PUNICODE_STRING  RegistryPath)
    25. {
    26.     UNREFERENCED_PARAMETER(RegistryPath);

    27.     DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
    28.     DriverObject->DriverUnload = Unload;

    29.     PETHREAD Thread = NULL;
    30.     //通过线程ID获取EThread;284是桌面的一个线程ID
    31.     NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
    32.     DbgPrint("[WANG] Thread:%p\n", Thread);
    33.     if (!NT_SUCCESS(Status)) {
    34.         return STATUS_SUCCESS;
    35.     }

    36.     //成功了以后就可以开始拿表了 就是ethread + 偏移
    37.     KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
    38.     //Shadow表的话就拿指针+1就可以了
    39.     KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;

    40.     DbgPrint("[WANG] KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
    41.     //Shadow表直接+1就行了,因为它是结构体指针
    42.     DbgPrint("[WANG] KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);

    43.     //ethread成功获取了的话对象的引用计数要--
    44.     if (Thread) {
    45.         ObfDereferenceObject(Thread);
    46.     }
    47.     return STATUS_SUCCESS;
    48. }
    复制代码


    • 接下来就可以HookAPI了
  • 可以选择Hook:OpenProcess,模拟读写进程内存做保护的情况,因为HookAPi的话太多了,读啊写啊的,但是不管
  • 读还是写都要先打开获取句柄,但是不知道OpenProcess的API编号是多少,可以通过Ntdll导出函数来查看
  • Ntdll里有个ZwOpenProcess,它里面就写了API编号,是7Ah.得到API编号后就可以对它进行Hook了
  • Hook的话要知道它的参数,可以通过WRK查一下就知道了
  • 这样就可以自己写一个伪造的了,然后把SSDT对应的表项改为自己的就可以了
    • 代码示例
      1. #include <ntddk.h>

      2. typedef struct _KSERVICE_TABLE_DESCRIPTOR {
      3. PULONG_PTR Base;
      4. PULONG Count;
      5. ULONG Limit;
      6. PUCHAR Number;
      7. } KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;

      8. //SSDT表
      9. PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
      10. //ShadowSSDT表
      11. PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;

      12. //声明函数
      13. NTSTATUS __stdcall PsLookupThreadByThreadId(
      14. __in HANDLE ThreadId,
      15.     __deref_out PETHREAD* Thread
      16.     );

      17. //要保存一下旧的OpenProcess函数
      18. NTSTATUS (*g_OldNtOpenProcess)(
      19. __out PHANDLE ProcessHandle,
      20. __in ACCESS_MASK DesiredAccess,
      21. __in POBJECT_ATTRIBUTES ObjectAttributes,
      22. __in_opt PCLIENT_ID ClientId);

      23. LONG g_RefCount = 0;//引用计数

      24. //自己伪造的OpenProcess函数
      25. NTSTATUS FakeNtOpenProcess(
      26. __out PHANDLE ProcessHandle, //句柄
      27. __in ACCESS_MASK DesiredAccess,
      28. __in POBJECT_ATTRIBUTES ObjectAttributes,//路径在这里
      29. __in_opt PCLIENT_ID ClientId //进程ID在这里写了
      30. )
      31. {
      32.     InterlockedIncrement(&g_RefCount);//引用计数++,为了避免多线程同步问题,用延迟锁来++
      33.     //拦截的话可以不返回失败,返回成功,让对方找不到bug

      34.     //判定一下参数
      35.     if (ClientId != NULL) {
      36.         //当前ID不等于指定的线程ID就说明不是自己操作
      37.         if (PsGetCurrentProcessId() != (HANDLE)1984) {
      38.             //并且这个参数PID也等于指定的线程ID
      39.             if (ClientId->UniqueProcess == (HANDLE)1984) {
      40.                 //这就说明是别的进程在操作了
      41.                 return  STATUS_INVALID_BUFFER_SIZE; //给它随便返回一个错误码
      42.             }
      43.         }
      44.         //这样就对指定进程做了个保护,就是只要不是自己操作,就返回错误码,是自己操作就正常调旧的

      45.         DbgPrint("FakeNtOpenProcess UniqueProcess %d\n", ClientId->UniqueProcess);
      46.     }
      47.     else {
      48.         //打印一下日志就行了
      49.         DbgPrint("FakeNtOpenProcess %wZ\n", ObjectAttributes->ObjectName);
      50.     }


      51.     //做监控的话就记录一下,然后给它调旧的就行了
      52.     NTSTATUS Status  = g_OldNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);;

      53.     InterlockedDecrement(&g_RefCount);//返回之前--
      54.     return Status;
      55. };


      56. //开启写保护
      57. void EnableWP() {
      58.     //获取CR0寄存器.
      59.     ULONG_PTR cr0 = __readcr0();
      60.     //把第16位置为1
      61.     cr0 |= 0x10000;
      62.     //在写回去
      63.     __writecr0(cr0);
      64. }

      65. //关闭写保护
      66. void DisableWP() {
      67.     //获取CR0寄存器.
      68.     ULONG_PTR cr0 = __readcr0();
      69.     //把第16位清0
      70.     cr0 &= ~0x10000;
      71.     //在写回去
      72.     __writecr0(cr0);
      73. }

      74. /*驱动卸载函数 clean_up*/
      75. VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
      76. {

      77.     //卸载的时候把他还原回去
      78.     DisableWP();
      79.     //再把它改掉
      80.     KeServiceDescriptorTable->Base[0x7a] = (ULONG_PTR)g_OldNtOpenProcess;
      81.     EnableWP();

      82.     DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);
      83.     while (g_RefCount != 0); //引用计数不等于0就不卸载
      84. }


      85. /*1.驱动入口函数*/
      86. NTSTATUS DriverEntry(
      87. __in struct _DRIVER_OBJECT* DriverObject,
      88. __in PUNICODE_STRING  RegistryPath)
      89. {
      90.     UNREFERENCED_PARAMETER(RegistryPath);

      91.     DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
      92.     DriverObject->DriverUnload = Unload;

      93.     //PETHREAD Thread = NULL;
      94.     ////通过线程ID获取EThread
      95.     //NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
      96.     //DbgPrint("[WANG] Thread:%p\n", Thread);
      97.     //if (!NT_SUCCESS(Status)) {
      98.     //    return STATUS_SUCCESS;
      99.     //}

      100.     //因为没用到Shadow表,所以可以拿自己的
      101.     PETHREAD Thread = PsGetCurrentThread();

      102.     //成功了以后就可以开始拿表了 就是ethread + 偏移
      103.     KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
      104.     //Shadow表的话就拿指针+1就可以了
      105.     KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;

      106.     DbgPrint("[WANG] KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
      107.     //Shadow表直接+1就行了,因为它是结构体指针
      108.     DbgPrint("[WANG] KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);
      109.     //修改SSDT表第7A项,就是OpenProcess先保存一下,要强转成函数指针
      110.     g_OldNtOpenProcess =  (NTSTATUS(*)(__out PHANDLE ,__in ACCESS_MASK ,
      111.     __in POBJECT_ATTRIBUTES ,__in_opt PCLIENT_ID))
      112.     KeServiceDescriptorTable->Base[0x7a];

      113.     //因为SSDT表的内存地址是不可写的,所以要关闭写保护.然后在修改
      114.     DisableWP();
      115.     //再把它改掉
      116.     KeServiceDescriptorTable->Base[0x7a] = (ULONG_PTR)FakeNtOpenProcess;
      117.     EnableWP();

      118.     //ethread成功获取了的话对象的引用计数要--
      119.     //if (Thread) {
      120.     //    ObfDereferenceObject(Thread);
      121.     //}
      122.     return STATUS_SUCCESS;
      123. }

      124.     //这段代码在卸载的时候还是有问题的,考虑不够.
      复制代码
      这些都是在做保护,如果病毒作者他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|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2024-12-24 11:24 , Processed in 0.051784 second(s), 26 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

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