哎,幾個(gè)月以來(lái)沒(méi)有寫博客了,時(shí)間太緊,精力又有限。今天正好有這個(gè)時(shí)間,打算寫一篇今天在網(wǎng)上討論的一個(gè)問(wèn)題。
我想大家應(yīng)該都聽過(guò)“國(guó)際C語(yǔ)言混亂代碼大賽( IOCCC , The International Obfuscated C Code Contest)”吧,今天無(wú)意間在網(wǎng)上討論到這個(gè)問(wèn)題。我有意將main函數(shù)改變了一下,居然編譯通過(guò)了,于是想利用這個(gè)特性,寫一個(gè)“詭異”的代碼。(寫完之后發(fā)現(xiàn),IOCCC居然也有類似的參賽獲獎(jiǎng)作品,悲劇,早知道我也去參賽了。。。)
進(jìn)入正題吧,代碼是這樣的:
將這兩句代碼copy到XXX.c里,用VC編譯,能順利通過(guò),并執(zhí)行,輸出結(jié)果為:
This program cannot be run in DOS mode.
$
或者換一種寫法:
輸出結(jié)果一致。
這么一來(lái),為什么這兩段詭異的代碼能夠通過(guò)編譯并運(yùn)行呢?可以總結(jié)為兩個(gè)疑問(wèn):
1. 為什么能通過(guò)編譯?
2. 為什么能輸出這么一句字符串?也沒(méi)用看到調(diào)用printf,更沒(méi)有看到該字符串。
我們一個(gè)一個(gè)解決,對(duì)于第一個(gè)疑問(wèn):
首先,這兩段代碼都必須使用.c文件進(jìn)行編譯,在.cpp文件下是不能通過(guò)編譯的,也就是說(shuō)必須用C編譯器編譯,C++編譯器不能通過(guò)。
其次,正因?yàn)槭荂編譯器,同時(shí)又是Visual studio環(huán)境,那么對(duì)于函數(shù)的參數(shù)個(gè)數(shù),類型等的檢查不會(huì)很嚴(yán)格,對(duì)于入口函數(shù)main,編譯器在查找main函數(shù)符號(hào)并鏈接時(shí),不會(huì)嚴(yán)格檢查。因此,在這個(gè)地方將main函數(shù)用數(shù)組形式表達(dá)也能順利鏈接。GCC下也是可以順利編譯通過(guò)的,只是要改一下代碼才能成功運(yùn)行,本文就不再累述了,這個(gè)不是重點(diǎn)。
到此,第一個(gè)疑問(wèn)就解決了。那么第二個(gè)疑問(wèn)就相對(duì)復(fù)雜很多了,我們一步一步分析。
首先,main數(shù)組被鏈接為main函數(shù),那么main數(shù)組內(nèi)部的整數(shù)(int,4字節(jié))就是main函數(shù)執(zhí)行的代碼字節(jié)(機(jī)器碼),這里有點(diǎn)類似shellcode的原理。至于什么是機(jī)器碼,這里就不做解釋了,可以在我之前的博客里或者網(wǎng)絡(luò)上找到答案。既然是機(jī)器碼,那么這些整數(shù)就一定代表具體的執(zhí)行邏輯,由于是直接寫的機(jī)器碼(整數(shù)),我們只能看反匯編代碼,我們以第一個(gè)例子為例:
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數(shù)組的內(nèi)存:
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.....
橫杠為省略部分內(nèi)存,大家可以發(fā)現(xiàn),第一排的24個(gè)字節(jié)(6個(gè)int)正是main數(shù)組里的6個(gè)整數(shù),最后4個(gè)字節(jié)即為printf函數(shù)的首地址:0x0042b0e0(在你的平臺(tái)下通常不一樣)。那么我們看上面的反匯編代碼,前面的機(jī)器碼也是內(nèi)存里的24個(gè)字節(jié),我們來(lái)分析一下反匯編代碼:
頭兩句紅色的匯編代碼:
call 0x00492005
pop eax
這兩句的功能主要是為了取得EIP寄存器的值,當(dāng)執(zhí)行完pop eax這句代碼之后,eax的值為0x00492005,這樣便取得了EIP的地址。
至于這兩句代碼為什么能取得EIP的地址在之前的博文里也有相關(guān)的講解,我們知道call指令理解分為兩步操作,一是將call指令的下一條指令的代碼地址壓棧,二是進(jìn)行跳轉(zhuǎn)。
這里call指令的下一句代碼是 pop eax ,它的代碼地址是0x00492005。call指令在壓入這個(gè)地址值到棧里之后,再跳轉(zhuǎn)到pop eax這句。此刻,pop eax就會(huì)將剛剛壓入的代碼地址(0x00492005)彈出到eax里,這樣eax里就獲得了pop eax的代碼地址。為什么要這么麻煩呢,是因?yàn)閮?nèi)斂匯編不支持mov eax,eip這類的操作,所以就借助call的特性來(lái)獲得EIP。
那么,為什么要獲取這個(gè)地址呢,目的是為了獲取后面printf函數(shù)的地址所存儲(chǔ)的位置(也就是相對(duì)于main數(shù)組首地址的偏移量,也就是main數(shù)組最后一個(gè)int的內(nèi)存地址),也就是 0x00492014 。這個(gè)地址也就是前面獲取的EIP值 0x00492005 + 0x0f 。因此,上面綠色的那句匯編代碼 add eax, 0fh 就不用再解釋了吧,add之后,eax的值則為 0x00492014 。
到此,printf函數(shù)的地址值存放的位置也知道了,這時(shí)該考慮怎么能夠調(diào)用printf輸出前面那串字符了。乍一看,這串字符似乎很熟悉,對(duì)!的確很熟悉,這串字符正是PE文件頭里的信息,exe文件里有這么一個(gè)信息,那么我們?nèi)ツ膬赫业竭@串字符的內(nèi)存地址然后傳入printf呢?
平時(shí)調(diào)試程序的時(shí)候應(yīng)該能夠注意到exe通常默認(rèn)都會(huì)從0x00400000這個(gè)內(nèi)存地址開始加載,當(dāng)然也有時(shí)候不是這個(gè)地址開始的,例如在win7和vista下,如果編譯器開啟了隨機(jī)基地址選項(xiàng)時(shí),那么每次運(yùn)行exe時(shí),就會(huì)隨機(jī)一個(gè)基地址進(jìn)行加載,這時(shí)就不一定是從0x00400000這個(gè)內(nèi)存地址開始加載了。本文只針對(duì)從0x00400000這個(gè)地址加載的情況進(jìn)行分析。
好了,我們來(lái)看看0x00400000這個(gè)內(nèi)存地址下的內(nèi)存情況:
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開始即為之前輸出的那串字符,一直到'$'字符后才結(jié)束,即到0x00400079才結(jié)束輸出。
分析到此,前面的反匯編里的黑色粗體一句的匯編就已經(jīng)很明了了,它正是將0x0040004e這個(gè)地址傳遞給printf函數(shù),讓其輸出這串字符。
之后的一句藍(lán)色的call代碼,即調(diào)用printf函數(shù),前面已經(jīng)將printf的地址值在main數(shù)組里的地址值存到了eax里,此刻只需要將eax下的地址值取出來(lái),call過(guò)去就可以了,也就等價(jià)于:
call main[ 5] // 偽代碼
調(diào)用完printf輸出之后,pop eax則是為了平衡棧,因?yàn)閜rintf是__cdecl調(diào)用約定,所以調(diào)用者需要平衡棧。pop eax就等價(jià)于add esp,4,這里為了節(jié)約幾個(gè)字節(jié),pop eax只占一個(gè)字節(jié)。
好了,反匯編代碼就分析得差不多了。原理其實(shí)很簡(jiǎn)單,至于第二種寫法與第一種只是printf函數(shù)的地址存放的位置不一樣。我們來(lái)看反匯編代碼:
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個(gè)字節(jié),剛好是 0x00492000 ,也就是變量"______"的地址。printf函數(shù)的地址值就存在這里,所以需要減去9,也就是0x00492009 - 9。其他部分的代碼與前面的一致。("______"變量和main數(shù)組的地址在內(nèi)存上是連續(xù)的)
詭異的代碼就如此的產(chǎn)生了,其實(shí)如果將main數(shù)組翻譯為內(nèi)斂匯編的版本,如下:
這兩個(gè)版本不能運(yùn)行,只能通過(guò)編譯。因?yàn)閜rintf的函數(shù)地址存放的位置不能確定了,我們使用數(shù)組是可以確定的。另外在ret指令后,如果不是4的整數(shù)倍,那么寫成main數(shù)組的時(shí)候,就需要填充字節(jié),在main數(shù)組里我填充為0。
以上兩個(gè)版本可能在某些時(shí)候不能運(yùn)行成功,因?yàn)閙ain數(shù)組處于數(shù)據(jù)段,數(shù)據(jù)段的內(nèi)存可能沒(méi)有執(zhí)行權(quán)限,因此會(huì)出錯(cuò)。在實(shí)際中,可以修改內(nèi)存權(quán)限。
綜述:
1. 本文的例子不具有實(shí)用價(jià)值,只作為研究之用,其目的在于了解函數(shù)調(diào)用模型的本質(zhì)以及匯編層的框架和指令的利用。重在原理性的研究,拓展思維。
2. 個(gè)人認(rèn)為,很多不具有實(shí)際實(shí)用價(jià)值的東西并非不值得研究,研究的目的不在于結(jié)果,在于過(guò)程,吸收有利的,拋棄無(wú)用的。
3. 對(duì)于本文的實(shí)例,原理性的東西如函數(shù)調(diào)用模型,在實(shí)際中有用處很多,很典型的例子就是通過(guò)dump文件進(jìn)行錯(cuò)誤查找和分析,這里的dump文件可能是自定義的dump格式。
好了,本文到此就告一段落,歡迎交流。
---如需轉(zhuǎn)載,請(qǐng)注明出處,謝謝支持----
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元

