38

Linux下分析bin文件的10种方法

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU3NTgyODQ1Nw%3D%3D&%3Bmid=2247489650&%3Bidx=2&%3Bsn=215940481a428759898042aed3c34cd0
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Y73aamf.jpg!web3IJRFbm.jpg!web

点击「阅读原文」查看良许原创精品视频。

这世界有10种人,一种人懂二进制,另一种人不懂二进制。       ——鲁迅

大家好,我是良许。

二进制文件是我们几乎每天都需要打交道的文件类型,但很少人知道他们的工作原理。这里所讲的二进制文件,是指一些可执行文件,包括你天天要使用的 Linux 命令,也是二进制文件的一种。

Linux 系统给我们提供了非常多用于分析二进制文件的工具,不管你在 Linux 下从事的是何种工作,知道这些工具也会让你对你的系统更加了解。

在本文中,将介绍几种最常用的用于分析二进制文件的工具及命令,这些工具在大部分发行版里可以直接使用,如果不能直接用的话,可以自行安装。

file

file 命令用于分析文件的类型。

如果你需要分析二进制文件,可以首先使用 file 命令来切入。我们知道,在 Linux 下, 一切皆文件 ,但并不是所有的文件都具有可执行性,我们还有各种各样的文件,比如:文本文件,管道文件,链接文件,socket文件,等等。

在对一个文件进行分析之前,我们可以首先使用 file 命令来分析它们的类型。当然除此之外,我们还可以看到一些其它信息。

$ file /bin/pwd
/bin/pwd: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0d264bacf2adc568f0e21cbcc9576df434c44380, stripped

ldd

ldd 命令可以用于分析可执行文件的依赖

我们使用 file 命令来分析一个可执行文件的时候,有时候可以看到输出中有 dynamically linked 这样的字眼。这个是啥意思呢?

大部分程序,都会使用到第三方库,这样就可以不用重复造轮子,节约大量时间。最简单的,我们写C程序代码的话,肯定会使用到 libc 或者 glibc 库。当然,除此之外,还可能使用其它的库。

那我们在什么情况下需要分析程序的依赖库呢?有一个场景大家肯定经历过。你去你同事那边拷备他写好的程序放到自己的环境下运行,有时候可能会跑不起来。当然跑不起来的原因可能很多,但其中一个原因可能就是缺少对应的依赖库。

这时候,ldd 就派上用场了。它可以分析程序需要一些什么依赖库,你只要把对应的库放在对应的位置就可以了。

$ ldd /bin/pwd
        linux-vdso.so.1 =>  (0x00007ffeb73e5000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f908b321000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f908b6ef000)

ltrace

ltrace的功能是能够跟踪进程的库函数调用。

我们可以使用 ldd 命令来找到程序的依赖库,但是,一个库里少则几个,多则几千个函数,怎么知道现在程序调用的是什么函数呢?

ltrace 命令就是用来做这个事的。在下面的例子里,我们可以看到程序调用的函数,以及传递进去的参数,同时你也可以看到函数调用的输出。

