【詭異的精簡C語言程式】main函數隱藏

來源:互聯網
上載者:User

哎,幾個月以來沒有寫部落格了,時間太緊,精力又有限。今天正好有這個時間,打算寫一篇今天在網上討論的一個問題。

我想大家應該都聽過“國際C語言混亂代碼大賽(IOCCC, The International Obfuscated C Code Contest)”吧,今天無意間在網上討論到這個問題。我有意將main函數改變了一下,居然編譯通過了,於是想利用這個特性,寫一個“詭異”的代碼。(寫完之後發現,IOCCC居然也有類似的參賽獲獎作品,悲劇,早知道我也去參賽了。。。)

進入正題吧,代碼是這樣的:

#include <stdio.h>int main[] = { 232,-1065134080,5138447,285147200,50008,(int)printf };

將這兩句代碼copy到XXX.c裡,用VC編譯,能順利通過,並執行,輸出結果為:

This program cannot be run in DOS mode.
$

或者換一種寫法:

#include <stdio.h>int ______ = ( int )printf;int main[] = { 232,-394045440,5138441,285147200,50008 };

輸出結果一致。

這麼一來,為什麼這兩段詭異的代碼能夠通過編譯並運行呢?可以總結為兩個疑問:

1. 為什麼能通過編譯?

2. 為什麼能輸出這麼一句字串?也沒用看到調用printf,更沒有看到該字串。

我們一個一個解決,對於第一個疑問:

首先,這兩段代碼都必須使用.c檔案進行編譯,在.cpp檔案下是不能通過編譯的,也就是說必須用C編譯器編譯,C++編譯器不能通過。

其次,正因為是C編譯器,同時又是Visual studio環境,那麼對於函數的參數個數,類型等的檢查不會很嚴格,對於入口函數main,編譯器在尋找main函數符號並連結時,不會嚴格檢查。因此,在這個地方將main函數用數組形式表達也能順利連結。GCC下也是可以順利編譯通過的,只是要改一下代碼才能成功運行,本文就不再累述了,這個不是重點。

到此,第一個疑問就解決了。那麼第二個疑問就相對複雜很多了,我們一步一步分析。

首先,main數組被連結為main函數,那麼main數組內部的整數(int,4位元組)就是main函數執行的代碼位元組(機器碼),這裡有點類似shellcode的原理。至於什麼是機器碼,這裡就不做解釋了,可以在我之前的部落格裡或者網路上找到答案。既然是機器碼,那麼這些整數就一定代表具體的執行邏輯,由於是直接寫的機器碼(整數),我們只能看反組譯碼代碼,我們以第一個例子為例:

00492000 E8 00 00 00 00   call        _main+5 (492005h)
00492005 58                          pop         eax 
00492006 83 C0 0F              add         eax,0Fh
00492009 68 4E 00 40 00   push        40004Eh
0049200E FF 10                    call        dword ptr [eax]
00492010 58                          pop         eax 
00492011 C3                          ret             
00492012 00 00                    add         byte ptr [eax],al
00492014 E0 B0                    loopne      00491FC6
00492016 42                          inc         edx 
00492017 00 E0                    add         al,ah 

在看main數組的記憶體:

0x00492000  e8 00 00 00 00 58 83 c0 0f 68 4e 00 40 00 ff 10 58 c3 00 00 e0 b0 42 00       ?....X??.hN.@...X?..??B.

0x00492018  e0 b0 42 00 —————————————————————————------       ??B..............HI.....

橫杠為省略部分記憶體,大家可以發現,第一排的24個位元組(6個int)正是main數組裡的6個整數,最後4個位元組即為printf函數的首地址:0x0042b0e0(在你的平台下通常不一樣)。那麼我們看上面的反組譯碼代碼,前面的機器碼也是記憶體裡的24個位元組,我們來分析一下反組譯碼代碼:

頭兩句紅色的彙編代碼:

call 0x00492005

pop eax

這兩句的功能主要是為了取得EIP寄存器的值,當執行完pop eax這句代碼之後,eax的值為0x00492005,這樣便取得了EIP的地址。

至於這兩句代碼為什麼能取得EIP的地址在之前的博文裡也有相關的講解,我們知道call指令理解分為兩步操作,一是將call指令的下一條指令的代碼地址壓棧,二是進行跳轉。

這裡call指令的下一句代碼是pop eax,它的代碼地址是0x00492005。call指令在壓入這個地址值到棧裡之後,再跳轉到pop eax這句。此刻,pop eax就會將剛剛壓入的代碼地址(0x00492005)彈出到eax裡,這樣eax裡就獲得了pop eax的代碼地址。為什麼要這麼麻煩呢,是因為內斂彙編不支援mov eax,eip這類的操作,所以就藉助call的特性來獲得EIP。

那麼,為什麼要擷取這個地址呢,目的是為了擷取後面printf函數的地址所儲存的位置(也就是相對於main數組首地址的位移量,也就是main數組最後一個int的記憶體位址),也就是0x00492014。這個地址也就是前面擷取的EIP值0x00492005 + 0x0f。因此,上面綠色的那句彙編代碼add eax, 0fh就不用再解釋了吧,add之後,eax的值則為0x00492014

到此,printf函數的地址值存放的位置也知道了,這時該考慮怎麼能夠調用printf輸出前面那串字元了。乍一看,這串字元似乎很熟悉,對!的確很熟悉,這串字元正是PE檔案頭裡的資訊,exe檔案裡有這麼一個資訊,那麼我們去哪兒找到這串字元的記憶體位址然後傳入printf呢?

平時偵錯工具的時候應該能夠注意到exe通常預設都會從0x00400000這個記憶體位址開始載入,當然也有時候不是這個地址開始的,例如在win7和vista下,如果編譯器開啟了隨機基地址選項時,那麼每次運行exe時,就會隨機一個基地址進行載入,這時就不一定是從0x00400000這個記憶體位址開始載入了。本文只針對從0x00400000這個地址載入的情況進行分析。

好了,我們來看看0x00400000這個記憶體位址下的記憶體情況:

0x00400000  4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 b8 00 00 00 00 00 00 00          MZ?.............?.......
0x00400018  40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    @.......................
0x00400030  00 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00 0e 1f ba 0e 00 b4 09 cd      ............?.....?..?.?
0x00400048  21 b8 01 4c cd 21 54 68 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f        !?.L?!This program canno
0x00400060  74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 6d 6f 64 65 2e 0d 0d 0a       t be run in DOS mode....
0x00400078  24 00 00 00 00 00 00 00 25 3c f5 d5 61 5d 9b 86 61 5d 9b 86 61 5d 9b 86      $.......%<??a]??a]??a]??

可以看出,上面紅色的部分從0x0040004e開始即為之前輸出的那串字元,一直到'$'字元後才結束,即到0x00400079才結束輸出。

分析到此,前面的反組譯碼裡的黑色粗體一句的彙編就已經很明了了,它正是將0x0040004e這個地址傳遞給printf函數,讓其輸出這串字元。

之後的一句藍色的call代碼,即調用printf函數,前面已經將printf的地址值在main數組裡的地址值存到了eax裡,此刻只需要將eax下的地址值取出來,call過去就可以了,也就等價於:

call main[ 5 ]  // 虛擬碼

調用完printf輸出之後,pop eax則是為了平衡棧,因為printf是__cdecl呼叫慣例,所以調用者需要平衡棧。pop eax就等價於add esp,4,這裡為了節約幾個位元組,pop eax只佔一個位元組。

好了,反組譯碼代碼就分析得差不多了。原理其實很簡單,至於第二種寫法與第一種只是printf函數的地址存放的位置不一樣。我們來看反組譯碼代碼:

00492000 E0 B0                     loopne      00491FB2
00492002 42                           inc         edx 
00492003 00                           db          00h 
00492004 E8 00 00 00 00    call        _main+5 (492009h)
00492009 58                           pop         eax 
0049200A 83 E8 09               sub         eax,9
0049200D 68 4E 00 40 00   push        40004Eh
00492012 FF 10                     call        dword ptr [eax]
00492014 58                           pop         eax 
00492015 C3                          ret             
00492016 00 00                     add         byte ptr [eax],al

綠色的一句代碼變成了sub,向前減9個位元組,剛好是0x00492000,也就是變數"______"的地址。printf函數的地址值就存在這裡,所以需要減去9,也就是0x00492009 - 9。其他部分的代碼與前面的一致。("______"變數和main數組的地址在記憶體上是連續的)

詭異的代碼就如此的產生了,其實如果將main數組翻譯為內斂彙編的版本,如下:

版本一:int __declspec( naked ) main( void ){    __asm    {        call __geteip__geteip:        pop  eax         // 擷取EIP        add  eax, 0dh__entry:        push 0x0040004e  // 為printf函數壓入參數        call [ eax ]        pop  eax         // 平衡棧        ret    }}版本二:int __declspec( naked ) main( void ){    __asm    {        call __geteip__geteip:        pop  eax        sub  eax, 09h__entry:        push 0x0040004e        call [ eax ]        pop  eax        ret    }}

這兩個版本不能運行,只能通過編譯。因為printf的函數地址存放的位置不能確定了,我們使用數組是可以確定的。另外在ret指令後,如果不是4的整數倍,那麼寫成main數組的時候,就需要填充位元組,在main數組裡我填充為0。

以上兩個版本可能在某些時候不能運行成功,因為main數組處於資料區段,資料區段的記憶體可能沒有執行許可權,因此會出錯。在實際中,可以修改記憶體許可權。

綜述:

1. 本文的例子不具有實用價值,只作為研究之用,其目的在於瞭解函數調用模型的本質以及彙編層的架構和指令的利用。重在原理性的研究,拓展思維。

2. 個人認為,很多不具有實際實用價值的東西並非不值得研究,研究的目的不在於結果,在於過程,吸收有利的,拋棄無用的。

3. 對於本文的執行個體,原理性的東西如函數調用模型,在實際中有用處很多,很典型的例子就是通過dump檔案進行錯誤尋找和分析,這裡的dump檔案可能是自訂的dump格式。

好了,本文到此就告一段落,歡迎交流。

---如需轉載,請註明出處,謝謝支援----

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.