【技術分享】深耕保護模式(四)

在x86系統下,總說一個進程有4GB空間,那么按照這個說法來說,在windows上起一個進程就要占用4GB空間,兩個進程就要占用8GB空間,但是實際上是我們電腦的物理內存往往只有8GB,16GB多一點的可能有32GB,我們卻啟動了幾十個進程,這顯然是矛盾的。
實際上,我們所說的進程有4GB內存空間,這個概念是虛擬的。cpu會經過一定算法從虛擬內存地址找到物理內存地址。

這里還有幾個概念:線性地址、有效地址、物理地址
如下指令:
MOV eax,dword ptr ds:[0x12345678]
其中,0x12345678 是有效地址
ds.Base + 0x12345678 是線性地址
物理地址就是真正在內存條上的地址,不是虛擬出來的。

每個進程都有一個CR3,(準確的說是都一個CR3的值,CR3本身是個寄存器,一個核,只有一套寄存器)
CR3指向一個物理頁,一共4096字節,從CR3到物理頁的過程如圖:

下面在10-10-12分頁模式下從線性地址找到物理地址。要想當前xp系統是10-10-12分頁,需要修改boot.ini文件。
將noexecute 改成 execute。

寫入一句話到記事本,并通過CE找到他的線性地址。

采用10-10-12分頁方式拆解這個線性地址。(十位,十位和十二位)

拆完以后CPU首先去找CR3寄存器,CR3寄存器是一個唯一存儲物理地址的寄存器,CR3中存了一個值,這個值指向一個物理頁,這個也有4096個字節,也就是他的第一級,第一部分分的高十位就是確定這個地址在第一級的哪個位置,第二個十位就是確定在第二級的哪個位置,最后12位就是確定在4096個字節的物理頁的v哪個地址,4096 = 2 ^ 12;第一級中每個成員是4個字節,4096個字節可以存放1024 = 2 ^ 10個地址,同樣第二級也是一樣。
通過windbg獲取notepad的cr3。

這里還得計算幾個偏移。前面兩個都是目錄,由于一個是4個字節所以需要乘以4。

在!dd表示查看物理地址。這里第一層是0
kd> !dd 15d45000+0

第一級找到了,要去掉最后三位,這三位是屬性。
kd> !dd 15bad000+2A8

第三級一樣的。使用!db一個字節一個字節的查看。
kd> !dd 15ba8000+A40kd> !db 15ba8a40

物理地址就已經找到了。
在白皮書描述中整個過程如下(線性地址到物理地址):

PDE和PTE
Cr3寄存器起到了不可或缺的作用。那Cr3寄存器中存儲的究竟是什么呢?
Cr3寄存器不同于其他寄存器,在所有的寄存器中,只有Cr3寄存器存儲的地址是 物理地址,其他寄存器存儲的都是 線性地址。
Cr3寄存器所存儲的物理地址指向了一個頁目錄表(Page-Directory Table,PDT),也就是我們前面所說的查找時的第一級。在Windows中,一個頁的大小通常為4KB(有4MB的),即一個頁(頁目錄表)可以存儲1024個頁目錄表項(PDE)。
而第二級為頁表(PTT), 每個頁表的大小為4KB,即一個頁表可以存儲1024個頁表項(PTE)。

這種設計方式正是10-10-12分頁的由來,由于前面兩級是四個字節一組,那么索引為2的10次方就可以獲取到每一項(整個是4096字節),也就是10位;而最后一級物理頁,一個字節一組,所以需要4096組,索引也要指到4096,也就是2的12次方,正好12位。
上面說到10-10-12分頁還有一個大頁(4MB),實際上是沒有頁表(PTT)這一級,也就是PDE直接去索引物理頁,那么就是2^10*2^12,正好是4MB。
頁表項(PTE)具有以下特征:
- PTE可以指向一個物理頁,也可以不指向物理頁
- 多個PTE可以指向同一個物理頁
- 一個PTE只能指向一個物理頁
實驗
我們都知道地址0是絕對不能寫入的,如果寫入回報0xC0000005錯誤,那么是什么原因不能寫入呢?他的本質實際上就是0地址沒有對應的物理頁,也就是上面所說的“PTE可以指向一個物理頁,也可以不指向物理頁”,0地址實際上就沒有對應的物理頁。
那么我們可以自己將線性地址0的PTE掛載到物理頁上,這樣就可以讀寫了。運行這樣一段代碼:
#include "stdafx.h"int main(int argc, char* argv[]){ int x = 1; printf("x的地址:%x\n",&x);
*(int*)0 = 123; printf("0地址數據:%d\n",*(int*)0); return 0;
}
我們要做的就是將線性地址0的物理頁掛載到局部變量x的物理頁,讓兩個PTE指向的是同一個物理頁。
還是先找到當前進程的cr3。

獲取x的線性地址:0x0012ff7c,并對其經行分解。


然后找到其對應的物理地址
kd> !dd 1a9e9000 + 0kd> !dd 1a9b4000 + 4BCkd> !db 1a790000 + f7c

讓線性地址0的PTE指向同一塊物理地址。
如果線性地址為0,那么他就沒有PTE,所以這里要寫一個PTE。
kd> !dd 1a9b4000

由于二級偏移也是0,那么這里就把二級偏移直接寫成物理頁的首地址。也就是1a790067。
kd> !ed 1a9b4000 1a790067
回到程序重新執行,在線性地址0的位置已經寫入了123。

此時用圖形化表示為:

PDE&PTE屬性
PDE和PTE的低12位實際上是表明屬性,這個在之前的練習中已經了解過了。


物理頁的屬性
物理頁的屬性 = PDE屬性 & PTE屬性
P位和段的P位是一樣的,表示當前PDE或者PTE是否有效,所以PDE與PTE的P位 P=1 才是有效的物理頁。
R/W屬性
R/W位表示是否是可讀可寫的。R/W = 0 只讀,R/W = 1 可讀可寫,只有當PDE和PTE的R/W位都為1的時候,該物理頁才是可讀可寫的。
觀察下面一段代碼:
#include "stdafx.h"#include <windows.h>int main(int argc, char* argv[]){ char* str = "Hello World"; printf("線性地址:%x\n",str);
getchar();
DWORD dwVal = (DWORD)str;
*(char*)dwVal = 'M'; printf("%s",str); return 0;
}
直接執行是會報錯的,因為str指向的是常量區中的一個字符串,這是不可以寫的,但是如果我們更改物理頁對應的PDE和PTE的R/W屬性,則可以成功改寫。
直接執行Access Violation。

拆分線性地址:

通過Cr3找到PTE,發現最后12位屬性中R/W位為0。(屬性為025)

那么這里就需要讓R/W位為1,屬性變為027。
!ed 30b4088 19c48027

代碼能夠順利執行,字符串成功被修改。

U/S屬性
- U/S = 0 特權用戶
- U/S = 1 普通用戶
特權用戶也就意味著只有高權限才能訪問,普通用戶普通權限即可訪問。
觀察這樣一段代碼,直接訪問肯定是失敗。
int main(int argc, char* argv[]){
PDWORD p = (PDWORD)0x8003F00C;
getchar(); printf("高2G地址:%x\n",*p); return 0;
}

我們三環程序是無法直接訪問高兩G內存空間的,這里可以用之前的調用門提權訪問,也可以通過修改頁屬性來訪問。
這里具體細節和上面修改R/W差不多。

可以發現這個地址的PDE和PTE的U/S位都是0。

kd> !ed 1d50a800 0003b167kd> !ed 3b0fc 0003f167
這里一不小心把程序放過去了,直接結束了沒截圖,并沒有報錯,也就不重新做這個實驗了。
P/S位
只對PDE有意義,PS == PageSize的意思 當PS==1的時候 PDE直接指向
物理頁 無PTE,低22位是頁內偏移。
線性地址只能拆成2段:大小為4MB 俗稱“大頁”
A 位
是否被訪問(讀或者寫)過 訪問過置1 即使只訪問一個字節也會導致PDE PTE置1
D 位
臟位 是否被寫過 0沒有被寫過 1被寫過
頁目錄表(PDE)基址
如果系統要保證某個線性地址是有效的,那么必須為其填充正確的PDE與PTE,如果我們想填充PDE與PTE那么必須能夠訪問PDT與PTT。那么存在2個問題:
1、一定已經有“人”為我們訪問PDT與PTT掛好了PDE與PTE,我們只有找到這個線性地址就可以了。
2、這個為我們掛好PDE與PTE的“人”是誰?
結論就是有一個特殊的地址:0xC0300000。存儲的值就是PDT。
獲取cr3

