背景介紹
??從逆向的角度看,當我們拿到一個二進制需要分析時首先會考慮從函數(shù)搜索、常量字符串等角度找到一個切入口。比如做軟件破解時優(yōu)先根據(jù)界面報錯信息定位驗證函數(shù)、做go二進制逆向時首先根據(jù)函數(shù)名縮小分析范圍。關于字符串的搜集沒什么好說的,掃描二進制搜集不同編碼常量字符串即可。本文主要討論IDA如何將匯編代碼映射到正確函數(shù)名。
0x00 函數(shù)導出表(Export Table)
??當我們在寫代碼調(diào)用一個動態(tài)庫中的文件時,第一步是 LoadLibrary將dll載入內(nèi)存,第二步使用GetProcAddress函數(shù)得到指定函數(shù)的地址。那么GetProcAddress是如何知道某個函數(shù)在內(nèi)存中的地址呢?這就是函數(shù)導出表在發(fā)揮作用。函數(shù)導出表(Export Table)用于記錄可執(zhí)行文件或動態(tài)鏈接庫中需要被其他程序調(diào)用的函數(shù)或變量,通過函數(shù)導出表,其他程序可以獲取到被導出函數(shù)的地址,并通過該地址來調(diào)用相應的函數(shù)。需要注意的是:函數(shù)導出表通常用于Windows操作系統(tǒng)下的動態(tài)鏈接庫(DLL),但是exe中也是可能存在導出函數(shù)的。
IDA中加載exe分析后在Export頁面可以看到函數(shù)導出表信息:

從代碼化角度看,可以通過golang的
github.com/l0g1n/pefile-go庫解析PE文件的函數(shù)導出表:
pe, err := pefile.NewPEFile(`C:\Users\Spring\Desktop\windowsbrowser.exe`)
if err != nil {
fmt.Println("Ooopss looks like there was a problem")
fmt.Println(err)
os.Exit(2)
}
var imageBase uint64
if pe.OptionalHeader64 != nil {
imageBase = pe.OptionalHeader64.Data.ImageBase
} else {
imageBase = uint64(pe.OptionalHeader.Data.ImageBase)
}
fmt.Printf("imageBase:%x\n", imageBase)
if pe.ExportDirectory != nil {
fmt.Println("\nDIRECTORY_ENTRY_EXPORT\n")
for _, entry := range pe.ExportDirectory.Exports {
fmt.Printf("Ordinal:%d Name:%s Address:0x%x\n", entry.Ordinal,
string(entry.Name), uint64(entry.Address)+imageBase)
}
}
其輸出結果如下:
imageBase:400000
DIRECTORY_ENTRY_EXPORT
Ordinal:1 Name:_cgo_dummy_export Address:0x1dc5430
Ordinal:2 Name:authorizerTrampoline Address:0xce0910
Ordinal:3 Name:callbackTrampoline Address:0xce0670
Ordinal:4 Name:clibCommitData Address:0xc37e80
Ordinal:5 Name:clibFail Address:0xc37f30
Ordinal:6 Name:clibFinish Address:0xc37ee0
Ordinal:7 Name:clibLog Address:0xc37f80
Ordinal:8 Name:clibUpdateStatus Address:0xc37e10
Ordinal:9 Name:commitHookTrampoline Address:0xce0800
Ordinal:10 Name:compareTrampoline Address:0xce0770
Ordinal:11 Name:doneTrampoline Address:0xce0730
Ordinal:12 Name:preUpdateHookTrampoline Address:0xce0990
Ordinal:13 Name:rollbackHookTrampoline Address:0xce0860
Ordinal:14 Name:stepTrampoline Address:0xce06d0
Ordinal:15 Name:updateHookTrampoline Address:0xce08a0
0x01 調(diào)試信息(DWARF)
??正常情況下,gcc在編譯時會抹掉代碼中函數(shù)名信息而轉為地址。那么當我們在做開發(fā)過程中需要對程序進行調(diào)試時,一定要在gcc編譯時添加-g參數(shù)才可以使用gdb進行單行跟蹤。那么gcc的-g到底做了什么事情就可以讓gdb輕松知道當前程序運行在了第一行,其函數(shù)名稱是什么呢? 這就是DWARF在發(fā)揮作用。
??DWARF(Debugging With Attributed Record Formats)是一種調(diào)試信息格式,主要用于將源代碼和可執(zhí)行文件中的調(diào)試信息進行關聯(lián)。它可以幫助開發(fā)人員在程序崩潰或出現(xiàn)錯誤時,快速地定位問題并進行修復。DWARF記錄了源代碼中的變量名、類型信息、函數(shù)名等,它同時支持多種編程語言,包括C、C++、Go等,可以為不同的編程語言提供相應的調(diào)試信息。
??使用以下代碼,在Linux平臺使用gcc8.3編譯作為例子:
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("hello\n");
}
使用 gcc main1.c -g -o main編譯.使用readelf -S main 命令查看文件的所有節(jié)信息,包括存儲了dwarf信息的.debug_info節(jié)信息:
There are 35 section headers, starting at offset 0x41a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
... 省略很多不重要的節(jié)信息
[27] .debug_aranges PROGBITS 0000000000000000 0000304c
0000000000000030 0000000000000000 0 0 1
[28] .debug_info PROGBITS 0000000000000000 0000307c
000000000000032a 0000000000000000 0 0 1
[29] .debug_abbrev PROGBITS 0000000000000000 000033a6
00000000000000e1 0000000000000000 0 0 1
[30] .debug_line PROGBITS 0000000000000000 00003487
0000000000000111 0000000000000000 0 0 1
[31] .debug_str PROGBITS 0000000000000000 00003598
0000000000000239 0000000000000001 MS 0 0 1
[32] .symtab SYMTAB 0000000000000000 000037d8
0000000000000678 0000000000000018 33 50 8
[33] .strtab STRTAB 0000000000000000 00003e50
0000000000000203 0000000000000000 0 0 1
使用dwarfdump讀取main的調(diào)試信息,以下是部分輸出:
< 1><0x000002e2> DW_TAG_subprogram
DW_AT_external yes(1)
DW_AT_name main
DW_AT_decl_file 0x00000001 /home/spring/main1.c
DW_AT_decl_line 0x00000002
DW_AT_decl_column 0x00000005
DW_AT_prototyped yes(1)
DW_AT_type <0x00000065>
DW_AT_low_pc 0x00001135
DW_AT_high_pc <offset-from-lowpc>34
DW_AT_frame_base len 0x0001: 9c: DW_OP_call_frame_cfa
DW_AT_GNU_all_tail_call_sites yes(1)
DW_AT_sibling <0x00000323>
< 2><0x00000304> DW_TAG_formal_parameter
DW_AT_name argc
DW_AT_decl_file 0x00000001 /home/spring/main1.c
DW_AT_decl_line 0x00000002
DW_AT_decl_column 0x0000000e
DW_AT_type <0x00000065>
DW_AT_location len 0x0002: 916c: DW_OP_fbreg -20
< 2><0x00000313> DW_TAG_formal_parameter
DW_AT_name argv
DW_AT_decl_file 0x00000001 /home/spring/main1.c
DW_AT_decl_line 0x00000002
DW_AT_decl_column 0x0000001a
DW_AT_type <0x00000323>
DW_AT_location len 0x0002: 9160: DW_OP_fbreg -32
從上面輸出至少可以得到以下信息:
/home/spring/main1.c第2行有一個函數(shù)名稱為main,有2個參數(shù),分別是 int argc和char** argv。main函數(shù)在內(nèi)存中地址起始位置是 0x00001135,長度是 34個字節(jié)。
當然基于開源庫 libdwarf (https://github.com/davea42/libdwarf-code)我們也可以定制化輸出內(nèi)容。
比如基于libdwarf封裝了一個go庫用于輸出調(diào)試信息中所有函數(shù)名和對應虛擬地址,其輸出如下:
symbol name: main => address : 0x00001135
0x02 go語言符號特征
??參考文章: 如何消除Go的編譯特征.md
??使用go build .進行編譯,會連符號和調(diào)試信息一起編譯到里面。這里的調(diào)試信息是剛才說的DWARF信息。當使用go build -ldflags "-s -w" main.go編譯時可以移除DWARF調(diào)試信息,但是依然會攜帶很多go語言特征,比如go函數(shù)名稱與地址映射表。文章中提到:gostrip可以混淆函數(shù)名,結構體名稱,文件名等諸多編譯特征。換一個思路想:我們也基于gostrip做二次開發(fā),讀取go語言二進制中函數(shù)名和地址信息。
基于go-strip二次開發(fā)后獲取函數(shù)信息后輸出如下:
Name:runtime/debug.ParseBuildInfo.func2 Entry:50db00
Name:runtime/debug.ParseBuildInfo Entry:50dc80
Name:runtime/debug.ParseBuildInfo.func1 Entry:50eea0
Name:main.main Entry:51b700
Name:main.main.func1 Entry:51c320
0x03 FLIRT
??無論導出函數(shù),調(diào)試信息還是go語言符號特征都是基于文件結構分析直接得到的準確函數(shù)信息,那么針對使用靜態(tài)鏈接不不包含調(diào)試信息的C程序怎么辦呢?IDA有一個特色功能是FLIRT。FLIRT全稱是 Fast Library Identification and Recognition Technology。該技術利用庫文件中二進制函數(shù)的機器碼,來快速識別文件中的庫函數(shù),使得反匯編代碼可讀性更強。在IDA的安裝包中sig文件夾下其實包含了很多標準庫函數(shù)的特征信息,如下圖所示:
