GNU/Linux >> Linux の 問題 >  >> Linux

Linux 実行可能ファイルを逆アセンブル、変更、再アセンブルする方法は?

これを行うための信頼できる方法はないと思います。マシン コード形式は非常に複雑で、アセンブリ ファイルよりも複雑です。コンパイルされたバイナリ (たとえば、ELF 形式) を取得して、同じ (または十分に類似した) バイナリにコンパイルされるソース アセンブリ プログラムを生成することは実際には不可能です。違いを理解するには、GCC コンパイルの出力を直接アセンブラー (gcc -S) と比較します。 ) 対実行可能ファイルの objdump の出力 (objdump -D ).

私が考えることができる2つの主要な合併症があります。まず、マシン コード自体は、ポインター オフセットなどのために、アセンブリ コードと 1 対 1 で対応していません。

たとえば、Hello world への C コードを考えてみましょう:

int main()
{
    printf("Hello, world!\n");
    return 0;
}

これは x86 アセンブリ コードにコンパイルされます:

.LC0:
    .string "hello"
    .text
<snip>
    movl    $.LC0, %eax
    movl    %eax, (%esp)
    call    printf

.LCO は名前付き定数で、printf は共有ライブラリ シンボル テーブル内のシンボルです。 objdump の出力と比較してください:

80483cd:       b8 b0 84 04 08          mov    $0x80484b0,%eax
80483d2:       89 04 24                mov    %eax,(%esp)
80483d5:       e8 1a ff ff ff          call   80482f4 <[email protected]>

まず、定数 .LC0 は現在、メモリ内のどこかのランダム オフセットです。アセンブラとリンカはこれらの定数の場所を自由に選択できるため、この定数を正しい場所に含むアセンブリ ソース ファイルを作成することは困難です。

第二に、これについては完全にはわかりませんが (位置に依存しないコードなどに依存します)、printf への参照は、実際にはそのコードのポインター アドレスでまったくエンコードされていないと思いますが、ELF ヘッダーには実行時にアドレスを動的に置き換えるルックアップ テーブル。したがって、逆アセンブルされたコードは、ソース アセンブリ コードと完全には対応していません。

要約すると、ソース アセンブリには シンボル があります コンパイルされたマシン コードには アドレス があります

2 つ目の大きな問題は、動的にリンクするライブラリや、元のコンパイラによってそこに配置されたその他のメタデータなど、元の ELF ファイル ヘッダーに存在していたすべての情報をアセンブリ ソース ファイルに含めることができないことです。これを再構築するのは難しいでしょう。

私が言ったように、特別なツールがこのすべての情報を操作できる可能性はありますが、再アセンブルして実行可能ファイルに戻すことができるアセンブリ コードを単純に生成できるとは考えにくいです。

実行可能ファイルのほんの一部を変更することに関心がある場合は、アプリケーション全体を再コンパイルするよりもはるかに巧妙な方法をお勧めします。 objdump を使用して、関心のある関数のアセンブリ コードを取得します。手動で「ソース アセンブリ構文」に変換します (ここでは、入力と同じ構文で逆アセンブリを実際に生成するツールがあればいいのにと思います)。 、必要に応じて変更します。完了したら、それらの関数だけを再コンパイルし、objdump を使用して、変更したプログラムのマシン コードを見つけます。次に、16 進エディタを使用して、元のプログラムの対応する部分の上に新しいマシン コードを手動で貼り付けます。新しいコードが古いコードと正確に同じバイト数になるように注意してください (そうしないと、すべてのオフセットが間違っている可能性があります)。 )。新しいコードが短い場合は、NOP 命令を使用して埋め込むことができます。長い場合、問題が発生する可能性があり、代わりに新しい関数を作成して呼び出す必要がある場合があります。


hexdump でこれを行います そしてテキストエディタ。あなたは本当になければなりません マシン コードとそれを格納するファイル形式に慣れ、「逆アセンブル、変更、および再アセンブル」と見なされるものに柔軟に対応できます。

「部分的な変更」 (バイトを書き換えるが、バイトの追加や削除は行わない) だけで済む場合は、(比較的言えば) 簡単です。