$ ltrace /bin/pwd
__libc_start_main(0x401760, 1, 0x7ffff6524cc8, 0x404a00 <unfinished ...>
getenv("POSIXLY_CORRECT")                        = nil
strrchr("/bin/pwd", '/')                         = "/pwd"
setlocale(LC_ALL, "")                            = "en_US.utf8"
bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
textdomain("coreutils")                          = "coreutils"
__cxa_atexit(0x4022f0, 0, 0, 0x736c6974756572)   = 0
getopt_long(1, 0x7ffff6524cc8, "LP", 0x606d00, nil) = -1
getcwd(nil, 0)                                   = ""
puts("/home/alvin"/home/alvin
)                              = 12
free(0x22bc030)                                  = <void>
exit(0 <unfinished ...>
__fpending(0x7f3048865400, 0, 64, 0x7f3048865eb0) = 0
fileno(0x7f3048865400)                           = 1
__freading(0x7f3048865400, 0, 64, 0x7f3048865eb0) = 0
__freading(0x7f3048865400, 0, 2052, 0x7f3048865eb0) = 0
fflush(0x7f3048865400)                           = 0
fclose(0x7f3048865400)                           = 0
__fpending(0x7f30488651c0, 0, 3328, 0xfbad000c)  = 0
fileno(0x7f30488651c0)                           = 2
__freading(0x7f30488651c0, 0, 3328, 0xfbad000c)  = 0
__freading(0x7f30488651c0, 0, 4, 0xfbad000c)     = 0
fflush(0x7f30488651c0)                           = 0
fclose(0x7f30488651c0)                           = 0
+++ exited (status 0) +++

strace

strace 命令可以用于追踪程序运行过程中的系统调用及信号。

通过上面的介绍,我们知道 ltrace 命令是用来追踪函数调用的。strace 命令类似,但它追踪的是系统调用。何为系统调用?简单说就是我们可以通过系统调用与内核进行交互,完成我们想要的任务。

例如,如果我们想在屏幕上打印某些字符,可以使用 printf 或 puts 函数,而这两个都是 libc 的库函数,在更底层,他们都是调用 write 这个系统调用。

$ strace -f /bin/pwd
execve("/bin/pwd", ["/bin/pwd"], [/* 24 vars */]) = 0
brk(NULL)                               = 0xbc9000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f918ba69000
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=38684, ...}) = 0
mmap(NULL, 38684, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f918ba5f000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20&\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2156160, ...}) = 0
mmap(NULL, 3985888, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f918b47b000
mprotect(0x7f918b63e000, 2097152, PROT_NONE) = 0
mmap(0x7f918b83e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) = 0x7f918b83e000
mmap(0x7f918b844000, 16864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f918b844000
close(3) 
…………
+++ exited with 0 +++

hexdump

hexdump 命令用来查看二进制文件的 16 进制编码,但实际它能查看任何文件,而不限于二进制文件。

一个二进制文件,如果你直接使用文本编辑器打开的话,将看到一堆乱码。这时候,你就可以使用 hexdump 命令来查看它的内容了。

hexdump 的显示格式是:左边是字节序号,中间是文件的 16 进制编码,如果是可打印字符的话就会显示在右边。

通过使用这个命令,我们就可以大概知道这个二进制文件里面有什么内容,后面要做什么处理就比较方便了。

$ hexdump -C /bin/pwd | 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  17 19 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  50 7a 00 00 00 00 00 00  |@.......Pz......|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 1e 00 1d 00  |[email protected]...@.....|
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

strings 命令可以用来打印二进制文件中可显示的字符。

什么是可显示字符?简单说你在显示器上看到的字符都是可显示字符,比如:abcABC,.:。

我们知道,一个二进制文件里面的内容很多是非显示字符,所以无法直接用文本处理器打开。程序在被开发的时候,我们经常会加一些调试信息,比如:debug log, warn log, error log,等等。这些信息我们就可以使用 strings 命令看得到。

$ strings /bin/pwd | head
/lib64/ld-linux-x86-64.so.2
libc.so.6
fflush
strcpy
__printf_chk
readdir
setlocale
mbrtowc
strncmp
optind

readelf

readelf 一般用于查看 ELF 格式的文件信息。

ELF(Executable and Linkable Format)即可执行连接文件格式,是一种比较复杂的文件格式,但其应用广泛。当你使用 file 命令发现某个文件是 ELF 文件时,你就可以使用 readelf 命令来读取这个文件的信息。

$ readelf -h /bin/pwd
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:               0x401917
  Start of program headers:          64 (bytes into file)
  Start of section headers:          31312 (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:         30
  Section header string table index: 29

objdump

objdump是用查看目标文件或者可执行的目标文件的构成的GCC工具。

我们知道,程序在开发完成之后,需要经过编译,才可以生成计算机可以识别的二进制文件。我们写的代码计算机不能直接执行,需要编译成汇编程序,计算机才能依次执行。

objdump 命令可以读取可执行文件,然后将汇编指令打印出来。所以如果你想看懂 objdump 的结果,你就需要有一些汇编基础才可以。

$ objdump -d /bin/pwd | head

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


Disassembly of section .init:

0000000000401350 <.init>:
  401350:       48 83 ec 08             sub    $0x8,%rsp
  401354:       48 8b 05 6d 5c 20 00    mov    0x205c6d(%rip),%rax        # 606fc8 <__ctype_b_loc@plt+0x205878>
  40135b:       48 85 c0                test   %rax,%rax

nm

nm命令主要是列出目标文件的符号(说白了就是一些函数和全局变量等)。

如果你编译出来的程序没有经过 strip ,那么 nm 命令可以挖掘出隐含在可执行文件中的重大秘密。它可以帮你列出文件中的变量及函数,这对于我们进行反向操作具有重大意义。

下面我们通过一小段简单的程序来讲解 nm 命令的用途。在编译这个程序时,我们加上了 -g 选项,这个选项可以使编译出来的文件包含更多有效信息。

$ 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

gdb 就是所谓的 GNU debugger。

gdb 大家或多或少都有听说过。我们在使用一些 IDE 写代码的时候,可以进行打断点、步进、查看变量值等方式调试,其实这些 IDE 底层调用的也是 gdb 。

对于 gdb 的用法,可以写很多,本文就暂且不深入了。下面先演示一小段 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 下进行程序开发的时候,那么你免不了跟二进制文件打交道。熟练使用以上介绍的 10 个命令,将会对你的工作产生很大的帮助。

良许个人微信

添加良许个人微信即送3套程序员必读资料

→ 精选技术资料共享

→ 高手如云交流社群

NjayqiQ.jpg!web

本公众号全部博文已整理成一个目录,请在公众号里回复「 m 」获取!

推荐阅读:

哈哈,良许的专辑功能终于开通啦~(附使用指南)

这款网络排查工具,堪称神器!文末赠书

从 Java 小白到收获 BAT offer ,分享我这 2 年的经验

5T技术资源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等等。在公众号内回复「 1024 」,即可免费获取!!

eyQvie2.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK