最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

逆向基礎(chǔ)(一)

2023-08-05 17:25 作者:雪兒飛啊飛  | 我要投稿

第一章

CPU簡介

CPU就是執(zhí)行所有程序的工作單元。

詞匯表:

Instruction:CPU的原指令,例如:將數(shù)據(jù)在數(shù)據(jù)區(qū)與寄存器之間進行轉(zhuǎn)移操作,對數(shù)據(jù)進行操作,算術(shù)操作。原則上每種CPU會有自己獨特的一套指令構(gòu)架(Instruction Set Architecture(ISA))。

Machine code: CPU的指令碼(機器碼),每條指令都會被譯成指令碼。

Assembly Language: 匯編語言,助記碼和其他一些例如宏那樣的特性組成的便于程序員編寫的語言。

CPU register:CPU寄存器,每個CPU都有一些通用寄存器(General Purpose Registers(GPR))。X86有8個,x86-64(amd64)有16個,ARM有16個,最簡單去理解寄存器的方法就是,把寄存器想成一個不需要類型的臨時變量。想象你在用高級編程語言,并且只有8個32bit的變量。只用這些可以完成非常多的事情。

那么機器碼跟程序語言有什么區(qū)別那?對于人類來講,使用例如C/C++, Java, Python這樣編程語言會比較簡單,但是CPU更喜歡低級抽象的東西。但愿有一天CPU也能直接來執(zhí)行高級語言的語句,但那肯定會非常的復(fù)雜。相反人類使用匯編語言會感覺不很方便,因為它非常的低級。而且很難用它寫非常長的代碼并不出現(xiàn)錯誤。有一種將高級語言轉(zhuǎn)換到匯編語言的程序,它被叫做編譯器。

第二章

Hello,world!

讓我們用最著名的代碼例子開始吧:

#!cpp #include <stdio.h> int main() { ??? printf("hello, world"); ??? return 0; };

2.1 x86

2.1.1 MSVC-x86

在MSVC 2010中編譯一下:

#!bash ? ? cl 1.cpp /Fa1.asm

(/Fa 選項表示生產(chǎn)匯編列表文件)

#!bash CONST ? SEGMENT $SG3830 DB ? ? ?'hello, world', 00H CONST ? ENDS PUBLIC ?_main EXTRN ? _printf:PROC ; Function compile flags: /Odtp _TEXT ? SEGMENT _main ? PROC ? ? ? ?push ? ?ebp ? ? ? ?mov ? ? ebp, esp ? ? ? ?push ? ?OFFSET $SG3830 ? ? ? ?call ? ?_printf ? ? ? ?add ? ? esp, 4 ? ? ? ?xor ? ? eax, eax ? ? ? ?pop ? ? ebp ? ? ? ?ret ? ? 0 _main ? ENDP _TEXT?? ENDS

MSVC生成的是Intel匯編語法。Intel語法與AT&T語法的區(qū)別將在后面討論。

編譯器會把1.obj文件連接成1.exe。

在我們的例子當中,文件包含兩個部分:CONST(放數(shù)據(jù))和_TEXT(放代碼)。

字符串“hello,world”在 C/C++ 類型為const char*,然而他沒有自己的名稱。

編譯器需要處理這個字符串,就自己給他定義了一個$SG3830。

所以例子可以改寫為:

#!cpp #include <stdio.h> const char *$SG3830="hello, world"; int main() { ? ?printf($SG3830); ? ?return 0; };

我們回到匯編列表,正如我們看到的,字符串是由0字節(jié)結(jié)束的,這也是 C/C++的標準。

在代碼部分,_TEXT,只有一個函數(shù):main()。

函數(shù)main()與大多數(shù)函數(shù)一樣都有開始的代碼與結(jié)束的代碼。

函數(shù)當中的開始代碼結(jié)束以后,調(diào)用了printf()函數(shù):CALL _printf。

在PUSH指令的幫助下,我們問候語字符串的地址(或指向它的指針)在被調(diào)用之前存放在棧當中。

當printf()函數(shù)執(zhí)行完返回到main()函數(shù)的時候,字符串地址(或指向它的指針)仍然在堆棧中。

當我們都不再需要它的時候,堆棧指針(ESP寄存器)需要改變。

#!bash ADD ESP, 4?

意思是ESP寄存器加4。

為什么是4呢?由于是32位的代碼,通過棧傳送地址剛好需要4個字節(jié)。

在64位系統(tǒng)當中它是8字節(jié)。

ADD ESP, 4” 實際上等同于“POP register”。

一些編輯器(如Intel C++編譯器)在同樣的情況下可能會用 POP ECX代替ADD(例如這樣的模式可以在Oracle RDBMS代碼中看到,因為它是由Intel C++編譯器編譯的),這條指令的效果基本相同,但是ECX的寄存器內(nèi)容會被改寫。

Intel C++編譯器可能用POP ECX,因為這比ADD ESP, X需要的字節(jié)數(shù)更短,(1字節(jié)對應(yīng)3字節(jié))。

在調(diào)用printf()之后,在C/C++代碼之后執(zhí)行return 0,return 0是main()函數(shù)的返回結(jié)果。

代碼被編譯成指令?XOR EAX, EAX

XOR事實上就是異或,但是編譯器經(jīng)常用它來代替?MOV EAX, 0?原因就是它需要的字節(jié)更短(2字節(jié)對應(yīng)5字節(jié))。

有些編譯器用SUB EAX, EAX?就是EXA的值減去EAX,也就是返回0。

最后的指令RET 返回給調(diào)用者,他是C/C++代碼吧控制返還給操作系統(tǒng)。

2.1.2 GCC-x86

現(xiàn)在我們嘗試同樣的C/C++代碼在linux中的GCC 4.4.1編譯

#!bash gcc 1.c -o 1

