標籤:
當初由於一些原因以及興趣,學習了一段時間軟體逆向,對於軟體加密解密有了點粗略的瞭解。而後看到某些同學辛辛苦苦的搞出個軟體,自己費心費力去加密,但搞出來後往往能被秒破,實不忍心。今天大概總結下一些基本的軟體加密手段,以供參考,高手勿噴。
關於解密
軟體解密主要有2個層次,一個俗稱爆破,就是不分析密碼編譯演算法,只修改一些與驗證相關的跳轉指令來使得軟體正常運行,另一個就是能真正破解密碼編譯演算法,進而寫出註冊機。破解手段通常有靜態分析和動態分析兩種方式,目前二者的代表工具是IDA和OllyDbg(OD)。
密碼編譯演算法與代碼
加密首先必須設計一套密碼編譯演算法,這個可以用現成的如MD5,SHA之類的演算法,也可以自己設計個稍微簡單點的演算法。一般情況,作為一個開發人員,設計一個簡單的密碼編譯演算法應該問題不大的,但是演算法設計必須要嚴密,不能出現漏網之魚。比如一個時間限制的演算法,如果只記錄開始結束時間,然後用目前時間去判斷,這樣的演算法通過修改系統時間就給繞過去了,就不夠嚴密,需要改善;例如可再記錄一個最近一次已耗用時間,這樣就可以處理修改系統時間的漏洞了。
有了一個完善的密碼編譯演算法,最直接也最容易想到的做法就是把使用者輸入的密碼用演算法轉換後與儲存的金鑰組比,一致則驗證通過,不一致則驗證失敗。這樣的加密程式估計新手也能快速爆破了。那麼在代碼編寫時,需要注意下面幾點
首先,密碼編譯演算法盡量不出現在程式中。比如你的密碼編譯演算法是\(f\),使用者輸入密碼\(x\),程式儲存的秘鑰為\(y\),那麼只有在\(y==f(x)\)時才能驗證通過。避免\(f\)的具體實現出現在程式中,可以防止破解者分析你的密碼編譯演算法從而寫出註冊機,那麼可以設計另外一組演算法\(g\)和\(h\)使得\(y==f(x)\;\Leftrightarrow \; g(y)==h(f(x))\),記\(s=hf\),這樣在程式裡就只會出現\(g\)和\(s\)而不會出現\(f\)了。例如下面代碼:
1 #define MAX_LEN 256 2 int Validation(char *py, char *px) 3 { 4 char azy[MAX_LEN] = {0}; 5 char azx[MAX_LEN] = {0}; 6 char *ptmp = NULL; 7 8 //這裡密碼編譯演算法實質是將數字轉換為小寫字母 9 //但此處分別直接將待匹配密鑰py和使用者密碼px轉大寫字元後對比10 //而不是將px轉小寫字母后與py比較11 ptmp = azy;12 while(*py != ‘\0‘)13 {14 *ptmp++ = (*py++) & (~0x20); //這裡是把所有字母轉為大寫15 }16 17 ptmp = azx;18 while(*px != ‘\0‘)19 {20 *ptmp++ = (*px++) + 0x10;//這裡把所有數字轉大寫字母21 }22 23 return strcmp(azy, azx);24 25 }
View Code
密碼編譯演算法\(f\)是把數字映射為小寫字母,但驗證過程中,直接把使用者輸入密碼映射到大寫字母(即為\(s\)函數),同時將儲存密碼也轉換到大寫字母(\(g\)函數),再進行比較,這樣就避免了密碼編譯演算法\(f\)出現在程式中。當然這裡演算法很簡單,也許能推匯出\(f\),但隨著演算法複雜性增加就會非常難了。
第二,盡量別用if…else判斷驗證結果。用了if…else結構判斷,必然會有一個jmp指令,別人只要定位到該指令修改jmp條件,就徹底被爆破了。可以將驗證結果作為索引去達到目的,比如上面的密碼編譯演算法,若使用者輸入12345列印驗證成功,否則失敗。如下代碼:
1 int main() 2 { 3 char aKey[] = "abcde"; 4 char aPassword[MAX_LEN] = {0}; 5 printf("input password:\n"); 6 gets(aPassword); 7 8 int nRes = Validation(aKey, aPassword); 9 10 //這裡直接使用if...else判斷11 if(nRes != 0)12 {13 printf("validation failed!\n");14 return 1;15 }16 printf("validation success!\n");17 18 //這裡講驗證結果作為索引19 char aaPrintInfo[][MAX_LEN] = {"validation success!", "validation failed!"};20 printf("%s\n",aaPrintInfo[nRes]);21 22 23 return 0;24 }
View Code
如果不得不用if…else結構,可將if語句與驗證函式分散開,對於靜態分析代碼的難度會有所增加。
第三,就是一些關鍵提示資訊不要放在堆內而放在棧內。OD有個尋找字串功能可以把程式堆內的字串列出來,新手最喜歡用這個來定位跳轉點爆破了。
1 void main()2 {3 char a[]= "this is in stack";4 char *b = "this is in heap";5 6 printf("%s\n%s\n", a, b, "also in heap");7 }
View Code
這段代碼有3個字串(a,b和“also in heap”),編譯後通過OD載入並尋找字串,如:
可以看到存放在堆中的字串被搜尋出來了,從而可以快速定位到對應代碼位置:
中選中行的edx存放的就是字串a,但卻不會被搜尋出來。
加殼
不得不說,雖然上面做了那麼些工作,對於破解來說也僅僅增加了一點點的難度,一般的新手努力點也不難搞定。那麼通過軟體加殼的方式可以把那些不會脫殼的新手們擋在門外。
對於普通的PE檔案,將其按二進位開啟可以直接解析其內部的資料或者指令,殼就相當於一個加鎖的箱子,讓人不能直接看到PE檔案的真正內容而只能看到加密後的內容,在程式運行時在將其解密到記憶體從而運行。也就是說,對於加殼的程式,靜態分析是不可行的,必須要脫殼後才能分析,即便是動態調試也可能會很所難度。
軟體殼有壓縮殼和加密殼,一般壓縮殼主要是減小PE檔案的大小,而加密殼則是為了防止PE檔案被反編譯、調試和修改等。常用的一些殼如UPX,ASP等等都有專門的加殼與脫殼工具,目前據說最難搞定的還是vmprotect,在看雪網站有多種加殼工具,大家可自行參考。
加殼後PE檔案的大小以及程式進入點都會發生變化,可以使用PEID來查看相關加殼資訊。是程式加殼前後的資訊,可以看到PE檔案的很多資訊都不一樣了:
反調試
如果通過加殼保護了程式,固然不錯。但目前大多數的殼都有了脫殼機,有很大風險被脫掉,那我們還得要加強防範,這就是程式反調試。反調試的基本思想是檢測程式當前是否在被調試,若是則做一些保護措施,如退出、崩潰等手段。
運行一個程式,其進程內有很多地方會標識當前進程是否在被調試,通過檢測這些變數就可以簡單地判斷出來從而進行處理。Windows系統還提供了一個IsDebuggerPresent的API來供調用,不過該函數名聲太大,很多調試器都會繞過它。這個部落格列得比較詳細,值得參考。
另外,若在程式某個位置打了軟體斷點,此處會被調試器修改為0xCC,當執行到該處時才會修改回去,因此還有一類方法就是程式校正,如CRC校正或MD5校正。基本做法是將當前PE檔案做為輸入,產生一個字串,通過判斷字串是否改變可判斷程式是否被調試或修改。
還有一種比較粗暴的做法。目前windows程式調試器用得較多時OD和SoftIce,可以通過枚舉系統當前進程來判斷這兩款調試器是否在運行,若運行則認為程式在被調試。也許別人在調試別的程式呢,不管那麼多了,為了安全起見,不得不“寧肯錯殺三千也絕不漏網一人”。判斷系統是否有OD在啟動並執行代碼如下:
1 #include "tlhelp32.h" 2 bool IsODRuning() 3 { 4 HANDLE hwnd; 5 PROCESSENTRY32 tp32; //結構體 6 tp32.dwSize = sizeof(PROCESSENTRY32); 7 TCHAR *str= _TEXT("OLLYDBG.EXE"); 8 bool bFindOD=false; 9 hwnd=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL); 10 if(INVALID_HANDLE_VALUE!=hwnd) 11 { 12 Process32First(hwnd,&tp32); 13 do{ 14 15 if(0==wcsicmp(str,tp32.szExeFile)) 16 { 17 bFindOD=true; 18 break; 19 } 20 }while(Process32Next(hwnd,&tp32)); 21 } 22 CloseHandle(hwnd); 23 24 return bFindOD;25 }
View Code
最後,還有利用異常處理的方法。比如下面代碼,通過人為故意產生一個中斷異常,然後在異常處理中去驗證,這樣在調試的時候中斷異常就是一個斷點,從而程式不會進入異常處理。代碼如下:
1 long g_label = 0; 2 LONG Handle(EXCEPTION_POINTERS *pExceptionInfo ) 3 { 4 if(EXCEPTION_BREAKPOINT == pExceptionInfo->ExceptionRecord->ExceptionCode) 5 { 6 //validation 7 8 if(/*success*/) 9 {10 pExceptionInfo->ContextRecord->Eip = g_label;11 12 return EXCEPTION_CONTINUE_EXECUTION;13 } 14 }15 return EXCEPTION_EXECUTE_HANDLER;16 }17 18 void main()19 {20 21 //===exception validation begin22 LPTOP_LEVEL_EXCEPTION_FILTER lpOld;23 lpOld = SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)Handle);24 25 __asm26 {27 push label_ok;28 pop g_label;29 int 3;30 }31 32 label_ok: 33 SetUnhandledExceptionFilter(lpOld);34 //===exception validation end35 //do your things......36 }
View Code
當然也可以採用其他異常(如除0異常),但用異常的一個缺點就是自己寫代碼調試的時候也很不方便。
以上就是我大概瞭解的反調試技術,不過加解密是具有強烈對抗性的,現在一些調試器都增加了反反調試手段,讓程式的反調試失效。
驅動保護與硬體加密狗
程式做好上面的保護,基本上已經具有一定的自我保護能力了,一般個人寫的軟體已經足夠。如果你寫的是商業軟體,需要高度防範破解,那可以採用驅動保護或硬體加密狗。具體採用何種根據軟體來定。
如果軟體是一些專業性較強的,可以採用硬體加密狗來保護;如果軟體是像網路遊戲那樣面向廣泛福士群體的,採用加密狗就不現實了,一般都採用驅動保護,企鵝的遊戲基本都有TenProtect的驅動保護,盛大的GPK保護等都是比較典型的例子。
加密狗我沒仔細研究過,就不好多說了。上面那些反調試的手段都運行在ring3層級,而驅動則運行ring0層級,驅動保護的做法主要是hook系統底層的一些API,通過檢驗調用者來區分外部調試修改還是程式自己的操作。比如開啟進程的操作,所有調試器都需要調用,通過驅動層hook該函數來防止調試器開啟或附加到程式進程。
結語
自從接觸了這些東西,才知道“涉密不上網,上網不涉密”的真正意義。要知道這一行高手很多,即便用盡各種手段,也不可能保證軟體絕對安全,只要軟體運行就會留下痕迹,就有被破解的可能。
現在已一年多沒搞這些了,以後估計也沒時間去搞,當初學習的時候雖然很累,但卻感覺很充實很有興趣,甚至還想換那方向的工作,謹以此作為對那段時間學習的總結。
雖然寫了這麼些加密的東西,我個人還是更崇尚開源,如果不是那麼必要,還是希望大家能多把源碼與人分享,共同進步。
windows程式防狼術入門