你有沒有想過編譯器如何查看你的數據結構? Compiler Explorer 可以幫助你了解源代碼和設備代碼之間的關係,但它在數據佈局方面提供的支持不多。你可能聽說過填充、對齊和“普通舊數據類型”,甚至通過將一種結構嵌入到另一種結構中來模擬C 中的繼承。但是,你能猜出所有這些類型的確切內存佈局,而無需查看你平台的ABI 引用或標準庫的源代碼嗎?
有了必要的ABI 知識,關於C 結構的推理就相對簡單了。然而,更複雜的c++類型則是完全不同的情況,特別是當使用模板和繼承時。理想情況下,我們能夠將所有這些複雜的類型轉換成簡單的C結構,這樣我們就可以更容易地推斷它們在內存中的佈局。這正是relic -headergen的目的,這是我在Trail of Bits實習期間開發的一個工具。在這篇文章中,我將解釋它的工作原理。
rellic-headergenrellic-headergen的目的是生成C類型定義,這些定義等價於包含在LLVM位碼文件中的那些定義,這些定義不一定是從C源代碼生成的。這有助於分析包含複雜數據佈局的程序的過程。下圖提供了rellic-headergen 功能的示例。
左側窗口顯示我們的源代碼,我們在底部窗口中執行第一個命令將代碼編譯為LLVM位碼,然後使用第二個命令通過rellich headergen運行它。右邊的窗口顯示rellic-headergen的輸出,它是與輸入c++代碼的佈局匹配的有效C代碼。
該工具的工作原理是假設被分析的程序可以被編譯成具有完整調試信息的LLVM位碼。該實用程序開始構建一個包含調試信息可用的所有類型的列表,從函數(“subprogram”)定義開始。
現在,該實用程序需要決定定義類型的順序,但考慮到C 語言的要求,這不是一項簡單的任務:當引用尚未定義的類型時,該語言需要明確前置聲明,例如,結構不能包含其類型僅被前向聲明的字段。
解決這個問題的一種方法是預防性地前置前聲明所有當前的類型。然而,這是不夠的。例如,結構不能包含類型尚未完全定義的字段,儘管它可以包含類型是指向正向聲明類型的指針的字段。
因此,該實用程序根據類型定義形成一個有向無環圖(DAG),它可以在其上找到拓撲排序。
一旦該實用程序找到了一個拓撲排序,它就可以按照這個順序檢查類型,並且確信任何字段的類型都已完全被定義。
關於結構的惡作劇DWARF 元數據提供了一些信息,我們可以使用它來恢復它描述的類型的C 結構定義:
類型的大小;
每個字段的類型;
每個字段的偏移量;
類型最初是結構體還是聯合體;
rellic-headergen 的重建算法首先按照偏移量遞增的順序對字段進行排序,然後定義一個新的結構來添加每個字段。元數據沒有提供關於原始定義是否被聲明為打包的信息,因此rellic-headergen 首先嘗試直接生成佈局,如元數據指定的那樣。如果生成的佈局與作為輸入的佈局不匹配,該實用程序將從頭開始並生成一個打包佈局,並根據需要手動插入填充。
現在,我們可以使用任意數量的複雜啟發式方法來確定每個字段從結構開始的偏移量,但事情可能會變得非常棘手,尤其是在位字段的情況下。更好的方法是從已經制定出邏輯的東西中獲取這些信息:編譯器。
幸運的是,rellic-headergen已經使用Clang來生成定義。不幸的是,查詢Clang本身關於字段的偏移量並不是那麼簡單,因為Clang只允許檢索完整定義的佈局信息。為了解決API的這個特殊問題,實用程序生成臨時結構定義,其中包含當前正在處理的字段之前的所有字段。
結構和繼承當我在處理更多涉及的用例時,我偶然發現了一些ABI 以不立即顯而易見的方式工作的實例。例如,處理C++ 繼承需要小心,因為簡單的方法並不總是正確的。
轉換成
這似乎是個好主意,在實踐中也很有效,但這種方法的可擴展性不太好。例如,以下代碼段不能以這種方式轉換:
原因是在int 為4 個字符寬的設備上,結構A 通常在y 之後包含3 個額外的填充字符。因此,將結構A 直接嵌入B 將使z 位於偏移量8 處。為了最大限度地減少結構中的填充量,編譯器選擇將派生類型的字段直接放置在基本結構中。
此外,從技術上講,空結構在C 中無效。它們可以通過GCC 和Clang 擴展使用,並且在C++ 中有效,但它們存在一個問題:空結構的sizeof 永遠不會為0。相反,它通常為1。除了其他原因,這是為了在像下面這樣的代碼片段中,每個字段都保證有單獨的地址:
上面的例子工作得很好,但是在有些地方,用簡單的方法處理空結構是行不通的。考慮如下:
此示例生成以下DWARF 元數據:
如果我們遵循dw_tag_繼承和DW_TAG_member相同的邏輯,就會得到這樣的轉換:
這和原來的定義不一樣!字段b最終的偏移量與0不同,因為字段的大小不能為0。讓所有這些c++細節工作是有挑戰性的,但非常值得。現在我們可以使用rellic-headergen將任意c++類型轉換為普通的C類型。許多逆向工程工具嵌入了某種形式的基本C解析支持,以便用戶提供“類型庫”,描述設備代碼使用的類型。這些基本的解析器通常不支持任何c++,所以rellic-headergen彌補了這一缺陷。
relic -headergen的改進空間?rellic-headergen還有進一步改進的機會,該實用程序的目標之一是能夠從已優化的代碼中恢復字段訪問模式。考慮以下程序:
該程序產生以下位碼:
在這個位碼中,關於x結構的原始信息已經丟失了。本質上,如果Clang/LLVM在發出位碼或從已編譯的設備碼中提升位碼之前執行優化,這可能會導致生成的位碼級別過低,從而在調試元數據中找到的類型信息與位碼本身中的信息不匹配。在這種情況下,relic -headergen無法自行解決這種不匹配。改進實用程序以便能夠在未來解決這些問題將是有益的,當嘗試將位移位和掩碼與字段訪問匹配以生成盡可能接近原始代碼的反編譯代碼時,了解結構的確切佈局可能很有用。
此外,rellic-headergen 也無法處理使用不同DWARF 功能的語言。例如,Rust 對有區別的聯合使用了一種特別的表示,這對於實用程序來說很難處理。有朝一日可以向該實用程序添加功能來處理這些DWARF 功能。
總結儘管rellic-headergen 目前的範圍非常狹窄,但在使用C 和C++ 代碼庫時它已經非常強大,因為它能夠提取rellic 本身的類型信息,包括LLVM 和Clang。在導航使用調試信息構建的二進製文件時,它已經提供了有用的見解,但是擴展其功能集以能夠從更多不同的代碼庫中提取信息將使其在處理更大的項目時更加有用。
Recommended Comments