下一步,在IDA反匯編的幫助下,我們看看main()函數(shù)是如何被創(chuàng)建的。

(IDA,與MSVC一樣,也是顯示Intel語法)。

我也可以是GCC生成Intel語法的匯編代碼,添加參數(shù)

#!bash -S -masm=intel

匯編代碼:

#!bash main??????????? proc near var_10????????? = dword ptr -10h ??????????????? push??? ebp ??????????????? mov???? ebp, esp ??????????????? and???? esp, 0FFFFFFF0h ??????????????? sub???? esp, 10h ??????????????? mov???? eax, offset aHelloWorld ; "hello, world" ??????????????? mov???? [esp+10h+var_10], eax ??????????????? call _printf ??????????????? mov eax, 0 ??????????????? leave ??????????????? retn main??????????? endp

結(jié)果幾乎是相同的,“hello,world”字符串地址(保存在data段的)一開始保存在EAX寄存器當中,然后保存到棧當中。

同樣的在函數(shù)開始我們看到了

AND ESP, 0FFFFFFF0h

這條指令該指令對齊在16字節(jié)邊界在ESP寄存器中的值。這導(dǎo)致堆棧對準的所有值。

SUB ESP,10H在棧上分配16個字節(jié)。 這里其實只需要4個字節(jié)。

這是因為,分配堆棧的大小也被排列在一個16字節(jié)的邊界。

該字符串的地址(或這個字符串指針),不使用PUSH指令,直接寫入到堆??臻g。 var_10,是一個局部變量,也是printf()的參數(shù)。

?然后調(diào)用printf()函數(shù)。

不像MSVC,當gcc編譯不開啟優(yōu)化,它使用MOV EAX,0清空EAX,而不是更短的代碼。

最后一條指令,LEAVE相當于MOV ESP,EBP和POP EBP兩條指令。

換句話說,這相當于指令將堆棧指針(ESP)恢復(fù),EBP寄存器到其初始狀態(tài)。

這是必須的,因為我們在函數(shù)的開頭修改了這些寄存器的值(ESP和EBP)(執(zhí)行MOV EBP,ESP/AND ESP...)。

2.1.3 GCC:AT&T 語法

我們來看一看在AT&T當中的匯編語法,這個語法在UNIX當中更普遍。

#!bash gcc -S 1_1.c

我們將得到這個:

#!bash .file?? "1_1.c" .section??? .rodata .LC0: ??????? .string "hello, world" ??????? .text ??????? .globl? main ??????? .type?? main, @function main: .LFB0: ??????? .cfi_startproc ??????? pushl?? %ebp ??????? .cfi_def_cfa_offset 8 ??????? .cfi_offset 5, -8 ??????? movl??? %esp, %ebp ??????? .cfi_def_cfa_register 5 ??????? andl??? $-16, %esp ??????? subl??? $16, %esp ??????? movl??? $.LC0, (%esp) ??????? call??? printf ??????? movl??? $0, %eax ??????? leave ??????? .cfi_restore 5 ??????? .cfi_def_cfa 4, 4 ??????? ret ??????? .cfi_endproc .LFE0: ??????? .size?? main, .-main ??????? .ident? "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3" ? ? ? ? .section??????? .note.GNU-stack,"",@progbits?

有很多的宏(用點開始)。現(xiàn)在為了簡單起見,我們先不看這些。(除了 .string ,就像一個C字符串編碼一個null結(jié)尾的字符序列)。然后,我們將看到這個:

#!bash .LC0: ? ? ? ?.string "hello, world" main: ? ? ? ?pushl ? %ebp ? ? ? ?movl ? ?%esp, %ebp ? ? ? ?andl ? ?$-16, %esp ? ? ? ?subl ? ?$16, %esp ? ? ? ?movl ? ?$.LC0, (%esp) ? ? ? ?call ? ?printf ? ? ? ?movl ? ?$0, %eax ? ? ? ?leave ? ? ? ?ret

在Intel與AT&T語法當中比較重要的區(qū)別就是:

操作數(shù)寫在后面

在Intel語法中:<instruction> <destination operand> <source operand> 在AT&T語法中:<instruction> <source operand> <destination operand>

有一個理解它們的方法: 當你面對intel語法的時候,你可以想象把等號放到2個操作數(shù)中間,當面對AT&T語法的時候,你可以放一個右箭頭(→)到兩個操作數(shù)之間。

AT&T: 在寄存器名之前需要寫一個百分號(%)并且在數(shù)字前面需要美元符($)。方括號被圓括號替代。 AT&T: 一些用來表示數(shù)據(jù)形式的特殊的符號

l ? ? ?long(32 bits) w ? ? ?word(16bits) b ? ? ?byte(8 bits)

讓我們回到上面的編譯結(jié)果:它和在IDA里看到的是一樣的。只有一點不同:0FFFFFFF0h 被寫成了$-16,但這是一樣的,10進制的16在16進制里表示為0x10。-0x10就等同于0xFFFFFFF0(這是針對于32位構(gòu)架)。

外加返回值這里用的MOV來設(shè)定為0,而不是用XOR。MOV僅僅是加載(load)了變量到寄存器。指令的名稱并不直觀。在其他的構(gòu)架上,這條指令會被稱作例如”load”這樣的。

2.2 x86-64

2.2.1 MSVC-x86-64

讓我們來試試64-bit的MSVC:

#!bash $SG2989 DB ? ? ?’hello, world’, 00H main ? ?PROC ? ? ? ?sub ? ? rsp, 40 ? ? ? ?lea ? ? rcx, OFFSET FLAT:$SG2923 ? ? ? ?call ? ?printf ? ? ? ?xor ? ? eax, eax ? ? ? ?add ? ? rsp, 40 ? ? ? ?ret ? ? 0 main ENDP

