上一節(jié)搅轿,我們開發(fā)了一個流氓程序病涨,當他運行起來后,能夠把自己的數(shù)據(jù)寫入到另一個進程的數(shù)據(jù)內存中璧坟。之所以產生這樣的漏洞既穆,是因為被入侵進程的數(shù)據(jù)段所對應的全局描述符在全局描述符表中。惡意程序通過在全局描述符表中查找沸柔,當找到目標程序的內存描述符后循衰,將對應的描述符加載到自己的ds寄存器里铲敛,于是惡意程序訪問內存時褐澎,就相當于讀寫目標程序的內存。
要防范此類入侵伐蒋,最好的辦法是讓惡意程序無法讀取自己內存段對應的描述符工三,但是如果不把自己的內存描述符放置在全局描述符表中的話,還能放哪里呢先鱼?Intel X86架構還給我們提供了另一種選擇俭正。除了全局描述符表(GDT)外,X86還提供了另一種數(shù)據(jù)結構叫局部描述符表(LDT),局部描述符表的結構跟全局描述符表一模一樣焙畔。不同的是掸读,全局描述符表只能存在一份,而局部描述符表可以是每個進程一份宏多。當進程被內核加載運行時儿惫,它可以讓CPU加載自己的局部描述符表,然后把自己的數(shù)據(jù)段描述符和代碼段描述符存入局部描述符表伸但。局部描述符表只能由相應的進程訪問肾请,其他進程想要訪問本進程的局部描述符表時會被CPU拒絕。
全局描述符表和局部描述符表就構成了一個級聯(lián)層次更胖。CPU先訪問全局描述符表铛铁,全局描述符表中的一個描述符指向局部描述符表的起始地址,內核調用指令lldt 却妨, 指令的參數(shù)是指向局部描述符表起始地址的描述符在全局描述符表中的偏移饵逐,指令執(zhí)行后,局部描述符表就被CPU所加載彪标。當程序被加載時梳毙,CPU會從局部描述符表中獲得程序的代碼段和數(shù)據(jù)段。由于局部描述符表的訪問僅限當前進程捐下,其他進程訪問不了账锹,因此其他進程就無法獲取到本進程數(shù)據(jù)段和代碼段的相關信息萌业。
全局描述符表和局部描述符表的結構如下:
我們看看如何在代碼中使用上局部描述符表。打開multi_task.h文件奸柬,我們看看TSS數(shù)據(jù)結構的定義:
struct TSS32 {
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
int es, cs, ss, ds, fs, gs;
int ldtr, iomap;
};
注意代碼中的ldtr,在上圖中生年,有一個描述符指向了局部描述符表的起始位置,ldtr就是該描述符在全局描述符中的下標廓奕。由于局部描述符表是跟各自進程相關的抱婉,所以每個進程都可以為自己分配一個局部描述符表,因此在表示進程的TASK數(shù)據(jù)結構中铃肯,我們增加局部描述符表的定義:
struct TASK {
int sel, flags;
int priority;
int level;
struct FIFO8 fifo;
struct TSS32 tss;
struct CONSOLE console;
struct Buffer *pTaskBuffer;
struct SHEET *sht;
//change here add stack record
int cons_stack;
//change here
struct SEGMENT_DESCRIPTOR ldt[2];
};
最末尾的ldt就是進程對應的局部描述符表,顯然它只含有兩個描述符押逼,目前我們的進程只含有數(shù)據(jù)段和代碼段步藕,因此兩個描述符足夠了。進入multi_task.c看看如何將附帶在進程對象上的局部描述符加載到CPU里挑格。
struct TASK *task_init(struct MEMMAN *memman) {
int i;
struct TASK *task;
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *)get_addr_gdt();
taskctl = (struct TASKCTL *)memman_alloc_4k(memman, SIZE_OF_TASKCTL);
for (i = 0; i < MAX_TASKS; i++) {
taskctl->tasks0[i].flags = 0;
taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
//change here
taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;
set_segmdesc(gdt + TASK_GDT0 + i, 103, (int)&taskctl->tasks0[i].tss,
AR_TSS32);
//change here
set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int)taskctl->tasks0[i].ldt, AR_LDT);
}
....
}
TASK_GDT0 的值是7范删,在全局描述符表中若锁,前7個描述符有專門用途仲器,從第7個往后就用來指向進程對應的任務門描述符(TSS),當前我們的系統(tǒng)內核最多支持同時運行的進程數(shù)是MAX_TASK,因此從第7個描述符往后數(shù)MAX_TASK個描述符煤率,全都是用來指向用戶進程的任務門描述符。接下來的描述符則用來指向用戶進程的局部描述符表乏冀,代碼中我們設置了tasks[i].tss.ldtr的值蝶糯,這個值就是上圖中,全局描述符表里指向局部描述符表的那個描述符對應的下標辆沦。語句:
set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int)taskctl->tasks0[i].ldt, AR_LDT);
它的作用是將局部描述符表的起始地址放置到全局描述符表對應的描述符中昼捍,AR_LDT的值是0x0082,用來表示當前描述符是專門指向一個局部描述符表的描述符。在分配任務對象的函數(shù)task_alloc中肢扯,我們要把一條語句注釋掉:
struct TASK *task_alloc(void) {
int i;
struct TASK *task;
for (i = 0; i < MAX_TASKS; i++) {
if (taskctl->tasks0[i].flags == 0) {
....
//task->tss.ldtr = 0;
}
}
}
由于tss里面的ldtr變量指向的是全局描述符表中用來對應局部描述符表的那個描述符下標妒茬,所以此處不再把它初始化為0.接著我們回到write_vga_desktop.c,在函數(shù)cmd_execute_program中蔚晨,做相應修改:
void cmd_execute_program(char* file) {
....
//change here
// set_segmdesc(gdt + code_seg, 0xfffff, (int) appBuffer->pBuffer, 0x409a + 0x60);
set_segmdesc(task->ldt + 0, 0xfffff, (int) appBuffer->pBuffer, 0x409a + 0x60);
//new memory
char *q = (char *) memman_alloc_4k(memman, 64*1024);
appBuffer->pDataSeg = (unsigned char*)q;
//change here
// set_segmdesc(gdt + mem_seg, 64 * 1024 - 1,(int) q ,0x4092 + 0x60);
set_segmdesc(task->ldt + 1, 64*1204 - 1, (int) q, 0x4092 + 0x60);
....
//change here
// start_app(0, code_seg*8,64*1024, mem_seg*8, &(task->tss.esp0));
start_app(0, 0*8+4,64*1024, 1*8+4, &(task->tss.esp0));
....
}
原來我們在加載用戶進程時乍钻,會把用戶進程的代碼段和數(shù)據(jù)段設置到全局描述符表gdt中,現(xiàn)在我們改變了蛛株,我們把它設置到局部描述發(fā)表中团赁,局部描述符表對應的正是task->ldt,它只有兩個描述符育拨,我們把用戶進程的代碼段放入到第一個描述符谨履,把用戶進程的數(shù)據(jù)段放入到第二個描述符。在調用start_app把跳轉到用戶進程的代碼時熬丧,我們傳給該函數(shù)的代碼段編號為 08, 0就是代碼段在局部描述符表中的位置笋粟,這里要注意的是我們還“+4”,加4告訴CPU,當前的段在局部描述符表中析蝴,要到局部描述符表中去查找害捕,后面的參數(shù)18+4,表示數(shù)據(jù)段在表中的下標是1闷畸,加4也是告訴CPU到局部描述符表中去查找相應的段尝盼。
我們總結一下當前進程加載的基本邏輯:
1,每一個控制臺進程都對應著一個數(shù)據(jù)結構叫TSS
2佑菩,在全局描述符表中含有一個表項對應著這個TSS數(shù)據(jù)結構
3盾沫,當啟動控制臺進程時,內核用一個jmp指令殿漠,指令的參數(shù)就是步驟2中表項對在全局描述符表中的下標
4赴精,CPU執(zhí)行jmp指令時,把指令后面對應的表項從全局描述符表中拿到绞幌,讀取表項蕾哟,找到TSS結構在內存中的地址,接著使用指令ltr把tss結構的信息加載到CPU中
5,CPU根據(jù)加載的TSS數(shù)據(jù)結構信息谭确,把用戶進程的代碼和數(shù)據(jù)加載到內存中帘营。同時讀取TSS結構中l(wèi)dtr這個變量的值
6,CPU知道TSS中l(wèi)dtr變量對應的就是是全局描述符表中的一個表項逐哈,這個表項指向的是進程局部描述符表所在的位置
7仪吧,CPU根據(jù)TSS.ldtr指向的表項,獲得局部描述符表的內存地址鞠眉,執(zhí)行指令lldt把局部描述符表加載到CPU里薯鼠。
8,CPU開始執(zhí)行進程的第一條指令
9械蹋,進程運行后出皇,再把自己的代碼段和數(shù)據(jù)段設置到局部描述符表中,就像我們上面cmd_execute_program函數(shù)所做的那樣哗戈。
上面代碼完成后郊艘,我們再次加載內核,運行crack程序看看是什么結果:
crack程序運行時奔潰掉了唯咬。這是因為我們不再把客戶進程的數(shù)據(jù)段設置在全局描述符表中下標為30的描述符中纱注,于是當crack程序妄圖加載下標為30的描述符時,CPU發(fā)現(xiàn)這個描述符并為被初始化胆胰,于是就產生了錯誤異常狞贱,引發(fā)的異常會使得CPU的控制權交還給內核,內核在異常處理中會強行中的crack程序蜀涨,這樣crack程序就無法入侵客戶進程了瞎嬉。
如果crack進程要想成功入侵客戶進程,那么必須獲得客戶進程的局部描述符表厚柳,但該表只能被對應的進程所訪問氧枣,其他進程是沒有權限也沒有辦法訪問的,這樣客戶進程的代碼和數(shù)據(jù)就能得到完好的保護别垮,惡意進程也無計可施便监。對代碼更詳細的講解和調試演示,請參看視頻:
Linux kernel Hacker, 從零構建自己的內核
更多技術信息碳想,包括操作系統(tǒng)烧董,編譯器,面試算法移袍,機器學習解藻,人工智能,請關照我的公眾號: