寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。
簡述
在安全領域,你或多或少聽過注入這個名詞,並瞭解高中注入手段:遠端執行緒注入、APC
注入、訊息注入、輸入法注入、修改PE
結構注入。這一切的一切的目的就是將自己的Dll
注入到目標程式實現自己的目的。但是一旦涉及注入自己的可執行程式碼,如果注入Dll
,這種方式在0環是極易被發現的,並不是隱蔽性很好的攻擊方式。如果注入ShellCode
執行,執行完後抹除的話,隱蔽性就明顯的提高。下面我們以Dll
注入來介紹並以最簡單的方式實現以下它們的功能。
遠端執行緒注入
實現
既然注入Dll
,我們就得寫一個,如下是其程式碼:
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
MessageBox(NULL, L"注入成功!!!By.WingSummer.", L"CnBlog", MB_ICONINFORMATION);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
這個Dll
的作用就是注入成功之後進行彈窗提示,表示注入成功!我們開始進行一下知識鋪墊。
如何載入Dll
呢?我們平時載入的時候會呼叫LoadLibrary
這個函式,如下是函式原型:
HMODULE WINAPI LoadLibraryW(
_In_ LPCWSTR lpLibFileName
);
既然是注入,肯定不是我們自己呼叫。讓一個程式碼執行就需要執行緒,如果在對方建立執行緒需要使用如下函式:
HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
使用LoadLibrary
這個函式,需要傳參一個字串地址,而這個地址正好可以用lpParameter
提供,但是,這個地址是被注入的程式,我們需要在被注入的程式寫一個字串。可以在被注入程式申請一塊記憶體,其函式原型如下:
LPVOID WINAPI VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
申請好了地址,就需要寫字串,需要用到的函式如下:
BOOL WINAPI WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_reads_bytes_(nSize) LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T* lpNumberOfBytesWritten
);
而在其他程式中申請記憶體和寫記憶體都需要相應的程式控制程式碼,我們可以開啟程式,需要的函式如下:
HANDLE WINAPI OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);
OpenProcess
函式的引數dwProcessId
表示的是程式ID
,這個我們通過輸入的方式進行。
有了以上知識鋪墊之後,我們就可以寫程式碼了。但是你可能有疑問,LoadLibrary
的地址被注入和注入程式是一樣的嗎?當然是的。獲取函式的時候,我們還需要GetProcAddress
函式,其函式原型如下:
FARPROC WINAPI GetProcAddress(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
具體程式碼實現如下:
#include <iostream>
#include<Windows.h>
using namespace std;
#define DllPath L"*:\\****\\DllTest.dll" //根據自己的 Dll 路徑來定
int main()
{
HMODULE lib = LoadLibrary(L"kernel32.dll");
if (lib)
{
FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
if (loadlib)
{
cout << "請輸入注入 PID:";
DWORD pid;
cin >> pid;
HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hprocess)
{
LPVOID addr = VirtualAllocEx(hprocess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
if (addr)
{
if (WriteProcessMemory(hprocess, addr, DllPath, sizeof(DllPath), NULL))
{
HANDLE hthread = CreateRemoteThread(hprocess, NULL, NULL, (LPTHREAD_START_ROUTINE)loadlib, addr, 0, NULL);
if (hthread)
{
cout << "注入成功!!!" << endl;
CloseHandle(hthread);
}
CloseHandle(hprocess);
}
else
{
cout << "WriteProcessMemory 失敗!" << endl;
}
}
else
{
cout << "VirtualAllocEx 失敗!" << endl;
}
}
else
{
cout << "OpenProcess 失敗!" << endl;
}
}
else
{
cout << "獲取 LoadLibraryW 地址失敗!" << endl;
}
}
else
{
cout << "獲取 kernel32.dll 地址失敗!" << endl;
}
system("pause");
return 0;
}
如下是實驗效果圖:
注意事項
- 注意程式的位數,64位程式注入64位的
DLL
,32位注入32位的。 - 如果注入高許可權的程式,請具有相應的許可權。
- 如果注入系統服務程式的話,需要通過使用未匯出的函式
ZwCreateThreadEx
,在ntdll
裡面,需要手動獲取。由於會話隔離機制,你無法使用彈窗的形式驗證注入成功。
APC 注入
實現
APC
中文名稱為非同步過程呼叫,它是Windows
十分重要的機制,如果想要學習其內部細節,請自行學習 羽夏看Win系統核心 的APC
篇。下面我們重點介紹最小化實現。
我們利用建立程式的方式來實現,為什麼呢?我們來看一下它的函式原型:
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
在最後一個函式中,裡面包含程式控制程式碼和主執行緒控制程式碼,我向執行緒傳送APC
的時候就十分方便。下面我們繼續看QueueUserAPC
的函式原型:
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
下面我們來開始寫程式碼:
#include <iostream>
#include<Windows.h>
using namespace std;
#define DllPath L"*:\\****\\DllTest.dll" //根據自己的 Dll 路徑來定
int main()
{
HMODULE lib = LoadLibrary(L"kernel32.dll");
if (lib)
{
FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
if (loadlib)
{
cout << "建立記事本程式開始實驗,按任意鍵繼續……";
cin.get();
WCHAR app[] = L"notepad.exe";
STARTUPINFO info = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(NULL, app, NULL, NULL, NULL, 0, NULL, NULL, &info, &pi);
if (ret)
{
LPVOID addr = VirtualAllocEx(pi.hProcess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
if (addr)
{
if (WriteProcessMemory(pi.hProcess, addr, DllPath, sizeof(DllPath), NULL))
{
if (QueueUserAPC((PAPCFUNC)loadlib, pi.hThread, (ULONG_PTR)addr))
{
WaitForSingleObjectEx(pi.hThread, -1, TRUE); //觸發 APC
cout << "注入成功!!!" << endl;
}
}
else
{
cout << "WriteProcessMemory 失敗!" << endl;
}
}
else
{
cout << "VirtualAllocEx 失敗!" << endl;
}
}
else
{
cout << "建立程式失敗!" << endl;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
else
{
cout << "獲取 LoadLibraryW 地址失敗!" << endl;
}
}
else
{
cout << "獲取 kernel32.dll 地址失敗!" << endl;
}
system("pause");
return 0;
}
效果圖如下:
注意事項
- 注意程式的位數,64位程式注入64位的
DLL
,32位注入32位的。 - 裡面的相關細節請學習我在文中提到的教程,這些東西並不是一言兩語就能說明白的。
訊息注入
在Windows
中大部分的應用程式都是基於訊息機制的,它們都有一個訊息過程函式,根據不同的訊息完成不同的功能。Windows
作業系統提供的鉤子機制就是用來截獲和監視系統中這些訊息的。按照鉤子作用的範圍不同,它們又可以分為區域性鉤子和全域性鉤子。區域性鉤子是針對某個執行緒的;而全域性鉤子則是作用於整個系統的基於訊息的應用。全域性鉤子需要使用DLL
檔案,在DLL
中實現相應的鉤子函式。
至於為什麼全域性鉤子必須是DLL
,簡單思考就可以得到答案,因為我們需要對任何GUI
程式進行掛鉤,既然到使用者程式只有DLL
能做到。
我們需要使用SetWindowsHookEx
函式進行掛鉤,如下是其函式原型:
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId)
第一個引數就是表示要安裝的鉤子程式的型別,第二個是處理函式,第三個是包含由lpfn引數指向的鉤子過程的DLL控制程式碼,最後一個引數是與鉤子程式關聯的執行緒識別符號,如果此引數為0,則鉤子過程與系統中所有執行緒相關聯。
在作業系統中安裝全域性鉤子後,只要程式接收到可以發出鉤子的訊息,全域性鉤子的DLL
檔案就會由作業系統自動或強行地載入到該程式中。因此,設定全域性鉤子可以達到DLL
注入的目的。建立一個全域性鉤子後,在對應事件發生的時候,系統就會把DLL載入到發生事件的程式中,這樣,便實現了DLL
注入。
為了能夠讓DLL
注入到所有的程式中,程式設定WH_GETMESSAGE
訊息的全域性鉤子。下面我們開始實現DLL
:
#include "pch.h"
// 共享記憶體
#pragma data_seg("shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:shared,RWS")
#define EXPORT extern "C" __declspec(dllexport)
HMODULE ghModule;
// 鉤子回撥函式
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
EXPORT BOOL SetGlobalHook()
{
g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, ghModule, 0);
if (NULL == g_hHook)
{
return FALSE;
}
return TRUE;
}
// 解除安裝鉤子
EXPORT BOOL UnsetGlobalHook()
{
if (g_hHook)
{
::UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
ghModule = hModule;
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
上面程式碼實現了全域性鉤子的設定、鉤子回撥函式的實現以及全域性鉤子的解除安裝,這些操作都需要用到全域性鉤子的控制程式碼作為引數。而全域性鉤子是以DLL
形式載入到其他程式空間中的,而且程式都是獨立的,所以任意修改其中一個記憶體裡的資料是不會影響另一個程式的。那麼,如何將鉤子控制程式碼傳遞給其他程式呢?為了解決這個問題,這裡採用的方法是在DLL中建立共享記憶體。
共享記憶體是指突破程式獨立性,多個程式共享同一段記憶體。在DLL
中建立共享記憶體,就是在DLL
中建立一個變數,然後將DLL
載入到多個程式空間,只要一個程式修改了該變數值,其他程式DLL
中的這個值也會改變,就相當於多個程式共享一個記憶體。
在上面的程式碼中,使用#pragma data_seg
建立了一個名為shared
的資料段,然後使用/section:shared,RWS
把shared
資料段設定為可讀、可寫、可共享的共享資料段。
下面我們實現載入全域性鉤子的程式:
#include <iostream>
#include<Windows.h>
using namespace std;
#define DllPath L"E:\\VsProject\\C++\\DllTest\\x64\\Debug\\DllTest.dll"
typedef BOOL(*SetGlobalHook)();
typedef BOOL (*UnsetGlobalHook)();
int main()
{
HMODULE lib = LoadLibrary(DllPath);
if (lib)
{
SetGlobalHook sethook = (SetGlobalHook)GetProcAddress(lib, "SetGlobalHook");
UnsetGlobalHook unsethook = (UnsetGlobalHook)GetProcAddress(lib, "UnsetGlobalHook");
if (sethook&&unsethook)
{
if (sethook())
{
cout << "已被 Hook ,按任意鍵取消 Hook ……" << endl;
cin.get();
unsethook();
}
}
else
{
cout << "獲取函式失敗!!!" << endl;
}
}
else
{
cout << "載入全域性 Hook 失敗!!!" << endl;
}
system("pause");
return 0;
}
然後我們載入鉤子之後,啟動新的記事本,就可以發現DLL
被注入了。
輸入法注入
IME
輸入法實際就是一個DLL
檔案,只不過字尾為IME
罷了,需要匯出必要的介面供系統載入輸入法時呼叫。我們可以在此IME
檔案的DllMain
函式的入口通過呼叫LoadLibrary
函式來載入需要注入的DLL
。
對於IME
,必須匯出如下函式:
ImeConversionList //將字串/字元轉換成目標字串/字元
ImeConfigure //設定ime引數
ImeDestroy //退出當前使用的IME
ImeEscape //應用軟體訪問輸入法的介面函式
ImeInquire //啟動並初始化當前ime輸入法
ImeProcessKey //ime輸入鍵盤事件管理函式
ImeSelect //啟動當前的ime輸入法
ImeSetActiveContext //設定當前的輸入處於活動狀態
ImeSetCompositionString //由應用程式設定輸入法編碼
ImeToAsciiEx //將輸入的鍵盤事件轉換為漢字編碼事件
NotifyIME //ime事件管理函式
ImeRegisterWord //向輸入法字典註冊字串
ImeUnregisterWord //刪除被註冊的字串
ImeGetRegisterWordStyle
ImeEnumRegisterWord
其中最重要的就是ImeInquire
函式,當切換到此輸入法時此函式就會被呼叫啟動並初始化輸入法。引數lpIMEInfo
用於輸入對輸入法初始化的內容結構,引數lpszUIClass
為輸入法的視窗類。lpszUIClass
對應的視窗類必須已註冊,我們應該在DllMain
入口處註冊此視窗類,我們來看一下函式原型:
BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption);
由於實現起來還是比較複雜的,其原理就是用輸入法弄個殼,安裝好,被觸發到然後執行目的碼,具體就不實現了。
下一篇
羽夏逆向指引——符號