在x86-64里,所有被擴展到64位的寄存器都有R-前綴。并且盡量不用棧來傳遞函數(shù)的參數(shù)了,大量使用寄存器來傳遞參數(shù),非常類似于fastcall。

在win64里,RCX,RDX,R8,R9寄存器被用來傳遞函數(shù)參數(shù),如果還有更多就使用棧,在這里我們可以看到printf()函數(shù)的參數(shù)沒用通過棧來傳遞,而是使用了rcx。 讓我們針對64位來看,作為64位寄存器會有R-前綴,并且這些寄存器向下兼容,32位的部分使用E-前綴。

如下圖所示,這是RAX/EAX/AX/AL在64位x86兼容cpu里的情況 ??

在main()函數(shù)會返回一個int類型的值,在64位的程序里為了兼容和移植性,還是用32位的,所以可以看到EAX(寄存器的低32位部分)在函數(shù)最后替代RAX被清空成0。

2.2.2 GCC-x86-64

這次試試GCC在64位的Linux里:

#!bash ? ? ? ?.string "hello, world" main: ? ? ? ?sub ? ? rsp, 8 ? ? ? ?mov ? ? edi, OFFSET FLAT:.LC0 ; "hello, world" ? ? ? ?xor ? ? eax, eax ?; number of vector registers passed ? ? ? ?call ? ?printf ? ? ? ?xor ? ? eax, eax ? ? ? ?add ? ? rsp, 8 ? ? ? ?ret

在Linux,*BSD和Mac OS X里使用同一種方式來傳遞函數(shù)參數(shù)。頭6個參數(shù)使用RDI,RSI,RDX,RCX,R8,R9來傳遞的,剩下的要靠棧。

所以在這個程序里,字串的指針被放到EDI(RDI的低32位部)。為什么不是64位寄存器RDI那?

這是一個重點,在64位模式下,對低32位進行操作的時候,會清空高32位的內(nèi)容。比如 MOV EAX,011223344h將會把值寫到RAX里,并且清空RAX的高32位區(qū)域。 如果我們打開編譯好的對象文件(object file(.o)),我們會看到所有的指令:

Listing 2.8:GCC 4.4.6 x64

#!bash .text:00000000004004D0 ? ? ? ? ? ? ? ? ?main proc near .text:00000000004004D0 48 83 EC 08 ? ? ? ? ? sub rsp, 8 .text:00000000004004D4 BF E8 05 40 00 ? ? ? ?mov edi, offset format ; "hello, world" .text:00000000004004D9 31 C0 ? ? ? ? ? ? ? ? xor eax, eax .text:00000000004004DB E8 D8 FE FF FF ? ? ? ?call _printf .text:00000000004004E0 31 C0 ? ? ? ? ? ? ? ? xor eax, eax .text:00000000004004E2 48 83 C4 08 ? ? ? ? ? add rsp, 8 .text:00000000004004E6 C3 ? ? ? ? ? ? ? ? ? ?retn .text:00000000004004E6 ? ? ? ? ? ? ? ? ?main endp

就像看到的那樣,在04004d4那行給edi寫字串指針的那句花了5個bytes。如果把這句換做給rdi寫指針,會花掉7個bytes.就是說GCC在試圖節(jié)省空間,為此數(shù)據(jù)段(data segment)中包含的字串不會被分配到高于4GB地址的空間上。

可以看到在printf()函數(shù)調(diào)用前eax被清空了,這樣做事因為要eax被用作傳遞向量寄存器(vector registers)的個數(shù)。

參考【21】 MichaelMatz/JanHubicka/AndreasJaeger/MarkMitchell. Systemvapplicationbinaryinterface.amdarchitecture processor supplement, . Also available as http://x86-64.org/documentation/abi.pdf.

2.3 ARM

根據(jù)作者自身對ARM處理器的經(jīng)驗,選擇了2款在嵌入式開發(fā)流行的編譯器,Keil Release 6/2013和蘋果的Xcode 4.6.3 IDE(其中使用了LLVM-GCC4.2編譯器),這些可以為ARM兼容處理器和系統(tǒng)芯片(System on Chip)(SOC))來進行編碼。比如ipod/iphone/ipad,windows8 rt,并且包括raspberry pi。

2.3.1 未進行代碼優(yōu)化的Keil編譯:ARM模式

讓我們在Keil里編譯我們的例子

#!bash armcc.exe –arm –c90 –O0 1.c

armcc編譯器可以生成intel語法的匯編程序列表,但是里面有高級的ARM處理器相關(guān)的宏,對我們來講更希望看到的是IDA反匯編之后的結(jié)果。

Listing 2.9: Non-optimizing Keil + ARM mode + IDA #!bash .text:00000000 ? ? ? ? ? ? ? ? ?main .text:00000000 10 40 2D E9 ? ? ? ? ? ? ?STMFD SP!, {R4,LR} .text:00000004 1E 0E 8F E2 ? ? ? ? ? ? ?ADR R0, aHelloWorld ; "hello, world" .text:00000008 15 19 00 EB ? ? ? ? ? ? ?BL __2printf .text:0000000C 00 00 A0 E3 ? ? ? ? ? ? ?MOV R0, #0 .text:00000010 10 80 BD E8 ? ? ? ? ? ? ?LDMFD SP!, {R4,PC} .text:000001EC 68 65 6C 6C+aHelloWorld ?DCB "hello, world",0 ; DATA XREF: main+4

針對ARM處理器,我們需要預(yù)備一點知識,要知道ARM處理器至少有2種模式:ARM模式和thumb模式,在ARM模式下,所有的指令都被激活并且都是32位的。在thumb模式下所有的指令都是16位的。Thumb模式比較需要注意,因為程序可能需要更為緊湊,或者當微處理器用的是16位內(nèi)存地址時會執(zhí)行的更快。但也存在缺陷,在thumb模式下可用的指令沒ARM下多,只有8個寄存器可以訪問,有時候ARM模式下一條指令就能解決的問題,thumb模式下需要多個指令來完成。

