こんにちは!セキュリティの世界へようこそ。インシデントレスポンスやデジタルフォレンジックの世界では、日々巧妙化するサイバー攻撃の痕跡を追いかけています。
今回は、メモリフォレンジックの少しディープなテーマ、「Windowsにおけるページテーブル解析による物理メモリと仮想メモリの対応付け」についてお話しします。
「なんだか難しそうな名前だな……」と思いましたか? 大丈夫です! 小難しい専門用語はちょっと置いておいて、まずは身近な例えから一歩ずつ優しく紐解いていきましょう。
—
1. 仮想メモリとページテーブルを「マンションの郵便受け」に例えてみよう
パソコンの中には「メモリ」と呼ばれる机の上のようなスペースがあります。WindowsなどのOSは、この机の上を上手に管理するために、「仮想メモリ」という仕組みを使っています。
ここで、パソコンの中を「巨大なマンション」、アプリを「そこに住む住人たち」に例えてみましょう。
- 仮想メモリ(住人の住所):アプリから見えている世界です。「私の部屋番号は 101号室 だよ」と、アプリは自分の住所を勘違い(あるいは信頼)しています。
- 物理メモリ(実際の部屋):パソコンに実際に刺さっているメモリ(RAM)というハードウェア上の実体です。「実際には、あのアプリのデータはマンションの3階の奥のほうに置いてある」という現実の場所です。
- ページテーブル(管理人さんの住所対応表):住人が「101号室の荷物取って!」と言ったとき、管理人さんが「あ、101号室のデータは、実際には3階のB-4という棚にバラバラに置いてあるな」と対応付けるための秘密の台帳です。
攻撃者は、この管理人さんの台帳(ページテーブル)をこっそり書き換えたり、悪用したりして、セキュリティの目を盗もうとします。だからこそ、私たちアナリストは、このページテーブルの仕組みを理解し、隠されたデータを見つけ出す必要があるのです。
—
2. なぜ攻撃者はページテーブルを狙うのか?
サイバー攻撃者やマルウェアは、自分たちの正体や悪だくみの証言(怪しいプログラムの断片など)を、あえて物理メモリのあちこちにバラバラに隠して保存します。人間でいう「証拠隠滅のシュレッダーがけ」のようなものです。
もし、私たちが「あ、怪しいデータがあるな」と気づいても、それが物理メモリ上のどこにどう散らばっているのかを知らなければ、元のファイル(例えば、盗まれたパスワードのリストや、送り出そうとしている不正なプログラム)を復元することはできません。
ここで登場するのが、ページテーブル解析です。バラバラにちぎられたパズルのピース(物理メモリ上の断片)を、ページテーブルという設計図を使って正しく並べ替え、元のきれいなファイル構造として再構築する技術になります。これができるようになると、メモリの奥底に隠された悪意あるデータを丸裸にできるようになります。
—
3. 実務で役立つ!メモリ解析の雰囲気をPythonで体験してみよう
「理屈は分かったけれど、実際にどうやってデータを紐解くの?」という方のために、Pythonを使ってページテーブルの仕組みをシンプルなコードで模してみましょう。
実際のフォレンジックツール(Volatilityなど)の裏側でも、このような計算やマッピングが行われています。一歩ずつコードを見ていきましょう。
# 仮想メモリのアドレスから物理メモリ上の場所を引くためのシミュレーションスクリプト
class PageTableManager:
def __init__(self):
# 管理人さんの台帳(ページテーブル)を辞書で表現します
# キー: 仮想アドレスのページ番号, バリュー: 実際の物理アドレスのページ番号
self.page_table = {
0x01: 0x8A, # 仮想ページ 0x01 は、実際の物理ページ 0x8A にある
0x02: 0x3F, # 仮想ページ 0x02 は、実際の物理ページ 0x3F にある
0x03: 0xFF # 仮想ページ 0x03 は、実際の物理ページ 0xFF にある
}
self.page_size = 4096 # 一般的なWindowsのページサイズ(4KB = 4096バイト)
def translate_address(self, virtual_address):
"""
仮想アドレスを物理アドレスに変換する関数
"""
# 仮想アドレスから「ページ番号」と「ページ内のオフセット(ズレ)」を計算する
page_number = virtual_address // self.page_size
offset = virtual_address % self.page_size
# ページテーブルを確認する
if page_number in self.page_table:
physical_page = self.page_table[page_number]
# 物理アドレスを計算して返す
physical_address = (physical_page * self.page_size) + offset
return physical_address
else:
# ページが見つからない場合はエラー(ページフォールト等)
return None
# --- 実行のテスト ---
if __name__ == "__main__":
manager = PageTableManager()
# 例として、仮想アドレス 8192 ( 2ページ目の先頭付近 ) を変換してみます
target_v_addr = 8192
result_p_addr = manager.translate_address(target_v_addr)
if result_p_addr is not None:
print(f"[*] 仮想アドレス [0x{target_v_addr:X}] は、")
print(f" 物理アドレス [0x{result_p_addr:X}] に対応付けられています!")
else:
print("[-] 該当するページテーブルのエントリが見つかりませんでした。")
このコードのように、OSは常に仮想アドレスと物理アドレスを計算して、アプリのデータを安全(かつ効率的)に読み書きしています。フォレンジック調査では、この「対応付けのルールそのものが書き換えられていないか」をチェックすることが非常に重要になるのです。
—
4. 新人のIT担当者・開発者が知っておくべきセキュリティの勘所
ここまで読んでくださった読者の皆さん、お疲れ様でした! 最後に、現場の視点から「私たち日々の開発やインフラ運用で、どう身を守ればいいのか」というアドバイスをいくつかお伝えします。
1. メモリのdump(保全)ができる環境を心構えておく
万が一サーバーがサイバー攻撃を受けたとき、電源を切ってしまうと物理メモリ上の証拠(マルウェアの活動記録など)はすべて消えてしまいます。「いざという時にメモリのイメージを安全にファイルとして保存する手順」を、チームであらかじめ共有しておきましょう。
2. モダンなOSの保護機能を過信しすぎない
WindowsにはKASLR(Kernel Address Space Layout Randomization)など、アドレスをランダムにして攻撃を難しくする素晴らしい機能が備わっています。しかし、巧妙な攻撃者は脆弱性を突いてそのガードをくぐり抜けてきます。「仕組みがあるから絶対安全」ではなく、「破られたらどうやって検知・解析するか」を考えるのがプロのエンジニアの視点です。
インシデントレスポンスやメモリフォレンジックは、まるでパズルを解くようなエキサイティングな世界です。難しい用語に出会っても、今回のように「身の回りの仕組みに例えたらどうなるだろう?」と少しずつ紐解いていけば、必ず自分の血肉になります。
一歩ずつ、確実にスキルアップしていきましょう!
コメント