あなたは本当に 既存の命令を置き換えたくないのは、両方ともハードコーディングされたimmediate<で、プログラムカウンターに対するジャンプ/ブランチ/ロード/ストアのために、マシンコード内で影響を受ける相対オフセットを手動で調整する必要があるためです。 /em> 値 および レジスタで計算されたもの .

バイトを削除しなくても、いつでも回避できるはずです。より複雑な変更にはバイトの追加が必要になる場合があり、さらに難しくなります。

ステップ 0 (準備)

実際に objdump -D でファイルを適切に逆アセンブルしました または、実際にそれを理解し、変更が必要な場所を見つけるために最初に通常使用するものは何でも、変更する正しいバイトを見つけるのに役立つ次の点に注意する必要があります:

<オール>
  • 変更する必要があるバイトの「アドレス」(ファイルの先頭からのオフセット)。
  • これらのバイトの現在の生の値 (--show-raw-insn objdump へのオプション ここで本当に役に立ちます)。
  • hexdump -R かどうかも確認する必要があります あなたのシステムで動作します。そうでない場合は、残りの手順で xxd を使用します コマンドまたは同様の hexdump の代わりに 以下のすべてのステップで (使用するツールのドキュメントを参照してください。hexdump のみを説明します それは私がよく知っているものなので、今のところこの回答で。

    ステップ 1

    hexdump -Cv でバイナリ ファイルの生の 16 進数表現をダンプします。 .

    ステップ 2

    hexdump を開く ed ファイルを開き、変更しようとしているアドレスのバイトを見つけます。

    hexdump -Cv の短期集中コース 出力:

    <オール>
  • 一番左の列は、バイトのアドレスです (objdump のように、バイナリ ファイル自体の先頭からの相対位置) 提供します)
  • 一番右の列 (| で囲まれています) 文字) は、バイトの「人間が読める」表現です。各バイトに一致する ASCII 文字がそこに書き込まれ、. ASCII 印刷可能文字にマップされないすべてのバイトを表します。
  • 重要なものはその間にあります - 各バイトはスペースで区切られた 2 つの 16 進数で、1 行あたり 16 バイトです。
  • 注意:objdump -D とは異なります 、各命令のアドレスを提供し、エンコードされていると文書化されている方法に基づいて、命令の生の16進数を示します hexdump -Cv ファイルに表示される順序で各バイトを正確にダンプします。これは、エンディアンの違いにより命令バイトが逆の順序になっているマシンでは、最初は少し混乱する可能性があり、特定のアドレスとして特定のバイトを期待している場合にも混乱を招く可能性があります.

    ステップ 3

    変更が必要なバイトを変更します。明らかに、生のマシン命令エンコーディング (アセンブリ ニーモニックではない) を把握し、正しいバイトを手動で書き込む必要があります。

    注:しない 一番右の列の人間が読める表現を変更する必要があります。 hexdump 「アンダンプ」すると無視されます。

    ステップ 4

    hexdump -R を使用して、変更された hexdump ファイルを「アンダンプ」します。 .

    ステップ 5 (健全性チェック)

    objdump あなたの新しく unhexdump ed ファイルを開き、変更した逆アセンブリが正しいように見えることを確認します。 diff objdump に対して

    真剣に、このステップをスキップしないでください。機械語コードを手動で編集するとき、私は間違いを犯すことがよくありますが、これが私がそれらのほとんどを見つける方法です.

    最近 ARMv8 (リトル エンディアン) バイナリを変更したときの実際の動作例を次に示します。 (質問には x86 というタグが付けられています。 、しかし x86 の例は手元にありません。基本的な原則は同じで、手順が異なるだけです。)

    私の状況では、特定の「これを行うべきではない」手持ちチェックを無効にする必要がありました:私の例のバイナリでは、 objdump --show-raw-insn -d で 私が気になった行を次のように出力します (コンテキストのために前後に 1 つの指示を与えます):

         f40:   aa1503e3    mov x3, x21
         f44:   97fffeeb    bl  af0 <[email protected]>
         f48:   f94013f7    ldr x23, [sp, #32]
    

    ご覧のとおり、私たちのプログラムは error にジャンプすることで「うまく」終了しています。 関数 (プログラムを終了します)。受け入れられない。そのため、その命令をノーオペレーションに変えます。そこで、バイト 0x97fffeeb を探しています アドレス/ファイル オフセット 0xf44 .

    これが hexdump -Cv です そのオフセットを含む行。

    00000f40  e3 03 15 aa eb fe ff 97  f7 13 40 f9 e8 02 40 39  |[email protected]@9|
    

    関連するバイトが実際にどのように反転されるか (アーキテクチャのリトル エンディアン エンコーディングは、他のものと同様に機械語命令にも適用されます) と、これがどのバイトがどのバイト オフセットにあるのか、少し直感的ではないことに注意してください:

    00000f40  -- -- -- -- eb fe ff 97  -- -- -- -- -- -- -- --  |[email protected]@9|
                          ^
                          This is offset f44, holding the least significant byte
                          So the *instruction as a whole* is at the expected offset,
                          just the bytes are flipped around. Of course, whether the
                          order matches or not will vary with the architecture.
    

    とにかく、私は 0xd503201f という他の逆アセンブリを見て知っています nop に逆アセンブルします それは私のノーオペレーション命令の良い候補のようです。 hexdump の行を修正しました ed ファイルに応じて:

    00000f40  e3 03 15 aa 1f 20 03 d5  f7 13 40 f9 e8 02 40 39  |[email protected]@9|
    

    hexdump -R でバイナリに戻す 、新しいバイナリを objdump --show-raw-insn -d で逆アセンブルしました 変更が正しいことを確認しました:

         f40:   aa1503e3    mov x3, x21
         f44:   d503201f    nop
         f48:   f94013f7    ldr x23, [sp, #32]
    

    その後、バイナリを実行すると、必要な動作が得られました。関連するチェックによってプログラムが異常終了することはなくなりました。

    マシン コードの変更が成功しました。

    !!!警告!!!

    それとも私は成功しましたか?この例で私が見逃していたことに気が付きましたか?

    プログラムのマシンコードを手動で変更する方法について尋ねているので、おそらくあなたは何をしているのか知っているでしょう。しかし、学ぶために読んでいるかもしれない読者のために、詳しく説明します:

    最後だけを変更しました エラーケースブランチの命令!プログラムを終了する関数へのジャンプ。しかし、ご覧のとおり、x3 を登録します。 mov によって変更されていました すぐ真上に!実際、合計 4 error を呼び出すプリアンブルの一部として、レジスタが変更されました。 、そして1つのレジスターがありました。 if を超える条件付きジャンプから始まる、そのブランチの完全なマシン コードを次に示します。 条件付き if の場合、ジャンプ先のブロックと終了 取られません:

         f2c:   350000e8    cbnz    w8, f48
         f30:   b0000002    adrp    x2, 1000
         f34:   91128442    add x2, x2, #0x4a1
         f38:   320003e0    orr w0, wzr, #0x1
         f3c:   2a1f03e1    mov w1, wzr
         f40:   aa1503e3    mov x3, x21
         f44:   97fffeeb    bl  af0 <[email protected]>
         f48:   f94013f7    ldr x23, [sp, #32]
    

    分岐後のすべてのコードは、プログラムの状態が条件付きジャンプの前と同じであるという前提で、コンパイラによって生成されました。 !しかし、 error への最後のジャンプを行うだけで 関数コードはノーオペレーションです。そのコードに到達するコードパスを作成しました一貫性のない/不正なプログラム状態 !

    私の場合、これは実際に思われる 問題はありません。だから私は幸運になりました。 とても ラッキー:変更したバイナリ (ちなみに、セキュリティ クリティカルなバイナリ) を実行した後でのみ :setuid する機能がありました 、 setgidSELinux コンテキストを変更します !) これらのレジスターの変更が、その後のコード パスに影響を与えたかどうかのコード パスを実際に追跡するのを忘れていたことに気付きました!

    それは壊滅的だった可能性があります-これらのレジスタのいずれかが、現在上書きされている以前の値が含まれていると仮定して、後のコードで使用された可能性があります!そして私は、コードについて細心の注意を払って慎重に考えることで知られている人物であり、コンピュータのセキュリティに常に注意を払うための衒学者であり執着家でもあります。

    引数がレジスタからスタックに流出する関数を呼び出していたらどうなるでしょうか (x86 などでは非常に一般的です)。条件付きジャンプに先行する命令セットに実際に複数の条件付き命令があった場合はどうなるでしょうか (たとえば、古い ARM バージョンではよくあることです)。最も単純に見える変更を行った後、私はさらに無謀に一貫性のない状態になっていたでしょう!

    したがって、これは私の注意事項です: バイナリを手動でいじると、文字通りすべてを剥ぎ取ります 安全 あなたとマシンとオペレーティングシステムが許可するものとの間。文字通りすべて プログラムのミスを自動的に検出するためにツールで行った進歩はなくなった .

    では、これをより適切に修正するにはどうすればよいでしょうか。読み進めてください。

    コードの削除

    効果的に /論理的に 複数の命令を「削除」する場合、「削除」したい最初の命令を、「削除された」命令の最後にある最初の命令への無条件ジャンプに置き換えることができます。この ARMv8 バイナリの場合、次のようになります。

         f2c:   14000007    b   f48
         f30:   b0000002    adrp    x2, 1000
         f34:   91128442    add x2, x2, #0x4a1
         f38:   320003e0    orr w0, wzr, #0x1
         f3c:   2a1f03e1    mov w1, wzr
         f40:   aa1503e3    mov x3, x21
         f44:   97fffeeb    bl  af0 <[email protected]>
         f48:   f94013f7    ldr x23, [sp, #32]
    

    基本的に、コードを「殺す」(「デッドコード」に変える)。補足:バイナリに埋め込まれたリテラル文字列で同様のことを行うことができます:より小さな文字列に置き換えたい場合は、ほとんどの場合、文字列を上書きすることで回避できます (「C- string")、必要に応じて、ハードコーディングされた文字列のサイズを、それを使用するマシン コードで上書きします。

    また、すべての不要な命令を no-op に置き換えることもできます。言い換えれば、不要なコードを「no-op sled」と呼ばれるものに変えることができます:

         f2c:   d503201f    nop
         f30:   d503201f    nop
         f34:   d503201f    nop
         f38:   d503201f    nop
         f3c:   d503201f    nop
         f40:   d503201f    nop
         f44:   d503201f    nop
         f48:   f94013f7    ldr x23, [sp, #32]
    

    それらを飛び越えるのに比べて、CPU サイクルを浪費しているだけだと思いますが、しかし より簡単 したがって、間違いに対してより安全 、使用するオフセット/アドレスの把握を含め、ジャンプ命令をエンコードする方法を手動で把握する必要がないため、それほど考える必要はありません ノーオペレーション スレッドの場合。

    エラーは簡単です:2 つ間違えました その無条件分岐命令を手動でエンコードするとき。そして、それは常に私たちのせいではありません.1回目は、私が持っていたドキュメントが古かった/間違っていて、エンコーディングで1ビットが無視されたと言っていましたが、実際にはそうではなかったので、最初の試行でゼロに設定しました.

    コードを追加する

    できる 理論的には、この手法を使用して 追加 マシン命令も同様ですが、より複雑であり、私はそれを行う必要がなかったので、現時点では実際の例はありません.

    機械語の観点からは、簡単です。コードを追加したい場所で 1 つの命令を選択し、それを追加する必要がある新しいコードへのジャンプ命令に変換します (命令を追加することを忘れないでください)。追加されたロジックにそれが必要ない場合を除き、新しいコードに置き換えられ、追加の最後に戻りたい命令にジャンプします)。基本的に、新しいコードを「接合」しています。

    しかし、その新しいコードを実際に配置する場所を見つける必要があり、これが難しい部分です.

    あなたが本当に 幸運なことに、ファイルの最後に新しいマシン コードを追加するだけで、「そのまま動作」します。新しいコードは、残りのコードと一緒に、予想される同じマシン命令、つまりアドレス空間にロードされます。適切に実行可能とマークされたメモリページに。

    私の経験では hexdump -R は、一番右の列だけでなく、一番左の列も無視します。そのため、手動で追加したすべての行に文字通りゼロのアドレスを入力するだけでうまくいきます。

    運が悪ければ、コードを追加した後、同じファイル内のいくつかのヘッダー値を実際に調整する必要があります:オペレーティング システムのローダーがバイナリに実行可能セクションのサイズを説明するメタデータが含まれていることを期待している場合 (歴史的な理由から)多くの場合「テキスト」セクションと呼ばれます) それを見つけて調整する必要があります。昔は、バイナリは生のマシン コードにすぎませんでした。現在では、マシン コードは一連のメタデータにラップされています (たとえば、Linux の ELF など)。

    それでも少し運が良ければ、ファイル内に既にある残りのコードと同じ相対オフセットでバイナリの一部として適切にロードされる「デッド」スポットがファイル内にある可能性があります (そして、デッド スポットはコードに適合し、CPU 命令のワード アライメントが必要な場合は適切にアライメントされます)。その後、上書きできます。

    本当に運が悪い場合は、コードを追加することはできず、マシン コードで埋めることができるデッド スペースはありません。その時点で、基本的に実行可能形式に精通している必要があり、これらの制約内で合理的な時間内に手動でやってのけることができ、それを台無しにしない合理的な可能性があるものを見つけられることを願っています。 .


    @mgiuca は、技術的な観点からこの回答に正しく対処しました。実際、実行可能プログラムを再コンパイルしやすいアセンブリ ソースに逆アセンブルすることは、簡単な作業ではありません。

    議論に少し追加すると、技術的に複雑ではありますが、探索するのに興味深いテクニック/ツールがいくつかあります。

    <オール>
  • 静的/動的インストルメンテーション .この手法には、実行可能形式の分析、特定の目的のための特定のアセンブリ命令の挿入/削除/置換、実行可能ファイル内の変数/関数へのすべての参照の修正、および変更された新しい実行可能ファイルの発行が含まれます。私が知っているツールには、PIN、ハイジャッカー、PEBIL、DynamoRIO などがあります。このようなツールを設計された目的とは異なる目的に合わせて構成するのは難しい場合があり、実行形式と命令セットの両方を理解する必要があることを考慮してください。
  • 完全な実行可能逆コンパイル .この手法では、実行可能ファイルから完全なアセンブリ ソースを再構築しようとします。仕事をしようとするオンライン逆アセンブラーを一目見たいと思うかもしれません。いずれにしても、さまざまなソース モジュールに関する情報と、場合によっては関数/変数名が失われます。
  • リターゲット可能な逆コンパイル .この手法は、コンパイラ フィンガープリントを調べて、実行可能ファイルからより多くの情報を抽出しようとします。 (つまり、既知のコンパイラによって生成されたコードのパターン)およびその他の決定論的なもの。主な目標は、C ソースなどの高レベルのソース コードを実行可能ファイルから再構築することです。これにより、関数/変数名に関する情報を取り戻すことができる場合があります。 -g でソースをコンパイルすることを検討してください 多くの場合、より良い結果が得られます。 Retargetable Decompiler を試してみてください。
  • このほとんどは、脆弱性評価および実行分析の研究分野からのものです。これらは複雑な手法であり、多くの場合、ツールを箱から出してすぐに使用することはできません.それでも、一部のソフトウェアをリバース エンジニアリングしようとする場合、これらは非常に役立ちます。


    Linux
    1. Linuxでサービスを管理および一覧表示する方法

    2. LinuxにAnsibleをインストールしてテストする方法

    3. Linux でソース コードからソフトウェアをコンパイルしてインストールする方法

    1. LinuxでFlatpakをインストールして使用する方法

    2. Linux で夏時間 (DST) を無効にしてタイムゾーンを変更する方法

    3. Linux でバイナリ実行可能ファイルを逆アセンブルしてアセンブリ コードを取得する方法は?

    1. Linux – Linuxディストリビューションが安全で、悪意のあるコードがないことを確認する方法は??

    2. Linux カーネルモジュールのコーディング方法は?

    3. メモリ不足の状態でも実行可能コードをメモリに保持する方法は? Linuxで