從ARMv7開始引入了thumb-2指令集。這是一個加強的thumb模式。擁有了更多的指令,通常會有誤解,感覺thumb-2是ARM和thumb的混合。Thumb-2加強了處理器的特性,并且媲美ARM模式。程序可能會混合使用2種模式。其中大量的ipod/iphone/ipad程序會使用thumb-2是因為Xcode將其作為了默認模式。

在例子中,我們可以發(fā)現(xiàn)所有指令都是4bytes的,因為我們編譯的時候選擇了ARM模式,而不是thumb模式。

最開始的指令是”STMFD SP!, {R4, LR}”,這條指令類似x86平臺的PUSH指令,會寫2個寄存器(R4和LR)的變量到棧里。不過在armcc編譯器里輸出的匯編列表里會寫成”PUSH {R4, LR}”,但這并不準確,因為PUSH命令只在thumb模式下有,所以我建議大家注意用IDA來做反匯編工具。

這指令開始會減少SP的值,已加大??臻g,并且將R4和LR寫入分配好的棧里。

這條指令(類似于PUSH的STMFD)允許一次壓入好幾個值,非常實用。有一點跟x86上的PUSH不同的地方也很贊,就是這條指令不像x86的PUSH只能對sp操作,而是可以指定操作任意的寄存器。

“ADR R0, aHelloWorld”這條指令將PC寄存器的值與”hello, world”字串的地址偏移相加放入R0,為什么說要PC參與這個操作那?這是因為代碼是PIC(position-independet code)的,這段代碼可以獨立在內(nèi)存運行,而不需要更改內(nèi)存地址。ADR這條指令中,指令中字串地址和字串被放置的位置是不同的。但變化是相對的,這要看系統(tǒng)是如何安排字串放置的位置了。這也就說明了,為何每次獲取內(nèi)存中字串的絕對地址,都要把這個指令里的地址加上PC寄存器里的值了。

BL __2print”這條指令用于調(diào)用printf()函數(shù),這是來說下這條指令時如何工作的:

將BL指令(0xC)后面的地址寫入LR寄存器; 然后把printf()函數(shù)的入口地址寫入PC寄存器,進入printf()函數(shù)。

當printf()函數(shù)完成之后,函數(shù)會通過LR寄存器保存的地址,來進行返回操作。

函數(shù)返回地址的存放位置也正是“純”RISC處理器(例如:ARM)和CISC處理器(例如x86)的區(qū)別。

另外,一個32位地址或者偏移不能被編碼到BL指令里,因為BL指令只有24bits來存放地址,所有的ARM模式下的指令都是4bytes(32bits),所以一條指令里不能放滿4bytes的地址,這也就意味著最后2bits總會被設(shè)置成0,總的來說也就是有26bits的偏移(包括了最后2個bit一直被設(shè)為0)會被編碼進去。這也夠去訪問大約±32M的了。

下面我們來看“MOV R0, #0“這條語句,這條語句就是把0寫到了R0寄存器里,這是因為C函數(shù)返回了0,返回值當然是放在R0里的。

最后一條指令是”LDMFD SP!, R4,PC“,這條指令的作用跟開始的那條STMFD正好相反,這條指令將棧上的值保存到R4和PC寄存器里,并且增加SP棧寄存器的值。這非常類似x86平臺里的POP指令。最前面那條STMFD指令成對保存了R4,和LR寄存器,LDMFD的時候?qū)敃r這兩個值保存到了R4和PC里完成了函數(shù)的返回。

我前面也說過,函數(shù)的返回地址會保存到LD寄存器里。在函數(shù)的最開始會把他保存到棧里,這是因為main()函數(shù)里還需要調(diào)用printf()函數(shù),這個時候就會影響LD寄存器。在函數(shù)的最后就會將LD拿出棧放入PC寄存器里,完成函數(shù)的返回操作。最后C/C++程序的main()函數(shù)會返回到類似系統(tǒng)加載器上或者CRT里面。

匯編代碼里的DCB關(guān)鍵字用來定義ASCII字串數(shù)組,就像x86匯編里的DB關(guān)鍵字。

2.3.2未進行代碼優(yōu)化的Keil編譯: thumb模式

讓我們用下面的指令講例程用Keil的thumb模式來編譯一下。

#!bash armcc.exe –thumb –c90 –O0 1.c

我們可以在IDA里得到下面這樣的代碼: Listing 2.10:Non-optimizing Keil + thumb mode + IDA

#!bash .text:00000000 ? ? ? ? ?main .text:00000000 10 B5 ? ? ? ? ? ? ? ? ? ?PUSH {R4,LR} .text:00000002 C0 A0 ? ? ? ? ? ? ? ? ? ?ADR R0, aHelloWorld ; "hello, world" .text:00000004 06 F0 2E F9 ? ? ? ? ? ? ?BL __2printf .text:00000008 00 20 ? ? ? ? ? ? ? ? ? ?MOVS R0, #0 .text:0000000A 10 BD ? ? ? ? ? ? ? ? ? ?POP {R4,PC} .text:00000304 68 65 6C 6C+aHelloWorld ?DCB "hello, world",0 ; DATA XREF: main+2

我們首先就能注意到指令都是2bytes(16bits)的了,這正是thumb模式的特征,BL指令作為特例是2個16bits來構(gòu)成的。只用16bits沒可能加載printf()函數(shù)的入口地址到PC寄存器。所以前面的16bits用來加載函數(shù)偏移的高10bits位,后面的16bits用來加載函數(shù)偏移的低11bits位,正如我說過的,所有的thumb模式下的指令都是2bytes(16bits)。但是這樣的話thumb指令就沒法使用更大的地址。就像上面那樣,最后一個bits的地址將會在編碼指令的時候省略??偟膩碇v,BL在thumb模式下可以訪問自身地址大于±2M大的周邊的地址。

