保護模式篇——段暫存器

寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?搭建好環境了嗎?沒有的話就不要繼續了。


🔒 華麗的分割線 🔒


什麼是段暫存器

  當我們用彙編讀寫某一個地址時,比如用下面的程式碼:

mov dword ptr ds:[0x123456], eax

  其實我們真正讀寫的地址是:ds.base + 0x123456。並不是0x123456,不過正好的是ds段暫存器的基址是0而已。

一些段暫存器

  段暫存器有這幾個:ES、CS、SS、DS、FS、GS、LDTR、TR,它們各有自己特殊的用途。

段暫存器的結構

  段暫存器的結構可用下圖表示:

  段暫存器具有96位,但我們可見的只有16位。我們可以用OD隨意載入一個程式,如下圖所示:

段暫存器的讀寫

  既然是暫存器了,那就可以進行讀寫操作,如下將介紹讀寫段暫存器的操作:

  • Mov指令:MOV AX,ES,但只能讀16位的可見部分;MOV DS,AX寫段暫存器,寫的是96位。
  • 讀寫LDTR的指令為:SLDT/LLDT
  • 讀寫TR的指令為:STR/LTR

段暫存器屬性探測

  我介紹過段暫存器有96位,但我們只能看見16位,那如果證明AttributeBaseLimit的存在呢?我們將在下面進行初步探測。

段暫存器成員簡介

  既然討論段暫存器屬性,首先要知道它們存著啥,下面表格的內容是我從虛擬機里查詢到的值,可能和我的不一樣,但無所謂。它們的屬性我已查詢並把它們的許可權寫到表格中,之所以為什麼我之後將會介紹。

Windows作業系統並不會使用GS暫存器,故用-表示。

段暫存器 Selector Attribute Base Limit
ES 0023 可讀、可寫 0 0xFFFFFFFF
CS 001B 可讀、可執行 0 0xFFFFFFFF
SS 0023 可讀、可寫 0 0xFFFFFFFF
DS 0023 可讀、可寫 0 0xFFFFFFFF
FS 003B 可讀、可寫 0x7FFDE000 0xFFF
GS

探測Attribute

  如果你沒有使用Visual C++6.0的話,我先將如何建立工程並寫程式碼簡單介紹一下,但只介紹一遍。打開Visual C++6.0,通過File->New打開新建項目,選擇Win32 Console Application,輸入你的Project name,如下圖所示:

  選中帶有 Hello World 的工程,這樣基本上IDE就幫我們建好要做實驗的工程了。

  然後IDE會展示幫我們新建的內容資訊,如下圖所示,直接點確定即可,工程新建完畢。

  按照下圖指示打開源程式碼文件,刪掉用不到的printf("Hello World!\n");。就能開始做實驗了。

  怎麼建工程我已詳細說明,那就正式進入正題,我們將用以下程式碼進行驗證Attribute

#include "stdafx.h"

int a=0;
int main(int argc, char* argv[])
{
    _asm
    {
        mov ax,cs;
        mov dword ptr ds:[a],10;
        mov ds,ax;
        mov dword ptr ds:[a],20;
    }
    return 0;
}

  然後在main函數的第一句程式碼下斷點,然後單步運行,運行過第一句給變數a賦值的彙編程式碼時,成功通過,如下圖所示:

  運行到第二個給變數a賦值的彙編程式碼時,彈出一個錯誤資訊框,如下圖所示

  翻譯過來就是地址訪問衝突,這是什麼原因呢?這就是由於cs段暫存器是可讀的,而不是可寫的。原來的ds是可讀可寫的,但將cs通過ax賦值給ds時候,ds不再是原來的ds,而是cs,故會引發此錯誤。

探測Base

  老生常談程式的零地址無法訪問。但零地址一定是無法訪問嗎?我們將用以下程式碼進行驗證Base

#include "stdafx.h"

int a=0;
int main(int argc, char* argv[])
{
    _asm
    {
        mov ax,fs;
        mov es,ax;
        mov eax,es:[0];        //不要用DS,否則編譯不過去
        mov dword ptr ds:[a],eax;
    }
    return 0;
}

  編譯運行通過,變數a被正常賦值,如下圖所示:

探測Limit

  我們將用以下程式碼進行驗證Limit

#include "stdafx.h"

int a=0;
int main(int argc, char* argv[])
{
    _asm
    {
        mov eax,fs:[0x1000];
        mov dword ptr ds:[a],eax;
    }
    return 0;
}

  執行第一句彙編就發生記憶體訪問衝突錯誤,就是因為0x1000超出了它的Limit的值0xFFF,如下圖所示:

踩坑問題

  裡面有一些坑我還沒讓你踩,你踩一踩看看。

  1. 在驗證屬性的時候,用下面的程式碼,結果運行mov dword ptr ds:[a],10正常通過,放開程式跑後記憶體訪問衝突。
#include "stdafx.h"

int main(int argc, char* argv[])
{
    int a=0;
    _asm
    {
        mov ax,cs;
        mov ds,ax;
        mov dword ptr ds:[a],10;
    }
    return 0;
}

🎉 踩坑解決方案 🎉


  為什麼會出現這個問題呢?我明明程式碼很正常但就不行呢,難道只是因為局部變數的問題呢。但全局變數和局部變數在記憶體上根本沒有區別。全局變數只是一個死地址,局部變數是該變數所在函數臨時的地址,但訪問上根本沒有區別。我們看一看編譯器到底把咱們的內聯彙編到底翻譯成了什麼?為什麼會出現這個問題呢?我明明程式碼很正常但就不行呢,難道只是因為局部變數的問題呢。但全局變數和局部變數在記憶體上根本沒有區別。全局變數只是一個死地址,局部變數是該變數所在函數臨時的地址,但訪問上根本沒有區別。我們看一看編譯器到底把咱們的內聯彙編到底翻譯成了什麼?

  前面的內斂彙編編譯器給我一五一十的直接翻譯了,但這個mov dword ptr ds:[a],10;內聯彙編給我翻譯成了啥,過分了!結果壓根沒有用ds的許可權來訪問,而是默認的ss來訪問,這和預期結果一樣才怪。


2.在探測Base屬性的時候,使用gs作為試驗暫存器,單步執行到mov eax,gs:[0],出現記憶體訪問衝突錯誤。

#include "stdafx.h"

int a=0;
int main(int argc, char* argv[])
{
    _asm
    {
        mov ax,fs;
        mov gs,ax;
        mov eax,gs:[0];
        mov dword ptr ds:[a],eax;
    }
    return 0;
}
🎉 踩坑解決方案 🎉


  是因為每次單步調試,就會觸發單步調試異常,進入內核,內核會把gs清零了,故導致實驗無法成功。


下一篇

  保護模式篇——段描述符與段選擇子