背景介绍
从逆向的角度看,当我们拿到一个二进制需要分析时首先会考虑从函数搜索、常量字符串等角度找到一个切入口。比如做软件破解时优先根据界面报错信息定位验证函数、做go二进制逆向时首先根据函数名缩小分析范围。关于字符串的搜集没什么好说的,扫描二进制搜集不同编码常量字符串即可。本文主要讨论IDA如何将汇编代码映射到正确函数名。
0x00 函数导出表(Export Table)
当我们在写代码调用一个动态库中的文件时,第一步是 LoadLibrary将dll载入内存,第二步使用GetProcAddress函数得到指定函数的地址。那么GetProcAddress是如何知道某个函数在内存中的地址呢?这就是函数导出表在发挥作用。函数导出表(Export Table)用于记录可执行文件或动态链接库中需要被其他程序调用的函数或变量,通过函数导出表,其他程序可以获取到被导出函数的地址,并通过该地址来调用相应的函数。需要注意的是:函数导出表通常用于Windows操作系统下的动态链接库(DLL),但是exe中也是可能存在导出函数的。
IDA中加载exe分析后在Export页面可以看到函数导出表信息:
从代码化角度看,可以通过golang的
github.com/l0g1n/pefile-go
库解析PE文件的函数导出表:
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 调试信息(DWARF)
正常情况下,gcc在编译时会抹掉代码中函数名信息而转为地址。那么当我们在做开发过程中需要对程序进行调试时,一定要在gcc编译时添加-g
参数才可以使用gdb进行单行跟踪。那么gcc的-g
到底做了什么事情就可以让gdb轻松知道当前程序运行在了第一行,其函数名称是什么呢? 这就是DWARF在发挥作用。
DWARF(Debugging With Attributed Record Formats)是一种调试信息格式,主要用于将源代码和可执行文件中的调试信息进行关联。它可以帮助开发人员在程序崩溃或出现错误时,快速地定位问题并进行修复。DWARF记录了源代码中的变量名、类型信息、函数名等,它同时支持多种编程语言,包括C、C++、Go等,可以为不同的编程语言提供相应的调试信息。
使用以下代码,在Linux平台使用gcc8.3编译作为例子:
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("hello\n");
}
使用 gcc main1.c -g -o main
编译.使用readelf -S main
命令查看文件的所有节信息,包括存储了dwarf信息的.debug_info节信息:
There are 35 section headers, starting at offset 0x41a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
... 省略很多不重要的节信息
[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的调试信息,以下是部分输出:
< 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行有一个函数名称为main,有2个参数,分别是 int argc和char** argv。main函数在内存中地址起始位置是 0x00001135,长度是 34个字节。
当然基于开源库 libdwarf (https://github.com/davea42/libdwarf-code)我们也可以定制化输出内容。
比如基于libdwarf封装了一个go库用于输出调试信息中所有函数名和对应虚拟地址,其输出如下:
symbol name: main => address : 0x00001135
0x02 go语言符号特征
参考文章: 如何消除Go的编译特征.md
使用go build .进行编译,会连符号和调试信息一起编译到里面。这里的调试信息是刚才说的DWARF信息。当使用go build -ldflags "-s -w" main.go
编译时可以移除DWARF调试信息,但是依然会携带很多go语言特征,比如go函数名称与地址映射表。文章中提到:gostrip可以混淆函数名,结构体名称,文件名等诸多编译特征。换一个思路想:我们也基于gostrip做二次开发,读取go语言二进制中函数名和地址信息。
基于go-strip二次开发后获取函数信息后输出如下:
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
无论导出函数,调试信息还是go语言符号特征都是基于文件结构分析直接得到的准确函数信息,那么针对使用静态链接不不包含调试信息的C程序怎么办呢?IDA有一个特色功能是FLIRT。FLIRT全称是 Fast Library Identification and Recognition Technology。该技术利用库文件中二进制函数的机器码,来快速识别文件中的库函数,使得反汇编代码可读性更强。在IDA的安装包中sig文件夹下其实包含了很多标准库函数的特征信息,如下图所示:
网友评论