寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。
小白鼠
本次示例使用C++
,也就是編譯型語言,為了方便檢視彙編程式碼和編碼故使用VS 2022
,其他IDE
即可,程式碼如下:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int x = 0;
cout << "請輸入金鑰:" << endl;
cin >> x;
if (x == 1234)
{
cout << "成功,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
}
else
{
cout << "失敗,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
}
system("pause");
return 0;
}
是不是程式碼很簡單?判斷你輸入的是不是1234
,是的話就是成功,反之失敗。這是最簡單的校驗示例了。
分析
程式碼的流程十分簡單,我們可以用下面的流程圖進行表示:
由於是初次分析編譯型程式,我們直接在IDE
先看看它的反彙編:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
00E42550 push ebp
00E42551 mov ebp,esp
00E42553 sub esp,0D0h
00E42559 push ebx
00E4255A push esi
00E4255B push edi
00E4255C lea edi,[ebp-10h]
00E4255F mov ecx,4
00E42564 mov eax,0CCCCCCCCh
00E42569 rep stos dword ptr es:[edi]
00E4256B mov eax,dword ptr [__security_cookie (0E4C004h)]
00E42570 xor eax,ebp
00E42572 mov dword ptr [ebp-4],eax
00E42575 mov ecx,offset _3226632D_ConsoleApplication3@cpp (0E4F066h)
00E4257A call @__CheckForDebuggerJustMyCode@4 (0E41384h)
int x = 0;
00E4257F mov dword ptr [x],0
cout << "請輸入金鑰:" << endl;
00E42586 mov esi,esp
00E42588 push offset std::endl<char,std::char_traits<char> > (0E4103Ch)
00E4258D push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xc3\xdc\xd4\xbf\xa3\xba" (0E49B30h)
00E42592 mov eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E42597 push eax
00E42598 call std::operator<<<std::char_traits<char> > (0E411A9h)
00E4259D add esp,8
00E425A0 mov ecx,eax
00E425A2 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E425A8 cmp esi,esp
00E425AA call __RTC_CheckEsp (0E4128Fh)
cin >> x;
00E425AF mov esi,esp
00E425B1 lea eax,[x]
00E425B4 push eax
00E425B5 mov ecx,dword ptr [__imp_std::cin (0E4D098h)]
00E425BB call dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0E4D09Ch)]
00E425C1 cmp esi,esp
00E425C3 call __RTC_CheckEsp (0E4128Fh)
if (x == 1234)
00E425C8 cmp dword ptr [x],4D2h
00E425CF jne __$EncStackInitStart+0A0h (0E425FCh)
{
cout << "成功,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
00E425D1 mov esi,esp
00E425D3 push offset std::endl<char,std::char_traits<char> > (0E4103Ch)
00E425D8 push offset string "\xb3\xc9\xb9\xa6\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49B64h)
00E425DD mov eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E425E2 push eax
00E425E3 call std::operator<<<std::char_traits<char> > (0E411A9h)
00E425E8 add esp,8
00E425EB mov ecx,eax
00E425ED call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E425F3 cmp esi,esp
00E425F5 call __RTC_CheckEsp (0E4128Fh)
}
00E425FA jmp __$EncStackInitStart+0C9h (0E42625h)
else
{
cout << "失敗,By.寂靜的羽夏,CNBLOG Only!!!" << endl;
00E425FC mov esi,esp
00E425FE push offset std::endl<char,std::char_traits<char> > (0E4103Ch)
00E42603 push offset string "\xca\xa7\xb0\xdc\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49D40h)
00E42608 mov eax,dword ptr [__imp_std::cout (0E4D0DCh)]
00E4260D push eax
00E4260E call std::operator<<<std::char_traits<char> > (0E411A9h)
00E42613 add esp,8
00E42616 mov ecx,eax
00E42618 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)]
00E4261E cmp esi,esp
00E42620 call __RTC_CheckEsp (0E4128Fh)
}
system("pause");
00E42625 mov esi,esp
00E42627 push offset string "pause" (0E49B40h)
00E4262C call dword ptr [__imp__system (0E4D1D4h)]
00E42632 add esp,4
00E42635 cmp esi,esp
00E42637 call __RTC_CheckEsp (0E4128Fh)
return 0;
00E4263C xor eax,eax
}
看不懂彙編的意思的,請自學彙編。看不懂整體組合語言的作用,請學習我的教程 羽夏看C語言 即可看懂。
輸出資訊最關鍵的當然是判斷了,也就是下面這個部分:
if (x == 1234)
00E425C8 cmp dword ptr [x],4D2h
00E425CF jne __$EncStackInitStart+0A0h (0E425FCh)
如果我把jne
給幹掉,無論我輸入什麼,都會輸出正確。也就是我們把判斷失敗的鏈條給砍斷了,只能走這條路。
上面只改彙編程式碼的,通過修改彙編以修改程式碼執行流程實現自己的目的,就是我們所謂的暴力破解,也就是patch
,俗稱打補丁。如果我逆向分析得到金鑰就是1234
,直接輸入,這東西也就是所謂的key
。當然,現實中驗證不可能這麼簡單,它們往往是通過某種演算法,通過獲取計算機獨特的資訊生成註冊碼(如果我們能夠知道演算法是什麼,根據演算法來寫出算key
的軟體,俗稱序號產生器
),然後根據校驗函式進行校驗,大大增大了分析難度。這就是說為什麼爆破比序號產生器簡單很多。
但是爆破也不是那麼輕鬆的,如果在校驗函式中生成了大量的中間結果,如果它再拿來這東西校驗的話,被破解的難度就會提高一個層次。發現被破解,直接退出程式,這往往是付費軟體防破解的一個手段。
當然,我們破解軟體的時候不可能是拿到別人的原始碼按照本篇文章開頭那樣進行分析。我們來真槍實戰一下。假設這個軟體是我們未知的,如何分析這個程式。
小試牛刀
本小節是按照常規套路進行分析,分析不同型別的軟體可能流程不太一樣,比如分析病毒你肯定不能直接在真機上跑,有些病毒還有反虛擬機器的模組,甚至變形多型;編譯型程式碼軟體分析和解釋型程式碼軟體分析也不太一樣;只是分支操作有所不同。接下來是編譯型軟體的常規操作:
查殼,看看是啥軟體編寫的:
直接拖到IDA
瞅一瞅,如果沒報錯,說明軟體可以直接分析。如果報錯,說明軟體被加殼或者加密。當然這軟體沒有加殼,所以正常,由於軟體短小精悍,我們很快定位到主函式部分,下面是它的流程:
看到彙編程式碼後,IDA
會有一些註釋,看下圖:
這個明明是字串,但IDA
沒有識別出來。這個是經常會遇到的情況。有時候IDA
識別不出Unicode
字串,甚至識別錯誤以為是函式偏移,不要僅依賴這個軟體。那麼我們如何處理呢?把游標放到上面字串的首地址,按下ALT + A
,將會彈出下面的窗體:
這個就是將資料轉化為字串,由於這個是普通的ASCII
字串,直接選C-style
即可。
轉換成功後結果如下:
聽說IDA
有一個F5
的功能,的確,它是IDA
的一個外掛。如果沒有選擇偵錯程式的情況下,它會預設呼叫Hex-ray
這個外掛翻譯成偽C程式碼
。但是不要過度依賴這個外掛,因為翻譯出來的東西很多是不準確的,甚至是錯誤的。比如函式呼叫的引數個數、多個變數其實就是一個變數、型別不對等等。這些東西都需要依賴自身的開發經驗和分析進行調整。經過分析和重新命名後,如下所示:
通過分析,我們既得到了Key
,也分析透了函式流程。
暴力破解
IDA
其實可以patch
軟體的,怎麼搞自行學習,比較麻煩,一旦patch
就會導致原來的分析流程變化,自認為不太適用,如果用於去除花指令的話另當別論。在動態偵錯程式裡修改是最快捷的,最舒服的方式。如下的動圖進行演示:
結語
學習本篇指引,如果IDA
不會使用的話,請自行找教程進行練習。本系列教程只是個指引,故不提供IDA
使用教程。IDA
有如下基本操作:函式和變數的重新命名、跳轉、型別轉化、搜尋字串、查詢引用是最常用的操作了,請自行學習。
下一篇
羽夏逆向指引——補丁