PE感染逆向之修復(Serverx.exe專殺工具出爐手記)


作 者: achillis
時 間: 2008-10-04,16:06
鏈 接: [url]http://bbs.pediy.com/showthread.php?t=73948[/url]

這篇文章是在Servex.exe專殺工具寫完之後的一些小小感想,關於PE感染和修復,大牛飄過~

  自某日不小心中了Serverx.exe病毒,電腦中大部分EXE文件被感染,系統盤system32目錄下的病毒文件Serverx.exe總也殺不掉,因爲即使刪掉了,每次運行了被感染的程序之後還會再次生成。(關於此毒的詳細資料,有興趣的直接在百度搜索"Serverx.exe"或"憤怒天使"即可)在網上求助無果,遂自己動手,研究了一下這個病毒的感染方式,最終找到了恢復被感染文件的方法,下面將此過程分享給大家。

遇到這種感染型的病毒,恢復時需要解決幾個問題。
第一個問題,感染後的文件能否恢復?

那麼我們從被感染後的EXE運行情況來看看。以調用Winrar解壓縮爲例,可以正常實現解壓過程。但在解壓後就應退出的Winrar仍在運行。如果用Process Explorer觀察進程樹的話,會發現是Winrar本身又重新運行了一個Winrar進程來完成解壓縮功能,而解壓完成後,第一個運行的Winrar仍在繼續運行,用FileMon監視的話會發現它有大量的文件訪問操作,從訪問的目錄順序和規律來看是在遍歷並尋找EXE進行感染。而這兩個Winrar的路徑是一樣的,也就是說實現感染功能的和實現解壓功能的是同一個程序,解壓功能仍然是被感染後的Winrar完成的,這說明一個問題:感染後的文件功能沒有被破壞!這非常重要,也是因爲程序的功能沒有被破壞纔有下面的恢復過程。

第二個問題,此病毒是何種感染方式?
典型的感染方式有“夾心餅”式的捆綁感染,替換資源或添加資源式的感染,準確說這些不算是“正宗”的PE感染。其它典型的方式有加新區段,寫入shellcode(這裏把病毒自己注入的代碼也稱爲shellcode),然後跳回原入口。還有一些高級的感染方式,不一一列舉。
爲了分析這個病毒的感染方式,我將感染前後的Winrar.exe進行對比,首先會發現文件大小要增加,再用PEID(或其它同類工具)查看一下相關數據,首先發現入口所在區段不一樣了,由.text段變成了最後的.rsrc段,入口處代碼也不一樣了。既然感染跟區段有關,那麼就來看一下感染前後區段數據的變化:
感染前: 
名称:
感染後: 
名称:
可以看到,區段數並沒有增加,仍爲7個,但是最後一個區段的虛擬大小(VirtualSize)和原始大小(RawSize)都有所增加,且段屬性標記也被修改了。這樣我們對此病毒的感染方式有所瞭解了,就是增加最後一個段的大小,寫入shellcode,並修改段屬性爲可執行,並不是某些人分析的增加新區段方式。具體地把一些數據列出來並進行計算(所有數據都是Winrar.exe的):
感染前文件大小:929792 Bytes   感染後:974742 Bytes  增加:44950=0xAF96
感染前最後一個節的數據:
VirtualSize:0x00032000   RawSize:0x00031200   Flags:0x40000040
感染後最後一個節的數據:
VirtualSize:0x0003C196   RawSize:0x0003C196   Flags:0xE0000040
增加的大小:VirtualSize:0x0000A196  RawSize:0x0000AF96
經過計算,最後一個節的RawSize增加的大小剛好等於文件增加的大小,這個用WinHex打開感染前後的文件比較一下就可以證實,病毒確實是在原始文件末的地方開始寫入shellcode的。對PE知識的瞭解使我們知道節表中的RawSize對應於這個節在文件中實際的大小,我們更證實了之前的想法。可知病毒的感染方法是這樣的:根據shellcode計算需要的空間大小nSize,然後將文件增加nSize,寫入shellcode,然後修正PE頭中最後一個區段的原始大小、虛擬大小,修改區段屬性
爲可執行,因爲映像大小被修改,還要修正IMAGE_OPTIONAL_HEADER結構中的SizeOfImage。Shellcode中必然是做了些壞事然後跳回到原入口處繼續執行。這是我們目前分析得到的信息。

