多核环境下的hook探究

2022-05-17 18:12:52 浏览数 (1)

文章首发奇安信攻防社区:https://forum.butian.net/share/1361

前言

r0层多核下hook高并发函数存在的问题是:在使用如memcpy的时候,无法一次性拷贝5个字节的硬编码。即有可能拷贝到一半,别的线程去执行了代码导致蓝屏。

解决的办法有:

  1. 1. 短跳中转
  2. 2. 中断门
  3. 3. 找一条一次性修改8字节的指令

这里将使用第三种方法实现。

SwapContext

这是线程切换核心函数,Windows几乎无时无刻在执行这个函数,所以属于是高并发函数。

本文将在多核环境下通过hook SwapContext作为实现。

需要获取的是SwapContext的地址:采用的方法是暴力搜索特征码,由于需要在ntoskrnl.exe文件中搜索特征码,所以需要获取ntoskrnl.exe的基址和大小。

整个过程为:

  1. 1. 0环下fs:[0]指向KPCR,fs:[0x34]即指向KdVersionBlock
  2. 2. KdVersionBlock地址对应的结构体为_DBGKD_GET_VERSION64_DBGKD_GET_VERSION64 18h的指向PsLoadedModuleList的地址
  3. 3. 取出PsLoadedModuleList中的内容,即KLDR_DATA_TABLE_ENTRY结构
  4. 4. 在KLDR_DATA_TABLE_ENTRY结构中找到 18h位置上的DllBase(基址)和 20h位置上的SizeOfImage(大小)。

可以用如下图示表示:

注意:一个核意味着就有一个kpcr,而只有cpu编号为1对应的kpcr才有KdVersionBlock的值,其余核对应KPCR的KdVersionBlock值均为null,所以我们这里要使用KeSetSystemAffinityThread对执行该线程的cpu进行绑定,确保当前线程运行在一号cpu上。

综上所述,这一段代码如下

代码语言:javascript复制
PVOID DllBase = NULL;
SIZE_T viewSize = 0;
KeSetSystemAffinityThread(1);
__asm{
          push eax;
          push ebx;
          mov eax, fs: [0x34];
          add eax, 18h;   
          mov eax, [eax];   
          mov eax, [eax]; 
          mov ebx, [eax   18h];
          mov DllBase, ebx;
          mov ebx, [eax   20h];
          mov viewSize, ebx;
          pop ebx;
          pop eax;
     }
KeRevertToUserAffinityThread();//恢复线程运行的处理器

获取到基址和大小后,然后就是提取硬编码,这里就选取最前面的16个字节硬编码,一组四个四节,分四组。

代码语言:javascript复制
ULONG opCodeArray[TRAITCODELEN] = { 0xc626c90a ,0x9c022d46 ,0x05408b8d  ,0xdde80000 };

使用循环在ntoskrnl.exe模块中遍历这段特征码。

OpCode1-4对应着四段特征码。返回的值就为SwapContext的地址。

代码语言:javascript复制
ULONG endDllAddr = (ULONG)DllBase   viewSize;
for (ULONG i = (ULONG)DllBase; i < endDllAddr; i  )
{
    try
    {
        if (*(PULONG)i == OpCode1 && *(PULONG)(i   4) == OpCode2
            && *(PULONG)(i   8) == OpCode3 && *(PULONG)(i   0xC) == OpCode4)
        {
            // 返回函数地址偏移
            return i;
        }
    }
    except(1)
    {
        continue;
    }
}

获取os版本和分页模式

操作系统版本判断用RtlGetVersion获取前正在运行的操作系统的版本信息。

分页模式通过对cr4,PAE位的判断,如果为1,则为29912分页,如果为0,则为101012分页。

