これらの簡単なコマンドとツールは、バイナリファイルの分析タスクを簡単に実行するのに役立ちます。
"この世界には10種類の人間がいる:バイナリーを理解する人間と理解しない人間"
あなたは毎日バイナリファイルを使っていますが、バイナリファイルについてほとんど知りません。バイナリというのは、コマンドラインツールから本格的なアプリケーションまで、あなたが毎日実行している実行ファイルのことです。
Linuxには、バイナリを簡単に分析するためのツールが豊富に用意されています。仕事の役割に関係なく、Linuxで仕事をするのであれば、これらのツールの基本を知ることは、システムをよりよく理解するのに役立ちます。
この記事では、これらのLinuxツールやコマンドの中で最も人気のあるものをいくつか紹介します。もし見つからなければ、パッケージマネージャを使っていつでもインストールして調べることができます。覚えておいてください:適切な場面で適切なツールを使えるようになるには、多くの忍耐と練習が必要です。
file
機能:ファイルの種類を判別します。
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]=d3b2794dcb1f7ec86, stripped$ file /etc/passwd/etc/passwd: ASCII text
ldd
内容: 共有オブジェクトの依存関係を表示します。
実行可能なバイナリファイルに対して上記のfileコマンドを使用した場合、出力に必ず""メッセージが表示されます。これは何を意味するのでしょうか?
ソフトウェアを開発するときは、ライブラリを再作成しないようにしましょう。出力を印刷したり、標準入力から読み込んだり、ファイルを開いたりといった、ほとんどのソフトウェア・プログラムが必要とする共通のタスクがあります。これらの共通タスクはすべて、独自のバリエーションを書くのではなく、誰もが使用できる共通の関数セットに抽象化されています。これらの共通関数は、libc または glibc と呼ばれるライブラリに含まれています。
実行ファイルが依存しているライブラリーはどうやって見つけるのでしょうか?そこで ldd コマンドの出番です。動的にリンクされたバイナリに対してこのコマンドを実行すると、依存するライブラリとそのパスがすべて表示されます。
$ ldd /bin/lslinux-vdso.so.1 => (0x00007ffef5ba1000)libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fea9f)libcap.so.2 => /lib64/libcap.so.2 (0x00007fea9f)libacl.so.1 => /lib64/libacl.so.1 (0x00007fea9f)libc.so.6 => /lib64/libc.so.6 (0x00007fea9f)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(0x, 0x7ffd, 0x <unfinished ...>strrchr("ls", '/') = nilsetlocale(LC_ALL, "") = "en_US.UTF-8"bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"textdomain("coreutils") = "coreutils"__cxa_atexit(0x, 0, 0, 0x56572) = 0isatty(1) = 1getenv("QUOTING_STYLE") = nilgetenv("COLUMNS") = nilioctl(1, x7ffd) = 0<< snip >>fflush(0x7ff7baae61c0) = 0fclose(0x7ff7baae61c0) = 0+++ exited (status 0) +++
hexdump
内容:ファイルの内容をASCII、10進数、16進数、または8進数で表示します。
不明なファイルをhexdumpで開くと、そのファイルの内容を正確に確認することができます。また、いくつかのコマンドラインオプションを使用して、ファイルデータを ASCII で表示することもできます。これにより、ファイルの種類を確認することができます。
$ hexdump -C /bin/ls | head7f 00 00 00 |.ELF............|e 00 00 d4 00 |..>......B@.....|00 00 f0 c3 |@...............|38 00 1e 00 |....@.8...@.....|00 00 00 00 |........@.......|00 00 00 00 |@.@.....@.@.....|f8 00 f8 00 |................|00 00 00 00 |................|00 00 00 00 |8.......8.@.....|00 00 1c 00 |8.@.............|
strings
内容:ファイルから印字可能な文字列を出力します。
バイナリの中の印字可能な文字を探すだけなら、hexdumpは使いすぎのように思えますし、stringsコマンドを使うこともできます。
ソフトウェアを開発する際には、印刷メッセージ、デバッグ情報、ヘルプメッセージ、エラーなど、あらゆる種類のテキスト/ASCII情報が追加されます。これらの情報がすべてバイナリファイルに存在する限り、stringsコマンドを使って画面にダンプすることができます。
$ strings /bin/ls
readelf
内容:ELFファイルに関する情報を表示します。
ELF()は、Linuxだけでなく、さまざまなUNIXシステムで、実行可能ファイルやバイナリ・ファイルの主流となっているファイル形式です。file コマンドのようなツールを使用して、ファイルが ELF 形式であることがわかったら、次のステップとして readelf コマンドとそのさまざまなオプションを使用して、ファイルをさらに分析します。
$ readelf -h /bin/lsELF Header:Magic: 7fClass: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: EXEC (Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0xStart of program headers: 64 (bytes into file)Start of section headers: (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 9Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
オブジャンダンプ
内容:オブジェクトファイルの情報を表示します。
バイナリファイルはあなたが書いたソースコードから作成され、このソースコードはコンパイラと呼ばれるツールによってコンパイルされます。このコンパイラは、ソースコードに関連する機械語命令を生成し、その機械語命令はCPUによって特定のタスクのために実行されます。これらの機械語コードは、アセンブリ言語として知られるニーモニックによって解読することができます。アセンブリ言語は、プログラムによって実行され、最終的にCPU上で実行される操作を理解するのに役立つ命令のセットです。
objdump ユーティリティは、バイナリファイルまたは実行可能ファイルを読み込んで、アセンブリ言語命令を画面にダンプします。objdump コマンドの出力を理解するには、アセンブリ言語の知識が不可欠です。
覚えておいてください:アセンブリ言語はアーキテクチャ固有です。
$ objdump -d /bin/ls | head/bin/ls: file format elf64-x86-64Disassembly of section .init:<_init@@Base>:83 ec 08 sub $0x8,%rsp8b e 21 00 mov 0x218e6d(%rip),%rax # 61afc8 <__gmon_start__>40215b: test %rax,%rax
strace
機能:システムコールとシグナルを追跡します。
以前紹介したltraceを使ったことがある人は、straceも同じようなものだと思ってください。唯一の違いは、strace ユーティリティはコールをトレースするライブラリではなく、システムコールをトレースすることです。システムコールは、カーネルとインターフェイスして仕事をするものです。
例えば、画面に何かを印刷したい場合、標準ライブラリlibcのprintf関数やputs関数を使いますが、その下には、実際に画面に印刷するwriteというシステムコールがあります。
$ strace -f /bin/lsexecve("/bin/ls", ["/bin/ls"], [/* 17 vars */]) = 0brk(NULL) = 0xmmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=40661, ...}) = 0mmap(NULL, 40661, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fclose(3) = 0<< snip >>fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fwrite(1, "R2 RH ", 7R2 RHclose(1) = 0munmap(0x7f, 4096) = 0close(2) = 0exit_group(0) = ?+++ exited with 0 +++
nm
内容:オブジェクトファイル内のシンボルをリストアップします。
使用しているバイナリ・ファイルがストリップされていない場合、nmコマンドはコンパイル中にバイナリ・ファイルに埋め込まれた貴重な情報を提供してくれます。nmはバイナリ・ファイルから変数や関数を特定するのに役立ちます。バイナリ・ファイルのソース・コードにアクセスできない場合、これがどれほど役に立つか想像できるでしょう。
nmを実証するために、-gオプションでコンパイルされた小さなプログラムを簡単に書くと、このバイナリがストリップされていないことがわかります。
$ cat hello.c#include <stdio.h>int main() {printf("Hello world!");return 0;$ gcc -g hello.c -o hello$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3de46c8efb98bce4ad21568ba3d8a5d, not stripped$ ./helloHello world!$$ nm hello | taile20 d __JCR_END__e20 d __JCR_LIST__b0 T __libc_csu_finiT __libc_csu_initU __libc_start_main@@GLIBC_2.2.5d T mainU printf@@GLIBC_2.2.5t register_tm_clonesT _startD __TMC_END__
gdb
機能: GNU Debugger。
バイナリのすべてを静的に解析できるわけではありません。ltraceやstraceのようにバイナリを実行するコマンドの一部が実行されるのは事実ですが、ソフトウェアは様々な条件で構成されており、異なる代替パスが実行される可能性があります。
このような経路を分析する唯一の方法は、プログラムが任意の時点で停止または一時停止し、さらに先の行を実行する前に情報を分析できるランタイム環境です。
そのためにデバッガがあり、Linuxではgdbがデバッガのデファクトスタンダードです。プログラムをロードしたり、特定の場所にブレークポイントを設定したり、メモリやCPUレジスタを解析したり、その他多くのことができます。上記の他のツールを補完し、より多くのランタイム解析を行うことができます。
一つ注意しなければならないのは、gdbを使ってプログラムをロードすると、そのプログラム独自の(gdb)プロンプトが表示されるということです。それ以降のコマンドはすべて、終了するまでこのgdbプロンプトで実行されます。
コンパイルされたhelloプログラムを使用し、gdbで動作を確認します。
$ gdb -q ./helloReading symbols from /home/flash/hello...done.(gdb) break mainBreakpoint 1 at 0x: file hello.c, line 4.(gdb) info breakNum Type Disp Enb Address What1 breakpoint keep y 0x in main at hello.c:4(gdb) runStarting program: /home/flash/./helloBreakpoint 1, main () at hello.c:44 printf("Hello world!");Missing separate debuginfos, use: debuginfo-install glibc-2..el7_6.6.x86_64(gdb) bt#0 main () at hello.c:4(gdb) cContinuing.Hello world q
結論
これらのネイティブLinuxバイナリ解析ツールの使用に慣れ、それらが提供する出力を理解したら、 ような、より高度で専門的なオープンソースバイナリ解析ツールに進むことができます。