第三個問題,要恢復被感染的文件,需要修復哪些數據?
結合前面分析出的病毒的感染過程,要修復感染後的文件,至少要知道以下幾個信息,同時也是需要修復的數據。
(1)  感染前的文件大小。前面已經知道,病毒直接從文件末開始寫入shellcode,要去掉病毒寫入的內容,就必須知道感染前原文件大小。
(2)  感染前最後一個區段的RawSize。因爲RawSize增加的大小等於文件增加的大小,所以知道了感染前文件大小也就是RawSize的大小。
(3)  感染前最後一個區段的VirtualSize。在獲取正確的RawSize之後就可以根據對齊來計算VirtuslSize,也就是說第二個問題解決了,這個問題也就解決了。
(4)  感染前的映像和。在獲取最後一個區段正確的VirtualSize之後就可以計算映像大小之和,第三個問題解決了,這個問題也就解決了。
(5)  感染前的入口點。這個關鍵數據的重要性不需多說吧?前四個信息是一環扣一環,所以總的來說,要獲取的關鍵數據只有兩個,感染前的文件大小和感染之前的入口點

第四個問題,如何獲取感染前的文件大小?
並不是所有程序都像Winrar這樣有備份,可以進行對比分析。最一般的情況,只有一個感染後的EXE文件,如何獲取感染前的文件大小?而且這個方法還必須要通用,因爲將來我們要修復的是所有被感染的EXE文件。這個問題我想了很久,後來想到一個辦法,病毒是從原文件末開始寫入shellcode的,又觀察了幾個感染後的程序,可以看到入口代碼都是一樣的,也就是說寫入的shellcode都一樣,沒有變形處理等。基於病毒固定的shellcode特徵,而且是從文件末開始寫入shellcode的,我們可以從文件最後將前搜索匹配此shellcode特徵,找到的shellcode的起始位置也就是原文件結束的位置,此時距文件頭的偏移就是原來文件的大小了!
用OllyDbg載入感染後的Winrar.exe,來獲取一下shellcode特徵:
00524200    60                   pushad
00524201    78 03              js short 有毒_Win.00524206
00524203    79 01              jns short 有毒_Win.00524206
00524205  ^ EB E8             jmp short 有毒_Win.005241EF
00524207    74 11              je short 有毒_Win.0052421A
就取其前8字節作爲特徵,定義如下:
char Signature[]="\x60\x78\x03\x79\x01\xEB\xE8\x74";
搜索的代碼比較簡單,CreateFile打開目標文件然後CreateFileMapping,MapViewOfFile進行映射,從後向前使用memcmp進行比較即可。代碼如下:
void SearchCode(char *buf,DWORD dwBufSize)
 {
  //第一個參數即映射後的內存地址,第二個參數爲映射的內存大小
  int i=0;
  //SIGNATURELEN即前面定義的特徵碼長度,即8
  char *p=buf+dwBufSize-SIGNATURELEN;
  //輸出內存基址
  printf("p=0x%08X\n",p);
  do 
  {
    if (memcmp(p,Signature,SIGNATURELEN)==0)
    {
      //若匹配,則輸出當前指針位置,距開始的偏移,距結束的偏移
printf("[%d]Found Signatue 0x%08X offset=0x%08X size=0x%08X\n",i,p,p-buf,buf+dwBufSize-p);
      i++;
    }
    p--;
  } while(p!=buf);
  printf("Search OK!\n");
}
運行結果如下:
 名称:
來分析一下這個結果吧!
在感染後的Winrar中,我們逆向搜索到了十處匹配的特徵碼,在最後一處,距文件起始處的偏移爲0x000E3000,換成十進制就是929972,如果你對這個數還有印象的話,就會知道,這就是感染前的文件大小!而距結尾處的偏移爲0x0000AF96,這個就是文件增加的大小,也是最後一個區段的RawSize增加的大小!這些激動人心的數據不僅證實了我們前面所有關於感染方式的論證是正確的,而且獲取原始文件大小的問題也得到了解決!

