web-dev-qa-db-ja.com

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

とにかくこれを行うことができますか?私はobjdumpを使用しましたが、私が知っているどのアセンブラーでも受け入れられるアセンブリ出力を生成しません。実行可能ファイル内の命令を変更して、後でテストできるようにしたいと思います。

48
FlagCapper

これを行う確実な方法はないと思います。マシンコードの形式は非常に複雑で、アセンブリファイルよりも複雑です。コンパイルされたバイナリ(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 <printf@plt>

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

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

要約すると、ソースアセンブリにはsymbolsがありますが、コンパイルされたマシンコードにはaddressesがあり、これを元に戻すのは困難です。

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

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

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

29
mgiuca

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

議論にいくつかのビットを追加するために、技術的に複雑ですが、探求するのに興味深いかもしれないいくつかのテクニック/ツールがあります。

  1. 静的/動的計測。この手法では、実行可能形式の分析、特定の目的のための特定のアセンブリ命令の挿入/削除/置換、実行可能ファイル内の変数/関数へのすべての参照の修正、および変更された新しい実行可能ファイルの生成を行います。私が知っているいくつかのツールは: [〜#〜] pin [〜#〜]Hijacker[〜#〜] pebil [〜#〜 ]DynamoRIO 。そのようなツールを、それらが設計された目的とは異なる目的に構成するのは難しい場合があり、実行可能形式と命令セットの両方を理解する必要があることを考慮してください。
  2. 完全な実行可能な逆コンパイル。この手法は、実行可能ファイルから完全なアセンブリソースを再構築しようとします。仕事をしようとする Online Disassembler をちらりと見たいと思うかもしれません。とにかく、さまざまなソースモジュールおよびおそらく関数/変数名に関する情報を失います。
  3. 再ターゲット可能な逆コンパイル。この手法は、コンパイラのフィンガープリント(つまり、既知のコンパイラによって生成されたコードのパターン)およびその他の確定的なものを調べて、実行可能ファイルからより多くの情報を抽出しようとします。主な目標は、実行可能ファイルからCソースなどの高レベルのソースコードを再構築することです。これは、関数/変数名に関する情報を取り戻すことができる場合があります。 -gを使用してソースをコンパイルすると、より良い結果が得られることがよくあります。 Retargetable Decompiler を試してみてください。

このほとんどは、脆弱性の評価と実行分析の研究分野に由来しています。それらは複雑な技術であり、多くの場合、ツールはそのままではすぐには使用できません。それにもかかわらず、一部のソフトウェアをリバースエンジニアリングしようとするときに、それらは非常に役立つヘルプを提供します。

7
ilpelle

バイナリアセンブリ内のコードを変更するには、通常、3つの方法があります。

  • 定数のような些細なことであれば、16進エディタで場所を変更するだけです。あなたがそれを最初から見つけることができると仮定します。
  • コードを変更する必要がある場合は、LD_PRELOADを使用してプログラムの一部の関数を上書きします。ただし、関数が関数テーブルにない場合は機能しません。
  • LD_PRELOADを介してロードする関数に直接ジャンプするように修正したい関数のコードをハックし、同じ場所にジャンプします(これは上記2つの組み合わせです)。

もちろん、議会が何らかのセルフインテグリティチェックを行う場合は、2番目のものだけが機能します。

編集:それが明らかでない場合、バイナリアセンブリをいじるのは非常に高度な開発者向けの作業であり、本当に具体的な質問でない限り、ここで質問するのは難しいでしょう。

7
Cine

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

「スポットの変更」(バイトの書き換えは行うが、バイトの追加や削除は行わない)だけで済むのであれば、(比較的言えば)簡単です。

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

あなたは常にバイトを削除しないで逃げることができるはずです。より複雑な変更にはバイトの追加が必要になる場合があり、さらに難しくなります。

ステップ0(準備)

実際にobjdump -Dまたは実際に最初に使用して実際にそれを理解し、変更する必要のあるスポットを見つけるために何を使ってファイルを適切に分解したら、次のことを行う必要があります。変更する正しいバイトを見つけるのに役立つ次のことに注意してください。

  1. 変更する必要があるバイトの「アドレス」(ファイルの先頭からのオフセット)。
  2. 現在のバイトの生の値(objdump--show-raw-insnオプションは、ここでは非常に役立ちます)。

ステップ1

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

ステップ2

hexdumpedファイルを開き、変更するアドレスのバイトを見つけます。

hexdump -Cv出力のクイッククラッシュコース:

  1. 左端の列はバイトのアドレスです(objdumpが提供するのと同じように、バイナリファイル自体の先頭を基準にしています)。
  2. 右端の列(|文字で囲まれている)は、バイトの「人間が読める」表現です-各バイトに一致するASCII文字がそこに書き込まれ、. ASCII印刷可能な文字にマップしないすべてのバイトを表します。
  3. 重要なものはその間にあります-各バイトはスペースで区切られた2つの16進数で、1行あたり16バイトです。

注意:objdump -Dは、各命令のアドレスを提供し、エンコードされたものとしてドキュメント化された方法に基づいて命令の未加工の16進数を表示するのとは異なり、hexdump -Cvは、各バイトをファイル。これは、エンディアンの違いが原因で命令バイトが逆順であるマシンで最初に混乱する可能性があり、特定のバイトを特定のアドレスとして期待しているときに方向が乱れる場合もあります。

ステップ3

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

注:しないでください右端の列の人間が読める形式を変更する必要があります。 hexdumpは、「ダンプ解除」すると無視します。

ステップ4

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

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

objdumpに新しくunhexdumpedファイルを追加し、変更した逆アセンブリが正しいことを確認します。オリジナルのdiffに対するobjdumpです。

真剣に、このステップをスキップしないでください。機械コードを手動で編集するとき、私は間違いを犯すことが多く、これが私がそれらのほとんどを捕らえる方法です。

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

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

     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <error@plt>
     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  |..........@...@9|

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

00000f40  -- -- -- -- eb fe ff 97  -- -- -- -- -- -- -- --  |..........@...@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.

とにかく、他の逆アセンブリを見ると、0xd503201fnopに逆アセンブルすることがわかっているので、これは私のno-op命令の良い候補のようです。 hexdumpedファイルの行を適宜変更します。

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

hexdump -Rを使用してバイナリに変換し直し、objdump --show-raw-insn -dを使用して新しいバイナリを逆アセンブルし、変更が正しいことを確認します。

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

次に、バイナリを実行して、希望する動作が得られました。関連するチェックでプログラムが異常終了することはなくなりました。

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

!!!警告!!!

または私は成功しましたか?この例で私が見逃したものを見つけましたか?

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

エラーケースブランチのlast命令のみを変更しました!問題を終了する関数にジャンプします。しかし、ご覧のとおり、レジスタx3は上記のmovによって変更されていました。実際、合計 4つの(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 <error@plt>
     f48:   f94013f7    ldr x23, [sp, #32]

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

私の場合、これは実際にはのように見え、問題を引き起こしません。運が良かったです。 Very ラッキー:変更したバイナリを実行した後でのみ(偶然にも security-critical binary でした): setuidsetgid、および変更 SELinuxコンテキスト!)これらのレジスターの変更が後で発生するコードパスに影響を与えるかどうかのコードパスを実際に追跡するのを忘れたことに気付きました!

それは破局的であった可能性があります-それらのレジスタのいずれかが、現在上書きされた以前の値を含んでいると想定して、後のコードで使用された可能性があります!そして、私は、コードについて細心の注意を払って考えていること、そして常にコンピュータのセキュリティに誠実であることの責任者およびステッカーとして人々が知っている種類の人です。

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

つまり、この私の注意点:バイナリを手動でいじるのは、文字通り取り除かれますevery安全あなたとマシンとオペレーティングシステムの間許可します。文字通り all 自動的にプログラムの間違いを見つけるためにツールで行った進歩 gone

では、これをより適切に修正するにはどうすればよいでしょうか?読む。

コードを削除する

効果的に/ 論理的に複数の命令を「削除」するには、「削除」する最初の命令を無条件ジャンプで置き換えることができます「削除された」命令の最後の最初の命令に。この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 <error@plt>
     f48:   f94013f7    ldr x23, [sp, #32]

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

すべての不要な命令を何もしないで置き換えることもできます。つまり、不要なコードを「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サイクルを浪費しているだけだと思います、しかしそれは simpler であり、したがって safeミスに対して、ジャンプ命令をエンコードする方法を手動で把握する必要がないため、使用するオフセット/アドレスを把握する必要があります。と考える必要はありません。多く何もしないスレッド。

明確にするために、エラーは簡単です。無条件分岐命令を手動でエンコードすると、 two(2)回失敗しました。そして、それが常に私たちの責任であるとは限りません。初めてだったのは、私が持っていたドキュメントが古くなっていて間違っていて、実際にはそうでなかったときに1ビットがエンコードで無視されたからです。

コードを追加する

あなたは could 理論的にこのテクニックを使用して add 機械語命令も追加しますが、それはより複雑であり、私がやる必要がなかったので、現時点では、動作する例はありません。

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

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

あなたが really ラッキーである場合は、ファイルの最後に新しいマシンコードを追加するだけで、「うまくいく」でしょう。新しいコードは、残りは、予想される同じマシン命令、適切にマークされた実行可能ファイルとしてマークされたメモリページに該当するアドレススペーススペースに配置されます。

私の経験では、hexdump -Rは右端の列だけでなく左端の列も無視します。つまり、手動で追加したすべての行に文字通りゼロのアドレスを入力すれば、問題は解決します。

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

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

本当に運が悪い場合は、コードを追加するだけでなく、マシンコードで埋めることができるデッドスペースがありません。その時点で、基本的には実行可能形式に精通している必要があり、これらの制約内で、妥当な時間内に手動でプルオフして、それをめちゃくちゃにしない合理的な可能性がある、人間が実行可能な何かを理解できることを願っています。 。

4
mtraceur

私の「ciアセンブラ逆アセンブラ」は、逆アセンブルが何であれ、バイトごとに同じバイナリに再アセンブルする必要があるという原則に基づいて設計された唯一のシステムです。

https://github.com/albertvanderhorst/ciasdis

Elf-executableとその分解および再組み立ての例が2つあります。もともとは、コード、インタープリターコード、データ、グラフィック文字で構成されるブートシステムを、リアルモードからプロテクトモードへの移行などの機能を使用して変更できるように設計されていました。 (成功しました。)例は、実行可能ファイルからのテキストの抽出も示しています。これは、後でラベルに使用されます。 debianパッケージはIntel Pentiumを対象としていますが、プラグインはDec Alpha、6809、8086などで利用できます。

分解の品質は、どれほどの労力を費やすかに依存します。たとえば、elfファイルであるという情報も提供しない場合、逆アセンブリは1バイトで構成され、再アセンブリは簡単です。例では、ラベルを抽出するスクリプトを使用して、変更可能な本当に使用可能なリバースエンジニアリングプログラムを作成しています。何かを挿入または削除すると、自動生成されたシンボリックラベルが再計算されます。

バイナリBLOBについてはまったく想定されていませんが、もちろん、インテルの逆アセンブリは、Dec Alphaバイナリにはほとんど役に立ちません。

miasm

https://github.com/cea-sec/miasm

これは、最も有望な具体的な解決策のようです。プロジェクトの説明によると、ライブラリは次のことができます。

  • Elfesteemを使用したPE/ELF 32/64 LE/BEのオープン/変更/生成
  • X86の組み立て/分解/ ARM/MIPS/SH4/MSP430

したがって、基本的には次のようになります。

  • eLFを解析して内部表現(逆アセンブリ)にする
  • あなたが望むものを修正する
  • 新しいELF(アセンブリ)を生成する

テキストの逆アセンブリ表現を生成するとは思わない、おそらくPythonデータ構造をウォークスルーする必要があります。

TODOは、ライブラリを使用してそのすべてを行う方法の最小限の例を見つけます。良い出発点は example/disasm/full.py のようです。これは、特定のELFファイルを解析します。主要な最上位構造はContainerで、Container.from_streamでELFファイルを読み取ります。 TODO後で再組み立てする方法は?この記事はそれを行うようです: http://www.miasm.re/blog/2016/03/24/re150_rebuild.html

この質問は、他にそのようなライブラリがあるかどうかを尋ねます: https://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statically-modify-elf-executables =

関連する質問:

この問題は自動化できないと思います

一般的な問題は完全に自動化できないと思います。一般的な解決策は基本的にバイナリを「リバースエンジニアリングする方法」と同等です。

意味のある方法でバイトを挿入または削除するには、すべての可能なジャンプが同じ場所にジャンプし続けることを確認する必要があります。

正式には、バイナリの制御フローグラフを抽出する必要があります。

ただし、たとえば https://en.wikipedia.org/wiki/Indirect_branch などの間接分岐では、そのグラフを特定するのは簡単ではありません。以下も参照してください。 間接ジャンプ先の計算

あなたがしたいかもしれないもう一つのこと:

  • バイナリインストルメンテーション-既存のコードの変更

興味があれば、Pin、Valgrind(またはこれを行うプロジェクト:NaCl-Googleのネイティブクライアント、おそらくQEmu)をチェックしてください。

Ptrace(つまり、gdbのようなデバッガー)の監視下で実行可能ファイルを実行し、実際のファイルを変更せずに、実行中に実行を制御できます。もちろん、影響を与えたい特定の命令が実行可能ファイルのどこにあるかを見つけるなど、通常の編集スキルが必要です。

0
user502515