至于其他指令:PUSH和POP,它們跟上面講到的STMFD跟LDMFD很類似,但這里不需要指定SP寄存器,ADR指令也跟上面的工作方式相同。MOVS指令將函數(shù)的返回值0寫到了R0里,最后函數(shù)返回。

2.3.3開啟代碼優(yōu)化的Xcode(LLVM)編譯: ARM模式

Xcode 4.6.3不開啟代碼優(yōu)化的情況下,會產(chǎn)生非常多冗余的代碼,所以我們學(xué)習(xí)一個盡量小的版本。

開啟-O3編譯選項

#!bash Listing2.11:Optimizing Xcode(LLVM)+ARM mode __text:000028C4 ? ? ? ? _hello_world __text:000028C4 80 40 2D E9 ? ? ? ? ? ? ? ? ? ? STMFD ? SP!, {R7,LR} __text:000028C8 86 06 01 E3 ? ? ? ? ? ? ? ? ? ? MOV ? ? R0, #0x1686 __text:000028CC 0D 70 A0 E1 ? ? ? ? ? ? ? ? ? ? MOV ? ? R7, SP __text:000028D0 00 00 40 E3 ? ? ? ? ? ? ? ? ? ? MOVT ? ?R0, #0 __text:000028D4 00 00 8F E0 ? ? ? ? ? ? ? ? ? ? ADD ? ? R0, PC, R0 __text:000028D8 C3 05 00 EB ? ? ? ? ? ? ? ? ? ? BL ? ? ?_puts __text:000028DC 00 00 A0 E3 ? ? ? ? ? ? ? ? ? ? MOV ? ? R0, #0 __text:000028E0 80 80 BD E8 ? ? ? ? ? ? ? ? ? ? LDMFD ? SP!, {R7,PC} __cstring:00003F62 48 65 6C 6C+aHelloWorld_0 ? ?DCB "Hello world!",0

STMFD和LDMFD對我們來說已經(jīng)非常熟悉了。

MOV指令就是將0x1686寫入R0寄存器里。這個值也正是字串”Hello world!”的指針偏移。

R7寄存器里放入了棧地址,我們繼續(xù)。

MOVT R0, #0指令時將R0的高16bits寫入0。這是因為普通情況下MOV這條指令在ARM模式下,只對低16bits進行操作。需要記住的是所有在ARM模式下的指令都被限定在32bits內(nèi)。當然這個限制并不影響2個寄存器直接的操作。這也是MOVT這種寫高16bits指令存在的意義。其實這樣寫的代碼會感覺有點多余,因為”MOVS R0,#0x1686”這條指令也能把高16位清0?;蛟S這就是相對于人腦來說編譯器的不足。

ADD R0,PC,R0“指令把R0寄存器的值與PC寄存器的值進行相加并且保存到R0寄存器里面,用來計算”Hello world!”這個字串的絕對地址。上面已經(jīng)介紹過了,這是因為代碼是PIC(Position-independent code)的,所以這里需要這么做。

BL指令用來調(diào)用printf()的替代函數(shù)puts()函數(shù)。

GCC將printf()函數(shù)替換成了puts()。因為printf()函數(shù)只有一個參數(shù)的時候跟puts()函數(shù)是類似的。

printf()函數(shù)的字串參數(shù)里存在特殊控制符(例如 ”%s”,”\n” ,需要注意的是,程序里字串里沒有“\n”,因為在puts()函數(shù)里這是不需要的)的時候,兩個函數(shù)的功效就會不同。

為什么編譯器會替換printf()到puts()那?因為puts()函數(shù)更快。

puts()函數(shù)效率更快是因為它只是做了字串的標準輸出(stdout)并不用比較%符號。

下面,我們可以看到非常熟悉的”MOV R0, #0”指令,用來將R0寄存器設(shè)為0。

2.3.4 開啟代碼優(yōu)化的Xcode(LLVM)編譯thumb-2模式

在默認情況下,Xcode4.6.3會生成如下的thumb-2代碼

Listing 2.12:Optimizing Xcode(LLVM)+thumb-2 mode

#!bash __text:00002B6C ? ? ? ? _hello_world __text:00002B6C 80 B5 ? ? ? ? ? PUSH ? ?{R7,LR} __text:00002B6E 41 F2 D8 30 ? ? MOVW ? ?R0, #0x13D8 __text:00002B72 6F 46 ? ? ? ? ? MOV ? ? R7, SP __text:00002B74 C0 F2 00 00 ? ? MOVT.W ?R0, #0 __text:00002B78 78 44 ? ? ? ? ? ADD ? ? R0, PC __text:00002B7A 01 F0 38 EA ? ? BLX ? ? _puts __text:00002B7E 00 20 ? ? ? ? ? MOVS ? ?R0, #0 __text:00002B80 80 BD ? ? ? ? ? POP ? ? {R7,PC} ... __cstring:00003E70 48 65 6C 6C 6F 20+aHelloWorld DCB "Hello world!",0xA,0

BL和BLX指令在thumb模式下情況需要我們回憶下剛才講過的,它是由一對16-bit的指令來構(gòu)成的。在thumb-2模式下這條指令跟thumb一樣被編碼成了32-bit指令。非常容易觀察到的是,thumb-2的指令的機器碼也是從0xFx或者0xEx的。對于thumb和thumb-2模式來說,在IDA的結(jié)果里機器碼的位置和這里是交替交換的。對于ARM模式來說4個byte也是反向的,這是因為他們用了不同的字節(jié)序。所以我們可以知道,MOVW,MOVT.W和BLX這幾個指令的開始都是0xFx。

