當初因爲一些緣由以及興趣,學習了一段時間軟件逆向,對於軟件加密解密有了點粗略的瞭解。然後看到某些同窗辛辛苦苦的搞出個軟件,本身費心費力去加密,但搞出來後每每能被秒破,實不忍心。今天大概總結下一些基本的軟件加密手段,以供參考,高手勿噴。html
關於解密算法
軟件解密主要有2個層次,一個俗稱爆破,就是不分析加密算法,只修改一些與驗證相關的跳轉指令來使得軟件正常運行,另外一個就是能真正破解加密算法,進而寫出註冊機。破解手段一般有靜態分析和動態分析兩種方式,目前兩者的表明工具是IDA和OllyDbg(OD)。windows
加密算法與代碼安全
加密首先必須設計一套加密算法,這個能夠用現成的如MD5,SHA之類的算法,也能夠本身設計個稍微簡單點的算法。通常狀況,做爲一個開發者,設計一個簡單的加密算法應該問題不大的,可是算法設計必需要嚴密,不能出現漏網之魚。好比一個時間限制的算法,若是隻記錄開始結束時間,而後用當前時間去判斷,這樣的算法經過修改系統時間就給繞過去了,就不夠嚴密,須要改善;例如可再記錄一個最近一次運行時間,這樣就能夠處理修改系統時間的漏洞了。網絡
有了一個完善的加密算法,最直接也最容易想到的作法就是把用戶輸入的密碼用算法轉換後與保存的密鑰對比,一致則驗證經過,不一致則驗證失敗。這樣的加密程序估計新手也能快速爆破了。那麼在代碼編寫時,須要注意下面幾點ide
首先,加密算法儘可能不出如今程序中。好比你的加密算法是\(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該函數來防止調試器打開或附加到程序進程。
結語
自從接觸了這些東西,才知道「涉密不上網,上網不涉密」的真正意義。要知道這一行高手不少,即使用盡各類手段,也不可能保證軟件絕對安全,只要軟件運行就會留下痕跡,就有被破解的可能。
如今已一年多沒搞這些了,之後估計也沒時間去搞,當初學習的時候雖然很累,但卻感受很充實頗有興趣,甚至還想換那方向的工做,謹以此做爲對那段時間學習的總結。
雖然寫了這麼些加密的東西,我我的仍是更崇尚開源,若是不是那麼必要,仍是但願你們能多把源碼與人分享,共同進步。