はじめに
江添さんのブログ「OpenBSD、1985年に追加されたIntelの最新の誇大広告された機能を使わないことにより脆弱性を華麗に回避」で、x86にデバッグレジスタというものがあることを今更知りました。それを使って見ようとQEMUにFreeDOS入れて触ってみたらQEMUが固まった、という話です。
デバッグレジスタとは
デバッグする時に、プログラムの指定した場所に来たら動作を止めたり(ブレークポイント)、メモリの監視をして、ある場所の値の読み書きを検知(ウォッチポイント)したい時がよくあります。これをハードウェア的に支援するのがX86のデバッグレジスタです。デバッグレジスタはDR0
からDR7
まで8本ありますが、DR4
とDR5
は使いません。DR0
からDR3
の4本は、監視対象のアドレスを入れます。DR7
はデバッグコントロールレジスタと呼ばれ、どのような条件で止めるかを指定します。DR6
はデバッグステータスレジスタで、どのブレークポイントで止まったか等が指定されます。
これらのレジスタへの読み書きは特権命令となっており、通常のOSでは読み書きが許されません。ちょっとやってみましょう。デバッグレジスタDR7
に`
int
main(void) {
__asm__("movq %dr7, %rax");
}
これをMac上のg++でコンパイル、実行してみましょう。
$ g++ test.cpp
$ ./a.out
zsh: segmentation fault ./a.out
つれないお返事です。
ついでに、Windows環境のDEBUGでやってみま・・・あれ?DEBUGが無い!
DEBUGとはMSDOS時代から連綿とサポートされて来た、Microsoft純正のデバッガですが、なんとWindows7から搭載されなくなったそうです。わりと便利なのに残念。まぁどうせ、DEBUGで実行したとしてもSIGSEGVかSIGILLが出るだけでしょうが。
QEMU+FreeDOSの環境構築
現代のOSを使っていると、ユーザから直接特権命令を触ることはできません。触れなくもないですが、かなり面倒なことになります。
「俺も特権命令を使ってみたい!でも面倒なことはしたくない!」
そういう場合は、リングプロテクションの無いOSを使うのが最も簡単です。具体的にはDOSを使います。QEMUというプロセッサエミュレータを使い、FreeDOSというMS-DOS互換のフリーDOS実装を使って遊んでみましょう。
QEMUのインストール
brewで一発です。
$ brew instal qemu
FreeDOSのダウンロード
FreeDOS本家からISOイメージを取ってきます。「CDROM “standard” installer」を落とすのが良いでしょう。本稿執筆時点のバージョンは1.2です。
ディスクイメージの作成
FreeDOSをインストールするハードディスクのイメージを作りましょう。qemu-imgを使います。サイズは200MBもあれば良いと思います。
$ qemu-img create -f raw freedos.img 200M
FreeDOSのインストール
ダウンロードしたISOイメージと先程作成したディスクイメージを使ってQEMUを起動します。
$ qemu-system-i386 freedos.img -cdrom FD12CD.iso -boot d
これは、「i386の石をエミュレートし、freedos.imgをハードディスクイメージ(Cドライブ)とし、FD12CD.isoをCD-ROM(Dドライブ)に入れた状態で、Dドライブから起動しなさい(-boot d
)」という意味です。
あとは本家サイトの説明に従ってインストールするだけです。基本的に「Y」とか「1」とか「ESC」とか押すだけです。
インストールの終了後は
$ qemu-system-i386 freedos.img
でFreeDOSが実行できます。起動後、どういうオプションで起動するか1〜4で聞かれますが、なんでも良いです。なにもドライバを読み込まない4で起動するといろいろ安定しているという噂ですが、僕の環境では1で問題が起きたことはありません。起動するとプロンプトが出てきます。
おおー、昔懐かしいDOS画面!
DEBUGの使い方
さて、早速DEBUGコマンドを使ってみましょう。コマンドプロンプトでdebugと打ち込むとDEBUG.COMが起動します。
C:\> debug
-
この「-」がDEBUGのプロンプトです。例えば現在のレジスタの状態を表示してみましょうか。「r」 と打ち込んでみます(大文字小文字は区別されません)。
AXやらBXが全て0になっていますね。コードセグメントは083Fです。
Rの後ろに特定のレジスタを指定すると、そのレジスタの中身が表示されます。これで例えばeax
のような32bitレジスタの値も表示できます。
さて、何かコードを書いてみましょう。例えばAXに1を代入してみましょうか。プログラムを書くのは「a」です。通常、オフセットアドレス100から書き込みますので「a 100」とします。
- a 100
083F:0100
と、コードセグメント083F、オフセットアドレス0100のところに何か書けるようになりました。mov ax,1
1と入力し、改行を二回入れてみましょう。
- a 100
083F:0100 mov ax,1
083F:0103
-
さて、プログラムが入力されたかどうか、ダンプしてみましょう。ダンプコマンドは「u」です。100から102までダンプしてみます。
- u 100 102
083F:0100 B80100 MOV AX,0001
-
ちゃんと入力されたようです。
次は実行してみましょう。オフセットアドレス100から103まで実行します。コマンドは「g=100 103」です。スタートアドレスは「=」で指定します。キーボードが正しく設定されていないと「=」が見つからないので注意しましょう。うちのMacの場合は「~」のキーに「=」がいました。
正しくMOV AX,1
が実行され、AX
に1が代入されました。
では、いよいよデバッグレジスタを使ってみましょう。DR7
のEAX
の値に読み出し、その値を表示させます。
EAX
の値として「00000400」が入りました。これがDR7
の持つデフォルトの値です。特権命令である、「デバッグレジスタへの読み書き」がちゃんとできました。これで我々も特権階級の仲間入りです。
デバッグレジスタの使用
注意 少なくともうちの環境では、以下のコードを実行するとQEMUが固まり、Macも操作不能になります。実行は自己責任でお願いします。
デバッグレジスタによりブレークポイントをいれるためには
- コントロールレジスタ
CR4
の当該フラグを立てる - デバッグレジスタ
DR0
に、止めたい場所のリニアアドレスを代入する - デバッグコントロールレジスタ
DR7
に、DR0
で指定されたアドレスに来たら止めることを指示する
という作業をする必要があります。
コントロールレジスタのデバッグフラグを立てる
x86には制御レジスタ(Control Register)としてCR0
からCR4
が存在します。このうち、CPUの拡張機能の利用を指示するのがCR4
です。それぞれのビットが何を意味するのかは、例えばOS Project Wikiを参照していただくことにして、とりあえずビットの3番(0スタートなのに注意)がDebug Extensionです。これを有効にしましょう。
mov eax,cr4
bts eax,3
mov cr4,eax
CR4
の値をEAX
に読み込んで、3ビット目を立てて、その値をCR4
に返しています。これでDebug Extensionが有効になります。
デバッグレジスタに止めたい場所のリニアアドレスをいれる
今回はとりあえずDR0
を使うことにしましょう。ただし、コードを入力している段階では止めたい場所のアドレスがわからないため、とりあえず0を代入して、後で書き直すことにします。
mov eax,0
mov dr0,eax
デバッグコントロールレジスタにフラグをセット
デバッグレジスタの7番、DR7
は、デバッグコントロールレジスタです。どのビットが何を意味するのかは、例えばosdev.orgのWikiを参照していただくことにして、とりあえず0番目のビットを立てれば、DR0
が示すアドレスの命令が実行されるタイミングで止まります。
mov eax,dr7
bts eax,0
mov dr7,eax
DR7
の値をEAX
に代入し、0ビット目を立ててからDR7
に書き戻しているだけです。DR7
には、4つのデバッグレジスタごとに止める条件を「メモリの読み書きにする」もしくは「そのアドレスを実行しようとした時」を選ぶビットがありますが、デフォルト(00)では「そのアドレスの命令を実行しようとしたとき」が条件となります。
リニアアドレスの計算
ここまでの内容を入力してみましょう。ついでにnop
をいくつか入れて、最後にint 3
を入れておきます。int 3
まで来るとプログラムが止まるため、いちいち実行アドレスの終了アドレスを指定しなくて良いので便利です。
入力した画面がこちらです。
さて、このオフセットアドレス0120のnopにブレークポイントを置くことにしましょう。いま、セグメンテーションによるアドレスが表示されています。この「0B12:0120」のアドレスをリニアアドレスに変換しなければいけません。このうち「0B12」がセグメントアドレス、「0120」がオフセットアドレスで、セグメントアドレスは環境や実行状況により異なります。
リニアアドレスの計算はいろいろ面倒で、僕もきちんと理解している自信が無いのですが、とりあえず今回はセグメントを16倍してオフセットに足せばOKです。
(0x0b120+0x0120).to_s(16)
=> "b240"
というわけで、先程仮に0を代入していたところにb240を代入しましょう。オフセット10Bのところを書き直します。
-a 10B
0B12:010B mov eax,b240 (改行)
0B12:0111 (改行)
-
これでプログラムが完成しました。表示させて見ましょう。
これが、セグメントアドレス「0B12」において、オフセットアドレス「0120」にブレークポイントを置いたコード・・・のはずです。
「はず」というのは、これをそのまま実行すると、QEMUがホストのMacごと固まるからです。
もし、Macで以下を実行しようとする人がいる場合は、そのMacに別のマシンからSSHでリモートログインしておいてください。
- g=100 # オフセットアドレス0100から実行 (固まる)
実行後、全く操作できなくなりますが、リモートログインしたマシンから
$ ps aux |grep qemu
$ kill -KILL (番号)
でqemuのプロセス番号を調べて殺せば復活できます。
まとめ
「QEMUでデバッグレジスタを使って特権階級の仲間入りだぜ」とかやろうとしたらQEMUが固まりました。僕が何か間違ってるのか、それともQEMUが何かおかしいのかはわかりません。だれか詳しい方のフォローをお待ちしております。
どうでもいいですが、中学〜高校生の頃、仲間とMS-DOSでゲームを作って遊んでいました。当時、「アセンブラ2を使えない」というのがわりとコンプレックスでした。当時のマシンは非力で、ライブラリも整備されていなかったため、アセンブラを使わないと画像表示などは遅くて使い物になりません。僕が使っていたのはQuick Basic、そしてC言語に移行しましたが、アセンブラは使えなかったので、他の仲間がアセンブラでバリバリゲームエンジンを組んでいました。何度かアセンブラに挑戦しましたが、VRAMにちっちゃい線を描画するところまでで挫折しました。
アセンブリを「読む」ようになったのは修士の頃で、DEC Alphaのマシンを与えられてからです。C/C++でプログラムを組んだのですが、どうも性能が思ったよりよかったり悪かったりしたのでアセンブリを出力して見ると、コンパイラがそれはそれは頑張って最適化しているのを見て感心したのを覚えています。以来、コンパイラの最適化能力に興味を持ち、アセンブリを読むようになりました。アセンブリを「書く」ようになったのはつい最近です。気がつくと当時中1だった仲間がバリバリアセンブリを書いているのを羨ましそうに見ていた時から、もう○○年も過ぎていました。
こうしてエミュレータでDOS動かしてDEBUGを使っていると、当時を思い出してなんとも言えない気分になります・・・