版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
ARM匯編與逆向工程目錄TOC\h\h第一部分Arm匯編內(nèi)部機(jī)制\h第1章逆向工程簡介\h第2章ELF文件格式的內(nèi)部結(jié)構(gòu)\h第3章操作系統(tǒng)基本原理\h第4章Arm架構(gòu)\h第5章數(shù)據(jù)處理指令\h第6章內(nèi)存訪問指令\h第7章條件執(zhí)行\(zhòng)h第8章控制流\h第二部分逆向工程\h第9章Arm環(huán)境\h第10章靜態(tài)分析\h第11章動態(tài)分析\h第12章逆向arm64架構(gòu)的macOS惡意軟件第一部分
Arm匯編內(nèi)部機(jī)制如果你剛從書架上拿起這本書,那么你可能對學(xué)習(xí)如何對已編譯的Arm二進(jìn)制文件進(jìn)行逆向工程處理感興趣,因?yàn)橹饕募夹g(shù)供應(yīng)商現(xiàn)在正在擁抱Arm架構(gòu)。也許你是一位經(jīng)驗(yàn)豐富的x86-64逆向工程師,但想要保持領(lǐng)先并更多地了解正在開始占據(jù)處理器市場的架構(gòu);也許你準(zhǔn)備進(jìn)行安全分析,以查找基于Arm的軟件中的漏洞或分析基于Arm的惡意軟件;也許你剛開始進(jìn)行逆向工程,并且已經(jīng)達(dá)到需要了解更深入的細(xì)節(jié)才能實(shí)現(xiàn)目標(biāo)的階段。無論你在進(jìn)入基于Arm的逆向工程領(lǐng)域的旅程中處于什么位置,本書都是為了讓你為理解Arm二進(jìn)制文件的語言做好準(zhǔn)備,告訴你如何分析它們,更重要的是,讓你為使用未來的Arm架構(gòu)的設(shè)備做好準(zhǔn)備。學(xué)習(xí)匯編語言和如何分析編譯后的軟件在各種應(yīng)用中都很有用。就像學(xué)習(xí)每一種新的技能一樣,學(xué)習(xí)語法在開始的時候也會很困難,但隨著不斷地學(xué)習(xí),會越來越輕松。在本書的第一部分中,我們將著眼于Arm主要的Cortex-A架構(gòu)(特別是Armv8-A)的基礎(chǔ)知識以及在對為該平臺編譯的軟件進(jìn)行逆向工程時會遇到的主要指令。在本書的第二部分中,我們將探討逆向工程的一些常見工具和技術(shù)。我們還將通過實(shí)際示例(包括如何分析為蘋果M1芯片編譯的惡意軟件)展示不同應(yīng)用程序的基于Arm的逆向工程,以激發(fā)你的靈感。第1章
逆向工程簡介1.1匯編簡介如果你正在翻閱本書,那么你可能已經(jīng)聽說過Arm匯編語言,并且知道理解它是分析在Arm上運(yùn)行的二進(jìn)制文件的關(guān)鍵。但這種語言是什么,為什么會有這種語言?畢竟,程序員通常使用C/C++等高級語言來編寫代碼,幾乎沒有人會直接用匯編語言來編程。因?yàn)閷τ诔绦騿T來說,使用高級語言編程更加方便。不幸的是,這些高級語言對于處理器來說過于復(fù)雜,無法直接解析。程序員需要將這些高級程序編譯成處理器能夠運(yùn)行的二進(jìn)制機(jī)器碼。這種機(jī)器碼并不完全等同于匯編語言。如果你直接在文本編輯器中查看它,會發(fā)現(xiàn)它看起來非常難理解。處理器也不會直接運(yùn)行匯編語言,處理器只運(yùn)行機(jī)器碼,那么,為什么匯編語言在逆向工程中如此重要呢?為了理解匯編語言的用途,讓我們快速回顧一下計算機(jī)發(fā)展歷史,了解一下計算機(jī)是如何達(dá)到現(xiàn)在的狀態(tài)的,以及所有事物是如何互相聯(lián)系的。1.1.1位和字節(jié)在計算機(jī)發(fā)展的早期,人們決定創(chuàng)建計算機(jī)并讓它們執(zhí)行簡單的任務(wù)。計算機(jī)不會說我們?nèi)祟惖恼Z言——畢竟,它們只是電子設(shè)備——因此我們需要一種電子通信方式。在底層,計算機(jī)是通過電信號運(yùn)作的,這些信號是通過在兩個電壓水平之間進(jìn)行切換(開和關(guān))來形成的。第一個問題是,我們需要一種方法來描述這些“開”和“關(guān)”,才能將它們用于通信、存儲和簡單的系統(tǒng)狀態(tài)。既然有兩種狀態(tài),那么使用二進(jìn)制系統(tǒng)對這些值進(jìn)行編碼是非常自然的。每個二進(jìn)制位可以是0或1。盡管每個位(bit)只能存儲盡可能小的信息量,但將多個位串聯(lián)在一起可以表示非常大的數(shù)字。例如,數(shù)字30284334537只需要35位就可以表示出來,如下所示:這個系統(tǒng)已經(jīng)允許對比較大的數(shù)字進(jìn)行編碼,但現(xiàn)在我們面臨一個新的問題:在內(nèi)存(或磁帶)中,一個數(shù)字在哪里結(jié)束,下一個數(shù)字從哪里開始?對于現(xiàn)代讀者來說,這可能是一個奇怪的問題,但在計算機(jī)剛剛被設(shè)計出來的時候,這是一個嚴(yán)重的問題。最簡單的解決方案是創(chuàng)建固定大小的位分組。計算機(jī)科學(xué)家從不想錯過一個好的命名雙關(guān)語,他們將這組二進(jìn)制位稱為字節(jié)。那么,一個字節(jié)應(yīng)該有多少位?對于現(xiàn)代人來說,這個問題的答案似乎是顯而易見的,因?yàn)槲覀兌贾酪粋€字節(jié)是8位。但并非一開始就是這樣的。最初,不同的系統(tǒng)對其字節(jié)中的位數(shù)做出了不同的選擇。我們今天知道的8位字節(jié)的前身是6位二十進(jìn)制交換碼(BinaryCodedDecimalInterchangeCode,BCDIC)格式,用于表示早期IBM計算機(jī)(如1959年的IBM1620)的字母數(shù)字信息。在此之前,字節(jié)的長度通常為4位,更早的時候,一個字節(jié)代表大于1的任意位數(shù)。直到IBM于20世紀(jì)60年代在其大型計算機(jī)產(chǎn)品線System/360中引入8位擴(kuò)充的二十進(jìn)制交換碼(ExtendedBinaryCodedDecimalInterchangeCode,EBCDIC),并具有8位字節(jié)的可尋址內(nèi)存,字節(jié)才開始圍繞8位進(jìn)行標(biāo)準(zhǔn)化。這隨后促使其他廣泛使用的計算機(jī)系統(tǒng)(包括Intel8080和Motorola6800)采用了8位存儲大小。以下這段內(nèi)容摘自1962年出版的PlanningaComputerSystem一書,列出了采用8位字節(jié)的三個主要原因:1)其256個字符的總?cè)萘勘徽J(rèn)為足以滿足絕大多數(shù)應(yīng)用程序的需求。2)在這種容量的限制下,一個字符由一個字節(jié)來表示,因此任何特定記錄的長度不取決于該記錄中字符的重合度。3)8位字節(jié)在存儲空間上相當(dāng)經(jīng)濟(jì)。一個8位字節(jié)只可以存儲從00000000到11111111的256個不同的值中的一個。當(dāng)然,這些值的解釋取決于使用它的軟件。例如,我們可以在這些字節(jié)中存儲正數(shù),以表示從0到255(含)的正數(shù)。我們還可以使用二進(jìn)制補(bǔ)碼方案來表示從-128到127(含)的有符號數(shù)字。1.1.2字符編碼當(dāng)然,計算機(jī)并不僅僅使用字節(jié)來編碼和處理整數(shù)。它們還經(jīng)常存儲和處理人類可讀的字母和數(shù)字——稱為字符。早期的字符編碼(如ASCII)已經(jīng)確定使用每個字節(jié)的7位,但這只能提供有限的128個可能的字符。這允許對英語字母和數(shù)字以及一些符號字符和控制字符進(jìn)行編碼,但無法表示許多其他語言中使用的字母。EBCDIC標(biāo)準(zhǔn)使用8位字節(jié),選擇了一個完全不同的字符集,其代碼頁可以“交換”到不同的語言。但最終這種字符集過于煩瑣和不靈活。隨著時間的推移,人們逐漸認(rèn)識到需要一個真正通用的字符集來支持世界上所有現(xiàn)存的語言和特殊符號。這最終促成了1987年Unicode項(xiàng)目的建立。存在不同的Unicode編碼,但在Web上使用的主要編碼方案是UTF-8。ASCII字符集中的字符都被包含在了UTF-8中,而“擴(kuò)展字符”可以分布在多個連續(xù)的字節(jié)中。由于字符現(xiàn)在被編碼為字節(jié),因此我們可以用兩個十六進(jìn)制數(shù)字來表示字符。例如,字符A、R和M通常用圖1.1所示的八位數(shù)(octet)進(jìn)行編碼。圖1.1字符A、R和M以及它們的十六進(jìn)制值每個十六進(jìn)制數(shù)字都可以用從0000到1111的4位模式進(jìn)行編碼,如圖1.2所示。圖1.2十六進(jìn)制的ASCII值及其等效的8位二進(jìn)制值由于編碼一個ASCII字符需要兩個十六進(jìn)制的數(shù)字,8位似乎是存儲世界上大多數(shù)書面語言的文本的理想位數(shù),對于無法僅用8位表示的字符,可以使用多個8位來存儲。使用這種模式,我們可以更容易地解釋一長串位的含義。以下位模式編碼了單詞Arm:1.1.3機(jī)器碼和匯編與之前的機(jī)械計算器相比,計算機(jī)的一個獨(dú)特的強(qiáng)大之處在于,它們也可以將邏輯編碼為數(shù)據(jù)。這種代碼也可以存儲在內(nèi)存或磁盤上,并根據(jù)需要進(jìn)行處理或更改。例如,軟件更新可以完全改變計算機(jī)的操作系統(tǒng),而不需要購買一臺新機(jī)器。我們已經(jīng)看到了數(shù)字和字符是如何編碼的,但是邏輯如何編碼呢?這就是處理器架構(gòu)及其指令集發(fā)揮作用的地方。如果要從頭開始創(chuàng)建自己的計算機(jī)處理器,那么我們可以設(shè)計自己的指令編碼,將二進(jìn)制模式映射為處理器可以解釋和響應(yīng)的機(jī)器碼,這實(shí)際上是創(chuàng)建我們自己的“機(jī)器語言”。由于機(jī)器碼是為了“指示”電路執(zhí)行“操作”,因此也被稱為指令碼,或者更常見的操作碼(opcode)。在實(shí)踐中,大多數(shù)人使用現(xiàn)有的計算機(jī)處理器,因此使用處理器制造商定義的指令編碼。在Arm處理器上,指令編碼具有固定的大小,可以是32位或16位,具體取決于程序使用的指令集。處理器獲取并解釋每條指令,然后依次運(yùn)行每條指令以執(zhí)行程序的邏輯。每條指令都是一個二進(jìn)制模式或指令編碼,它遵循Arm架構(gòu)定義的特定規(guī)則。舉例來說,假設(shè)我們正在建立一個小型的16位指令集,并定義每條指令的模樣。我們的第一項(xiàng)任務(wù)是指定部分編碼,即指定要運(yùn)行的指令類型——稱為操作碼。例如,我們可以將指令的前7位設(shè)置為操作碼,并指定加法和減法的操作碼,如表1.1所示。因此手動編寫機(jī)器碼是可能的,但過于煩瑣。實(shí)際上,我們更希望用一些人類可讀的“匯編語言”來編寫匯編代碼,并將這些代碼轉(zhuǎn)換為機(jī)器碼的等效形式。為了做到這一點(diǎn),我們還應(yīng)該定義指令的簡寫形式,它們稱為指令助記符,如表1.2所示。表1.1加法和減法的操作碼表1.2加法和減法的助記符當(dāng)然,僅僅告訴處理器執(zhí)行“加法”是不夠的。我們還需要告訴它要將哪兩個值相加以及如何處理結(jié)果。例如,如果我們編寫一個執(zhí)行a=b+c操作的程序,b和c的值需要在指令開始執(zhí)行前存儲在某個地方,而且指令需要知道將結(jié)果a寫到哪里。在大多數(shù)處理器中,特別是在Arm處理器中,這些臨時值通常存儲在寄存器中,寄存器存儲一小部分“工作”值。程序可以將數(shù)據(jù)從內(nèi)存(或磁盤)中讀入寄存器中,以便進(jìn)行處理,并且可以在處理后將結(jié)果數(shù)據(jù)存放到長期存儲器中。寄存器的數(shù)量和命名規(guī)則取決于架構(gòu)。隨著軟件變得越來越復(fù)雜,程序往往需要同時處理更多的數(shù)值。在寄存器中存儲和操作這些值比直接在內(nèi)存中進(jìn)行操作要快,這意味著寄存器減少了程序需要訪問內(nèi)存的次數(shù),并且提升了執(zhí)行速度。回到我們之前的例子,假設(shè)我們設(shè)計了一條16位的指令來執(zhí)行一個操作,該操作將一個值加到一個寄存器中,并將結(jié)果寫入另一個寄存器。由于我們用7位來完成操作(ADD/SUB),因此剩下的9位可以用于編碼源寄存器(操作數(shù)寄存器)、目標(biāo)寄存器和我們想要加或減的常量值。在這個例子中,我們將剩余的位數(shù)平均分配,并分配了表1.3所示的快捷方式和相應(yīng)的機(jī)器碼。表1.3手動分配機(jī)器碼我們可以編寫一個小程序?qū)⒄Z法ADDR1,R0,#2(R1=R0+2)轉(zhuǎn)換為相應(yīng)的機(jī)器碼模式,而不是手動生成這些機(jī)器碼(見表1.4)。然后,將這個機(jī)器碼模式交給我們的示例處理器。表1.4機(jī)器碼編程我們構(gòu)建的位模式表示T32指令集中16位ADD和SUB指令的一個指令編碼。在圖1.3中,你可以看到它的組成部分以及它們在指令編碼中的順序。當(dāng)然,這只是一個簡化的例子?,F(xiàn)代處理器提供了數(shù)百條可能的指令,這些指令通常具有更復(fù)雜的子編碼。例如,Arm定義了加載寄存器指令(使用LDR助記符),該指令可以將一個32位的值從內(nèi)存加載到一個寄存器中,如圖1.4所示。圖1.316位Thumb編碼的ADD和SUB立即數(shù)指令在這條指令中,要加載的“地址”在寄存器2(R2)中指定,讀取的值被寫入寄存器3(R3)。在R2的兩邊使用括號的語法表示R2寄存器中的值將被解釋為內(nèi)存中的一個地址,而不是普通值。換句話說,我們不想將R2寄存器中的值復(fù)制到R3寄存器中,而是要獲取R2寄存器給定地址處內(nèi)存的內(nèi)容,并將該值加載到R3寄存器中。程序引用內(nèi)存位置的原因有很多,其中包括調(diào)用函數(shù)或?qū)?nèi)存中的值加載到寄存器中。圖1.4LDR指令從R2中的地址向寄存器R3加載一個值這本質(zhì)上是機(jī)器碼和匯編代碼之間的區(qū)別。匯編語言具有可讀性較強(qiáng)的語法,可以顯示如何解釋每條編碼指令。相比之下,機(jī)器碼是實(shí)際由處理器處理的二進(jìn)制數(shù)據(jù),其編碼由處理器設(shè)計者精確指定。1.1.4匯編由于處理器只能理解機(jī)器碼而不能理解匯編語言,因此我們需要一個程序?qū)⑹謱懙膮R編指令轉(zhuǎn)換為它們的機(jī)器碼等效形式。執(zhí)行這個任務(wù)的程序被稱為匯編器。實(shí)際上,匯編器不僅能夠理解指令,還能將單條指令轉(zhuǎn)換為機(jī)器碼,而且能夠解釋匯編器指令,匯編器指令可以指導(dǎo)匯編器執(zhí)行其他任務(wù),例如在數(shù)據(jù)和代碼之間切換或匯編不同的指令集。因此,匯編語言和匯編器語言只是看待同一件事情的兩種方式。匯編器指令和表達(dá)式的語法及含義取決于特定的匯編器。這些指令和表達(dá)式是匯編程序中可用的快捷方式。然而,嚴(yán)格來說,它們并不屬于匯編語言,而是匯編器應(yīng)該如何操作的指示。在不同的平臺上有不同的匯編器,例如用于匯編Linux內(nèi)核的GNU匯編器as,以及ARM工具鏈匯編器armasm和包含在VisualStudio中具有相同名稱(armasm)的Microsoft匯編器。舉個例子,假設(shè)我們想要在名為myasm.s的文件中匯編以下兩條16位指令:在這個程序中,前三行是匯編器指令。這些指令告訴匯編器數(shù)據(jù)應(yīng)該在哪里被匯編(在本例中,放在.text節(jié)),將代碼的入口點(diǎn)的標(biāo)簽(在本例中,稱為_start)定義為全局符號,最后指定它應(yīng)該使用Thumb指令集(T32)進(jìn)行編碼。Thumb指令集(T32)是Arm架構(gòu)的一部分,它允許指令的寬度為16位。我們可以使用GNU匯編器as,在運(yùn)行于Arm處理器上的Linux操作系統(tǒng)機(jī)器上編譯這個程序:匯編器讀取匯編語言程序myasm.s并創(chuàng)建一個名為myasm.o的目標(biāo)文件。這個文件包含4個字節(jié)的機(jī)器碼,對應(yīng)于我們的兩條2字節(jié)的十六進(jìn)制指令:匯編器另一個特別有用的功能是標(biāo)簽,它引用內(nèi)存中的特定地址,如分支目標(biāo)、函數(shù)或全局變量的地址。讓我們以匯編程序?yàn)槔哼@個程序首先給兩個寄存器填充數(shù)值,然后跳轉(zhuǎn)到標(biāo)簽mylabel執(zhí)行ADD指令。在執(zhí)行完ADD指令后,程序跳轉(zhuǎn)到result標(biāo)簽,執(zhí)行移動指令,然后跳轉(zhuǎn)到_exit標(biāo)簽結(jié)束。匯編器將使用這些標(biāo)簽為鏈接器提供提示,鏈接器為它們分配相對的內(nèi)存位置。圖1.5說明了程序的流程。圖1.5匯編程序示例的程序流程標(biāo)簽不僅可以用來引用跳轉(zhuǎn)指令,還可以用來獲取內(nèi)存位置的內(nèi)容。例如,下面的匯編代碼片段使用標(biāo)簽從內(nèi)存位置獲取內(nèi)容或跳轉(zhuǎn)到代碼中的不同指令:首先用ADR指令將變量myvalue的地址加載到寄存器R2中,并使用LDR指令將該地址的內(nèi)容加載到寄存器R3中。然后程序跳轉(zhuǎn)到標(biāo)簽mylabel所引用的指令,執(zhí)行ADD指令,再跳轉(zhuǎn)到標(biāo)簽result所引用的指令,如圖1.6所示。圖1.6ADR和LDR指令邏輯的說明作為一個稍微有趣的例子,下面的匯編代碼將HelloWorld!輸出到控制臺,然后退出。它使用一個標(biāo)簽來引用字符串hello,方法是通過ADR指令將標(biāo)簽mystring的相對地址放入寄存器R1中。在支持Arm架構(gòu)和指令集的處理器上匯編并鏈接此程序后,執(zhí)行時會輸出Hello?,F(xiàn)代匯編器通常被整合到編譯器工具鏈中,并且輸出可以合并成更大的可執(zhí)行程序的文件。因此,匯編程序通常不僅僅是將匯編指令直接轉(zhuǎn)換為機(jī)器碼,而是創(chuàng)建一個目標(biāo)文件,其中包括匯編指令、符號信息和編譯器鏈接程序的提示,最終負(fù)責(zé)創(chuàng)建在現(xiàn)代操作系統(tǒng)上運(yùn)行的完整可執(zhí)行文件。交叉匯編器如果在不同的處理器架構(gòu)上運(yùn)行我們的Arm程序,會怎樣?在Intelx86-64處理器上執(zhí)行myasm2程序?qū)a(chǎn)生一個錯誤,它會告訴我們由于可執(zhí)行格式的錯誤,二進(jìn)制文件不能被執(zhí)行。我們不能在x64機(jī)器上運(yùn)行Arm二進(jìn)制文件,因?yàn)檫@兩個平臺上的指令編碼方式不同。即使我們想在不同的架構(gòu)上執(zhí)行相同的操作,匯編語言和分配的機(jī)器碼也會有很大的不同。假設(shè)你想在三種不同的處理器架構(gòu)上執(zhí)行一條將十進(jìn)制數(shù)字1移到第一個寄存器的指令。盡管操作本身是一樣的,但指令編碼和匯編語言卻取決于架構(gòu)。以下列三種一般的架構(gòu)類型為例:●Armv8-A:64位指令集(AArch64)●Armv8-A:32位指令集(AArch32)●Intelx86-64指令集不僅是語法不同,而且不同指令集之間相應(yīng)的機(jī)器碼字節(jié)也有很大的差異。這意味著,為Arm32位指令集匯編的機(jī)器碼字節(jié)在不同指令集的架構(gòu)(如x64或A64)上具有完全不同的含義。反過來也是如此。相同的字節(jié)序列在不同的處理器上可能會有顯著不同的解釋,例如:●Armv8-A:64位指令集(AArch64)●Armv8-A:32位指令集(AArch32)換句話說,匯編程序需要使用我們想要運(yùn)行這些匯編程序的架構(gòu)的匯編語言編寫,并且必須用支持這種指令集的匯編器進(jìn)行匯編。然而,可能令人感到意外的是,可以在不使用Arm機(jī)器的情況下創(chuàng)建Arm二進(jìn)制文件。當(dāng)然,匯編器本身需要了解Arm語法,但如果該匯編器是為x64編譯的,則在x64機(jī)器上運(yùn)行它將使你能夠創(chuàng)建Arm二進(jìn)制文件。這種匯編器稱為交叉匯編器,允許你針對不同于當(dāng)前正在使用的目標(biāo)架構(gòu)的架構(gòu)進(jìn)行代碼匯編。例如,你可以在x86-64的Ubuntu機(jī)器上下載一個AArch32的匯編器,然后從那里匯編代碼。使用Linux命令file,我們可以看到,我們創(chuàng)建了一個32位Arm可執(zhí)行文件。1.2高級語言那么,為什么匯編語言沒有成為編寫軟件的主流編程語言?一個主要原因是匯編語言不具有可移植性。想象一下,為了支持每種處理器架構(gòu),每次都必須重新編寫整個應(yīng)用程序代碼庫!這是非常大的工作量。取而代之的是,新的語言已經(jīng)發(fā)展了起來,能將這些特定于處理器的細(xì)節(jié)抽象出來,使同一個程序可以很容易地在不同的架構(gòu)下被編譯。這些語言通常被稱為高級語言,與更貼近特定計算機(jī)硬件和架構(gòu)的低級匯編語言形成對比。這里的“高級語言”一詞本質(zhì)上是相對的。C和C++剛開始被認(rèn)為是高級語言,而匯編語言被認(rèn)為是低級語言。由于出現(xiàn)了更新的、更抽象的語言,如VisualBasic和Python,C/C++如今經(jīng)常被視為低級語言。歸根結(jié)底,這取決于你所站的角度和所問的對象。與匯編語言一樣,處理器不能直接理解高級源代碼。程序員需要使用編譯器將編寫的高級程序轉(zhuǎn)換成機(jī)器碼。和以前一樣,我們需要指定二進(jìn)制文件將在哪種架構(gòu)上運(yùn)行,并且使用交叉編譯器在非Arm系統(tǒng)上創(chuàng)建Arm架構(gòu)的二進(jìn)制文件。編譯器的輸出是一個可在特定的操作系統(tǒng)上運(yùn)行的可執(zhí)行文件,而且通常輸出給用戶的是二進(jìn)制可執(zhí)行文件,而不是程序的源代碼。因此往往當(dāng)我們想分析一個程序時,我們所擁有的只是編譯后的可執(zhí)行文件。不幸的是,對于逆向工程師來說,一般情況下,不可能逆轉(zhuǎn)編譯過程返回到原始源代碼。編譯器不僅是非常復(fù)雜的程序,在原始源代碼和生成的二進(jìn)制文件之間有許多層的迭代和抽象,而且其中許多步驟丟棄了方便程序員推理程序的人類可讀信息。在沒有要分析的軟件源代碼的情況下,根據(jù)要求的詳細(xì)程度,我們有兩種分析方式:反編譯或反匯編可執(zhí)行文件。1.3反匯編反匯編二進(jìn)制文件的過程包括將二進(jìn)制文件運(yùn)行的匯編指令從其機(jī)器碼格式重構(gòu)為人類可讀的匯編語言。反匯編最常見的用例包括惡意軟件分析、編譯器的性能和輸出準(zhǔn)確性驗(yàn)證、漏洞分析,以及針對閉源軟件缺陷進(jìn)行漏洞利用或概念驗(yàn)證開發(fā)。在這些應(yīng)用中,漏洞利用開發(fā)可能是最需要對實(shí)際匯編代碼進(jìn)行分析的。雖然漏洞發(fā)現(xiàn)通??梢酝ㄟ^模糊處理等技術(shù)來完成,但從檢測到的崩潰代碼構(gòu)建漏洞利用或發(fā)現(xiàn)為什么某些代碼區(qū)域無法被模糊測試覆蓋,通常需要扎實(shí)的匯編知識。在這種情況下,通過閱讀匯編代碼對漏洞的確切條件實(shí)現(xiàn)精細(xì)的掌握是至關(guān)重要的。編譯器分配變量和數(shù)據(jù)結(jié)構(gòu)的確切方式對于開發(fā)漏洞利用至關(guān)重要,因此深入了解匯編知識是必需的。通常一個看似“無法利用”的漏洞,實(shí)際上,只要再投入一點(diǎn)創(chuàng)造力和辛勤工作來真正理解易受攻擊的功能的內(nèi)部機(jī)制,便可變得可利用。反匯編可執(zhí)行文件可以通過多種方式進(jìn)行,我們將在本書的第二部分更詳細(xì)地研究這個問題。但是,目前快速查看可執(zhí)行文件的反匯編輸出的最簡單的工具之一是Linux工具objdump。讓我們編譯并反匯編以下write()程序:我們可以用GCC編譯這段代碼并指定-c選項(xiàng)。這個選項(xiàng)告訴GCC在不調(diào)用鏈接進(jìn)程的情況下創(chuàng)建目標(biāo)文件,因此我們可以只對編譯的代碼運(yùn)行objdump,而不看周圍所有目標(biāo)文件(如C運(yùn)行時)的反匯編。main函數(shù)的反匯編輸出如下:雖然像objdump這樣的Linux實(shí)用工具對快速反匯編小程序很有用,但較大的程序需要更方便的解決方案。如今存在各種反匯編器可以使逆向工程更高效,包括免費(fèi)的開源工具(如Ghidra)和昂貴的解決方案(如IDAPro)等。這些將在本書的第二部分中進(jìn)行詳細(xì)討論。1.4反編譯逆向工程的一個較新的創(chuàng)新是使用反編譯器。反編譯器比反匯編器更進(jìn)一步。反匯編器只是顯示程序的人類可讀的匯編代碼,而反編譯器則試圖從編譯的二進(jìn)制文件中重新生成等價的C/C++代碼。反編譯器的一個優(yōu)點(diǎn)是通過生成偽代碼顯著減少和簡化反匯編的輸出。當(dāng)快速瀏覽一個函數(shù)以從宏觀層面上了解程序正在執(zhí)行什么操作時,這可以使閱讀更加容易。當(dāng)然,反編譯的缺點(diǎn)是在這個過程中可能會丟失重要的細(xì)節(jié)。此外,由于編譯器在從源代碼到可執(zhí)行文件的轉(zhuǎn)換過程中本身是有損失的,因此反編譯器不能完全重建原始源代碼。符號名稱、局部變量、注釋以及大部分程序結(jié)構(gòu)在編譯過程中會被破壞。同樣,如果存儲位置被積極優(yōu)化的編譯器重復(fù)使用,那么試圖自動命名或重新標(biāo)記局部變量和參數(shù)的做法也會產(chǎn)生誤導(dǎo)。讓我們看一個C函數(shù)的例子,使用GCC編譯它,然后用IDAPro和Ghidra的反編譯器進(jìn)行反編譯,以顯示實(shí)際的情況。圖1.7顯示了Linux源代碼庫中ihex2fw.c文件中一個名為file_record的函數(shù)。在Armv8-A架構(gòu)上編譯C文件(沒有任何特定的編譯器選項(xiàng))并將可執(zhí)行文件加載到IDAPro7.6中后,圖1.8顯示了由反編譯器生成的file_record函數(shù)的偽代碼。圖1.9顯示了Ghidra10.0.4對同一函數(shù)的反編譯輸出。在這兩種情況下,如果我們仔細(xì)觀察,便可以看到原始代碼的影子,但是這些代碼遠(yuǎn)不如原始代碼易讀和直觀。換句話說,雖然在某些情況下反編譯器可以為我們提供程序的高層次概述,但它絕不是萬無一失的,也無法替代深入研究給定程序的匯編代碼。圖1.7ihex2fw.c源文件中file_record函數(shù)的源代碼圖1.8IDAPro7.6對編譯后的file_record函數(shù)的反編譯輸出圖1.9Ghidra10.0.4對編譯后的file_record函數(shù)的反編譯輸出話雖如此,反編譯器在不斷發(fā)展,并且越來越擅長重構(gòu)源代碼,特別是對于簡單的函數(shù)。雖然,使用你想在更高層次上進(jìn)行逆向工程的函數(shù)的反編譯器輸出是一個有用的輔助,但是當(dāng)你想要更深入地了解正在發(fā)生的事情時,請不要忘記查看反匯編輸出。第2章
ELF文件格式的內(nèi)部結(jié)構(gòu)本章可作為了解基本編譯過程和ELF文件格式內(nèi)部結(jié)構(gòu)的參考。如果你已經(jīng)熟悉了它的概念,可以跳過這一章,把它作為你在分析過程中可能需要的參考。2.1程序結(jié)構(gòu)在深入研究匯編指令和如何對程序二進(jìn)制文件進(jìn)行逆向工程之前,首先有必要了解一下這些程序二進(jìn)制文件最初來自哪里。程序最初是由軟件開發(fā)人員編寫的源代碼。源代碼向計算機(jī)描述程序應(yīng)該如何執(zhí)行以及在各種輸入條件下程序應(yīng)該進(jìn)行哪些計算。程序員使用的編程語言在很大程度上是程序員的偏好選擇。有些語言很適合解決數(shù)學(xué)和機(jī)器學(xué)習(xí)問題。有些語言為網(wǎng)站開發(fā)或構(gòu)建智能手機(jī)應(yīng)用程序進(jìn)行了優(yōu)化。而像C和C++這樣的語言足夠靈活,可用于各種可能的應(yīng)用類型,包括設(shè)備驅(qū)動程序、固件等低級系統(tǒng)軟件,系統(tǒng)服務(wù)以及視頻游戲、網(wǎng)絡(luò)瀏覽器和操作系統(tǒng)等大型應(yīng)用程序。因此,我們在二進(jìn)制分析中遇到的許多程序都是從C/C++代碼開始的。計算機(jī)不能直接執(zhí)行源代碼文件。在程序可以運(yùn)行之前,必須首先被翻譯成處理器知道如何執(zhí)行的機(jī)器指令。執(zhí)行這種翻譯的程序被稱為編譯器。在Linux上,GCC是一個常用的編譯器集合,包括一個C編譯器,用于將C代碼轉(zhuǎn)換為Linux可以直接加載和運(yùn)行的ELF二進(jìn)制文件。g++是編譯C++代碼的編譯器。圖2.1顯示了編譯概述。圖2.1編譯概述從某種意義上說,逆向工程是在執(zhí)行編譯器的逆向任務(wù)。在逆向工程中,我們從程序的二進(jìn)制文件開始逆向處理,嘗試以更高級的語言方式讓程序員了解程序執(zhí)行的流程。因此,了解ELF文件格式的組成部分及其作用對逆向工程是很有幫助的。2.2高級語言與低級語言C和C++通常被描述為高級語言,因?yàn)樗鼈冊试S程序員定義程序的結(jié)構(gòu)和行為,而不直接參考機(jī)器體系結(jié)構(gòu)本身。程序員可以使用抽象的編程概念來編寫C/C++代碼,例如使用if-else塊、while循環(huán)和程序員命名的局部變量,而不必考慮這些變量最終將如何映射到機(jī)器寄存器、內(nèi)存位置或生成的代碼中的具體機(jī)器指令。這種抽象通常對程序員非常有益。這種抽象和高級程序流程概念通常使得用C/C++編程比直接用匯編代碼編寫同等程序要快得多,錯誤也少得多。此外,由于C和C++與特定的機(jī)器體系結(jié)構(gòu)沒有強(qiáng)耦合關(guān)系,因此可以將相同的C/C++代碼編譯到不同的目標(biāo)處理器上運(yùn)行。C++和C的區(qū)別是它添加了大量新的語法、編程特性和高級抽象,從而使編寫大規(guī)模程序更加容易和快速。例如,C++為面向?qū)ο蟮木幊淘黾恿酥苯拥恼Z言支持,并使構(gòu)造函數(shù)、析構(gòu)函數(shù)和對象創(chuàng)建功能直接成為語言本身的組成部分。C++還引入了編程抽象,如接口、C++異常和運(yùn)算符重載,并通過更強(qiáng)大的類型檢查系統(tǒng)和模板支持,引入了額外的程序正確性編譯時檢查,這在原來的C編程語言中是不可能的。按照慣例,C和C++程序從main函數(shù)開始其核心程序邏輯。這個函數(shù)通常處理程序的命令行參數(shù),為程序的執(zhí)行做準(zhǔn)備,然后開始執(zhí)行核心程序邏輯本身。對于命令行程序來說,這可能涉及處理文件和輸入/輸出流。圖形化程序也可以處理文件和輸入流,但通常還會創(chuàng)建窗口來將圖形繪制到屏幕上以供用戶交互,并設(shè)置事件處理程序以響應(yīng)用戶輸入。與C和C++這樣的高級語言不一樣,程序員也可以選擇使用低級“匯編語言”來編寫代碼。這些匯編語言與它們所針對的目標(biāo)處理器緊密耦合,但可以讓程序員更加靈活地指定處理器應(yīng)該運(yùn)行哪些機(jī)器指令以及以哪種順序運(yùn)行。除了個人喜好之外,程序員選擇用低級語言編寫全部或部分程序的原因有很多。表2.1給出了一些低級語言的使用案例。表2.1匯編語言編程的使用案例(續(xù))在了解低級語言如何匯編之前,我們先看看編譯器如何將用C/C++等高級語言編寫的程序轉(zhuǎn)換成低級匯編代碼。2.3編譯過程編譯器的核心工作是將用C/C++這樣的高級語言編寫的程序轉(zhuǎn)換成等效的低級語言(如作為Armv8-A架構(gòu)中的A64指令集)程序。我們來看一個用C語言編寫的簡單示例程序:在Linux上,常用的C編譯器是GCC(GNUCompilerCollection)。默認(rèn)情況下,GCC不僅將C程序編譯成匯編代碼,還管理整個編譯過程,將編譯結(jié)果鏈接起來,最終得到ELF程序的二進(jìn)制文件,該文件可由操作系統(tǒng)直接執(zhí)行。我們可以通過以下命令行調(diào)用GCC,從源代碼中創(chuàng)建程序二進(jìn)制文件:我們還可以使用-v指令來指導(dǎo)GCC編譯器驅(qū)動程序,使其向我們提供幕后的細(xì)節(jié),如下所示:這個命令的輸出內(nèi)容很多,但如果我們查看輸出的末尾,便可以看到在編譯過程的最后階段,GCC在一個發(fā)送到臨時位置的匯編文件上調(diào)用了匯編器,如下所示:這是因?yàn)镚CC是一個編譯器的集合。C語言編譯器本身將C語言代碼轉(zhuǎn)換為匯編代碼清單,然后將其發(fā)送到匯編器轉(zhuǎn)換為目標(biāo)文件,最終鏈接到目標(biāo)二進(jìn)制文件中。我們可以通過命令行選項(xiàng)-S攔截匯編代碼清單,查看編譯器本身正在生成的內(nèi)容,例如,調(diào)用gccmain.c-S。GCC將把main.c中的程序編譯成一個匯編代碼清單,并將其寫入main.s文件。由于C++在大多數(shù)情況下是C語言的超集,因此我們也可以把這個示例當(dāng)作C++代碼來編譯。在這里,我們使用C++編譯器g++,通過命令行將代碼編譯成目標(biāo)二進(jìn)制文件:我們還可以通過命令行選項(xiàng)-S,即通過命令g++main.cpp-S,指示g++輸出其匯編代碼清單。如果我們允許GCC運(yùn)行完成,它最終會輸出一個可執(zhí)行的ELF文件,該文件可以直接從命令行執(zhí)行。例如,我們可以用Arm-devs和reverse-engineers這兩個命令行選項(xiàng)來運(yùn)行該程序,該程序會將其輸出內(nèi)容輸出到控制臺,如下所示:2.3.1不同架構(gòu)的交叉編譯用C/C++這樣的高級語言編寫程序的主要益處是,源代碼在默認(rèn)情況下不會與特定的處理器架構(gòu)強(qiáng)耦合。這使得同一個程序的源代碼可以被編譯到不同的目標(biāo)平臺上運(yùn)行。在其默認(rèn)配置中,GCC和g++將創(chuàng)建目標(biāo)二進(jìn)制文件,它們運(yùn)行于我們正在編譯的同一機(jī)器架構(gòu)上。例如,如果我們在64位的ArmLinux機(jī)器上運(yùn)行g(shù)ccmain.c-oexample.so,產(chǎn)生的example.so二進(jìn)制文件只能作為在64位Arm機(jī)器上運(yùn)行的ELF二進(jìn)制文件。如果我們在x86_64的Linux機(jī)器上運(yùn)行同樣的命令,得到的二進(jìn)制文件將只能運(yùn)行在x86_64機(jī)器上。查看ELF二進(jìn)制文件所針對的架構(gòu)的一種方法是通過file命令,如下所示:通常情況下,生成與正在運(yùn)行的系統(tǒng)相匹配的程序二進(jìn)制文件是一個有用的功能——我們通常希望編譯器生成的二進(jìn)制文件能夠立即在我們的開發(fā)機(jī)器上運(yùn)行。但是,如果開發(fā)機(jī)器與目標(biāo)機(jī)器的架構(gòu)不一樣呢?例如,如果開發(fā)機(jī)器是基于x86_64的,但我們想創(chuàng)建一個專門在64位Arm處理器上運(yùn)行的目標(biāo)二進(jìn)制文件,該怎么辦?對于這些情況,我們需要使用交叉編譯器。表2.2中列出的軟件包是最常用的GCC和g++的Arm交叉編譯器,用于創(chuàng)建可以在基于Arm的32位和64位Linux機(jī)器上運(yùn)行的二進(jìn)制文件。表2.2GCC和g++的Arm交叉編譯器在使用apt-get作為主軟件包管理器的系統(tǒng)上,我們可以通過以下命令安裝這些Arm交叉編譯器:安裝了這些交叉編譯器后,我們就可以直接從運(yùn)行不同架構(gòu)的開發(fā)機(jī)器上生成32位和64位Arm二進(jìn)制文件。為此,我們用特定目標(biāo)的交叉編譯器替換gcc。例如,一臺x86_64機(jī)器可以從C或C++代碼創(chuàng)建一個64位Arm二進(jìn)制文件,如下所示:我們可以用類似的方法針對32位Arm系統(tǒng)創(chuàng)建目標(biāo)二進(jìn)制文件,只需使用32位Arm交叉編譯器,如下所示:如果我們使用命令file檢查這些輸出二進(jìn)制文件,便可以看到它們分別是為64位和32位Arm架構(gòu)編譯的程序二進(jìn)制文件。2.3.2匯編和鏈接編譯器和手工編寫匯編代碼的程序員創(chuàng)建的匯編代碼清單作為匯編器的輸入。匯編器的工作是將人類可讀的機(jī)器指令描述轉(zhuǎn)換為與其等效的二進(jìn)制編碼指令,并按照程序員或編譯器的手動指示將程序的數(shù)據(jù)和元數(shù)據(jù)輸出到程序二進(jìn)制文件的其他部分。匯編器的輸出是一個目標(biāo)文件,目標(biāo)文件被編碼為ELF文件,最好將這些目標(biāo)文件視為部分ELF文件,需要通過最終鏈接過程將它們組合成一個整體,以創(chuàng)建最終的可執(zhí)行目標(biāo)二進(jìn)制文件。按照慣例,匯編代碼寫在.s文件中,可以使用匯編器將這些文件匯編成一個目標(biāo)文件,比如使用GNU匯編器(GAS),它是GCC/g++工具套件的一部分。在本書后面的章節(jié)中,我們將看到Armv8-A架構(gòu)上有哪些指令,以及它們?nèi)绾喂ぷ鳌H欢?,現(xiàn)在,定義幾個模板匯編程序是很有用的,你可以用它來創(chuàng)建基本的匯編程序。下面的程序是一個簡單的匯編程序,它使用write()系統(tǒng)調(diào)用來輸出一個字符串并退出。前三行定義了程序的架構(gòu)、節(jié)和全局入口點(diǎn)。write()函數(shù)需要三個參數(shù):文件描述符、指向存儲數(shù)據(jù)(如字符串)的緩沖區(qū)的指針,以及要從緩沖區(qū)寫入的字節(jié)數(shù)。這些參數(shù)都在前三個寄存器x0、x1和x2中指定。寄存器x8應(yīng)該包含write()系統(tǒng)調(diào)用的系統(tǒng)調(diào)用號,SVC指令會調(diào)用它。ascii字符串可以放在.text節(jié)的末尾(在所謂的字面量池中)或在.data或rodata節(jié)中。也可以使用庫函數(shù)來實(shí)現(xiàn)同樣的結(jié)果。下面的程序都執(zhí)行相同的基本任務(wù):一個用于64位Arm,另一個用于32位Arm。它們都在生成的ELF文件的.text節(jié)中定義了一個_start函數(shù),并將一個以零結(jié)尾的字符串Helloworld\n放置在生成的二進(jìn)制文件的.rodata(只讀數(shù)據(jù))節(jié)中。這兩種情況下的main函數(shù)都將這個字符串的地址加載到一個寄存器中,調(diào)用printf將字符串輸出到控制臺,然后調(diào)用exit(0)來退出該程序。如果開發(fā)機(jī)器與目標(biāo)架構(gòu)相匹配,則可以直接使用as命令匯編這些程序,如下所示:如果開發(fā)機(jī)器與目標(biāo)架構(gòu)不匹配,則可以使用GCC的交叉編譯器版本的as:嘗試直接運(yùn)行目標(biāo)文件通常不能成功。首先,我們必須鏈接二進(jìn)制文件。在GCC套件中,鏈接器二進(jìn)制文件被稱為ld(或aarch64-linux-gnu-ld和arm-linux-gnueabihf-ld,視情況而定)。我們必須向鏈接器提供所有的目標(biāo)文件以創(chuàng)建一個完整的程序二進(jìn)制文件,然后用-o選項(xiàng)指定鏈接器的輸出文件。對于write64.s程序,我們只需要一個名為write64.o的目標(biāo)文件,無須指定任何額外的庫就可以直接運(yùn)行。當(dāng)匯編程序使用特定的庫函數(shù),而不是直接使用系統(tǒng)調(diào)用時,它需要包含必要的目標(biāo)文件。對于printf64.s示例,我們指定print64.o為輸入目標(biāo)文件,但在程序運(yùn)行之前,它還需要包含其他幾個目標(biāo)文件。一個是libc.so,所以我們的程序可以訪問libc庫中的函數(shù)printf和exit。此外,它還需要三個目標(biāo)文件,它們共同構(gòu)成了C語言的運(yùn)行時庫,需要在調(diào)用main函數(shù)之前引導(dǎo)進(jìn)程。表2.3描述了程序所需要的目標(biāo)文件的依賴關(guān)系。表2.3所需目標(biāo)文件及它們的作用因此,最終的鏈接器命令行如下所示:由此產(chǎn)生的目標(biāo)二進(jìn)制文件print64.so就可以在64位Arm機(jī)器上運(yùn)行了:2.4ELF文件概述編譯和鏈接過程的最終輸出是一個可執(zhí)行和可鏈接格式(ExecutableandLinkableFormat,ELF)文件,它包含操作系統(tǒng)和加載器加載并運(yùn)行程序所需的所有信息。在最抽象的層面上,ELF文件可以被視為描述程序及其運(yùn)行方式的表集合。在ELF中,存在三種類型的表:ELF文件頭(位于文件開頭)、程序頭和節(jié)頭(描述如何將ELF程序加載到內(nèi)存中),以及ELF文件的邏輯節(jié)(告訴加載器如何準(zhǔn)備執(zhí)行)。2.5ELF文件頭在ELF文件的開頭是ELF文件頭。ELF文件頭描述了程序的全局屬性,如運(yùn)行程序的架構(gòu)、程序入口點(diǎn)以及文件中其他表的指針和大小。給定一個ELF文件,例如2.3.2節(jié)中的print32.so和print64.so程序,我們可以用readelf這樣的程序查看這些屬性和節(jié)。ELF文件頭可以通過readelf的-h參數(shù)來查看,如下所示:ELF文件頭分為四個主要組成部分:ELF文件頭信息字段、目標(biāo)平臺字段、程序入口點(diǎn)字段和表位置字段。2.5.1ELF文件頭信息字段ELF文件頭信息字段告訴加載器這是什么類型的ELF文件,并從magic字段開始。magic字段是一個常量16字節(jié)二進(jìn)制模式——稱為標(biāo)識模式,表明該文件本身是一個有效的ELF文件。它始終以相同的4字節(jié)序列開頭,從0x7f字節(jié)開始,然后是對應(yīng)于ASCII字符ELF的3個字節(jié)。class字段告訴加載器ELF文件本身是否使用32位或64位ELF文件格式。通常情況下,32位程序使用32位文件格式,而64位程序使用64位文件格式。在我們的例子中,我們可以看到Arm上的程序就是這種情況:32位Arm二進(jìn)制文件使用32位ELF文件格式,而64位二進(jìn)制文件使用64位格式。data字段告訴加載器應(yīng)該以大端序(big-endian)或小端序(little-endian)讀取ELF文件的字段。Arm上的ELF文件通常對ELF文件格式本身使用小端序編碼。我們將在本書后面看到端序是如何工作的,以及處理器如何在小端序和大端序模式之間動態(tài)地交換?,F(xiàn)在只需要知道這個字段只改變了操作系統(tǒng)和加載器讀取ELF文件結(jié)構(gòu)的方式,這個字段并不改變處理器在運(yùn)行程序時的行為。最后,version字段告訴加載器,我們正在使用第一個ELF文件版本格式。這個字段的設(shè)計是為了保證ELF文件格式在未來的兼容性。2.5.2目標(biāo)平臺字段目標(biāo)平臺字段告訴加載器ELF文件在哪種類型的機(jī)器上運(yùn)行。machine字段告訴加載器該程序在哪種類型的處理器上運(yùn)行。我們的64位程序?qū)⑦@個字段設(shè)置為AArch64,表示ELF文件將只在64位Arm處理器上運(yùn)行。我們的32位程序指定為ARM,這意味著它將只在32位Arm處理器上運(yùn)行,或者作為一個32位進(jìn)程在64位Linux機(jī)器上使用處理器的32位AArch32執(zhí)行模式。flags字段指定了加載器需要的額外信息,這是一個特定于架構(gòu)的結(jié)構(gòu)。例如,在我們的64位程序中,沒有定義特定架構(gòu)的標(biāo)志,這個字段將始終保持值為0。相比之下,對于我們的32位Arm程序,這個字段通知加載器,該程序被編譯為使用嵌入式ABI(EABI)配置文件版本5,并且該程序期望對浮點(diǎn)運(yùn)算的硬件支持。Arm規(guī)范定義了4個Arm專用的值,它們可以放在ELF程序頭的e_flags字段中,如表2.4所示。表2.4Arm32位e_flags值①/ArmHardFloatPort最后,type字段指定了ELF文件的目的。在這種情況下,type字段指定這些程序是動態(tài)鏈接的二進(jìn)制文件,系統(tǒng)加載器可以準(zhǔn)備并執(zhí)行它們。2.5.3程序入口點(diǎn)字段程序入口點(diǎn)字段告訴加載器程序的入口點(diǎn)在哪里。當(dāng)操作系統(tǒng)或加載器在內(nèi)存中準(zhǔn)備好程序并準(zhǔn)備開始執(zhí)行時,這個字段指定程序的啟動地址。盡管按照慣例,C和C++程序從main函數(shù)處“開始”,但程序?qū)嶋H上并不從這里開始執(zhí)行。它們從一個小的匯編代碼存根(傳統(tǒng)上在名為_start的符號處)中開始執(zhí)行。當(dāng)鏈接標(biāo)準(zhǔn)的C運(yùn)行時庫時,_start函數(shù)通常是一個小的代碼存根,它將控制權(quán)傳遞給libc輔助函數(shù)__libc_start_main。然后,這個函數(shù)為程序的main函數(shù)準(zhǔn)備參數(shù)并調(diào)用它,main函數(shù)將會運(yùn)行程序的核心邏輯,如果main函數(shù)返回到__libc_start_main,則main函數(shù)的返回值就會被傳遞給exit以正常退出程序。2.5.4表位置字段表位置字段對二進(jìn)制分析員來說一般是不感興趣的,除非你想編寫代碼來手動解析ELF文件。它們向加載器描述了文件中程序頭和節(jié)頭的位置和數(shù)量,并為包含字符串表(stringtable)和符號表(symboltable)的特殊節(jié)提供指針,我們將在后面介紹。加載器使用這些字段來準(zhǔn)備內(nèi)存中的ELF文件,以備執(zhí)行。2.6ELF程序頭程序頭(programheader)表實(shí)際上描述了如何有效地將ELF二進(jìn)制文件加載到內(nèi)存中,以便加載器進(jìn)行加載。程序頭與節(jié)頭的不同之處在于,盡管它們都描述了程序的布局,但程序頭是以映射為中心的,而節(jié)頭則以更細(xì)粒度的邏輯單元來描述。程序頭定義了一系列的段(segment),每個段都告訴內(nèi)核如何啟動程序。這些段指定了如何以及從哪里將ELF文件的數(shù)據(jù)加載到內(nèi)存中、程序是否需要運(yùn)行時加載器來引導(dǎo)它、主線程的線程本地存儲的初始布局,以及其他與內(nèi)核相關(guān)的元數(shù)據(jù),如程序是否應(yīng)該被賦予可執(zhí)行線程棧。我們先用readelf命令看一下64位print64.so程序的程序頭:這個程序有9個程序頭,每個程序頭都有一個相應(yīng)的類型,如PHDR或INTERP,每個類型都描述了如何解釋程序頭。節(jié)到段的列表顯示了每個給定段(segment)內(nèi)包含哪些邏輯節(jié)(section)。例如,這里我們可以看到INTERP段只包含.interp節(jié)。2.6.1PHDR程序頭PHDR(ProgramHeadDeR,HeadDeR程序)是包含程序頭表和元數(shù)據(jù)本身的meta段。2.6.2INTERP程序頭INTERP程序頭用來告訴操作系統(tǒng),ELF文件需要另一個程序的幫助來把自己載入內(nèi)存。在幾乎所有的情況下,這個程序?qū)⑹遣僮飨到y(tǒng)的加載器文件,它的路徑是/lib/ld-linux-aarch64.so.1。當(dāng)一個程序被執(zhí)行時,操作系統(tǒng)使用這個程序頭將支持的加載器加載到內(nèi)存中,并將加載器而不是程序本身安排為初始執(zhí)行目標(biāo)。如果程序使用動態(tài)鏈接的庫,則必須使用外部加載器。外部加載器管理程序的全局符號表,處理將二進(jìn)制文件連接在一起的過程(稱為重定位),并在準(zhǔn)備就緒時最終調(diào)用程序的入口點(diǎn)。除了加載器之外,幾乎所有復(fù)雜程序都會使用該字段來指定系統(tǒng)加載器。INTERP程序頭只與程序文件本身相關(guān),對于在初始程序加載期間或在程序執(zhí)行期間動態(tài)加載的共享庫,該值會被忽略。2.6.3LOAD程序頭LOAD程序頭告訴操作系統(tǒng)和加載器如何盡可能高效地將程序的數(shù)據(jù)加載到內(nèi)存中。每個LOAD程序頭都指示加載器創(chuàng)建一個具有給定大小、內(nèi)存權(quán)限和對齊標(biāo)準(zhǔn)的內(nèi)存區(qū)域,并告訴加載器文件中的哪些字節(jié)要放在該區(qū)域中。如果我們再看一下前面例子中的LOAD程序頭,便可以看到程序定義了兩個內(nèi)存區(qū)域,要用ELF文件中的數(shù)據(jù)來填充。第一個內(nèi)存區(qū)域的長度為0xa3c字節(jié),具有64KB的對齊要求,被映射為可讀、可執(zhí)行但不可寫。這個區(qū)域應(yīng)該用ELF文件本身的0到0xa3c字節(jié)填充。第二個內(nèi)存區(qū)域的長度為0x290字節(jié),應(yīng)該被加載到第一節(jié)之后的0x10db8字節(jié)的位置,應(yīng)該被標(biāo)記為可讀和可寫,并將從文件中的偏移量0xdb8開始被填充0x288字節(jié)。值得注意的是,LOAD程序頭不一定要用文件中的字節(jié)來填充其定義的整個區(qū)域。例如,我們的第二個LOAD程序頭只填充了0x290大小的區(qū)域的前0x288字節(jié)。剩下的字節(jié)將被填充為零。在這種特殊情況下,最后的8個字節(jié)對應(yīng)于二進(jìn)制文件的.bss節(jié),編譯器使用這種加載策略在加載過程中將該節(jié)預(yù)置為零。LOAD段從根本上說是幫助操作系統(tǒng)和加載器將數(shù)據(jù)從ELF文件中高效地加載到內(nèi)存中,并且它們與二進(jìn)制文件的邏輯節(jié)進(jìn)行粗略映射。例如,如果我們再次查看之前的readelf輸出,則可以看到兩個LOAD程序頭中的第一個將加載與ELF文件的17個邏輯節(jié)相對應(yīng)的數(shù)據(jù),包括只讀數(shù)據(jù)和程序代碼,而第二個LOAD程序頭指示加載器加載剩余的7個節(jié),包括負(fù)責(zé)全局偏移表的節(jié)以及.data和.bss節(jié),如下所示:2.6.4DYNAMIC程序頭DYNAMIC程序頭被加載器用于動態(tài)鏈接程序和它們的共享庫依賴項(xiàng),以及在程序被加載到與預(yù)期不同的地址時對程序應(yīng)用重定位功能以修復(fù)程序代碼和指針。我們將在本章后面討論dynamic節(jié)以及鏈接和重定位過程。2.6.5NOTE程序頭NOTE程序頭用來存儲關(guān)于程序本身的供應(yīng)商元數(shù)據(jù)。該節(jié)基本上描述了一個鍵值對表,其中每個條目都有一個字符串名稱映射到描述該條目的字節(jié)序列上。ELF手冊文件中給出了一系列眾所周知的NOTE值及其含義。我們還可以使用readelf來查看給定ELF文件中NOTE條目的可讀描述。例如,我們可以在我們的print64.so文件中這樣做,如下所示:在這里,我們可以看到可執(zhí)行文件的NOTE條目描述了程序期望使用的GNUABI版本(在本例中為LinuxABI3.7.0),以及分配給二進(jìn)制文件的唯一構(gòu)建ID值,通常用于將崩潰轉(zhuǎn)儲與導(dǎo)致它們的二進(jìn)制文件相關(guān)聯(lián),以便對崩潰進(jìn)行診斷和分析。2.6.6TLS程序頭另一個常見的程序頭是TLS程序頭。TLS程序頭定義了TLS條目表,該表存儲了程序所使用的線程局部變量的信息。線程本地存儲是一個更高級的主題,詳見2.9節(jié)。2.6.7GNU_EH_FRAME程序頭這個程序頭定義了程序的棧展開表在內(nèi)存中的位置。棧展開表既被調(diào)試器使用,也被C++異常處理運(yùn)行時函數(shù)使用,這些函數(shù)被負(fù)責(zé)處理C++throw關(guān)鍵字的例程在內(nèi)部使用。這些例程也處理try...catch...final語句,在保持C++自動銷毀器和異常處理語義的同時展開棧。2.6.8GNU_STACK程序頭處理器沒有提供可用于阻止程序指令在內(nèi)存區(qū)域內(nèi)執(zhí)行的不執(zhí)行內(nèi)存保護(hù)。這意味著代碼可以被寫入棧并直接執(zhí)行。在實(shí)踐中,很少有程序會合法地這樣做。相比之下,黑客通常會利用程序中的內(nèi)存損壞漏洞,并利用可執(zhí)行的棧區(qū)域直接從棧中執(zhí)行特別制作的指令。引入32位和64位Arm處理器以及其他制造商的處理器支持的不執(zhí)行(No-eXecute,NX)內(nèi)存權(quán)限,意味著有可能將棧明確標(biāo)記為不執(zhí)行區(qū)域,從而阻止這些類型的攻擊。在Arm處理器中,這種緩解措施由XN(eXecuteNever,絕不執(zhí)行)位控制。如果啟用(設(shè)置為1),則嘗試在該不可執(zhí)行區(qū)域中執(zhí)行指令將導(dǎo)致權(quán)限故障。不幸的是,Linux的問題是雖然很少有程序合法地將可執(zhí)行指令寫入棧以供執(zhí)行,但實(shí)際仍存在這種情況,這會導(dǎo)致應(yīng)用程序兼容性問題。操作系統(tǒng)不能默認(rèn)強(qiáng)制設(shè)置棧為不可執(zhí)行(NX),否則將破壞需要可執(zhí)行棧的少數(shù)程序。解決這個問題的方法是使用GNU_STACK程序頭。GNU_STACK程序頭的內(nèi)容本身可被忽略,但程序頭的內(nèi)存保護(hù)字段被用來定義程序的線程棧將被授予的內(nèi)存保護(hù)。這使得大多數(shù)從不運(yùn)行線程棧代碼的程序可以告訴操作系統(tǒng),將程序的線程棧標(biāo)記為不可執(zhí)行是安全的。鏈接器LD負(fù)責(zé)創(chuàng)建GNU_STACK程序頭,因此當(dāng)通過GCC編譯程序時,我們可以通過GCC命令行選項(xiàng)來設(shè)置棧是否可執(zhí)行。使用選項(xiàng)-znoexecstack可以禁用可執(zhí)行棧,使用-zexecstack可以手動將棧強(qiáng)制分配為可執(zhí)行棧。為了看到這是如何工作的,我們故意使用可執(zhí)行棧重新編譯程序,然后使用readelf查看GNU_STACK程序頭,如下所示:我們可以通過查看進(jìn)程的內(nèi)存映射來查看當(dāng)前正在運(yùn)行的程序的這種行為的效果。使用之前的示例程序?qū)崿F(xiàn)這種行為有點(diǎn)困難,因?yàn)樗鼈冊趩雍蠛芸炀屯顺隽?,但我們可以使用以下兩行代碼的程序,它只是永久休眠,以便我們可以在運(yùn)行時檢查其內(nèi)存,而不必使用調(diào)試器:如果我們使用-zexecstack選項(xiàng)編譯此程序,運(yùn)行此程序時應(yīng)將棧標(biāo)記為可執(zhí)行。首先,我們編譯該程序:接下來在另一個終端窗口中使用./execstack.so運(yùn)行該程序,并使用另一個終端窗口來查找該程序的進(jìn)程ID。一個簡單的命令是pidof命令:現(xiàn)在我們知道了正在運(yùn)行的程序的進(jìn)程ID,我們可以通過偽文件/proc/pid/maps查看其內(nèi)存映射,在本例中該偽文件是/proc/7784/maps。這里給出了這個文件的輸出結(jié)果(考慮到可讀性,行已略微縮短):我們可以看到這里棧的權(quán)限被標(biāo)記為rwx,這意味著棧是可執(zhí)行的。如果我們省略-zexecstack編譯器選項(xiàng),重復(fù)之前的步驟,我們將看到棧被標(biāo)記為rw-(即不可執(zhí)行),如下所示:檢查短暫程序的內(nèi)存比較困難。對于這類情況,我們需要使用調(diào)試器(例如GDB),并使用其infoprocmappings命令來查看進(jìn)程運(yùn)行時的內(nèi)存。2.6.9GNU_RELRO程序頭與GNU_STACK一樣,GNU_RELRO程序頭用作編譯器的漏洞利用緩解措施。RELRO(RelocationRead-Only)的主要目的是指示加載器在程序加載后但開始運(yùn)行前將程序二進(jìn)制文件的某些關(guān)鍵區(qū)域標(biāo)記為只讀,以阻止漏洞利用者輕而易舉地改寫它們所包含的關(guān)鍵數(shù)據(jù)。RELRO用于保護(hù)全局偏移表(GlobalOffsetTable,GOT),以及包含函數(shù)指針的init和fini表,程序?qū)⒃诔绦虻膍ain函數(shù)運(yùn)行之前以及在最后調(diào)用exit期間(或在main返回后)運(yùn)行這些表。RELRO程序頭的具體機(jī)制很簡單。它定義了一個內(nèi)存區(qū)域和一個最終應(yīng)用的內(nèi)存保護(hù)機(jī)制,該保護(hù)機(jī)制應(yīng)該在程序做好運(yùn)行準(zhǔn)備后通過mprotect調(diào)用來實(shí)現(xiàn)。我們再次使用readelf查看程序頭,看看它們?nèi)绾螒?yīng)用于RELRO程序頭。如果我們看一下節(jié)到段的映射,便可以看到這里RELRO要求加載器在程序啟動前將二進(jìn)制文件的.init_array、.fini_array、.dynamic和.got節(jié)標(biāo)記為只讀,分別保護(hù)程序初始化器、非初始化器、整個.dynamic節(jié)和全局偏移表。如果程序還定義了TLS數(shù)據(jù),那么.tdata節(jié)的TLS模板數(shù)據(jù)通常也會被RELRO區(qū)域所保護(hù)。RELRO緩解措施有兩種:部分RELRO和完整RELRO。可以通過表2.5所示的命令行選項(xiàng)指示鏈接器啟用部分RELRO、啟用完整RELRO,甚至禁用RELRO。表2.5RELRO選項(xiàng)部分RELRO和完整RELRO的主要區(qū)別在于,部分RELRO不保護(hù)全局偏移表中負(fù)責(zé)管理程序鏈接表的部分(通常稱為.plt.got),該部分用于惰性地綁定導(dǎo)入的函數(shù)符號。完整RELRO強(qiáng)制對所有庫函數(shù)調(diào)用進(jìn)行加載時綁定,因此可以將.got和.got.plt節(jié)都標(biāo)記為只讀。這可以防止一種常見的控制流漏洞利用技術(shù),該技術(shù)通過覆蓋.got.plt節(jié)的函數(shù)指針來重定向程序的執(zhí)行流,但同時也會稍微降低大型程序的啟動性能。我們可以使用命令行工具,如開源的checksec.sh工具(包含在Fedora中),通過以下語法來檢查是否在給定的程序二進(jìn)制文件上啟用了完整RELRO、部分RELRO或完全禁用RELRO:2.7ELF節(jié)頭程序頭是ELF文件的一個以數(shù)據(jù)為中心的視圖,它告訴操作系統(tǒng)如何有效地將程序直接放入內(nèi)存,與此相反,節(jié)頭將ELF二進(jìn)制文件分解為邏輯單元。ELF程序頭指定了ELF文件中節(jié)頭表的數(shù)量和位置。我們可以使用readelf工具查看給定二進(jìn)制文件節(jié)頭的信息,如下所示:另一種以更易讀的格式查看這些帶標(biāo)志的節(jié)頭的方法是使用objdump實(shí)用程序(考慮到可讀性,此處輸出已被截斷,只顯示基本部分)。與程序頭類似,我們可以看到每個節(jié)頭都描述了加載的二進(jìn)制文件中的一個內(nèi)存區(qū)域,它由地址和區(qū)域大小定義。每個節(jié)頭還有一個名稱、一個類型,以及可選的一系列輔助標(biāo)志字段,它們描述如何解釋節(jié)頭。例如,.text節(jié)被標(biāo)記為只讀代碼,而.data節(jié)被標(biāo)記為數(shù)據(jù),既不是代碼也不是只讀數(shù)據(jù),因此被標(biāo)記為讀/寫。其中一些節(jié)與程序頭等效項(xiàng)一一對應(yīng),這里不再贅述。例如,.interp節(jié)只包含程序頭INTERP使用的數(shù)據(jù),而NOTE節(jié)是NOTE程序頭的兩個條目。其他節(jié)(如.text、.data和.init_array)描述程序的邏輯結(jié)構(gòu),并由加載器在執(zhí)行前用于初始化程序。接下來,我們將介紹在逆向工程中遇到的最重要的ELF節(jié)以及它們的工作原理。2.7.1ELFmeta節(jié)二進(jìn)制文件有兩個節(jié)是meta節(jié),它們對ELF文件有特殊的意義,并被用于其他節(jié)表的查詢。它們是字符串表和符號表,前者定義了ELF文件使用的字符串,后者定義了其他ELF節(jié)引用的符號。字符串表首先要介紹的是字符串表(stringtable)。字符串表定義了ELF文件所需的所有字符串,但通常不包含程序所使用的字符串字面量。字符串表是ELF文件所使用的所有字符串的直接串聯(lián),每個字符串以終止零字節(jié)結(jié)尾。字符串表被ELF文件中具有字符串字段的結(jié)構(gòu)所使用。這些結(jié)構(gòu)通過字符串表的偏移來指定字符串的值,節(jié)表就是這樣的結(jié)構(gòu)。每個節(jié)都有一個名稱,比如.text、.data或者.strtab。例如,如果字符串.strtab在字符串表中的偏移量為67,那么.strtab節(jié)的節(jié)頭將在其name字段中使用數(shù)字67。在某種程度上,這給加載器創(chuàng)建了一個“雞生蛋”問題。如果加載器在知道字符串表的位置之前不能檢查各節(jié)的名稱,它怎么能知道哪一節(jié)是字符串表?為了解決這個問題,ELF程序頭提供了一個直接指向字符串表的指針。這允許加載器在解析ELF文件的其他節(jié)之前追蹤字符串表。符號表接下來要介紹的是符號表(symboltable)。符號表定義了程序二進(jìn)制文件所使用或定義的符號。表中的每個符號都定義了以下內(nèi)容:●一個唯一的名稱(指定為字符串表的偏移量)。●符號的地址(或值)?!穹柕拇笮?。●關(guān)于符號的輔助元數(shù)據(jù),如符號類型。符號表在ELF文件格式中被廣泛使用。其他引用符號的表會將其作為符號表的查找。2.7.2主要的ELF節(jié)ELF文件中許多常見的節(jié)僅僅定義了代碼或數(shù)據(jù)被加載到內(nèi)存的區(qū)域。從加載器的角度來看,加載器根本不解釋這些節(jié)的內(nèi)容——它們被標(biāo)記為PROGBITS(或NOBITS)。然而,對于逆向工程來說,識別這些節(jié)是很重要的。.text節(jié)按照慣例,由編譯器生成的機(jī)器碼指令將全部放在程序二進(jìn)制文件的.text節(jié)中。.text節(jié)被標(biāo)記為可讀、可執(zhí)行但不可寫。這意味著如果程序試圖意外地修改自己的程序代碼,該程序?qū)⒂|發(fā)分段故障。.data節(jié)在程序中定義的普通全局變量,無論是顯示定義為全局變量還是定義為靜態(tài)函數(shù)局部變量,都需要被賦予一個在程序生命周期內(nèi)靜態(tài)的唯一地址。默認(rèn)情況下,將在ELF文件的.data節(jié)中為這些全局變量分配地址,并為其設(shè)置初始值。例如,如果我們在程序中定義了全局變量intmyVar=3,myVar的符號將存在于.data節(jié),長度為4字節(jié),初始值為3,這個初始值將被寫入.data節(jié)。.data節(jié)通常被保護(hù)為可讀/可寫。盡管全局變量的初始值是在.data節(jié)定義的,但在程序執(zhí)行過程中,程序可以自由地讀取和覆蓋這些全局變量。.bss節(jié)對于那些未被程序員初始化或被初始化為零的全局變量,ELF文件提供了一種優(yōu)化:塊起始符號(.bss)節(jié)。該節(jié)的操作與.data節(jié)相同,只是其中的變量在程序開始前自動初始化為零。這避免了在ELF文件中存儲多個全局變量“模板”(這些模板僅包含零),從而使ELF文件更小,并避免了在程序啟動期間進(jìn)行一些不必要的文件訪問(為了將零從磁盤加載到內(nèi)存中)。.rodata節(jié)只讀數(shù)據(jù)節(jié).rodata用于存儲程序中不應(yīng)在程序執(zhí)行期間修改的全局?jǐn)?shù)據(jù)。該節(jié)存儲被標(biāo)記為const的全局變量,同時存儲在給定程序中使用的常量C字符串字面量。舉例來說,我們可以使用objdump工具來轉(zhuǎn)儲示例程序的只讀數(shù)據(jù)節(jié)的內(nèi)容,顯示字符串字面量Hello、and、%s和!都被輸出到最終二進(jìn)制文件的.rodata節(jié)中。.tdata和.tbss節(jié)編譯器在程序員使用線程局部變量時使用.tdata和.tbss節(jié)。線程局部變量是使用C++中的__thread_local關(guān)鍵字或者GCC或clang特定關(guān)鍵字__thread注釋的全局變量。2.7.3ELF符號在查看.dynamic節(jié)之前,我們首先需要了解ELF符號。在ELF文件格式中,符號是程序或外部定義符號中的命名(可選版本化)位置。在程序或共享二進(jìn)制文件中定義的符號在ELF文件的主符號表中指定。函數(shù)和全局?jǐn)?shù)據(jù)對象都可以有與之相關(guān)的符號名稱,但符號也可以分配給線程局部變量、運(yùn)行時內(nèi)部對象(如全局偏移表),甚至是位于特定函數(shù)內(nèi)部的標(biāo)簽。查看特定程序二進(jìn)制文件的符號表的一種方法是通過readelf-r命令行。例如,查看ld-linux-aarch64.so.1二進(jìn)制文件可以發(fā)現(xiàn)以下符號:查看ELF文件符號表的另一個工具是命令行工具nm,它有一些額外的功能,對查看編譯過的C++程序的符號很有用。例如,我們可以使用這個工具利用選項(xiàng)-g將符號限制在只導(dǎo)出的符號,還可以要求nm使用-C選項(xiàng)自動取消C++符號的裝飾,如下面來自libstdc++的符號列表(輸出已截斷):符號表中的每個符號條目都定義了以下屬性:●符號名稱?!穹柦壎▽傩?,例如符號是弱的、本地的,還是全局的?!穹栴愋停ǔJ潜?.6中所示的值之一?!穹査幍墓?jié)索引?!穹柕闹担ǔJ撬趦?nèi)存中的地址?!穹柕拇笮?。對于數(shù)據(jù)對象來說,這通常是數(shù)據(jù)對象的大小,單位是字節(jié);對于函數(shù)來說,是函數(shù)的長度,單位是字節(jié)。表2.6符號類型全局與本地符號符號的綁定屬性定義了符號在鏈接過程中是否應(yīng)該對其他程序可見。符號可以是本地的(STB_LOCAL)、全局的(STB_GLOBAL),也可以兩者都不是。本地符號是不應(yīng)該對當(dāng)前ELF文件以外的程序可見的符號。加載器會忽略這些符號以進(jìn)行動態(tài)鏈接。相比之下,全局符號則在程序或共享庫之外被顯式共享。整個程序中只允許有一個這樣的符號。弱符號符號也可以被定義為弱符號。弱符號對于創(chuàng)建函數(shù)的默認(rèn)實(shí)現(xiàn)非常有用,可以被其他庫所重寫。使用GCC編譯的C程序和C++程序可以使用__attribute__((weak))屬性語法或通過C/C++代碼中的#pragmaweak符號指令將函數(shù)和數(shù)據(jù)標(biāo)記為弱。例如,malloc和其他內(nèi)存分配例程經(jīng)常使用弱符號定義。這使得希望使用程序特定替代方案覆蓋這些默認(rèn)實(shí)現(xiàn)的程序可以這樣做,而無須進(jìn)行函數(shù)掛鉤。例如,程序可以鏈接到一個庫,該庫提供了針對與內(nèi)存分配相關(guān)的錯誤的附加檢查。由于該庫為這些內(nèi)存分配例程定義了一個強(qiáng)符號,因此該庫將覆蓋GLIBC提供的默認(rèn)實(shí)現(xiàn)。符號版本符號版本管理是一個高級主題,通常在編寫程序或?qū)Τ绦蜻M(jìn)行逆向工程時不需要使用,但在對系統(tǒng)庫(如glibc)進(jìn)行逆向工程時,偶爾會看到它。在之前的例子中,以@GLIBC_PRIVATE結(jié)尾的符號被“版本化”為GLIBC_PRIVATE版本,而以@GLIBC_2.17結(jié)尾的符號被“版本化”為GLIBC_2.17版本。在抽象層面上,符號版本的工作原理如下,程序需要以一種打破現(xiàn)有應(yīng)用程序二進(jìn)制接口(ApplicationBinaryInterface,ABI)的方式進(jìn)行更新,例如,更新一個函數(shù)以包括一個額外的參數(shù)并要求使用相同的名稱。如果程序是一個核心系統(tǒng)庫,這些類型的更改就會帶來問題,因?yàn)榇蚱艫BI的更改需要重新編譯依賴庫的每個程序。這個問題的解決方案是進(jìn)行符號版本控制。這里,程序同時定義了新符號和舊符號,但顯式地用不同版本標(biāo)記兩個符號。使用新版本編譯的程序?qū)o縫地使用新符號,而使用舊版本編譯的程序?qū)⑹褂门f符號,從而保持ABI兼容性。符號版本控制的另一個用途是從共享庫中導(dǎo)出一個符號,該符號不應(yīng)被某些特定其他庫之外的程序意外使用。在這種情況下,GLIBC_PRIVATE符號被用來“隱藏”內(nèi)部的glibc符號,因此只有內(nèi)部的GLIBC系統(tǒng)庫可以調(diào)用這些函數(shù),其他程序無法意外導(dǎo)入該符號。符號版本表的定義和分配是通過ELF文件的.gnu.version_d和.gnu.version節(jié)進(jìn)行管理的。映射符號映射符號是Arm架構(gòu)專用的特殊符號。它們的存在是因?yàn)锳rm二進(jìn)制文件中的.text節(jié)有時包含多種不同類型的內(nèi)容。例如,32位Arm二進(jìn)制文件可能包含32位Arm指令集編碼的指令、Thumb指令集編碼的指令,以及常量。映射符號用來幫助調(diào)試器和反匯編器確定如何解釋文本節(jié)中的字節(jié)。這些符號僅用于提供信息,不會改變處理器解釋節(jié)中數(shù)據(jù)的方式。表2.7顯示了32位和64位Arm的映射符號。表2.7映射符號映射符號也可以選擇在后面加一個句號,然后后面接字符序列,這不會改變其含義。例如,符號$d.realdata表示后面的序列是數(shù)據(jù)。2.8.dynamic節(jié)和動態(tài)加載在ELF文件格式中,.dynamic節(jié)用于指示加載器如何鏈接和準(zhǔn)備二進(jìn)制文件以供執(zhí)行。我們可以使用readelf-d命令詳細(xì)查看ELF文件的.dynamic節(jié)。這些節(jié)由加載器處理,最終形成一個可以運(yùn)行的程序。與我們看到的其他表一樣,每個條目都有相應(yīng)的類型,詳細(xì)說明了它的解釋方式,以及其數(shù)據(jù)相對于.dynamic節(jié)開頭的位置。令人困惑的是,DYNAMIC程序頭還維護(hù)著自己的符號表和字符串表,這些表與ELF文件的主字符串表和符號表無關(guān)。它們的位置由STRTAB和SYMTAB指定,它們的大小由STRSZ字段和SYMENT字段決定,前者是以字節(jié)為單位的字符串表大小,后者是動態(tài)符號表中的符號項(xiàng)數(shù)量。2.8.1依賴項(xiàng)加載加載器處理的第一個主要動態(tài)表項(xiàng)是NEEDED項(xiàng)。大多數(shù)現(xiàn)代程序都不是完全孤立的單元,都依賴于從系統(tǒng)和其他庫中導(dǎo)入的函數(shù)。例如,一個需要在堆上分配內(nèi)存的程序可能會使用malloc,但程序員不太可能自己編寫malloc實(shí)現(xiàn),相反會使用操作系統(tǒng)提供的默認(rèn)實(shí)現(xiàn)。在程序加載期間,加載器還會遞歸地加載程序的所有共享庫依賴項(xiàng)以及它們的依賴項(xiàng)。程序通過動態(tài)節(jié)中的NEEDED指令告訴加載器它依賴于哪些庫。程序使用的每個依賴項(xiàng)都有自己的NEEDED指令,加載器會依次加載每個依賴項(xiàng)。一旦共享庫完全可運(yùn)行并準(zhǔn)備好被使用,NEEDED指令就完成了。2.8.2程序重定位加載器的第二項(xiàng)任務(wù)是在加載程序的依賴項(xiàng)后執(zhí)行重定位和鏈接步驟。重定位表可以是兩種格式之一:REL或RELA。它們的編碼略有不同。重定位數(shù)分別在動態(tài)節(jié)的RELSZ或RELASZ字段中給出。我們可以使用readelf-r命令查看程序的重定位表。在給定程序二進(jìn)制文件中發(fā)現(xiàn)的重定位類型因指令集架構(gòu)而有很大不同。例如,我們可以在這個程序中看到,所有的重定位都是64位Arm專用的。重定位大致分為三類:●靜態(tài)重定位通常是指在程序二進(jìn)制文件中更新指針并動態(tài)重寫指令,以便在程序需要被加載到非默認(rèn)地址時使用?!駝討B(tài)重定位通常是指引用共享庫依賴項(xiàng)中的外部符號。●線程本地重定位通常是指為每個線程存儲一個偏移量,該偏移量指向線程本地存儲區(qū)域,以便給定的線程局部變量可以使用它。本章稍后將會討論線程本地存儲。靜態(tài)重定位我們已經(jīng)看到,ELF文件定義了一系列的程序頭,這些程序頭指定了ELF文件應(yīng)該被操作系統(tǒng)和加載器加載到內(nèi)存的方式和位置。傳統(tǒng)上,ELF程序文件將使用這種機(jī)制來準(zhǔn)確指定它們應(yīng)該被加載到內(nèi)存中的哪些地址,該地址稱為程序的首選地址。例如,程序文件通常會要求在內(nèi)存地址0x400000處加載,而共享庫會選擇一些遠(yuǎn)高于地址空間的其他固定地址。由于各種原因,加載器和操作系統(tǒng)可能會選擇將程序或共享庫加載到首選地址以外的地址。一個原因可能是首選地址的區(qū)域不可用,因?yàn)樵搮^(qū)域中已有其他東西,比如映射文件或其他共享庫。另一個常見原因是程序和操作系統(tǒng)支持地址空間布局隨機(jī)化(AddressSpaceLayoutRandomization,ASLR)。ASLR是一種漏洞利用緩
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 車隊(duì)安全培訓(xùn)總結(jié)反思
- 2026年消防安全及防火安全知識競賽試題及答案
- 車間負(fù)責(zé)人安全培訓(xùn)講話課件
- 2026年燃?xì)獍踩R競賽試題及答案
- 車間級安全培訓(xùn)目的課件
- 車間級安全培訓(xùn)學(xué)時課件
- 2026年煤礦采煤機(jī)(掘進(jìn)機(jī))操作考試試題及答案
- 銀行金融衍生品業(yè)務(wù)制度
- 2026年寄生蟲及檢驗(yàn)試題及答案
- 2026年電工考試題及答案
- DB34∕T 4700-2024 智慧中藥房建設(shè)與驗(yàn)收規(guī)范
- 穿越機(jī)基礎(chǔ)課件
- 谷歌員工關(guān)系管理案例
- 班級互動小游戲-課件共30張課件-小學(xué)生主題班會版
- 物流企業(yè)倉儲安全操作規(guī)程與培訓(xùn)教材
- 黃體酮破裂課件
- 中學(xué)學(xué)生教育懲戒規(guī)則實(shí)施方案(2025修訂版)
- ISO 9001(DIS)-2026與ISO9001-2015英文標(biāo)準(zhǔn)對照版(編輯-2025年9月)
- 結(jié)算審計踏勘現(xiàn)場實(shí)施方案詳細(xì)版
- 手機(jī)玻璃工廠年終總結(jié)報告
- 全國大學(xué)生職業(yè)規(guī)劃大賽《信息與計算科學(xué)》專業(yè)生涯發(fā)展展示
評論
0/150
提交評論