轉(zhuǎn)眼來到科銳學(xué)習(xí)已經(jīng)超過一年的時間了鬓椭,眼看三階段已經(jīng)進(jìn)入尾聲,內(nèi)核的學(xué)習(xí)也快要結(jié)束关划,記錄一下筆記和心得小染,也給剛接觸的朋友做一個參考。當(dāng)然贮折,學(xué)習(xí)新知識最好的辦法就是帶著目的來學(xué)習(xí)裤翩,所以在文章后半部分,介紹如何自己實現(xiàn)了一個WindowsAPI调榄。
本篇文章主要涉及兩個部分:
- 介紹內(nèi)核基礎(chǔ)知識
- 自己實現(xiàn)ReadProcessMemory
環(huán)境介紹:真機64位Windows10踊赠,虛擬機:32位XP_sp3
學(xué)習(xí)內(nèi)核的本質(zhì)其實是學(xué)習(xí)操作系統(tǒng)的原理,而學(xué)習(xí)的過程應(yīng)該是從CPU架構(gòu)出發(fā)每庆,操作系統(tǒng)作為使用CPU提供功能的例子筐带。
內(nèi)核可以分成兩類:
- 宏內(nèi)核:追求性能,大部分系統(tǒng)代碼放在0環(huán)扣孟,代表:Linux
- 微內(nèi)核:追求維護性烫堤,大部分系統(tǒng)代碼在3環(huán),代表:Minix(Linux作者老師的作品),其中著名的設(shè)計:動態(tài)鏈接庫鸽斟,在Windows中也使用
Windows算是微內(nèi)核和宏內(nèi)核特點都具有的操作系統(tǒng)拔创。
80x86處理器的工作模式
8086處理器有三種工作模式,分別是:實模式富蓄,保護模式剩燥,虛擬86模式,其中關(guān)系為:
16位匯編中
iret
可以進(jìn)入保護模式立倍。
分段式內(nèi)存管理
如何保證操作內(nèi)存的動作是否合法灭红?
GDT、LDT
操作系統(tǒng)通電后進(jìn)入實模式口注,做了一系列初始化的動作后進(jìn)入到保護模式变擒,在保護模式中,CPU執(zhí)行所有和內(nèi)存有關(guān)的操作都會通過查表來確定操作是否合法寝志,這個表就是GDT和LDT表娇斑,表的格式由CPU廠商決定,所以為了能兼容多款CPU材部,操作系統(tǒng)代碼里多用條件宏來實現(xiàn)毫缆。
地址的轉(zhuǎn)換
- 邏輯地址:在程序調(diào)試中見到的地址,實際上是:段+偏移的形式
- 線性地址:邏輯地址轉(zhuǎn)物理地址的中間層乐导,邏輯地址是段中的偏移地址然后加上基地址就是線性地址苦丁。
- 物理地址:物理內(nèi)存條上的真實地址
邏輯地址如何轉(zhuǎn)換到物理地址?
首先通過邏輯地址的偏移查第一次表得到線性地址物臂,再查第二次表得到物理地址旺拉。為什么要查第二次表呢?因為第二張表實際上是為了實現(xiàn)虛擬內(nèi)存鹦聪,那么就是說這段內(nèi)存可能是在磁盤上的账阻,訪問的時候會先查表,然后從磁盤上調(diào)到內(nèi)存中泽本,有些情況下(關(guān)閉了虛擬內(nèi)存)查第一次表的結(jié)果等價于物理地址淘太。
為什么要叫線性地址?
從邏輯地址轉(zhuǎn)換到線性地址规丽,是一塊平坦且連續(xù)的地址蒲牧,實際上對應(yīng)到物理地址上,并不是連續(xù)的赌莺。
如何計算線性地址的范圍冰抢?
例如:
- 設(shè)段A的基地址等于00012345H,段界限等于5678H艘狭,并且段界限以字節(jié)為單位(G=0)挎扰,那么段A對應(yīng)線性地址空間中從00012345H-000179BDH的區(qū)域翠订。
- 如果段界限以4K字節(jié)為單位 (G=1),那么段A對應(yīng)線性地址空間中從00012345H-0568B344H(=00012345H+5678000H+0FFFH) 的區(qū)域遵倦。
如何從邏輯地址 ===查表===>> 線性地址尽超?
這個表也叫做分段表,結(jié)構(gòu)如下圖
這種奇葩的做法來源于為了兼容286的歷史遺留問題梧躺。
描述符
用于表示上述定義段的三個參數(shù)的數(shù)據(jù)結(jié)構(gòu)稱為描述符似谁。每個描述符長8個字節(jié)。在保護方式下掠哥,每一個段都有一個相應(yīng)的描述符來描述巩踏。
存儲段描述符
存儲段是存放可由程序直接進(jìn)行訪問的代碼和數(shù)據(jù)的段。存儲段描述符描述存儲段续搀,所以存儲段描述符也被稱為代碼和數(shù)據(jù)段描述符塞琼。
描述符是一個8個字節(jié)的結(jié)構(gòu),具體結(jié)構(gòu)如下:
- Limit 0:15項和Limit 16:19項一起構(gòu)成20位的段界限目代。20位的段界限最大值為0xFFFFF,單位是字節(jié)或者分頁(有Flags項Gr位來確定)屈梁。在分頁機狀態(tài)下最大可以表達(dá)4G的內(nèi)存空間。
- Base0:23項和Base24:31一起構(gòu)成32位的段基址榛了,是線性還是物理地址也取絕于分頁機制是否開啟。
整個的解析結(jié)構(gòu)如下圖:
Access Byte
- Pr:存在位煞抬,對于一個有效的內(nèi)存分段此值必定為1霜大。
Privl(2bit):優(yōu)先級位,取值從0-3革答,對應(yīng)Ring0-Ring3級別战坤。
Ex:可執(zhí)行位,為1時表示此描述符對應(yīng)是代碼段残拐,為0時為數(shù)據(jù)段途茫。
- DC:
- 對于數(shù)據(jù)段,表于數(shù)據(jù)段的增長方向溪食,0表示向上囊卜。1表示向下,也就是偏移大于段基址错沃。
- 對于代碼段栅组,表示是否遵循一致原則。當(dāng)此位為1時枢析,也就是遵循一致原則玉掸,不同優(yōu)先級代碼跳轉(zhuǎn)時,優(yōu)先級同目標(biāo)代碼所在段一致醒叁。為0時司浪,剛跳轉(zhuǎn)時優(yōu)先級不變泊业。
- R/W:讀寫位
- 對于數(shù)據(jù)段,為1時表示可寫啊易,為0時表示不可寫脱吱。數(shù)據(jù)段總是可讀。
- 對于代碼段认罩,為1時表示可讀箱蝠,為0時表示不可讀。代碼段總是不可寫垦垂。
- Ac:保留位宦搬,設(shè)置為0,當(dāng)被訪問過時系統(tǒng)將其改寫為1劫拗。
Flags
- Gr:表示段界限的單位也叫做粒度得糜,為1時表示單位為4KB(一個頁面),為0時表示單位為1字節(jié)杀狡。
- Sz:區(qū)分是16位保護模式执庐,還是32位保護模式【品保可以同時有兩種類型描述符在同一個GDT中滓彰。
全局和局部描述符表
每個任務(wù)的局部描述符表LDT含有該任務(wù)自己的代碼段、數(shù)據(jù)段和堆棧段的描述符州袒,也包含該任務(wù)所使用的一些門描述符揭绑,如任務(wù)門和調(diào)用門描述符等。隨著任務(wù)的切換郎哭,系統(tǒng)當(dāng)前的局部描述符表LDT也隨之切換他匪。
全局描述符表GDT含有每一個任務(wù)都可能或可以訪問的段的描述符,通常包含描述操作系統(tǒng)所使用的代碼段夸研、數(shù)據(jù)段和堆棧段的描述符邦蜜,也包含多種特殊數(shù)據(jù)段描述符,如各個用于描述任務(wù)LDT的特殊數(shù)據(jù)段等亥至。在任務(wù)切換時悼沈,并不切換GDT。
通過LDT可以使各個任務(wù)私有的各個段與其它任務(wù)相隔離抬闯,從而達(dá)到受保護的目的井辆。通過GDT可以使各任務(wù)都需要使用的段能夠被共享。
GDT存儲在GDTR寄存器溶握, 通過匯編指令LGDT載入杯缺。它的操作碼是一個結(jié)構(gòu)的地址,這個結(jié)構(gòu)描述GDT的大小和地址睡榆。共6個字節(jié)萍肆,如下:
- Size項(2個字節(jié))是GDT的字節(jié)數(shù)減1(這也意味著GDT大小不可能為0)袍榆。2個字節(jié)對應(yīng)最大值是65535,也就是說一個GDT最大也就是65536字節(jié)(8192個內(nèi)存分段)塘揣。
- Offset項(4個字節(jié))指向GDT的線性地址(未開啟分頁機制則是物理地址)包雀。
LDT存在LDTR寄存器中,存有局部進(jìn)程的描述符表亲铡,LDTR中的內(nèi)容根據(jù)線程的切換不停切換才写,表中的內(nèi)容由操作系統(tǒng)來修改,若我們拿到0環(huán)權(quán)限奖蔓,自己修改LDTR赞草,改到目標(biāo)進(jìn)程,那么修改自己的內(nèi)存就相當(dāng)于修改了目標(biāo)進(jìn)程的內(nèi)存吆鹤,這是內(nèi)核修改的一個經(jīng)典招式
通過段選擇子確定邏輯地址到物理地址的轉(zhuǎn)化(未開啟分頁機制)
段選擇子
在保護方式下厨疙,虛擬地址空間(相當(dāng)于邏輯地址空間)中存儲單元的地址由段選擇子和段內(nèi)偏移兩部分組成。段選擇子長16位疑务,在32位程序下沾凄,CPU的段寄存器中保存的就是選擇子,其格式如下表所示:
- 段選擇子的高13位是描述符索引(Index):所謂描述符索引是指描述符在描述符表中的序號知允。
- 段選擇子的第2位是引用描述符表指示位撒蟀,標(biāo)記為TI(Table Indicator),TI=0指示從全局描述符表GDT中讀取描述符廊镜;TI=1指示從局部描述符表LDT中讀取描述符牙肝。
(windows不使用這種CPU的做法,而Linux使用)
- RPL:特權(quán)級描述符嗤朴,CPU比較這一項個描述符的特權(quán)級判斷訪問操作是否能進(jìn)行下去
具體通過邏輯地址查找線性地址的例子:
現(xiàn)有邏輯地址:23:13ac34b,假如段寄存器中的的選擇子Index為:0000000000100虫溜,RPL:11雹姊,先比對,是三環(huán)程序衡楞,繼續(xù)操作吱雏,去LDT表中的第4項拿到段首地址,加上偏移13ac34b瘾境,得到線性地址歧杏。
值得一提的是Windows中并沒有使用LDT,而Linux是使用了LDT的迷守,但是有意思的是在閱讀Windows源碼時發(fā)現(xiàn)微軟也留下了LDT的接口犬绒,難道微軟想什么時候順便兼容一下Linux?
分頁管理
80386開始支持存儲器分頁管理機制兑凿。分頁機制是存儲器管理機制的第二部分凯力。上述的段管理機制實現(xiàn)虛擬地址(由段和偏移構(gòu)成的邏輯地址)到線性地址的轉(zhuǎn)換茵瘾,分頁管理機制實現(xiàn)線性地址到物理地址的轉(zhuǎn)換。
線性地址到物理地址的轉(zhuǎn)換
線性地址到物理地址的轉(zhuǎn)換方式受很多變量的影響咐鹤,我們先以一個其中最具代表性的方式來講解基本概念和轉(zhuǎn)換流程拗秘,再來總結(jié)所有的轉(zhuǎn)換方式。
流程如圖所示采用了二級表的結(jié)構(gòu)
頁目錄表(PDE)
一級表稱為頁目錄表(Page Directory Entry)祈惶,共有1024(1k)個表項雕旨,每個表項的大小是4bit,總大小為4k捧请,表項內(nèi)容包括了頁表的指針和指向頁表的屬性凡涩。
頁表(PTE)
二級表稱為頁表(Page Table Entry),每張頁表里有1024(1k)個表項血久,每個表項的大小是4bit突照,總大小為4k,最多有1024(1k)張頁表氧吐,最大占用空間為4M讹蘑,而操作系統(tǒng)一般是動態(tài)申請頁表,大小大概在1M左右筑舅。表項內(nèi)容包括了物理地址的指針和屬性座慰。
CR3
控制寄存器CR3的高20位存放了指向頁目錄表的指針(這里存的是物理地址,如果這里存虛擬地址就會產(chǎn)生悖論)翠拣,每個進(jìn)程都會有一張PDE版仔,切換進(jìn)程其實就是CPU在切換CR3的值,這一點非常重要误墓,是我們自己實現(xiàn)ReadProcessMemory的基礎(chǔ)蛮粮!
表項
PDE和PTE的表項結(jié)構(gòu)基本相似,略有差別谜慌,如下圖所示:
-
頁目錄表表項
圖片.png -
頁表表項
圖片.png
結(jié)構(gòu)基本類似然想,高20位存指向目標(biāo)首地址,低12位表示指向目標(biāo)的屬性
P:Present欣范,存在標(biāo)志变泄,該標(biāo)志標(biāo)明當(dāng)前表項所指向的頁或頁表是否存在于內(nèi)存中。當(dāng)標(biāo)志位置位表示該頁在內(nèi)存中恼琼,當(dāng)標(biāo)志位清零表示該頁不在內(nèi)存中妨蛹,若CPU試圖訪問則會產(chǎn)生一個缺頁異常,值得一提的是CPU并不會主動操作該標(biāo)志位晴竞,而是讓操作系統(tǒng)來維護蛙卤。
R/W:Read or Write,讀寫標(biāo)志位颓鲜,當(dāng)標(biāo)志位置位表窘,所指頁表或頁是可讀可寫的典予,清零表示所指頁表或頁是只讀的。
U/S:User or Surpervisor乐严,用戶權(quán)限標(biāo)志瘤袖,置位時表示普通用戶權(quán)限也就是我們常說的3環(huán)權(quán)限,清零則表示超級用戶權(quán)限也就是0環(huán)權(quán)限昂验。
PWT:Page Write Through捂敌,頁直寫標(biāo)記,控制頁或頁表的直寫或回寫緩存策略既琴。
PCD:Page Cache Disabled占婉,頁層次的緩存禁用,控制頁或頁表的的緩存甫恩,置位時緩存被禁止逆济,清零時表示可以緩存。
A:Accessed磺箕,訪問標(biāo)志奖慌,指明這個頁或頁表是否曾經(jīng)被訪問過,當(dāng)指向的頁或頁表第一次載入內(nèi)存松靡,會清零標(biāo)志位简僧,當(dāng)頁或頁表第一被訪問,改標(biāo)志位置位
D :Dirty雕欺,臟位(在PDE的表項中岛马,該位是0,不使用此標(biāo)志位)屠列,指明該頁是否曾經(jīng)被寫入過啦逆,,當(dāng)指向的頁第一次載入內(nèi)存笛洛,會清零標(biāo)志位蹦浦,當(dāng)頁第一次寫操作完成,改標(biāo)志位置位
PAT:Page Table Attribute Index(PTE表項中的第7位)頁屬性索引撞蜂。
PS :Page Size(PDE表項中的第7位),該位指明指向的頁表尺寸侥袜,當(dāng)改標(biāo)志清零蝌诡,頁尺寸為4k。當(dāng)改標(biāo)志被置位枫吧,頁的尺寸為32位尋址的4M(物理地址拓展啟用浦旱,頁尺寸為2M)
G:Global,全局標(biāo)志九杂。
Avl:保留位颁湖。
線性地址:
- 線性地址的最高10位(即位22至位31)作為頁目錄表的索引
- 線性地址的中間10位(即位12至位21)作為所指定的頁目錄表中的頁表項的索引
- 線性地址的低12位作為32位物理地址的低12位宣蠕。
轉(zhuǎn)換實例:
如何搭建雙機調(diào)試環(huán)境,請自行谷歌
我們以GDT地址作為例子:
dd :查看虛擬內(nèi)存
現(xiàn)在我們有線性地址:0x8003f000甥捺,CR3:39000抢蚀。
- 拆分線性地址:1000000000 ? 0000111111??000000000000
- PDE Index:0x200
- PTE Index :0x3f
- Offset :0x0
- 頁目錄表首地址為CR3的高20位,找到對應(yīng)頁目錄表項:
圖片.png
!dd命令表示查看物理內(nèi)存镰禾, 0x200 * 4是因為表項是4個字節(jié)
-
表項 0x0003b163的前20位指向頁表的首地址皿曲,也就是0x0003b000,頁表的Index為0x3f吴侦,于是:
圖片.png - 表項0x0003f163的前20位指向物理地址頁屋休,也就是0x0003f000,加上Offset备韧,最后得到物理地址:0x0003f000劫樟。
-
檢驗:
圖片.png
所有轉(zhuǎn)換方式(查表方式):
以上我們所闡述的線性地址轉(zhuǎn)物理地址的方法適用于沒有物理地址拓展,且頁表大小為4k的情況织堂。
如何決定適用哪種查表方式叠艳?
查表方式根據(jù)頁表大小來決定,而頁表大小根據(jù)以下標(biāo)志決定:
- PG:分頁標(biāo)志捧挺,CR0的31位
- PSE : 頁尺寸拓展標(biāo)志虑绵,CR4的第4位
- PAE : 物理地址拓展標(biāo)志,CR4的第5位
- PS :頁表尺寸闽烙,PDE表項中的第7位
未開啟PAE翅睛,頁表大小4k
以下圖片均來自Intel手冊,詳細(xì)解釋請參考手冊
未開啟PAE黑竞,頁表大小4M
當(dāng)符合頁表大小是4M的情況下捕发,只需要查一次PDE表再加上偏移就能得出物理地址。
開啟PAE分頁機制的36位物理尋址很魂,
開啟PAE時扎酷,尋址的方式有所不同,CR3里保存的不再是PDE的首地址遏匆,而是一個保存了PDE指針的表的首地址法挨,這張表我們稱作頁目錄指針表(PDPT),對于線性地址的拆分也有所不同幅聘,高兩位作為了PDPT的索引凡纳。
頁表大小4K
當(dāng)開啟PAE且頁表大小是4k的情況,需要查三次表帝蒿,線性地址的21-29位作為PDE表的索引荐糜,12-20位作為PTE表的索引
頁表大小2M
說了這么多關(guān)于表的格式和查表的方法,在實踐中我們該如何利用呢?一個用處是我們經(jīng)常需要把虛擬地址轉(zhuǎn)換成物理地址暴氏,明白其轉(zhuǎn)換原理利于分析問題延塑,另外上面說了,每個進(jìn)程都會有一套自己的分頁機制答渔,切換進(jìn)程實際上是切換CR3中的內(nèi)容关带,那么如何實現(xiàn)我們自己的ReadProcessMemory呢?
問題變成了以下4步:
- 拿到指定進(jìn)程保存在CR3中的內(nèi)容
- 切換當(dāng)前的CR3
- 讀取指定內(nèi)存的內(nèi)容
- 還原CR3
第一步研儒,如何拿到指定進(jìn)程保存在CR3中的內(nèi)容豫缨?
我們知道在3環(huán)程序里,fs[0]保存的是線程環(huán)境塊TEB端朵,在0環(huán)好芭,保存的則是處理器控制區(qū)(_KPCR),其結(jié)構(gòu)如下圖:
從_EPROCESS中的進(jìn)程鏈表我們可以遍歷所有進(jìn)程冲呢,當(dāng)匹配到目標(biāo)進(jìn)程時舍败,拿出
目標(biāo)進(jìn)程DirectoryTableBase中保存的地址。
NTSTATUS GetProcessDirBase(IN DWORD dwPID, OUT PDWORD pDirBase)
{
PEPROCESS Process;
PEPROCESS CurProcess;
CHAR *pszImageName;
DWORD dwCurPID;
DWORD i;
__try
{
__asm
{
//ETHREAD
mov eax, fs:[124h]
//Current EPROCESS
mov eax, [eax + 44h]
mov Process, eax
}
CurProcess = Process;
i = 0;
//traversing EPROCESS
do
{
pszImageName = (char*)CurProcess + 0x174;
dwCurPID = (*(DWORD*)((char*)CurProcess + 0x084));
if (dwCurPID == dwPID)
{
*pDirBase = (*(DWORD*)((char*)CurProcess + 0x018));
return STATUS_SUCCESS;
}
//Next
CurProcess = (*(DWORD*)((char*)CurProcess + 0x088)) - 0x88;
} while (CurProcess != Process);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
dprintf("[MyReadProcessMemory] GetProcessDirBase __except \r\n");
}
return STATUS_INVALID_DEVICE_REQUEST;
}
第二步敬拓,切換當(dāng)前的CR3
切換CR3的值之前邻薯,我們需要屏蔽調(diào)當(dāng)前CPU核心的中斷,以防線程切換乘凸,如果是多核的CPU厕诡,每個核心都需要屏蔽掉中斷。同時营勤,為了預(yù)防內(nèi)存屬性不可寫灵嫌,暫時改掉CR0中表示所有內(nèi)存屬性的標(biāo)志,讓所有內(nèi)存暫時都可寫葛作。
__asm
{
//Shielding interrupt
cli
//close memory protect
mov eax, cr0
and eax, not 10000h
mov cr0, eax
mov eax, cr3
mov dwOldDirBase, eax
//swap CR3
mov eax, dwDirBase
mov cr3, eax
}
第三步寿羞,讀取指定內(nèi)存的內(nèi)容
申請一段空間,暫存一下讀取的數(shù)據(jù)赂蠢,記得要檢查目標(biāo)內(nèi)存地址是否有效
//Alloc ring0 Buff
char* szRing0Buf = (char*)MmAllocateNonCachedMemory(dwBufSize);
//check address invalid
if (MmIsAddressValid(dwTargetAdddress))
{
RtlCopyMemory(szRing0Buf, dwTargetAdddress, dwBufSize);
bIsRead = TRUE;
}
第四步绪穆,還原CR3
恢復(fù)內(nèi)存屬性,恢復(fù)中斷
__asm
{
mov eax, dwOldDirBase
mov cr3, eax
//Reset memory protect
mov eax, cr0
or eax, 10000h
mov cr0, eax
//Restore interrupt
sti
}
總結(jié)
內(nèi)核的學(xué)習(xí)也開始進(jìn)入尾聲虱岂,溫故而知新玖院,整理知識本身也是一種學(xué)習(xí)的過程。衷心感謝一年多以來錢老師第岖,張老師司恳,姚老師,戚老師绍傲,王老師,唐老師的指導(dǎo)。