在thumb-2指令里有一條是”MOVW R0, #0x13D8”,它的作用是寫數(shù)據(jù)到R0的低16位里面。

MOVT.W R0, #0”的作用類似與前面講到的MOVT指令,但它可以工作在thumb-2模式下。

還有些跟上面不同的地方,比如BLX指令替代了上面用到的BL指令,這條指令不僅將控制puts()函數(shù)返回的地址放入了LR寄存器里,并且講代碼從thumb模式轉(zhuǎn)換到了ARM模式(或者ARM轉(zhuǎn)換到thumb(根據(jù)現(xiàn)有情況判斷))。這條指令跳轉(zhuǎn)到下面這樣的位置(下面的代碼是ARM編碼模式)。

#!bash __symbolstub1:00003FEC _puts ? ? ? ? ? ? ; CODE XREF: _hello_world+E __symbolstub1:00003FEC 44 F0 9F E5 ? ? ? LDR PC, =__imp__puts

可能會有細心的讀者要問了:為什么不直接跳轉(zhuǎn)到puts()函數(shù)里面去?

因為那樣做會浪費內(nèi)存空間。

很多程序都會使用額外的動態(tài)庫(dynamic libraries)(Windows里面的DLL,還有*NIX里面的.so,MAC OS X里面的.dylib),通常使用的庫函數(shù)會被放入動態(tài)庫中,當然也包括標準C函數(shù)puts()。

在可執(zhí)行的二進制文件里(Windows的PE里的.exe文件,ELF和Mach-O文件)都會有輸入表段。它是一個用來引入額外模塊里模塊名稱和符號(函數(shù)或者全局變量)的列表。

系統(tǒng)加載器(OS loader)會加載所有需要的模塊,當在主模塊里枚舉輸入符號的時候,會把每個符號正確的地址與相應(yīng)的符號確立起來。

在我們的這個例子里,__imp__puts就是一個系統(tǒng)加載器加載額外模塊的32位的地址值。LDR指令只需要把這個值加載到PC里面去,就可以控制程序流程到puts()函數(shù)里去。

所以只需要在系統(tǒng)加載器里的時候,一次性的就能將每個符號所對應(yīng)的地址確定下來,這是個提高效率的好方式。

外加,我們前面也指出過,我們沒辦法只用一條指令并且不做內(nèi)存操作的情況下就將一個32bit的值保存到寄存器里,ARM并不是唯一的模式的情況下,程序里去跳入動態(tài)庫中的某個函數(shù)里,最好的辦法就是這樣做一些類似與上面這樣單一指令的函數(shù)(稱做thunk function),然后從thumb模式里也能去調(diào)用。

在上面的例子(ARM編譯的那個例子)中BL指令也是跳轉(zhuǎn)到了同一個thunk function里。盡管沒有進行模式的轉(zhuǎn)變(所以指令里不存在那個”X”)。

第三章? 函數(shù)開始和結(jié)束

函數(shù)開始的指令,是像下面這樣的代碼片段:

#!bash push ? ?ebp mov ? ? ebp, esp sub ? ? esp, X

這些指令做了什么:將寄存器EBP的值入棧,將ESP賦值給EBP,在棧中分配空間, 用來保存局部變量。

在函數(shù)執(zhí)行過程中,EBP是固定的,可以用來訪問局部變量和函數(shù)參數(shù)。也可以使用 ESP,但在函數(shù)運行過程中,ESP會變化,使用起來不方便。

#!bash mov ? ?esp, ebp pop ? ?ebp ret ? ?0

函數(shù)在運行結(jié)束時,會釋放在棧中所申請的內(nèi)存,EBP的值出棧,將代碼控制權(quán)還原 給調(diào)用者。

3.1 遞歸

函數(shù)調(diào)用開始和結(jié)束使遞歸變得難以理解。

舉個例子,有一次我寫了一個函數(shù)遍歷二叉樹右側(cè)節(jié)點。使用了看起來很高?上的遞歸函數(shù),但由于每次函數(shù)調(diào)用開始和結(jié)束都需要花費很長時間,它運行速度比迭代方 式要慢好多倍。

順便提一下,這就是尾部調(diào)用存在的原因。

第四章 棧

棧在計算科學(xué)中是最重要和最基本的數(shù)據(jù)結(jié)構(gòu)。

嚴格的來說,它只是在x86中被ESP,或x64中被RSP,或ARM中被SP所指向的一段程序內(nèi)存區(qū)域。 ?? ?訪問棧內(nèi)存,最常使用的指令是PUSH和POP(在x86和ARM Thumb模式中)。

PUSH指令在32位模式下,會將ESP/RSP/SP的值減去4(在64位系統(tǒng)中,會減去8),然后將操作數(shù)寫入到ESP/RSP/SP指向的內(nèi)存地址。

POP是相反的操作運算:從SP指向的內(nèi)存地址中獲取數(shù)據(jù),存入操作數(shù)(一般為寄存器), 然后將SP(棧指針)加4(或8)。

4.1 為什么棧反向增長?

按正常思維來說,我們會認為像其它數(shù)據(jù)結(jié)構(gòu)一樣,棧是正向增長的,比如:棧指針會指向高地址。

我們知道:

????? ?戶核心部分的映像文件被合理的分為三個部分,程度代碼段在內(nèi)存空閑部分運行。 在運行過程中,這部分是具有寫保護的,所有進程都可以共享訪問這個程序。在內(nèi)存空間 中,程序text區(qū)段開始的8k字節(jié)是不能共享的可寫區(qū)段,這個?大?小可以使?用系統(tǒng)函數(shù)來擴 ?大。在內(nèi)存?高位地址是可以像硬件指針?自由活動向下增長的棧區(qū)段。 ?????

