ぐだぐだ低レベルプログラミング(248)x86(32bit)、GDTをメモリから覗き見

Joseph Halfmoon

前回、「最近、特権命令に昇格した」奴らのナンヤカンヤを復習。早速そいつらSGDTとSIDTを使役して得られたヒントを元に「悪さ」を仕掛けてみます。GDTのメモリ領域に直接手を突っ込むという荒業です。まあ、現環境では特権付の一番偉いところでユーザコードが走っているので実際には「無法でもなんでもない」のでありますが。

※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら

※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
まずはSGDTとSIDT使ってみる

グローバル・デスクリプタ・テーブル(GDT)とインタラプト・デスクリプタ・テーブル(IDT)の内容をメモリにストアするSGDTとSIDT命令を使ってみるコードが以下に。以下のコードは「灰の中から蘇った」OpenWatcom Cコンパイラでコンパイルでき、その際、古のDOSイクステンダの1種であるDOS4GWのランタイムを内包しているので「バリバリの」32ビット・プロテクト・モードで実行されます。

#include <stdio.h>

struct DescTable {
    short limit;
    long base;
};

struct DescTable gdt;
struct DescTable *gdt_ptr = &gdt;
struct DescTable idt;
struct DescTable *idt_ptr = &idt;

void getGDTaddress(struct DescTable* tptr) {
    _asm {
        .386p
        mov ebx, tptr
        sgdt [ebx]
    }
}

void getIDTaddress(struct DescTable* tptr) {
    _asm {
        .386p
        mov ebx, tptr
        sidt [ebx]
    }
}

void main()
{
    getGDTaddress(gdt_ptr);
    printf("GDT LIM: 0x%04x BASE: 0x%08x\n", gdt.limit, gdt.base);
    getIDTaddress(idt_ptr);
    printf("IDT LIM: 0x%04x BASE: 0x%08x\n", idt.limit, idt.base);
}

上記、sgdt.c のコンパイルは以下のコマンドラインで。

wcl386 /l=dos4g /d2 sgdt.c

実行はFreeDOSのプロンプトから、sgdtと打つだけです。内部で環境整えてユーザーコードは32ビット・プロテクト・モードで実行されます。

実行結果が以下に。save_gdt_idt

一応、SGDTもSIDTも実行できているみたい。しかし、ベースアドレスはあてになりませぬ。なんといってもFreeDOSとその上のプログラムはページング使ったメモリドライバ上で動作しています。ユーザーコードから見えるアドレスがすなわち真の物理メモリだと思うなよ、と。

しかし、黄色枠で囲んだ部分、セグメント・リミットは違います。確実に物理的なメモリ実体へのヒントを与えてくれとります。

過去回で、LSL命令を使ってGDT内のデスクリプタを探ったデータと上記のデータを照合してみます。

Sel: 1 0x0001 GDT RPL=0, OK MEM Byte DPL=0 R/W(U) ACC Lim 0x00003FFF
Sel: 2 0x0002 GDT RPL=0, OK MEM Byte DPL=0 R/W(U) ACC Lim 0x000007FF
~途中略~
Sel: 44 0x002C GDT RPL=0, OK MEM 4Kpg DPL=0 E/R ACC Lim 0xFFFFFFFF
Sel: 45 0x002D GDT RPL=0, OK MEM 4Kpg DPL=0 R/W(U) ACC Lim 0xFFFFFFFF

GDTの1番目のデスクリプタは、リミット0x3FFFの「使用中」のデータセグメント、そしてGDTの2番目のデスクリプタは、リミット0x7FFの「使用中」のデータセグメントです。OS(ここではDOSイクステンダ)もGDTやらIDTやらのエントリを操作せにゃならんので、必ずGDT領域やIDT領域を読み書きできるセグメントとして扱えるデスクリプタが必要な筈。先頭付近にあるし、他に適当なセグメントも見当たらないので、以下のように推定いたしました。

    1. 第1のデスクリプタ:GDT領域をメモリとして読み書きするためのもの
    2. 第2のデスクリプタ:IDT領域をメモリとして読み書きするためのもの

であれば、第1のデスクリプタを適当なセグメント・レジスタにロードしてやってそのセグメントを通じてアクセスすれば、GDTテーブル内にアクセスできる筈。やってみませう。

GDTテーブルをメモリとしてアクセス

上記のように、GDTの第1のデスクリプタがGDT自身のメモリ・セグメントであると推定し、それをテキトー(今回はESセグメント)にロードすれば、GDTの内容をメモリとして読み書きできる筈。そのコードが以下に。

#include <stdio.h>

long desctempL;
long desctempH;

void getDescEntry(short sel, long adr) {
    _asm {
        .386p
        push es
        xor eax, eax
        mov ax, sel
        mov es, ax
        mov ebx, adr
        mov eax, es:[ebx]
        mov desctempL, eax
        mov eax, es:[ebx+4]
        mov desctempH, eax
        pop es
    }
}

void main()
{
    short gdtmemsel = (0x1 << 3);
    long adr = 1 << 3;
    getDescEntry(gdtmemsel, adr);
    printf("ADR: 0x%08x L: 0x%08x H: 0x%08x\n", adr, desctempL, desctempH);
    adr = 2 << 3;
    getDescEntry(gdtmemsel, adr);
    printf("ADR: 0x%08x L: 0x%08x H: 0x%08x\n", adr, desctempL, desctempH);
    adr = 44 << 3;
    getDescEntry(gdtmemsel, adr);
    printf("ADR: 0x%08x L: 0x%08x H: 0x%08x\n", adr, desctempL, desctempH);
    adr = 45 << 3;
    getDescEntry(gdtmemsel, adr);
    printf("ADR: 0x%08x L: 0x%08x H: 0x%08x\n", adr, desctempL, desctempH);
}

これまた、以下のコマンドラインにて、OpenWatcom Cコンパイラが灰の中から不死鳥のごとく蘇り、コードを吐きだしてくれます。

wcl386 /l=dos4g /d2 dmp2.c

実行結果と、それにお惚け老人がコメントを書き加えたものが以下に。dump_gdt_idt_memseg

黄色枠部分がLimit値の下16ビット部分です。また右側のオレンジ色っぽい16進一桁はLimit値の上8ビットです。合計24ビットですが、以前やったとおり、G(グラニュアリティ)ビットが1に立っているとLimitの1は4Kバイトページ1ページという意味になります。下の2つのセグメント(カレントのコード、データ)はピンクのところが0xCとなっていて、これはG=1のデータセグメントであることを示しています。つまりLimitは0xFFFFFFFF(下12ビットに1が入る)となって4Gバイトね。

お約束通りというか、予想的中というか、第1のセグメント・デスクリプタが無事GDTを指していることが分かりました。後はやりたい放題だな。

プロテクト・モードに時間かかりすぎ。フツーの32ビット命令に進めよ、そろそろ。

ぐだぐだ低レベルプログラミング(247)x86(16/32bit)、SGDTのなんやかんや へ戻る

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です