前言

會寫這篇是因為當初我在嘗試使用 IDA 的內建插件 F.L.I.R.T 的時候遇到了問題 ,一直產不出 function signature,翻了翻網路上的資料也沒有解掉,有趣的是網路上大家也都跟我一樣沒有頭緒,於是當時就折騰了一個晚上搞清楚這玩意是怎麼運作的,也順便記錄自己的所見所聞。這幾天心血來潮架了部落格,就順手放上來做為可喜可賀的第一篇啦

FLIRT

F.L.I.R.T 是由 Hex-Ray 出品的 PE/ELF signature recognition engine,透過辨識 library 中位於 function 開頭與結尾的特徵產出 signature,便可以運用在一些 strip 過的 static-linked binary 上,
以下利用官方提供的工具簡單兩句話產 ELF library 的 signature

$ ./pelf `gcc --print-file-name=libc.a` libc.pat
$ ./sigmake -n"Ubuntu 18.04 libc" libc.pat libc.sig

襪!真神奇

不過就是因為操作如此簡單,一但出問題的時候就不知道該怎麼辦了

img

所以就讓我們挖深一點吧

pelf

可以在 pelf 的同目錄底下找到設定檔 pelf.rtb,當中提到

/* Since 'pelf' is in charge of creating a '.pat' pattern file,                           */
/* that contains sequences of bytes representing code to be later                         */
/* matched in binaries, it is important that portions of that                             */
/* code that is [link|run]time-dependent be ignored when trying to                        */
/* match patterns.

從這段話我們可以知道在產生 function signature 的時候,需要將 code 中一些與 relocation 相關的 byte 去除掉,這道理也不難理解,當一支程式被以 static linked 方式編譯,其中可能只引用到函式庫中的部分 function,所以在 linking 階段,各個 object file 會由 linker 重組拼湊起來才形成一個完整的 ELF 程式,考量到部分讀者可能對 linker 做了什麼不甚熟悉,在這裡就稍微介紹一下
img
一段 code 從原始碼到被編譯成 ELF (或是其他可執行檔) 大概會經過 preprocesser -> compiler -> assembler -> linker 這四個階段。在 assembler 中,會將 compiler 輸出的 assembly 轉成 relocatable object file 格式,檔名後綴一般是 .o,檔案中包含了不少 metadata 如 function name, relocation info, variable symbols… 等,接著 linker 會從多個 object file 中讀取這些資料進行 linking,這階段主要完成下列兩件事:

  • symbol resolution:
    • 解析該 objfile 中的 symbol 是引用到其他 objfile 中的哪個 symbol
  • relocation:
    • symbol resolution 完成後,將對應的位置(如 function/variable address) 填入 objfile 中

詳細的內容可以參考 CS:APP 第7章

由於每次 linking 時 symbol 的相對位置很有可能會變動,所以每支 static linked 的 ELF 就算用到相同的 library function,比較時仍然會不同,也正是為何需要在產生 function signature 時將這些變動的因素去除掉

linking 過程中 linker 會利用 objfile 的 relocation entry 中提供的資訊來進行 relocation,結構如下:

typedef struct {
        Elf32_Addr      r_offset;
        Elf32_Word      r_info;
} Elf32_Rel;
 
typedef struct {
        Elf32_Addr      r_offset;
        Elf32_Word      r_info;
        Elf32_Sword     r_addend;
} Elf32_Rela;

typedef struct {
        Elf64_Addr      r_offset;
        Elf64_Xword     r_info;
} Elf64_Rel;
 
typedef struct {
        Elf64_Addr      r_offset;
        Elf64_Xword     r_info;
        Elf64_Sxword    r_addend;
} Elf64_Rela;

結構中有三個欄位,分別是

  • r_offset: 需要被填寫的部分在 objfile 中的何處
  • r_info: 分為高低位(各為 32 or 16 bits),表示 symbol 在 symbol table 中的 index 以及 relocation type
  • r_addend: 表示 symbol resolve 之後拿到的 address(或是 offset) 需要加上這個值之後才能寫入目標位置

我們可以利用 readelf -r -W `gcc --print-file-name=libc.a` 替我們條列出 objfile 當中的 relocation entries,這裡以 libc.a 為例

libc.a 是一個 ar 壓縮檔,裡面包含了許多與 libc 相關的 objfiles

    Offset             Info             Type               Symbol's Value  Symbol's Name + Addend
0000000000000001  000000080000000a R_X86_64_32            0000000000000000 _dl_starting_up + 0
0000000000000018  0000000800000002 R_X86_64_PC32          0000000000000000 _dl_starting_up - 4

Symbol’s Value 那欄有些有值是因為要 resolve 的 symbol 在同一個 objfile 中

也可以同時參考 objdump -D -r `gcc --print-file-name=libc.a` 幫助理解

0000000000000000 <__libc_init_first>:
   0:   b8 00 00 00 00          mov    eax,0x0
                        1: R_X86_64_32  _dl_starting_up
   5:   41 54                   push   r12
   7:   49 89 d4                mov    r12,rdx
   a:   48 85 c0                test   rax,rax
   d:   55                      push   rbp
   e:   48 89 f5                mov    rbp,rsi
  11:   53                      push   rbx
  12:   89 fb                   mov    ebx,edi
  14:   74 4a                   je     60 <__libc_init_first+0x60>
  16:   8b 05 00 00 00 00       mov    eax,DWORD PTR [rip+0x0]        # 1c <__libc_init_first+0x1c>
                        18: R_X86_64_PC32       _dl_starting_up-0x4
  1c:   85 c0                   test   eax,eax

瞭解為什麼之後就可以往下看看 config 檔裡究竟寫些什麼啦
因為 config 的格式在文檔中已經有詳細說明了,這裡只擷取重點放上來

TABLE <proc> [<proc>...]: 
      <reloc-nr>, <offset-lsb>, <offset-msb>, <size>
  • <proc>
    • machine number,可以在這裡
  • <reloc-nr>
    • relocation type
  • <offset-lsb>
    • 從第幾個 byte 開始忽略 (little-endian)
  • <offset-msb>
    • 從第幾個 byte 開始忽略 (big-endian)
  • <size>
    • 共忽略幾個 byte
    • 需要注意在 arm 家族的指令集中,relocation 回填的 offset 可能四散在多個 byte 中,都需要跳過

回到問題

img

瞭解 pelf 的運作方式之後解決這個問題的方式就變得相當直覺了 – 「缺什麼,我就加什麼」
這裡就是 pelf 缺少在遇到 relocation type 42 時該怎麼應對的設定,所以我們只需要查點資料填回去 pelf.rtb 中便可以正常執行了

接著就透過查閱文件把缺少的資料補齊,有關 relocation/instruction set 的文件可以參考

例如在解析 Ubuntu 的 glibc 2.27 時,會缺少以下幾項,將他補上就可以

  • X86_64

    26,   0,   0,   4    /*R_X86_64_GOTPC32*/
    42,   0,   0,   4    /*R_X86_64_REX_GOTP*/
  • i386

    43,   0,   0,   4    /*R_386_GOT32X*/

同樣的可以適用到如 arm 缺少設定報錯,或是 aarch64 在官方還沒有正式支援(pelf 可以讀,可是沒設定檔)的情況下,自己補上對應的 relocation type 設定就可以產出 signature 了

arm、aarch64 會稍微難填一點,這部分就交給讀者自己嘗試了

Bugs

在測試的過程中發現一個 bug,IDA Pro 7.0 提供的 pelf linux/mac 的版本在解析 aarch64 的 relocation table 時,會錯誤的 (以 32bit ELF 的方式解讀) 只讀取 1byte 來判斷 relocation type,所以我只好上了一個 patch 讓他改回讀取 4byte,不確定新版有沒有修正掉XD

題外話

push0ebp 寫了一個方便批次產出 Ubuntu 上各版本 glibc signature 的 script ALLirt 以及對應產出的 signatures sig-database,因為在實驗 FLIRT 功能的時候有使用過,在瞭解問題之後就順手幫他補上缺少的設定解掉 issue,沒想到後來他更新了 README
img
第一次被這樣感謝感覺還蠻開心的 :)

Reference

Relocation Sections
Difference between .o .a .so files