4.2 ??梢杂脕碜鍪裁?

4.2.1 保存函數(shù)返回地址以便在函數(shù)執(zhí)行完成時返回控制權(quán)

x86

當使用CALL指令去調(diào)用一個函數(shù)時,CALL后面一條指令的地址會被保存到棧中,使用無條件跳轉(zhuǎn)指令跳轉(zhuǎn)到CALL中執(zhí)行。 ? ?CALL指令等價于PUSH函數(shù)返回地址和JMP跳轉(zhuǎn)。

#!cpp void f() { ? ?f(); };

?MSVC 2008顯示的問題:

#!bash c:\tmp6>cl ss.cpp /Fass.asm Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86 Copyright (C) Microsoft Corporation. ?All rights reserved. ss.cpp c:\tmp6\ss.cpp(4) : warning C4717: ’f’ : recursive on all control paths, function will cause ? ?runtime stack overflow

但無論如何還是生成了正確的代碼:

#!bash ?f@@YAXXZ PROC ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; f ; File c:\tmp6\ss.cpp ; Line 2 ? ? ? ?push ebp ? ? ? ?mov ? ? ebp, esp ; Line 3 ? ? ? ?call ? ??f@@YAXXZ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ; f ; Line 4 ? ? ? ?pop ebp ? ? ? ?ret ? ? 0 ?f@@YAXXZ ENDP ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; f

??如果我們設(shè)置優(yōu)化(/0x)標識,生成的代碼將不會出現(xiàn)棧溢出,并且會運行的很好。

#!bash ?f@@YAXXZ PROC ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; f ; File c:\tmp6\ss.cpp ; Line 2 $LL3@f: ; Line 3 ? ? ? ?jmp ? ? SHORT $LL3@f ?f@@YAXXZ ENDP ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?; f

GCC 4.4.1 在這兩種條件下,會生成同樣的代碼,而且不會有任何警告。

ARM

ARM程序員經(jīng)常使用棧來保存返回地址,但有些不同。像是提到的“Hello,World!(2.3), RA保存在LR(鏈接寄存器)。然而,如果需要調(diào)用另外一個函數(shù),需要多次使用LR寄存器,它的值會被保存起來。通常會在函數(shù)開始的時候保存。像我們經(jīng)常看到的指令“PUSH R4-R7, LR”,在函數(shù)結(jié)尾處的指令“POP R4-R7, PC”,在函數(shù)中使?用到的寄存器會被保存到棧中,包括LR。

盡管如此,如果一個函數(shù)從未調(diào)用其它函數(shù),在ARM專用術(shù)語中被叫稱作葉子函數(shù)。因此,葉?函數(shù)不需要LR寄存器。如果一個函數(shù)很小并使用了少量的寄存器,可能不會?到棧。因此,是可以不使用棧而實現(xiàn)調(diào)用葉子函數(shù)的。在擴展ARM上不使用棧,這樣就會比在x86上運行要快。在未分配棧內(nèi)存或棧內(nèi)存不可用的情況下,這種方式是非常有用的。

4.2.2 傳遞函數(shù)參數(shù)

在x86中,最常見的傳參方式是“cdecl”:

#!bash push arg3 push arg2 push arg1 call f add esp, 4*3

被調(diào)用函數(shù)通過棧指針訪問函數(shù)參數(shù)。因此,這就是為什么要在函數(shù)f()執(zhí)行之前將數(shù)據(jù)放入棧中的原因。

來看一下其它調(diào)用約定。沒有意義也沒有必要強迫程序員一定要使用棧來傳遞參數(shù)。

這不是必需的,可以不使用棧,通過其它方式來實現(xiàn)。

例如,可以為參數(shù)分配一部分堆空間,存入?yún)?shù),將指向這部分內(nèi)存的指針存入EAX,這樣就可以了。然而,在x86和ARM中,使用棧傳遞參數(shù)還是更加方便的。

另外一個問題,被調(diào)用的函數(shù)并不知道有多少參數(shù)被傳遞進來。有些函數(shù)可以傳遞不同個數(shù)的參數(shù)(如:printf()),通過一些說明性的字符(以%開始)才可以判斷。如果我們這樣調(diào)用函數(shù)

#!cpp printf("%d %d %d", 1234);

printf()會傳?入1234,然后另外傳入棧中的兩個隨機數(shù)字。這就讓我們使用哪種方式調(diào)用 main()函數(shù)變得不重要,像main(),main(int argc, char *argv[])main(int argc, char *argv[], char *envp[])。

事實上,CRT函數(shù)在調(diào)?main()函數(shù)時,使用了下面的指令: ???? #!bash push envp push argv push argc call main ...

?如果你使用了沒有參數(shù)的main()函數(shù),盡管如此,但它們?nèi)匀辉跅V?只是無法使用。如果你使用了main(int argc, char *argv[]),你可以使用兩個參數(shù),第三個參數(shù)對你的函數(shù)是“不可見的”。如果你使用main(int argc)這種方式,同樣是可以正常運?的。

4.2.3 局部變量存放

局部變量存放到任何你想存放的地方,但傳統(tǒng)上來說,大家更喜歡通過將棧指針移動到棧底,來存放局部變量,當然,這不是必需的。

4.2.4 x86: alloca() 函數(shù)

對alloca()函數(shù)并沒有值得學(xué)習(xí)的。

該函數(shù)的作用像malloc()一樣,但只會在棧上分配內(nèi)存。

它分配的內(nèi)存并不需要在函數(shù)結(jié)束時,調(diào)用像free()這樣的函數(shù)來釋放,當函數(shù)運行結(jié)束,ESP的值還原時,這部分內(nèi)存會自動釋放。對alloca()函數(shù)的實現(xiàn)也沒有什么值得介紹的。