第五個問題,如何獲取原始入口點?
前面列出了修復文件所需的五個數據,而第四個問題的解決也使前四個數據都有了着落。病毒一定在shellcode中辦完自己的事後以某種方式跳回原入口點,保證原程序的功能不變。如何獲取原入口點呢?靜態分析已經不能解決問題了,我們來跟蹤一下病毒注入的shellcode吧。再次拿出Ollydbg,載入感染後的Winrar.exe,仔細跟蹤。(如果不小心跑飛了要立即結束!否則可能會有文件被感染)如果說前面的那些根據病毒外在表現的分析是黑盒分析的話,現在要進行的就是白盒分析了!
剛開始,shellcode搜索MZ標誌尋找kernel32.dll的基址,然後獲取LoadLibrary()和GetProcAddress()的地址,之後加載用到的dll並獲取相關函數地址,算是比較常規的過程啊。這些過程進程完之後,病毒做的第一件事,就是調用CreateMutex,創建互斥體“Angry Angel v3.0”,看來這也是此病毒被稱爲“憤怒天使”的原因了。
名称:
然後RtlGetLastWin32Error(),判斷是否爲ERROR_ALREADY_EXISTS,若存在則直接int 3,若不存在則繼續執行。爲什麼要int 3呢,有玄機,等會兒再看。繼續執行,就GetCommandLine(),然後再WinExec()執行之。這也就是前面提到感染後的程序運行時會有兩個進程的原因了。WinExec之後,調用GetSystemDirectory()獲取系統目錄,然後連接上病毒名Serverx.exe,判斷文件是否存在,存在則寫註冊表自啓動項。後面的過程我就不再分析了,大致還有向Explorer.exe進程注入代碼用於進程保護,另外開啓線程進行文件感染等,不再多說,多說就偏題了,我們現在關心的問題是如何獲取原入口點啊。也就是被病毒WinExec方式運行的進程,又如何實現原來的功能,也就是如何跳回到原入口點。把上面的過程再走一遍,因爲還是這個程序,還要走這個過程,但是在RtlGetLastWin32Error()時就有差別了,由於剛纔病毒已經創建了互斥體,這時就會得到最終錯誤碼爲ERROR_ALREADY_EXISTS,然後,病毒執行int 3,而不是想像中的jmp XXXXXXXX。Int 3會怎樣?會引發一個異常,於是想到病毒可能是在異常處理中跳回了原入口。異常處理的詳細過程不多說(其實我也只瞭解大概),大致就是依次調用SHE鏈中註冊的異常處理過程。在跟蹤中發現,在10次中斷在CreatMutex之後,再int 3執行ZwContinue()就會跳回到原入口。爲什麼是10次呢?稍後再說。反正我們知道最後一次int 3之後會跳回原入口,目前只關心最後一次int 3之後執行ZwContinue時做了什麼?

先了解一下ZwContinue()這個函數,從WRK中找到對應的NtContinue原型如下:
 NTSTATUS
 NtContinue (
    IN PCONTEXT ContextRecord,
    IN BOOLEAN TestAlert
   )
NtContinue用於恢復一個線程的執行。第一個參數是一個指向線程上下文信息的結構體指針,裏面有所有的線程環境信息,包括通用寄存器、段寄存器、浮點寄存器等。當然,還有我們想要的EIP。此時調用的代碼可以這麼表示:
NtContinue(0x0012FBBC,0);
結合CONTEXT的結構(詳細結構可參見WRK),可以知道異常恢復後的EIP,那麼Ctrl+G,填入EIP,F2下斷,再F8單步,果然在這裏斷了下來,再F7單步幾次,看到如下代碼:
名称: 
真是令人興奮啊!這個0x00401000正是感染前程序的入口點!
綜合前面得到的信息,來理一下思路吧。前面說搜索shellcode時搜索到了10處,而對CreateMutex下斷時也斷下來10次,int 3也執行了10次。這隻能說明,病毒將同一段shellcode寫入了10次!而每一段shellcode即將結束時int 3引發異常,在異常中進入了第二段同樣的shellcode,一直到第10次int 3時才跳回程序原入口處繼續執行。最後一段shellcode的起始地址是0x00524200,而push/ret距起始的偏移爲0x0052421A-0x00524200=0x1A,而下一次跳轉的地址偏移就是0x1B處的DWORD!來檢驗下我們的計算結果吧。Ctrl+F2重新載入,Ctrl+G跳到距入口也就是第一段shellcode起始地址0x0052E007偏移爲0x1A的地方,可以看到:
 名称:
再跳到這個0x0052CE78處看看:
名称:
和第一段shellcode的代碼是一模一樣的,只是地址不同而已。這時,我們可以得出感染後EXE的執行過程,簡單畫個圖表示一下(很粗糙哈):
名称:
其中每個灰色方塊代表一段shellcode,而前面斜線部分纔是真正的原始文件。病毒以最後一段shellcode的起點爲入口,每段shellcode結束時都跳入到前一段,最後一次跳回原始入口點,執行原程序的功能。理清之後,要獲取每一次轉移後的地址就容易了,在距每段shellcode開頭偏移0x1B處取一個DWORD的值,就是下一次轉移的目標!
把我們前面的SearchCode函數改進一下,增加識別轉移地址的功能,新函數如下:


void SearchCode(char *buf,DWORD dwBufSize)
{
  DWORD offset=0;
  int i=0;
  char *p=buf+dwBufSize-SIGNATURELEN;
  printf("p=0x%08X\n",p);
  do 
  {
    if (memcmp(p,Signature,SIGNATURELEN)==0)
    {
      printf("[%d]Found Signatue 0x%08X Entry=0x%08X 

offset=0x%08X size=0x%08X\n",i,p,*(DWORD*)(p+0x1B),p-buf,buf+dwBufSize-p);
      trueFileSize=p-buf;
      entryNow=*(DWORD*)(p+0x1B);
      i++;
    }
    p--;
  } while(p!=buf);
  printf("Search OK!\n");
}
其中的Entry就是轉移目標了!
輸出結果如下:
 
最後一次搜索到的就是原始的入口點了(如果從前往後搜索的話就是第一次),

第五個問題解決!
好了,五個問題都解決了,可以寫修復工具了!修復過程如下:
(1)  通過在感染後的文件中搜索shellcode得到原始入口點,原文件大小;
(2)  根據當前文件大小,得出增加部分的大小ExtraSize;
(3)  根據ExtraSize修正最後一個區段的RawSize,當前值減去多的部分即可得正確RawSize;
(4)  由正確的RawSize和內存對齊大小,計算正確的VirtualSize;
(5)  根據PE頭的大小和所有區段映像大小,計算總的SizeofImage;
(6)  原始入口點減去映像基址即得AddressofEntryPoint;
(7)  將文件大小減小ExtraSize,設置結束標記。
這就是完整的修復方案!限於篇幅,具體代碼不直接貼出來了,請在隨文代碼中查看。

修復後Winrar.exe與感染前相比,僅有3個字節的差異!差異主要體現在程序的校驗和和最後一個區段的屬性。校驗和只有SYS纔會用到,對於病毒感染的EXE和SCR格式來說並不重要,因此不再修正。而最後一個區段多了執行屬性,考慮到一些加殼後的程序的特殊性,不再修正這個值。修復效果堪稱完美!將以上代碼整理一下,再加上遍歷EXE和SCR、判斷是否感染、修復,就成了一個實用的專殺工具!至此,Serverx.exe出爐了!

初寫此程序時感覺還有些難,畢竟是從感染的逆方向入手,沒寫過也沒把握。就像破解一個程序,要了解作者的思路和方法,才能找到對策。
現在再看,好像一切都是那麼清晰,就像對着答案看題目一樣,不過當初做題時不容易啊~~文中如有錯誤,還請大家指出~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章