在 Linux 上分析二进制文件的 10 种方法

这些简单的命令和工具可以帮助您轻松完成分析二进制文件的任务。
168 位读者喜欢这个。

“世界上有 10 种人:懂二进制的人和不懂二进制的人。”

我们每天都与二进制文件打交道,但我们对它们知之甚少。 我说的二进制文件是指您每天运行的可执行文件,从您的命令行工具到成熟的应用程序。

Linux 提供了一套丰富的工具,可以轻松分析二进制文件! 无论您的工作角色是什么,如果您正在使用 Linux,了解这些工具的基础知识将有助于您更好地了解您的系统。

在本文中,我们将介绍一些最流行的 Linux 工具和命令,其中大多数将作为您的 Linux 发行版的一部分以原生方式提供。 如果没有,您始终可以使用您的软件包管理器来安装和探索它们。 请记住:在合适的场合学习使用正确的工具需要足够的耐心和实践。

file

作用:帮助确定文件类型。

这将是您二进制文件分析的起点。我们每天都与文件打交道。并非所有文件都是可执行类型;存在各种各样的文件类型。在开始之前,您需要了解正在分析的文件类型。 它是二进制文件、库文件、ASCII 文本文件、视频文件、图片文件、PDF、数据文件等等吗?

file 命令将帮助您识别您正在处理的确切文件类型。

$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=94943a89d17e9d373b2794dcb1f7e38c95b66c86, stripped
$ 
$ file /etc/passwd
/etc/passwd: ASCII text
$ 

ldd

作用:打印共享对象依赖项。

如果您已经在可执行二进制文件上使用了上面的 file 命令,您不会错过输出中的“动态链接”消息。 这是什么意思?

在开发软件时,我们尽量不要重复发明轮子。 大多数软件程序都需要一组常见的任务,例如打印输出或从标准输入读取数据,或打开文件等。所有这些常见任务都被抽象到一组通用函数中,每个人都可以使用这些函数而不是编写自己的变体。 这些通用函数被放在一个名为 libcglibc 的库中。

如何找到可执行文件依赖于哪些库? 这就是 ldd 命令发挥作用的地方。针对动态链接的二进制文件运行它会显示其所有依赖库及其路径。

$ ldd /bin/ls
	linux-vdso.so.1 =>  (0x00007ffef5ba1000)
	libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fea9f854000)
	libcap.so.2 => /lib64/libcap.so.2 (0x00007fea9f64f000)
	libacl.so.1 => /lib64/libacl.so.1 (0x00007fea9f446000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fea9f079000)
	libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fea9ee17000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fea9ec13000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fea9fa7b000)
	libattr.so.1 => /lib64/libattr.so.1 (0x00007fea9ea0e000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fea9e7f2000)
$ 

ltrace

作用:库调用跟踪器。

现在我们知道如何使用 ldd 命令找到可执行程序依赖的库。 但是,一个库可以包含数百个函数。 在这数百个函数中,我们的二进制文件实际使用了哪些函数?

ltrace 命令显示在运行时从库中调用的所有函数。 在下面的示例中,您可以看到正在调用的函数名称,以及传递给该函数的参数。 您还可以看到这些函数在输出的最右侧返回了什么。

$ ltrace ls
__libc_start_main(0x4028c0, 1, 0x7ffd94023b88, 0x412950 <unfinished ...>
strrchr("ls", '/')                                                                  = nil
setlocale(LC_ALL, "")                                                               = "en_US.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale")                                    = "/usr/share/locale"
textdomain("coreutils")                                                             = "coreutils"
__cxa_atexit(0x40a930, 0, 0, 0x736c6974756572)                                      = 0
isatty(1)                                                                           = 1
getenv("QUOTING_STYLE")                                                             = nil
getenv("COLUMNS")                                                                   = nil
ioctl(1, 21523, 0x7ffd94023a50)                                                     = 0
<< snip >>
fflush(0x7ff7baae61c0)                                                              = 0
fclose(0x7ff7baae61c0)                                                              = 0
+++ exited (status 0) +++
$ 

