寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。
補丁是什么
對於我們老一輩的長輩,補丁是在日常生活中最常見的東西,褲子或者衣服破了一個洞,如果比較大,就會找一塊布,然后縫在上面,這就是所謂的打補丁。對於計算機程序來說,補丁的作用好比衣物上的補丁,用來修補程序在代碼邏輯上的漏洞。不過對於破解人員來說,補丁就是所謂的通過修改二進制數據以實現自己繞過正版檢測的文件。在之前的教程,我們就見識到了所謂的補丁了,它是一個普通的補丁,直接寫到可執行文件當中。補丁有好幾種常見的類型,比如內存補丁、劫持補丁、硬件斷點補丁等等,它們雖然形式各異,本質特征就是更改程序的執行流程,原理都是一樣的,下面將對常見的補丁進行講解。
文件補丁
這種補丁應該比較常見,比如網上常見的替換exe
/dll
文件以及添加偽造正版信息文件實現破解就是所謂的文件補丁。在之前的教程 羽夏逆向指引——破解第一個程序 當中,我們在X32Dbg
中修改匯編指令,然后另存為可執行文件,重新生成的exe
就是文件補丁。對於這類補丁,我就不再贅述了。
劫持補丁
什么是劫持補丁呢?它用到了所謂的DLL
劫持技術。當一個可執行文件運行時,Windows
加載器將可執行模塊映射到進程的地址空間中,加載器分析可執行模塊的輸入表,並設法找出任何需要的DLL
,並將它們映射到進程的地址空間中。由於輸入表中只包含DLL
名而沒有它的路徑名,因此加載程序必須在磁盤上搜索DLL
文件。首先會嘗試從當前程序所在的目錄加載DLL
,如果沒找到,則在Windows
系統目錄中查找,最后是在環境變量中列出的各個目錄下查找。利用這個特點,先偽造一個系統同名的DLL
,提供同樣的輸出表,每個輸出函數轉向真正的系統DLL
。程序調用系統DLL
時會先調用當前目錄下偽造的DLL
,完成相關功能后,再跳到系統DLL
同名函數里執行。這個過程用個形象的詞來描述就是系統DLL
被劫持了。
如果看不懂這些東西的,證明你缺少PE
結構的知識,請自行學習,學會后再繼續。
但是僅僅通過劫持,我們還需要配合其他技術來實現補丁的功能,之后講解完本篇文章后,我們將使用劫持,配合其他手段,實現一個劫持補丁。
內存補丁
什么是內存補丁呢?如果使用某閣的破解工具你或許能有所耳聞。它有一個程序會引導啟動真正的程序,不過會處於掛起狀態,打好補丁后,然后放開程序運行。下面我們拿之前使用的程序開刀,實現一個內存補丁。
那么首先考慮,在Windows
平台如何創建程序?創建進程的API
還是挺多的,我們用CreateProcess
來實現:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
看到這個函數,是不是感覺非常頭大,不過沒關系,里面很多參數都是填NULL
的,意為使用系統默認值,代碼如下:
WCHAR filename[] = L"E:\\ConsoleApplication1.exe";
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(NULL, filename, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
這個函數的具體細節可以自行搜索學習,不要指望一個指引就講明白所有的細節。有些細節甚至你研究明白Windows
的系統內核你才能明白的,正應正了我的座右銘:IT深似海,一入出不來。
好,閑話不多說了。代碼中的filename
不能這么初始化:
LPWSTR filename = (LPWSTR)L"E:\\ConsoleApplicatio1.exe";
如果這樣聲明,這個就會放到常量區,而CreateProcess
使用這個數組時會進行一些操作,如果只讀,就會觸發異常。
CreateProcess
這個函數若返回非零,那么創建函數成功。下面我們將寫補丁,那么用到哪個API
呢?
寫補丁的話,我們需要用到WriteProcessMemory
這個函數:
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten
);
第一個參數就是你要寫進程的句柄,那么什么是句柄,如果真的要研究明白需要學習系統內核的知識。這里僅簡單描述一下:句柄類似一個編號,比如你要找一個人,這個人很神秘,隱姓埋名,只留下這個編號,但你知道在哪個組織去找,你拿這個編號讓內部人員給傳個話,內部人員就幫你做了這個事情。
第二個參數是要寫的地址。如果這個東西不懂的話,請重新學習匯編的相關知識。
第三個參數就是存儲要寫內容的地址,第四個參數是要寫入的大小,最后一個是成功寫入的字節數,填NULL
表示不想知道。好,開始寫代碼:
unsigned short buffer = 0x9090;
if (ret)
{
if (WriteProcessMemory(pi.hProcess, (LPVOID)0x401721, buffer, sizeof(buffer), NULL))
{
cout << "寫入補丁成功!" << endl;
}
}
為什么是寫這個補丁,長度為什么是這個長度,不會請返回學習硬編碼的知識。
補丁寫好了,但程序創建的時候是掛起的狀態,我們需要恢復執行的狀態程序才能跑起來,我們需要用到下面的API
:
DWORD WINAPI ResumeThread(_In_ HANDLE hThread);
填入的參數就是線程的句柄,所以我們應該這么寫:
ResumeThread(pi.hThread);
不過需要注意的是,前面我們創建進程的時候,會產生兩個句柄:一個是進程句柄,另一個是線程句柄。我們需要調用CloseHandle
函數銷毀,防止占用系統資源:
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
到現在為止,代碼就長成下面的樣子:
#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
WCHAR filename[] = L"E:\\ConsoleApplication1.exe";
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(NULL, filename, NULL, NULL, FALSE, CREATE_NEW_CONSOLE | CREATE_SUSPENDED, NULL, NULL, &si, &pi);
unsigned short buffer = 0x9090;
if (ret)
{
if (WriteProcessMemory(pi.hProcess, (LPVOID)0x401721, &buffer, sizeof(buffer), NULL))
{
cout << "寫入補丁成功!" << endl;
}
}
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
system("pause");
return 0;
}
好,我們看一看實際效果:
硬件斷點補丁
在知道什么是硬件斷點補丁,我們首先知道什么是硬件斷點。Intel 80306
以上的CPU
給我們提供了調試寄存器用於軟件調試,硬件斷點是通過設置調試寄存器實現的,如下圖所示:
DR0
-DR3
為設置斷點的地址,DR4
和DR5
保留,DR6
為調試異常產生后顯示的一些信息,DR7
保存了斷點是否啟用、斷點類型和長度等信息。我們在使用硬件斷點的時候,就是要設置調試寄存器,將斷點的位置設置到DR0
-DR3
中,斷點的長度設置到DR7
的LEN0
-LEN3
中,將斷點的類型設置到DR7
的RW0
-RW3
中,將是否啟用斷點設置到DR7
的L0
-L3
中。這就是我們3環調試器,使用GUI
下硬件斷點為我們做的事情。
可以看出,我們能夠下的硬件斷點是十分有限的,總共就4個。硬件斷點是和線程相關的,這些寄存器的信息存儲於線程上下文當中。我們通過設置線程上下文,實現硬件斷點的設置。代碼實現如下:
void SetHwBreakPoint(HANDLE HThread)
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(HThread,&ctx);
ctx.Dr0 = 0x040172C; //你要下斷點的位置
ctx.Dr7 = 0x01;
SetThreadContext(HThread,&ctx);
}
如果我們要設置硬件斷點,就必須獲取當前線程句柄,獲得進程句柄就可以修改線程上下文,是不是很簡單?不過在設置線程上下文的時候,必須掛起線程:為什么必須要掛起線程,這個是微軟告訴我們的:
Do not try to set the context for a running thread; the results are unpredictable. Use the SuspendThread function to suspend the thread before calling SetThreadContext.
也就是說,如果為了保證SetThreadContext
是正確的,就必須保證線程是掛起的,否則結果不可預測。我們只需要掛起,設置好線程上下文,然后恢復線程運行就行了。
設置好了硬件斷點,我們如何相應這個斷點觸發並進行處理呢?答案就是使用VEH
,結構化異常處理。
PVOID WINAPI AddVectoredExceptionHandler(
_In_ ULONG First,
_In_ PVECTORED_EXCEPTION_HANDLER Handler
);
上面的函數是添加一個結構化處理函數,第一個參數如果非零,那么異常發生時,第一個調用的就是它。什么?硬件斷點觸發就是一個異常?對的,當它觸發時,會產生STATUS_SINGLE_STEP
異常,如果在調試狀態,會由調試器接管。如果沒有調試器,但有異常處理程序,那么就會執行它。我們使用異常處理實現破解的程序代碼的如下:
DWORD NTAPI ExceptionHandler ( EXCEPTION_POINTERS*
ExceptionInfo )
{
if ( ( DWORD )
ExceptionInfo->ExceptionRecord->ExceptionAddress
== 0x040172C ) //判斷是不是我們下斷點的地址
{
ExceptionInfo->ContextRecord->Eip += 2; //兩個字節的指令
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
假如我們不想要這個函數了,恢復還是通過設置線程上下文,如何移除向量異常處理程序?我們需要用到下面的API
:
ULONG RemoveVectoredExceptionHandler(
PVOID Handle
);
這個句柄就是你的添加向量異常處理程序的句柄,具體咋用我就不贅述了,下面我們將會用硬件斷點 + 劫持技術實現破解我們的“小白鼠”。
硬件斷點劫持補丁
實現
假設我們劫持的Dll
是系統的,我們既然劫持它,導出的函數它有的我得也有,我劫持執行的函數必須還得調用原系統Dll
的函數,一個一個寫未免不太費勁。我們需要借助一個工具,實現幫我們生成劫持代碼模板,通過略微的修改實現我們的核心代碼,從而實現劫持。常見可以劫持的Dll
有:lpk.dll
、winmm.dll
、version.dll
、ws2_32.dll
等等。我們先看看這個工具長啥樣子:
接下來我們實戰一波,但是為了看到效果,我們需要在小白鼠上面加個代碼:
#include <iostream>
#include <Windows.h>
using namespace std;
int main(int argc, char* argv[])
{
int x = 0;
LoadLibrary(L"winmm.dll"); //假設我需要這個dll,我加載進去
cout << "請輸入密鑰:" << endl;
cin >> x;
if (x == 1234)
{
cout << "成功,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
}
else
{
cout << "失敗,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
}
system("pause");
return 0;
}
為什么要添加一句LoadLibrary
呢?因為劫持是有條件的,就是它必須加載Dll
才能行,別看導入表有一些,它們其實根本沒有加載:
可以看出,里面的導入表的Dll
根本沒有加載,就算你打算劫持里面的,沒加載,根本無效。然而這個程序實在太小了,所以用常規的方式加載個系統Dll
以供測試。
打開我們的AheadLib
工具,然后打開我們想要劫持的Dll
作為輸入,然后找到指定目錄作為輸出,就點擊生成,就在我們想要的位置生成代碼了:
下一步該創建一個動態鏈接庫項目,如下圖所示:
為了生成的名字和系統的Dll
一致,就用一樣的名字:
然后把生成的代碼覆蓋到dllmain.cpp
文件中,如下圖所示:
然而,這是不能直接生成代碼的,我們需要做一些配置。首先設置使用多字節字符集:
設置不使用預編譯頭:
注意還要在上面項目配置之前要注意設置平台為x86
:
如果不設置x86
,就會報錯如下:
然后需要做一些小修改並實現代碼,如下如所示修改和編碼實現:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AheadLib 命名空間
namespace AheadLib
{
HMODULE m_hModule = NULL; // 原始模塊句柄
DWORD m_dwReturn[193] = { 0 }; // 原始函數返回地址
const DWORD addr = 0x040172C;
DWORD NTAPI ExceptionHandler(EXCEPTION_POINTERS*
ExceptionInfo)
{
if ((DWORD)
ExceptionInfo->ExceptionRecord->ExceptionAddress
== addr)
{
ExceptionInfo->ContextRecord->Eip += 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void SetHwBreakPoint(HANDLE HThread)
{
CONTEXT ctx={0};
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(HThread, &ctx);
ctx.Dr0 = addr;
ctx.Dr7 = 0x01;
SetThreadContext(HThread, &ctx);
}
DWORD WINAPI ThreadProc(_In_ LPVOID
lpParameter)
{
HANDLE htread = OpenThread(THREAD_ALL_ACCESS,
TRUE, (DWORD)lpParameter);
if (htread)
{
SuspendThread(htread);
SetHwBreakPoint(htread);
ResumeThread(htread);
CloseHandle(htread);
return TRUE;
}
return FALSE;
}
// 加載原始模塊
inline BOOL WINAPI Load()
{
TCHAR tzPath[MAX_PATH];
TCHAR tzTemp[MAX_PATH * 2];
/*變動的代碼區:開始*/
BOOL isWowSystem = FALSE;
if (IsWow64Process((HANDLE)-1,
&isWowSystem))
{
if (isWowSystem)
{
UINT r = GetSystemWow64Directory(tzPath, MAX_PATH);
if (!r)
{
wsprintf(tzTemp, TEXT("無法加載 %s,程序無法正常運行。"), tzPath);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
}
}
else
{
GetSystemDirectory(tzPath, MAX_PATH);
}
}
lstrcat(tzPath, TEXT("\\winmm.dll"));
AddVectoredExceptionHandler(1 , (PVECTORED_EXCEPTION_HANDLER)ExceptionHandler);
HANDLE Hhandle = CreateThread(NULL, NULL, ThreadProc, (LPVOID)GetCurrentThreadId(), NULL,NULL ;
/*變動的代碼區:結束*/
m_hModule = LoadLibrary(tzPath);
if (m_hModule == NULL)
{
wsprintf(tzTemp, TEXT("無法加載 %s,程序無法正 運行。"), tzPath);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
}
return (m_hModule != NULL);
}
//其他區域的代碼保持不變,編譯即可
然后我們看看效果:
比如我不確定寫的劫持是否正確,或者寫劫持發現沒效果,如何進行調試呢?很簡單,把被劫持的程序拷一份到調試目錄,然后在項目配置中修改下面的路徑為被劫持的程序,正常下斷點調試即可:
反制措施
既然是劫持,我們如果想要反制,可以在程序代碼中加上自己所在目錄有沒有這個Dll
,雖然有一定的作用,但是對於能力有點的逆向者來說,這個東西是沒用的,尤其不是按照本實例進行動態加載的。如果不是動態加載,你根本掌握不了主導權。我在你檢查之前加載完畢,我直接patch
掉正確的路徑為錯誤的路徑,你根本查不到。
還有一個方式可以阻止Dll
劫持,在注冊表鍵值:KEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
里面存儲了一些項目,如果需要里面的Dll
,直接從系統目錄加載,而不通過先查一查當前目錄有沒有。這不失一個很好的反制措施,但是不幸的是,如果你沒有System
權限,你是根本修改不了該項目下的值的,修改后,需要重啟項目生效。