IDA识别到函数名称方法

背景介绍

  从逆向的角度看,当我们拿到一个二进制需要分析时首先会考虑从函数搜索、常量字符串等角度找到一个切入口。比如做软件破解时优先根据界面报错信息定位验证函数、做go二进制逆向时首先根据函数名缩小分析范围。关于字符串的搜集没什么好说的,扫描二进制搜集不同编码常量字符串即可。本文主要讨论IDA如何将汇编代码映射到正确函数名。

0x00 函数导出表(Export Table)

  当我们在写代码调用一个动态库中的文件时,第一步是 LoadLibrary将dll载入内存,第二步使用GetProcAddress函数得到指定函数的地址。那么GetProcAddress是如何知道某个函数在内存中的地址呢?这就是函数导出表在发挥作用。函数导出表(Export Table)用于记录可执行文件或动态链接库中需要被其他程序调用的函数或变量,通过函数导出表,其他程序可以获取到被导出函数的地址,并通过该地址来调用相应的函数。需要注意的是:函数导出表通常用于Windows操作系统下的动态链接库(DLL),但是exe中也是可能存在导出函数的。
IDA中加载exe分析后在Export页面可以看到函数导出表信息:

ida查看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文件夹下其实包含了很多标准库函数的特征信息,如下图所示:

ida自带的sig文件
如果在逆向过程中发现使用了某些三方库,也可以自行制作sig符号库,可以参考教程 : https://www.freebuf.com/articles/endpoint/235070.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容