Hexdump

作用:以 ASCII、十进制、十六进制或八进制显示文件内容。

通常,当您使用不知道如何处理该文件的应用程序打开文件时,会发生这种情况。 尝试使用 vim 打开可执行文件或视频文件; 您将看到的只是屏幕上抛出的乱码。

在 Hexdump 中打开未知文件可以帮助您了解文件到底包含什么。 您还可以选择使用一些命令行选项来查看文件中存在的数据的 ASCII 表示形式。 这可能有助于您了解它是什么类型的文件。

$ hexdump -C /bin/ls | head
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  d4 42 40 00 00 00 00 00  |..>......B@.....|
00000020  40 00 00 00 00 00 00 00  f0 c3 01 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 1f 00 1e 00  |....@.8...@.....|
00000040  06 00 00 00 05 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000050  40 00 40 00 00 00 00 00  40 00 40 00 00 00 00 00  |@.@.....@.@.....|
00000060  f8 01 00 00 00 00 00 00  f8 01 00 00 00 00 00 00  |................|
00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
00000080  38 02 00 00 00 00 00 00  38 02 40 00 00 00 00 00  |8.......8.@.....|
00000090  38 02 40 00 00 00 00 00  1c 00 00 00 00 00 00 00  |8.@.............|
$ 

strings

作用:打印文件中可打印字符的字符串。

如果 Hexdump 对于您的用例来说似乎有点过分,并且您只是在寻找二进制文件中的可打印字符,您可以使用 strings 命令。

在开发软件时,会向其中添加各种文本/ASCII 消息,例如打印信息消息、调试信息、帮助消息、错误消息等等。 假设所有这些信息都存在于二进制文件中,它将使用 strings 转储到屏幕。

$ strings /bin/ls

readelf

作用:显示有关 ELF 文件的信息。

ELF(可执行和可链接文件格式)是可执行文件或二进制文件的主要文件格式,不仅在 Linux 上,而且在各种 UNIX 系统上也是如此。 如果您已经使用了像 file 命令这样的工具,它会告诉您该文件是 ELF 格式的,那么下一步合乎逻辑的步骤是使用 readelf 命令及其各种选项来进一步分析该文件。

在使用 readelf 时,手头有一份实际的 ELF 规范参考会非常有用。 您可以在这里找到该规范。

$ readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4042d4
  Start of program headers:          64 (bytes into file)
  Start of section headers:          115696 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30
$ 

objdump

作用:显示来自目标文件的信息。

当您编写使用名为编译器(不出所料)的工具编译的源代码时,会创建二进制文件。 该编译器生成相当于源代码的机器语言指令,然后 CPU 可以执行这些指令以执行给定的任务。 该机器语言代码可以通过称为汇编语言的助记符来解释。 汇编语言是一组指令,可以帮助您了解程序正在执行的操作,并最终在 CPU 上执行的操作。

objdump 实用程序读取二进制文件或可执行文件,并将汇编语言指令转储到屏幕上。 了解汇编语言对于理解 objdump 命令的输出至关重要。

请记住:汇编语言特定于架构。

$ objdump -d /bin/ls | head

/bin/ls:     file format elf64-x86-64


Disassembly of section .init:

0000000000402150 <_init@@Base>:
  402150:	48 83 ec 08          	sub    $0x8,%rsp
  402154:	48 8b 05 6d 8e 21 00 	mov    0x218e6d(%rip),%rax        # 61afc8 <__gmon_start__>
  40215b:	48 85 c0             	test   %rax,%rax
$ 

strace

作用:跟踪系统调用和信号。

如果您使用了前面提到的 ltrace,可以将 strace 视为类似的东西。 唯一的区别是,strace 实用程序不是调用库,而是跟踪系统调用。 系统调用是您与内核交互以完成工作的方式。