這個函數(shù),如果精簡一下,就是將ESP指針指向棧底,根據(jù)你所需要的內(nèi)存大小將ESP指向所分配的內(nèi)存塊。讓我們試一下:

#!cpp #include <malloc.h> #include <stdio.h> void f() { ? ?char *buf=(char*)alloca (600); ? ?_snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); ? ?puts (buf); };

(_snprintf()函數(shù)作用與printf()函數(shù)基本相同,不同的地方是printf()會將結(jié)果輸出到的標準輸出stdout,?_snprintf()會將結(jié)果保存到內(nèi)存中,后面兩?代碼可以使用printf()替換,但我想說明小內(nèi)存的使用習(xí)慣。)

MSVC

讓我們來編譯 (MSVC 2010):

#!bash ... ??????? ? ? ? ?mov ? ?eax, 600 ? ? ? ? ; 00000258H ? ? ? ?call ? __alloca_probe_16 ? ? ? ?mov ? ?esi, esp ??????? ? ? ? ?push ? 3 ? ? ? ?push ? 2 ? ? ? ?push ? 1 ? ? ? ?push ? OFFSET $SG2672 ? ? ? ?push ? 600 ? ? ? ? ? ? ?; 00000258H ? ? ? ?push ? esi ? ? ? ?call ? __snprintf ??????? ? ? ? ?push ? esi ? ? ? ?call ? _puts ? ? ? ?add ? ?esp, 28 ? ? ? ? ?; 0000001cH ...

? ??這唯一的函數(shù)參數(shù)是通過EAX(未使用棧)傳遞。在函數(shù)調(diào)用結(jié)束時,ESP會指向 600字節(jié)的內(nèi)存,我們可以像使用一般內(nèi)存一樣來使用它做為緩沖區(qū)。

GCC + Intel格式

GCC 4.4.1不需要調(diào)用函數(shù)就可以實現(xiàn)相同的功能:

#!bash .LC0: ? ? ? ? ? .string "hi! %d, %d, %d\n" f: ? ? ? ? ? push ? ?ebp ? ? ? ? ? mov ? ? ebp, esp ? ? ? ? ? push ? ?ebx ? ? ? ? ? sub ? ? esp, 660 ? ? ? ? ? lea ? ? ebx, [esp+39] ? ? ? ? ? and ? ? ebx, -16 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ; align pointer by 16-bit border ? ? ? ? ? mov ? ? DWORD PTR [esp], ebx ? ? ? ? ? ? ? ? ; s ? ? ? ? ? mov ? ? DWORD PTR [esp+20], 3 ? ? ? ? ? mov ? ? DWORD PTR [esp+16], 2 ? ? ? ? ? mov ? ? DWORD PTR [esp+12], 1 ? ? ? ? ? mov ? ? DWORD PTR [esp+8], OFFSET FLAT:.LC0 ?; "hi! %d, %d, %d\n" ? ? ? ? ? mov ? ? DWORD PTR [esp+4], 600 ? ? ? ? ? ? ? ; maxlen ? ? ? ? ? call ? ?_snprintf ? ? ? ? ? mov ? ? DWORD PTR [esp], ebx ? ? ? ? ? call ? ?puts ? ? ? ? ? mov ? ? ebx, DWORD PTR [ebp-4] ? ? ? ? ? leave ? ? ? ? ? ret

?####GCC + AT&T 格式

我們來看相同的代碼,但使用了AT&T格式:

#!bash .LC0: ? ? ? ?.string "hi! %d, %d, %d\n" f: ? ? ? ?pushl %ebp ? ? ? ?movl ? ?%esp, %ebp ? ? ? ?pushl ? %ebx ? ? ? ?subl ? ?$660, %esp ? ? ? ?leal ? ?39(%esp), %ebx ? ? ? ?andl ? ?$-16, %ebx ? ? ? ?movl ? ?%ebx, (%esp) ? ? ? ?movl ? ?$3, 20(%esp) ? ? ? ?movl ? ?$2, 16(%esp) ? ? ? ?movl ? ?$1, 12(%esp) ? ? ? ?movl ? ?$.LC0, 8(%esp) ? ? ? ?movl ? ?$600, 4(%esp) ? ? ? ?call ? ?_snprintf ? ? ? ?movl ? ?%ebx, (%esp) ? ? ? ?call ? ?puts ? ? ? ?movl ? ?-4(%ebp), %ebx ? ? ? ?leave ? ? ? ?ret

?代碼與上面的那個圖是相同的。

例如:movl $3, 20(%esp)mov DWORD PTR [esp + 20],3是等價的,Intel的內(nèi)存地址增加是使用register+offset,而AT&T使用的是offset(%register)。

4.2.5 (Windows) 結(jié)構(gòu)化異常處理

SEH也是存放在棧中的(如果存在的話)。 想了解更多,請等待后續(xù)翻譯在(51.3)。

4.2.6 緩沖區(qū)溢出保護

想了解更多,請等待后續(xù)翻譯,在(16.2)。

4.3 典型的內(nèi)存布局

在32位系統(tǒng)中,函數(shù)開始時,棧的布局:


逆向基礎(chǔ)(一)的評論 (共 條)

分享到微博請遵守國家法律
齐齐哈尔市| 宜丰县| 博爱县| 大田县| 吉安市| 利辛县| 建宁县| 冕宁县| 莆田市| 夏邑县| 巫山县| 秦安县| 梁平县| 洞口县| 龙门县| 江口县| 玉门市| 永顺县| 开封市| 长乐市| 孝感市| 广德县| 华容县| 进贤县| 沛县| 朝阳区| 中超| 云南省| 开鲁县| 临夏市| 绥宁县| 郯城县| 山阴县| 陆良县| 图片| 仁怀市| 阿拉尔市| 厦门市| 万荣县| 阿坝县| 剑川县|