通常、私はアセンブリプログラムを作成し、その内容をダンプしてシェルコードを取得します。私が疑問に思っていたのは、アセンブリの代わりにC++プログラムを記述してから、アセンブリの代わりにthatをダンプして使用できるかどうかです。
Linuxカーネル は、生のマシン(その時点でBIOSコードしかない)に「挿入」され、多くの機能を提供するため、一種の究極のシェルコードと見なすことができます。そのカーネルはCで書かれています。
CまたはC++でシェルコードを記述する場合、ライブラリ呼び出しおよびlinking、同じ問題の2つの側面です。
Cコンパイラによって生成されたコードには、コードの他の部分、または外部関数や変数への参照が含まれる場合があります。たとえば、独自のコードで定義されている場合でも、static
変数にアクセスすると、アセンブリオペコードに「穴」が含まれます:アセンブリレベル(_gcc -S
_で取得するもの) Cコンパイラとしてgccを使用する場合)、基本的な「mov」オペコードと変数名が表示されます。オブジェクトファイル(プラットフォームに応じて_.o
_または_.obj
_)でバイナリに変換されると、変数の実際のアドレスがまだわからないため、穴はまだ埋められていません。オブジェクトファイルには、リンカにリンク段階で埋められる各穴の位置を通知するテーブル(「再配置」)が含まれています。
もちろん問題は、シェルコードを作成するときに、リンカのような贅沢さがないことです。あなたはすでに準備ができており、埋める穴がなく、ロードされるアドレスで実行できる一連のバイトを生成する必要があります。アドレスは必ずしも事前にわかっているわけではありません(一部は [〜#〜] aslr [〜#〜] )。 位置に依存しないコード と記述する必要があります。コンパイラーはそのためのコマンドラインフラグを提供することがよくあります(例:_-fPIC
_ with gcc
)。ただし、これは「真」のPICではありません。その種類のコードには、リンカーが埋める穴がまだありますdynamicリンカー;これは、特別なテーブル(GOT、別名「グローバルオフセットテーブル」)に穴が再グループ化されているため、PICであり、ダイナミックリンカーのタスクが簡単になります。
残念ながら、Cコンパイラ、さらにはC++コンパイラでは、ユーザーの同意なしにホールのあるコードが生成されます。たとえば、一般的なCコンパイラは、struct
値を処理するときに、memcpy()
(標準ライブラリ関数)への非表示の呼び出しでコードを生成します。非表示の関数と静的変数(オブジェクトvtableなど)でサポートされているC++機能のすべての道具が原因で、C++はさらに悪化します。
Linuxカーネルは [〜#〜] mmu [〜#〜] を使用してこれらの問題を解決します。ブートロードコード(手書きのアセンブリの一部)はMMU=を制御して、カーネルコードがあるRAMページを確認します。アドレス空間の固定された既知のアドレスに表示されます:コンパイル時にカーネルは明示的な仮定にリンクされます;カーネルはアドレス空間の正確な位置にある場合にのみ正しいアドレスで埋められますMMUはカーネル特権です。シェルコードからはもちろん、ユーザーランドコードからも実行できません。
シェルコードをアセンブリの一部にすることを想像できます。これは、実際には動的リンカーであり、CまたはC++コンパイラーによって生成されたバイナリコードの一部をロードし、動的にパッチを適用できます。つまり、「穴」を実際のロードアドレス。これが metasploit で提供されるエクスプロイトで発生します。エクスプロイト自体がその気の利いたコードをインストールします。これにより、高レベル言語で記述された、汎用DLLである「ペイロード」がインストールされます。完全な VNCサーバー 。
ただし、どのように配置しても、手動アセンブリでPICコードを実行する必要があります。独自のローダー/リンカーを作成することは、教育学的に優れた演習であり、初期のエクスプロイトを任意の制御レベルに活用するために使用できますが、その中心には、手作りのアセンブリが依然として必要です。
マシンコード命令にコンパイルされるあらゆる言語でシェルコードを書くことは完全に有効です。犠牲プログラムによってリンクされていない外部ライブラリがその操作に必要でない場合。
ただし、直接コンパイルされたコード(Cのみからでも)が有効な注入可能なシェルコードであるということはほとんどありません。最も一般的な理由は、文字列バッファインジェクションでNULL
バイトを削除する必要があることです。
コンパイルされたコードも肥大化し、関数のプロローグ、スタックフレームのセットアップ、プルダウンなど、さまざまな「簿記」タイプのタスクが実行されます。これらすべてにより、シェルコード(通常は非常に小さいもの)には適していません。
したがって、プログラムを記述してコンパイルし、マシンコードレベルで調整する必要があります。これは実際に、アセンブリを最初から作成することを少し気になる可能性がある多くの人々が採用する非常に一般的なアプローチです。
ただし、私の経験では、最終的なシェルコードをいじくり回してしまうので、終了する前に、そこに含まれるすべてのバイトに精通しているので、最初から直接、Assemblyを直接使用した方が早いでしょう。
もちろん、大きくて複雑なタスクを実行しようとしている場合は、より高いレベルの言語でそれを書くことは魅力的です。しかし実際には、エクスプロイトによって挿入される単純なstagerシェルコードを作成する方が通常はより良いアプローチです。次に、ステージャーは実際のペイロードをダウンロードして実行します。これにより、実際のペイロードを、独自のライブラリを自由にリンクする複雑なスタンドアロンの実行可能ファイルにすることができます。
もちろん、名前の由来は覚えておいてください。シェルコードは通常、ステージャーのタスクを実行することを目的としており、典型的なステージャーはexecve("/bin/sh", null, null).
です
TildalWaveが指摘するように、C++からコンパイルする場合でも、出力マシンコードをきめ細かく制御することが可能です。最も直接的な方法は、たとえば、C++ソース内にインラインアセンブリステートメントを含めることです。
__asm__ ("movl %eax, %ebx\n\t"
"movb %ah, (%ecx)");
コンパイラディレクティブを変更することもできます。編集するコードをコンパイルするときは、常にすべてのコンパイラ最適化を無効にする必要があります。命令の数を減らすためにコンパイラが奇妙なことよりも混乱するものはほとんどありません。
はい、c/c ++でシェルコードを書くことは可能ですが、コンパイラーに大きく依存しています。絶対アドレスを使用する機能を使用することはできません。シェルコードがロードされる絶対アドレスがわからないため、すべてが相対アドレスである必要があります。つまり、文字列、ライブラリ、グローバル変数、クラス継承などは使用できません。
この例を書いた here