kd> !dd 131fb000 + c00kd> !dd 131fb000 + c00kd> !dd 131fb000

可以看到通過這個線性地址實際上是重新解析了cr3寄存器。
也就是說,以后不需要Cr3,只需在當前程序內,通過C0300000這個線性地址就可以得到當前程序PDT的首地址了。
那么PDT的首地址可以找到,PTT的首地址呢?
頁表(PTT)基址
還是有個特殊的線性地址:0xC0000000
獲取debugview的線性地址。

這個線性地址對應的就是PDT表,而PDE表中第一個地址為第一張PTT表。
kd> !dd 18ae9000kd> !dd 0bc7a000

PDT表中第二個地址為第二張PTT表。
kd> !dd 18ae9000kd> !dd 194b9000

然后我們拆分c0000000地址。
kd> !dd 18ae9000 + c00kd> !dd 18ae9000kd> !dd 0bc7a000

可以看到0xc0000000對應的物理地址就是第一張PTT表。
再拆分c0001000地址。
kd> !dd 18ae9000 + c00kd> !dd 18ae9000 + 4kd> !dd 0bc7a000

0xc0001000對應的物理地址就是第二張PTT表。
所以實際上的對應關系應該如下圖所示:

根本就不存在什么PDT表,PDT表知識PTT表中的一個特殊的部分。
掌握了0xC0001000和0xC0300000,就掌握了一個進程所有的物理內存讀寫權限。
PDI和PTI分別指的是再PDT表和PTT表中的索引。
訪問頁目錄表(PDT)的公式:
0xC0300000 + PDI*4
訪問頁表(PTT)公式:
0xC0000000 + PDI*4096 + PTI*4
總結
1、頁表被映射到了從0xC0000000到0xC03FFFFF的4M地址空間。
2、在這1024個表中有一張特殊的表:頁目錄表。
3、頁目錄被映射到了0xC0300000開始處的4K地址空間。
寫入shellcode到0地址執行
這里直接看注釋,要自己捋一下。
// CallGate0Address.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <stdio.h>#include <stdlib.h>#include <windows.h>char buf[] = {0x6a,0x00,0x6a,0,0x6a,0,0x6a,0,0xE8,0,0,0,0,0xc3};
__declspec(naked) void callGate(){
_asm
{
push 0x30;
pop fs;
pushad;
pushfd;
lea eax,buf;
mov ebx,dword ptr ds:[0xc0300000]; //當0xc0300000位置上的值是0時,表明地址0對應的PDE沒有掛上,跳轉代碼為掛上buf對應的物理頁。
//不是0掛PTE就行了
test ebx,ebx;
je __gPDE;
shr eax,12; and eax,0xfffff;
shl eax,2;
add eax, 0xc0000000;
mov eax,[eax];
mov dword ptr ds:[0xc0000000],eax;
jmp __retR;
__gPDE: //獲取前10位偏移
shr eax,22; and eax,0x3ff; //乘以4
shl eax,2; //將buf對應的PDE掛到0地址
add eax, 0xc0300000;
mov eax,[eax];
mov dword ptr ds:[0xc0300000],eax;
__retR:
popfd;
popad;
retf;
}
}int main(int argc, char* argv[]){ unsigned int functionAddress = (unsigned int)MessageBox; //獲取在物理頁上的偏移,后12位。
int offset1 = ((unsigned int)buf) & 0xfff;
*((unsigned int*)&buf[9]) = functionAddress - (13 + offset1); char segmentGate[] = {0,0,0,0,0x48,0}; printf("MessageBox:%x callGate:%x buf:%x\n",MessageBox,callGate,buf);
system("pause");
_asm
{
call fword ptr segmentGate;
push 0x3b;
pop fs;
mov eax,offset1;
call eax;
} return 0;
}
添加調用門
kd> eq 8003f048 0040ec00`0008100a