代码语言:javascript复制
ULONG GetWindowsVersion()
{
    RTL_OSVERSIONINFOW lpVersionInformation = { sizeof(RTL_OSVERSIONINFOW) };
    if (NT_SUCCESS(RtlGetVersion(&lpVersionInformation)))
    {
        ULONG dwMajorVersion = lpVersionInformation.dwMajorVersion;
        ULONG dwMinorVersion = lpVersionInformation.dwMinorVersion;
        if (dwMajorVersion == 5 && dwMinorVersion == 1)
        {
            return WINXP;
        }
        else if (dwMajorVersion == 6 && dwMinorVersion == 1)
        {
            return WIN7;
        }
        else if (dwMajorVersion == 6 && dwMinorVersion == 2)
        {
            return WIN8;
        }
        else if (dwMajorVersion == 10 && dwMinorVersion == 0)
        {
            return WIN10;
        }
    }
    return 0;
}
// 获取操作系统分页模式
ULONG GetWindowsPageMode()
{
    ULONG PageMode = 0x1; // 默认为 2-9-9-12 分页
    __asm
    {
        _emit 0x0F;     // mov  eax, cr4;
        _emit 0x20;
        _emit 0xE0;
        test eax, 0x20;
        jnz  End;
        // 为 10-10-12 分页
        mov  dword ptr[PageMode], 0x0;
    End:
        ;
    }
    return PageMode;
}

除去保护

我们自己的代码空间可以随便读写,但是操作系统的代码想改,是需要去除掉保护。

这里最简单的方式就是改变cr0的wp位,将该位清0,注意这里首先要提升irql,即中断执行等级。

代码语言:javascript复制
KIRQL irQl;
// 修改Cr0寄存器, 去除写保护(内存保护机制)
KIRQL RemoveP()
{
    DbgPrint("RemovePn");
    // (PASSIVE_LEVEL)提升 IRQL 等级为DISPATCH_LEVEL,并返回旧的 IRQL
    irQl = KeRaiseIrqlToDpcLevel();
    ULONG_PTR cr0 = __readcr0(); //mov eax,cr0;
    cr0 &= ~0x10000; //将第16位(WP位)清0
    _disable(); //相当于 cli 指令,屏蔽软中断
    __writecr0(cr0); //mov cr0, eax
    DbgPrint("退出RemovePn");
    return irQl;
}

然后是恢复保护属性的代码

代码语言:javascript复制
KIRQL ResumeP()
{
    DbgPrint("ResumePn");
    ULONG_PTR cr0 = __readcr0();
    cr0 |= 0x10000; //WP复原为1
    _disable();
    __writecr0(cr0);mov cr0, eax
    // 恢复IRQL等级
    KeLowerIrql(irQl);
    DbgPrint("ResumeP退出n");
    return irQl;
}

CMPXCHG8B

该指令可一次性替换八个字节硬编码 。

cmpxchg8b mem64 指令的工作如下:

  1. 1. 比较 mem64 和 EDX:EAX。
  2. 2. 如果相等,那么把 ECX:EBX 存储到 mem64。
  3. 3. 如果不相等,那么把 mem64 存储到 EDX:EAX。
代码语言:javascript复制
ULONG DataLow = 0x0, DataHigh = 0x0;
VOID _declspec(naked) _fastcall FastSwapMemory(ULONG* TargetAddr, ULONG* SoulAddr)
{
    __asm
    {
        pushad;
        pushfd;

        mov esi, ecx; //ecx = TargetAddr
        mov edi, edx; //edx = SoulAddr(_fastcall调用约定)
        mov edx, 0x0;
        mov eax, 0x0;
        // 读取ShellCode
        lock CMPXCHG8B qword ptr[edi];
        mov DataLow, eax;
        mov DataHigh, edx;
        // 读取目标内存
        lock CMPXCHG8B qword ptr[esi]; //edx:eax = [TargetAddr]
        mov ebx, dword ptr[DataLow];
        mov ecx, dword ptr[DataHigh];
        // HOOK目标内存
        lock CMPXCHG8B qword ptr[esi]; //相等,把[SoulAddr]存储到 [TargetAddr]。

        popfd;
        popad;
        retn;
    }
}

HOOKFunction

通过逆向SwapContext可以知道,esi存储的实际上是下一线程,edi存储的是当前线程

这里就可以简单打印一下当前线程和下一线程信息。

代码语言:javascript复制
void _declspec(naked) HookSwapContextFunction()
{
    _asm
    {
        mov dword ptr[CurrentThread], edi;
        mov dword ptr[NextThread], esi;
    }
    _asm
    {
        pushad;
        pushfd;
    }

    DbgPrint("当前线程为:%xtt下一个线程为:%xn",CurrentThread,NextThread);

    _asm 
    {
        popfd;
        popad;
        mov eax, dword ptr[SwapContext];
        jmp eax;
    }
}

实现效果

加载驱动后成功hook了SwapContext并打印出线程信息。

0 人点赞