举例来说,如果您想在屏幕上打印一些东西,您将使用标准库 libc 中的 printfputs 函数; 但是,在底层,最终会调用一个名为 write 的系统调用,以实际在屏幕上打印一些东西。

$ strace -f /bin/ls
execve("/bin/ls", ["/bin/ls"], [/* 17 vars */]) = 0
brk(NULL)                               = 0x686000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f967956a000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=40661, ...}) = 0
mmap(NULL, 40661, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9679560000
close(3)                                = 0
<< snip >>
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9679569000
write(1, "R2  RH\n", 7R2  RH
)                 = 7
close(1)                                = 0
munmap(0x7f9679569000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++
$ 

nm

作用:列出目标文件中的符号。

如果您正在处理未剥离的二进制文件,则 nm 命令将为您提供在编译期间嵌入在二进制文件中的有价值的信息。 nm 可以帮助您识别二进制文件中的变量和函数。 您可以想象,如果您无法访问正在分析的二进制文件的源代码,这将多么有用。

为了展示 nm,我们将快速编写一个小程序并使用 -g 选项对其进行编译,并且我们还将使用 file 命令查看该二进制文件是否未被剥离。

$ cat hello.c 
#include <stdio.h>

int main() {
    printf("Hello world!");
    return 0;
}
$ 
$ gcc -g hello.c -o hello
$ 
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3de46c8efb98bce4ad525d3328121568ba3d8a5d, not stripped
$ 
$ ./hello 
Hello world!$ 
$ 


$ nm hello | tail
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
00000000004005b0 T __libc_csu_fini
0000000000400540 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
000000000040051d T main
                 U printf@@GLIBC_2.2.5
0000000000400490 t register_tm_clones
0000000000400430 T _start
0000000000601030 D __TMC_END__
$ 

gdb

作用:GNU 调试器。

好吧,并非二进制文件中的所有内容都可以进行静态分析。 我们的确执行了一些运行二进制文件的命令,例如 ltracestrace; 但是,软件由各种条件组成,这些条件可能导致执行各种替代路径。

分析这些路径的唯一方法是在运行时通过能够在任何给定位置停止或暂停程序,并且能够分析信息,然后再进一步向下移动。

这就是调试器发挥作用的地方,在 Linux 上,gdb 是事实上的调试器。 它可以帮助您加载程序、在特定位置设置断点、分析内存和 CPU 寄存器以及执行更多操作。 它补充了上面提到的其他工具,并允许您执行更多的运行时分析。

需要注意的一点是,一旦您使用 gdb 加载程序,您将看到其自己的 (gdb) 提示符。 在您退出之前,所有进一步的命令都将在此 gdb 命令提示符中运行。

我们将使用我们之前编译的“hello”程序,并使用 gdb 来查看它的工作原理。

$ gdb -q ./hello
Reading symbols from /home/flash/hello...done.
(gdb) break main
Breakpoint 1 at 0x400521: file hello.c, line 4.
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400521 in main at hello.c:4
(gdb) run
Starting program: /home/flash/./hello 

Breakpoint 1, main () at hello.c:4
4	    printf("Hello world!");
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64
(gdb) bt
#0  main () at hello.c:4
(gdb) c
Continuing.
Hello world![Inferior 1 (process 29620) exited normally]
(gdb) q
$ 

结论

一旦您能够熟练使用这些原生 Linux 二进制文件分析工具并了解它们提供的输出,您就可以转向更高级和专业的开源二进制文件分析工具,例如 radare2

下一步阅读什么
User profile image.
经验丰富的软件工程专业人士。 主要兴趣是安全、Linux、恶意软件。 热爱使用命令行。 对底层软件和了解事物的工作原理感兴趣。 此处表达的观点是我个人的,不代表我的雇主。

4 条评论

binwalk怎么样? 我认为,它是分析文件的必备工具,而且功能远不止这些...

我有一些不知道的东西,谢谢

谢谢兄弟

简单的 strings 命令可用于可视化文本错误消息,这些消息提供了二进制文件功能的提示

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.