寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看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位
,那如果證明Attribute
、Base
、Limit
的存在呢?我們將在下面進行初步探測。
段寄存器成員簡介
既然討論段寄存器屬性,首先要知道它們存着啥,下面表格的內容是我從虛擬機里查詢到的值,可能和我的不一樣,但無所謂。它們的屬性我已查詢並把它們的權限寫到表格中,之所以為什么我之后將會介紹。
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
,如下圖所示:
踩坑問題
里面有一些坑我還沒讓你踩,你踩一踩看看。請你思考出答案或者百思不得其解的時候再來看答案。
- 在驗證屬性的時候,用下面的代碼,結果運行
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
清零了,故導致實驗無法成功。