為MacOS構(gòu)建自定義Mach-O內(nèi)存加載器
在上一篇文章中,我們介紹了如何修復(fù)dyld以恢復(fù)內(nèi)存執(zhí)行。這種方法的優(yōu)點(diǎn)之一是,我們將加載Mach-O二進(jìn)制文件的許多復(fù)雜工作委托給macOS。但如果我們?cè)诓皇褂胐yld的情況下,創(chuàng)建我們自己的加載器呢?所有這些字節(jié)映射是如何工作的?
接下來,我們將介紹如何在不使用dyld的情況下在MacOS Ventura中為Mach-O包構(gòu)建內(nèi)存加載器,以及Mach-O文件的組成,dyld如何處理加載命令以將區(qū)域映射到內(nèi)存中。
為了配合蘋果向ARM架構(gòu)的遷移,這篇文章將重點(diǎn)介紹MacOS Ventura的AARCH64版本和針對(duì)MacOS 12.0及更高版本的XCode。
什么是Mach-O文件?
首先介紹一下Mach-O文件的架構(gòu),建議先閱讀一下Aidan Steele的Mach-O文件格式參考。
當(dāng)我們?cè)谔幚鞟RM版本的MacOS時(shí),會(huì)假設(shè)正在查看的Mach-O沒有被封裝在Universal 2格式中,因此在文件開頭我們首先會(huì)遇到的是Mach_header_64:
要構(gòu)造加載器,我們需要檢查以下幾個(gè)字段:
magic-此字段應(yīng)包含MH_magic_64的值;
Cputype-對(duì)于M1,應(yīng)為CPU_TYPE_ARM64。
filetype -我們將檢查這篇文章的MH_BUNDLE類型,但加載不同類型也應(yīng)該很容易。
如果Mach-O是正常的,我們可以立即處理mach_header_64結(jié)構(gòu)體后面的load命令。
加載命令
顧名思義,load命令是一種數(shù)據(jù)結(jié)構(gòu),用于指示dyld如何加載Mach-O區(qū)域。
每個(gè)load命令由load_command結(jié)構(gòu)表示:
cmd字段最終決定load_command實(shí)際表示的內(nèi)容,以LC_UUID的一個(gè)非常簡(jiǎn)單的load_command為例,該命令用于將UUID與二進(jìn)制數(shù)據(jù)關(guān)聯(lián)起來。其結(jié)構(gòu)如下:
如上所述,這與load_command結(jié)構(gòu)重疊,這就是為什么我們有匹配字段的原因。以下就是我們將看到的各種負(fù)載命令所支持的情況。
Mach-O段
加載Mach-O時(shí),我們要處理的第一個(gè)load_command是LC_SEGMENT_64。
segment命令告訴dyld如何將Mach-O的一個(gè)區(qū)域映射到虛擬內(nèi)存中,它應(yīng)該有多大,應(yīng)該有什么樣的保護(hù),以及文件的內(nèi)容在哪里。讓我們來看看它的結(jié)構(gòu):
出于本文的目的,我們將關(guān)注:
segname -段的名稱,例如__TEXT;
vmaddr -應(yīng)該加載段的虛擬地址。例如,如果它被設(shè)置為0x4000,那么我們將在分配的內(nèi)存基數(shù)+ 0x4000處加載段;
vmsize -要分配的虛擬內(nèi)存的大小;
fileoff -從文件開始到應(yīng)復(fù)制到虛擬內(nèi)存的Mach-O內(nèi)容的偏移量;
filesize -要從文件中復(fù)制的字節(jié)數(shù);
maxprot-應(yīng)分配給虛擬內(nèi)存區(qū)域的最大內(nèi)存保護(hù)值;
initprot -應(yīng)分配給虛擬內(nèi)存區(qū)域的初始內(nèi)存保護(hù);
nsects -遵循此段結(jié)構(gòu)的節(jié)數(shù)。
要注意,雖然dyld依賴mmap將Mach-O的片段拉入內(nèi)存,但如果我們的初始進(jìn)程是作為一個(gè)加固進(jìn)程執(zhí)行的(并且沒有com.apple.security.cs. c . data . data之類的文件)。使用mmap是不可能的,除非我們提供的bundle是使用與代理應(yīng)用程序相同的開發(fā)人員證書進(jìn)行簽名的。此外,我們正在嘗試構(gòu)建一個(gè)內(nèi)存加載器,因此在這種情況下從磁盤拉二進(jìn)制文件沒有多大意義。
為了解決這個(gè)問題,在此POC中,我們將預(yù)先分配我們的blob內(nèi)存并復(fù)制它,例如:
與之前的dyld文章一樣,我們需要在主機(jī)二進(jìn)制文件中使用正確的授權(quán)來允許無符號(hào)可執(zhí)行內(nèi)存。
節(jié)
從上面的字段中可以看到,段加載命令中存在另一個(gè)引用,這就是一個(gè)節(jié)(section)。
由于節(jié)位于段中,雖然它將繼承其內(nèi)存保護(hù),但它有自己的大小和要加載的文件內(nèi)容。每個(gè)段的數(shù)據(jù)結(jié)構(gòu)附加到segment命令中,其結(jié)構(gòu)為:
同樣,我們將只關(guān)注其中幾個(gè)字段,這些字段對(duì)于我們構(gòu)建加載器的直接目的很有幫助:
sectname -節(jié)的名稱,例如__text;
segname -與此節(jié)關(guān)聯(lián)的段的名稱;
addr -用于此節(jié)的虛擬地址偏移量;
size -文件中(以及虛擬內(nèi)存中的)節(jié)的大小;
offset - Mach-O文件中部分內(nèi)容的偏移量;
flags - flags可以分配給一個(gè)節(jié),這個(gè)節(jié)幫助確定reserved1,reserved2和reserved3中的值。
由于我們已經(jīng)分配了每個(gè)段,所以加載器將遍歷每個(gè)段描述符,確保將正確的文件內(nèi)容復(fù)制到虛擬內(nèi)存中。需要注意的是,在復(fù)制時(shí)可能需要更新內(nèi)存保護(hù)。MacOS for ARM不允許讀/寫/執(zhí)行內(nèi)存頁(yè)(除非com.apple.security.cs. c。allow-jit授權(quán)與MAP_JIT一起使用),因此我們需要在復(fù)制時(shí)適應(yīng)這一點(diǎn):
符號(hào)
隨著我們的加載器開始成型,接下來需要看看如何處理符號(hào)(Symbol)。符號(hào)在Mach-O二進(jìn)制文件的加載過程中扮演著重要的角色,它將名稱和序數(shù)關(guān)聯(lián)到內(nèi)存區(qū)域,以供我們稍后參考。
符號(hào)是通過LC_SYMTAB的加載命令來處理的,如下所示:
同樣,我們將關(guān)注構(gòu)建加載器所需的字段:
symoff -從文件開始到包含每個(gè)符號(hào)信息的nlist結(jié)構(gòu)數(shù)組的偏移量;
nsyms -符號(hào)(或nlist結(jié)構(gòu))的數(shù)量;
stroff -符號(hào)查找所使用的字符串的文件偏移量。
顯然,接下來我們需要知道nlist是什么:
此結(jié)構(gòu)為我們提供了有關(guān)命名符號(hào)的信息:
n_strx -從符號(hào)字符串字段到該符號(hào)字符串的偏移量;
n_value -包含符號(hào)的值,例如地址。
因?yàn)槲覀兩院笮枰梅?hào),所以我們的加載器需要存儲(chǔ)這些信息以備以后使用:
dylib’s
接下來是LC_LOAD_DYLIB加載命令,該命令引用在運(yùn)行時(shí)加載的額外dylib’s。
我們需要的項(xiàng)在dylib結(jié)構(gòu)成員中找到,特別是dylib.name.offset,它是從這個(gè)加載命令的開頭到包含要加載的dylib的字符串的偏移量。
稍后,當(dāng)涉及到重定位時(shí),我們將需要這些信息,其中dylib’s的導(dǎo)入順序起著重要作用,因此我們將構(gòu)建一個(gè)dylib’s數(shù)組,供以后使用:
遷移
現(xiàn)在就要介紹Mach-O更復(fù)雜的部分——遷移。
Mach-O是用XCode構(gòu)建的,目標(biāo)是macOS 12.0和更高版本,使用LC_DYLD_CHAINED_FIXUPS的加載命令。關(guān)于這一切是如何工作的,沒有太多的文檔,但Noah Martin對(duì)iOS 15查找鏈的研究值得參考,我們還可以在這里找到蘋果XNU repo中使用的結(jié)構(gòu)體的詳細(xì)信息。
Dyld’s的源代碼告訴我們,該加載命令以結(jié)構(gòu)linkedit_data_command開始:
使用dataoff便能找到標(biāo)頭:
我們需要做的第一件事是收集所有導(dǎo)入并構(gòu)造一個(gè)稍后將引用的有序數(shù)組。為此,我們將使用以下字段:
symbols_offset -從該結(jié)構(gòu)開始到導(dǎo)入所使用的符號(hào)字符串的偏移量;
imports_count -導(dǎo)入項(xiàng)的數(shù)量;
imports_format -任何導(dǎo)入符號(hào)的格式。
imports_offset -從該結(jié)構(gòu)開始到導(dǎo)入表的偏移量。
每個(gè)導(dǎo)入項(xiàng)的數(shù)據(jù)結(jié)構(gòu)都依賴于imports_format字段,但通常我看到的是DYLD_CHAINED_IMPORT格式:
可以看出這是一個(gè)32位數(shù)組項(xiàng),有l(wèi)ib_ordinal字段,它是我們之前從LC_LOAD_DYLIB加載命令構(gòu)建的有序dylib數(shù)組的索引。索引從1開始,而不是0,這意味著第一個(gè)索引是1,然后是2……
如果索引值為0或253,則該項(xiàng)引用this-image(當(dāng)前正在執(zhí)行的二進(jìn)制文件)。這就是我們之前構(gòu)造符號(hào)字典的原因,因?yàn)楝F(xiàn)在我們可以簡(jiǎn)單地將自己二進(jìn)制文件中引用的符號(hào)名稱解析為其地址:
name_offset是從dyld_chained_fixups_header收集的symbols_offset字符串的偏移量。
使用這些信息,我們需要構(gòu)建一個(gè)有序的導(dǎo)入數(shù)組,因?yàn)槲覀冃枰R上引用這個(gè)有序數(shù)組。
構(gòu)建了一個(gè)導(dǎo)入列表后,將開始鏈?zhǔn)絾?dòng),這可以從dyld_chained_fixups_header結(jié)構(gòu)的starts_offset標(biāo)頭字段中找到。
鏈?zhǔn)絾?dòng)的結(jié)構(gòu)是:
為了導(dǎo)航,我們需要遍歷seg_info_offset中的每個(gè)項(xiàng),這為我們提供了指向dyld_chained_starts_in_segment的指針列表:
首先要注意這個(gè)結(jié)構(gòu),有時(shí)segment_offset是0,但不知道為什么,看起來dyld也識(shí)別了這個(gè),只是忽略了它們。
我們需要找到每個(gè)reloc鏈的開始位置的字段如下:
pointer_format-鏈?zhǔn)褂玫腄YLD_CHAINED_PTR_結(jié)構(gòu)的類型;
segment_offset-段起始地址在內(nèi)存中的絕對(duì)偏移量;
page_count-page_start成員數(shù)組中的頁(yè)數(shù);
page_start-從頁(yè)面到鏈開始的偏移量。
當(dāng)我們?cè)谝粋€(gè)段中有一個(gè)有效的偏移量時(shí),我們可以開始遵循reloc鏈。遍歷每個(gè)項(xiàng),我們需要檢查第一位,以確定該項(xiàng)是一個(gè)rebase(設(shè)置為0)還是一個(gè)bind(設(shè)置為1):
在rebase的情況下,將該項(xiàng)轉(zhuǎn)換為dyld_chained_ptr_64_rebase,并使用目標(biāo)偏移量更新該項(xiàng)到已分配內(nèi)存的基數(shù)。
在綁定的情況下,我們使用dyld_chained_ptr_64_bind,序數(shù)字段是我們前面構(gòu)建的導(dǎo)入數(shù)組的偏移量。
然后,我們需要移動(dòng)到下一個(gè)bind或rebase,這是通過執(zhí)行next*4(4字節(jié)是步長(zhǎng))來完成的。我們重復(fù)此操作,直到下一個(gè)字段為0,表示鏈已結(jié)束。
構(gòu)建加載器
現(xiàn)在一切就緒,開始構(gòu)建加載器。步驟如下:
1.分配內(nèi)存區(qū)域;
2.根據(jù)LC_SEGMENT_64命令將每個(gè)段加載到虛擬內(nèi)存中;
3.將每個(gè)節(jié)加載到每個(gè)段中;
4.從LC_LOAD_DYLIB命令構(gòu)建dylib的有序集合;
5.從LC_SYMTAB命令構(gòu)建一個(gè)符號(hào)集合。
6.遍歷LC_DYLD_CHAINED_FIXUPS鏈并對(duì)每個(gè)reloc進(jìn)行bind或rebase。
一旦完成,我們就可以使用LC_SYMTAB中的數(shù)據(jù)來引用我們想要輸入的符號(hào)并傳遞執(zhí)行。如果一切順利,我們將看到Mach-O被加載到內(nèi)存中并開始執(zhí)行:
這個(gè)POC的所有代碼都已添加到Dyld-DeNeuralyzer項(xiàng)目。
雖然你可以使用其中的代碼加載C/ c++包,但如果你嘗試加載Objective-C包,你會(huì)看到如下的內(nèi)容:
這是因?yàn)樵诩虞dObjective-C Mach-O時(shí)dyld中發(fā)生了一些事情,具體原因我們下一部分再講。
本文翻譯自:https://blog.xpnsec.com/building-a-mach-o-memory-loader-part-1/