この記事では、Linux 環境で実行可能ファイルをリバース エンジニアリングするために使用できるツールとコマンドについて説明します。
リバース エンジニアリングとは、ソフトウェアの機能を解明する行為であり、利用可能なソース コードはありません。リバース エンジニアリングでは、ソフトウェアの正確な詳細が得られない場合があります。しかし、ソフトウェアがどのように実装されたかについてはかなりよく理解できます。
リバース エンジニアリングには、次の 3 つの基本的な手順が含まれます。
<オール>
I.情報収集
最初のステップは、ターゲット プログラムとその機能に関する情報を収集することです。この例では、「who」コマンドを使用します。 「who」コマンドは、現在ログインしているユーザーのリストを出力します。
1.文字列コマンド
Strings は、印刷可能な文字列をファイルに出力するコマンドです。それでは、これをターゲット (who) コマンドに対して使用してみましょう。
# strings /usr/bin/who
重要な文字列のいくつかは、
users=%lu EXIT COMMENT IDLE TIME LINE NAME /dev/ /var/log/wtmp /var/run/utmp /usr/share/locale Michael Stone David MacKenzie Joseph Arceneaux
about の出力から、「who」が 3 つのファイル (/var/log/wtmp、/var/log/utmp、/usr/share/locale) を使用していることがわかります。
続きを読む:Linux 文字列コマンドの例 (UNIX バイナリ ファイル内のテキストを検索)
2. nmコマンド
nm コマンドは、ターゲット プログラムからシンボルを一覧表示するために使用されます。 nm を使用すると、ローカル関数とライブラリ関数、および使用されるグローバル変数を知ることができます。 nm は、「strip」コマンドを使用してストライプ化されたプログラムでは動作しません。
注:デフォルトでは、「who」コマンドは削除されています。この例では、「who」コマンドをもう一度コンパイルしました。
# nm /usr/bin/who
これにより、以下がリストされます:
08049110 t print_line 08049320 t time_string 08049390 t print_user 08049820 t make_id_equals_comment 080498b0 t who 0804a170 T usage 0804a4e0 T main 0804a900 T set_program_name 08051ddc b need_runlevel 08051ddd b need_users 08051dde b my_line_only 08051de0 b time_format 08051de4 b time_format_width 08051de8 B program_name 08051d24 D Version 08051d28 D exit_failure
上記の出力では:
- t|T – シンボルが .text コード セクションに存在する
- b|B – シンボルは UN 初期化された .data セクションにあります
- D|d – シンボルは初期化された .data セクションにあります。
大文字または小文字によって、シンボルがローカルかグローバルかが決まります。
about 出力から、次のことがわかります。
- グローバル関数 (main、set_program_name、usage など) があります
- いくつかのローカル関数 (print_user、time_string など) があります
- グローバルに初期化された変数 (Version、exit_failure) があります
- UN 初期化された変数 (time_format、time_format_width など) があります
場合によっては、関数名を使用して、関数が何をするかを推測できます。
続きを読む:10 の実用的な Linux nm コマンドの例
情報を取得するために使用できる他のコマンドは
- ldd コマンド
- 定着コマンド
- lsof コマンド
- /proc ファイル システム
II.プログラムの動作の決定
3. ltrace コマンド
ライブラリ関数への呼び出しをトレースします。そのプロセスでプログラムを実行します。
# ltrace /usr/bin/who
出力を以下に示します。
utmpxname(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0 setutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 1 getutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(NULL, 384) = 0x09ed59e8 getutxent(0, 384, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 768) = 0x09ed59e8 getutxent(0x9ed59e8, 768, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1152) = 0x09ed59e8 getutxent(0x9ed59e8, 1152, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1920) = 0x09ed59e8 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 3072) = 0x09ed59e8 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78)
getutxent とそのライブラリ関数ファミリへの一連の呼び出しがあることがわかります。 ltrace は、関数がプログラムで呼び出された順序で結果を提供することにも注意してください。
これで、「who」コマンドが getutxent とその一連の関数を呼び出してログインしているユーザーを取得することで機能することがわかりました。
4. strace コマンド
strace コマンドは、プログラムによって行われたシステム コールをトレースするために使用されます。プログラムがライブラリ関数を使用しておらず、システム コールのみを使用している場合、単純な ltrace を使用すると、プログラムの実行を追跡できません。
# strace /usr/bin/who
[b76e7424] brk(0x887d000) = 0x887d000 [b76e7424] access("/var/run/utmpx", F_OK) = -1 ENOENT (No such file or directory) [b76e7424] open("/var/run/utmp", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 . . . [b76e7424] fcntl64(3, F_SETLKW, {type=F_RDLCK, whence=SEEK_SET, start=0, len=0}) = 0 [b76e7424] read(3, "\10\325"..., 384) = 384 [b76e7424] fcntl64(3, F_SETLKW, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0
malloc 関数が呼び出されるたびに、brk() システム コールが呼び出されることがわかります。 getutxent ライブラリ関数は実際に「open」システム コールを呼び出して「/var/run/utmp」を開き、読み取りロックを設定して内容を読み取り、ロックを解除します。
これで、who コマンドが utmp ファイルを読み取って出力を表示することを確認しました。
「strace」と「ltrace」の両方に、使用できる優れたオプションのセットがあります。
- -p pid – 指定された pid にアタッチします。プログラムが既に実行されていて、その動作を知りたい場合に役立ちます。
- -n 2 – ネストされた各呼び出しを 2 つのスペースでインデントします。
- -f – フォークをたどる
続きを読む:Linux でプログラムの実行をデバッグするための 7 つの Strace の例
III.ライブラリ呼び出しの傍受
5. LD_PRELOAD &LD_LIBRARY_PATH
LD_PRELOAD を使用すると、プログラムの特定の実行にライブラリを追加できます。このライブラリの関数は、実際のライブラリ関数を上書きします。
注:「suid」ビットが設定されたプログラムではこれを使用できません。
次のプログラムを見てみましょう。
#include <stdio.h> int main() { char str1[]="TGS"; char str2[]="tgs"; if(strcmp(str1,str2)) { printf("String are not matched\n"); } else { printf("Strings are matched\n"); } }
プログラムをコンパイルして実行します。
# cc -o my_prg my_prg.c # ./my_prg
「文字列が一致しません」と表示されます。
次に独自のライブラリを作成し、ライブラリ関数をインターセプトする方法を見ていきます。
#include <stdio.h> int strcmp(const char *s1, const char *s2) { // Always return 0. return 0; }
コンパイルして、LD_LIBRARY_PATH 変数を現在のディレクトリに設定します。
# cc -o mylibrary.so -shared library.c -ldl # LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
これで「library.so」という名前のファイルが作成されます。
LD_PRELOAD 変数をこのファイルに設定し、文字列比較プログラムを実行します。
# LD_PRELOAD=mylibrary.so ./my_prg
これで、strcmp 関数のバージョンを使用するため、「文字列が一致しました」と出力されます。
注:ライブラリ関数をインターセプトする場合、独自のライブラリ関数は元のライブラリ関数と同じプロトタイプを持つ必要があります。
プログラムのリバース エンジニアリングに必要な非常に基本的なことを説明しました。
リバース エンジニアリングの次のステップに進みたい人は、ELF ファイル形式とアセンブリ言語プログラムを理解することが大いに役立ちます。