當初由於一些原因以及興趣,學習了一段時間軟件逆向,對於軟件加密解密有了點粗略的了解。而后看到某些同學辛辛苦苦的搞出個軟件,自己費心費力去加密,但搞出來后往往能被秒破,實不忍心。今天大概總結下一些基本的軟件加密手段,以供參考,高手勿噴。
關於解密
軟件解密主要有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 }
加密算法\(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 }
如果不得不用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 }
這段代碼有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 }
最后,還有利用異常處理的方法。比如下面代碼,通過人為故意產生一個中斷異常,然后在異常處理中去驗證,這樣在調試的時候中斷異常就是一個斷點,從而程序不會進入異常處理。代碼如下:

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 begin 22 LPTOP_LEVEL_EXCEPTION_FILTER lpOld; 23 lpOld = SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)Handle); 24 25 __asm 26 { 27 push label_ok; 28 pop g_label; 29 int 3; 30 } 31 32 label_ok: 33 SetUnhandledExceptionFilter(lpOld); 34 //===exception validation end 35 //do your things...... 36 }
當然也可以采用其他異常(如除0異常),但用異常的一個缺點就是自己寫代碼調試的時候也很不方便。
以上就是我大概了解的反調試技術,不過加解密是具有強烈對抗性的,現在一些調試器都增加了反反調試手段,讓程序的反調試失效。
驅動保護與硬件加密狗
程序做好上面的保護,基本上已經具有一定的自我保護能力了,一般個人寫的軟件已經足夠。如果你寫的是商業軟件,需要高度防范破解,那可以采用驅動保護或硬件加密狗。具體采用何種根據軟件來定。
如果軟件是一些專業性較強的,可以采用硬件加密狗來保護;如果軟件是像網絡游戲那樣面向廣泛大眾群體的,采用加密狗就不現實了,一般都采用驅動保護,企鵝的游戲基本都有TenProtect的驅動保護,盛大的GPK保護等都是比較典型的例子。
加密狗我沒仔細研究過,就不好多說了。上面那些反調試的手段都運行在ring3級別,而驅動則運行ring0級別,驅動保護的做法主要是hook系統底層的一些API,通過檢驗調用者來區分外部調試修改還是程序自己的操作。比如打開進程的操作,所有調試器都需要調用,通過驅動層hook該函數來防止調試器打開或附加到程序進程。
結語
自從接觸了這些東西,才知道“涉密不上網,上網不涉密”的真正意義。要知道這一行高手很多,即便用盡各種手段,也不可能保證軟件絕對安全,只要軟件運行就會留下痕跡,就有被破解的可能。
現在已一年多沒搞這些了,以后估計也沒時間去搞,當初學習的時候雖然很累,但卻感覺很充實很有興趣,甚至還想換那方向的工作,謹以此作為對那段時間學習的總結。
雖然寫了這么些加密的東西,我個人還是更崇尚開源,如果不是那么必要,還是希望大家能多把源碼與人分享,共同進步。