4 保护操作系统(2)(harib18d)
到底怎样才能阻止crack1.hrb呢?它所干的坏事,其实就是擅自访问了本该由操作系统来管理的内存空间。我们需要为应用程序提供专用的内存空间,并且告诉它们“别的地方不许碰哦”。要做到这一点,我们可以创建应用程序专用的数据段,并在应用程序运行期间,将DS和SS指向该段地址。
操作系统用代码段……2 * 8
操作系统用数据段……1 * 8
应用程序用代码段……1003 * 8
应用程序用数据段……1004 * 8
(3 8~1002 8为TSS所使用的段)
至于应用程序专用的内存空间,好吧,就先分配64KB吧(反正我们还可以根据需要进行调节)。
本次的console.c节选
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
(中略)
char name[18], *p, *q; /*这里!*/
(中略)
if (finfo != 0) {
/*找到文件的情况*/
p = (char *) memman_alloc_4k(memman, finfo->size);
q = (char *) memman_alloc_4k(memman, 64 * 1024); /*这里!*/
*((int *) 0xfe8) = (int) p;
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) q, AR_DATA32_RW); /*这里!*/
(中略)
start_app(0, 1003 * 8, 64 * 1024, 1004 * 8); /*这里!*/
memman_free_4k(memman, (int) p, finfo->size);
memman_free_4k(memman, (int) q, 64 * 1024); /*这里!*/
cons_newline(cons);
return 1;
}
/*没有找到文件的情况*/
return 0;
}
这里出现的start_app是用来启动应用程序的函数。之前我们只是执行一个far-CALL,现在我们还要设置ESP和DS.SS。
本次的naskfunc.nas节选
_start_app: ; void start_app(int eip, int cs, int esp, int ds);
PUSHAD ; 将32位寄存器的值全部保存起来
MOV EAX,[ESP+36] ; 应用程序用EIP
MOV ECX,[ESP+40] ; 应用程序用CS
MOV EDX,[ESP+44] ; 应用程序用ESP
MOV EBX,[ESP+48] ; 应用程序用DS/SS
MOV [0xfe4],ESP ; 操作系统用ESP
CLI ; 在切换过程中禁止中断请求
MOV ES,BX
MOV SS,BX
MOV DS,BX
MOV FS,BX
MOV GS,BX
MOV ESP,EDX
STI ; 切换完成后恢复中断请求
PUSH ECX ; 用于far-CALL的PUSH(cs)
PUSH EAX ; 用于far-CALL的PUSH(eip)
CALL FAR [ESP] ; 调用应用程序
; 应用程序结束后返回此处
MOV EAX,1*8 ; 操作系统用DS/SS
CLI ; 再次进行切换,禁止中断请求
MOV ES,AX
MOV SS,AX
MOV DS,AX
MOV FS,AX
MOV GS,AX
MOV ESP,[0xfe4]
STI ; 切换完成后恢复中断请求
POPAD ; 恢复之前保存的寄存器值
RET
稍微有点复杂,大家看懂了吗?在向SS和DS赋值的时候,我们也同时向ES、FS和GS赋了值,这样做是为了以防万一。我们将操作系统栈的ESP保存在0xfe4这个地址,以便从应用程序返回操作系统时使用。
■■■■■
不过,光这样改还不够,当使用API时应用程序需要调用hrb_api,但hrb_api这个函数是用C语言编写的操作系统程序,因此如果不将段地址设回操作系统用的段就无法正常工作。于是我们还得修改_asm_hrb_api。
本次的naskfunc.nas节选
_asm_hrb_api:
; 为方便起见从开头就禁止中断请求
PUSH DS
PUSH ES
PUSHAD ; 用于保存的PUSH
MOV EAX,1*8
MOV DS,AX ; 先仅将DS设定为操作系统用
MOV ECX,[0xfe4] ; 操作系统的ESP
ADD ECX,-40
MOV [ECX+32],ESP ; 保存应用程序的ESP
MOV [ECX+36],SS ; 保存应用程序的SS
; 将PUSHAD后的值复制到系统栈
MOV EDX,[ESP ]
MOV EBX,[ESP+ 4]
MOV [ECX ],EDX ; 复制传递给hrb_api
MOV [ECX+ 4],EBX ; 复制传递给hrb_api
MOV EDX,[ESP+ 8]
MOV EBX,[ESP+12]
MOV [ECX+ 8],EDX ; 复制传递给hrb_api
MOV [ECX+12],EBX ; 复制传递给hrb_api
MOV EDX,[ESP+16]
MOV EBX,[ESP+20]
MOV [ECX+16],EDX ; 复制传递给hrb_api
MOV [ECX+20],EBX ; 复制传递给hrb_api
MOV EDX,[ESP+24]
MOV EBX,[ESP+28]
MOV [ECX+24],EDX ; 复制传递给hrb_api
MOV [ECX+28],EBX ; 复制传递给hrb_api
MOV ES,AX ; 将剩余的段寄存器也设为操作系统用
MOV SS,AX
MOV ESP,ECX
STI ; 恢复中断请求
CALL _hrb_api
MOV ECX,[ESP+32] ; 取出应用程序的ESP
MOV EAX,[ESP+36] ; 取出应用程序的SS
CLI
MOV SS,AX
MOV ESP,ECX
POPAD
POP ES
POP DS
IRETD ; 这个命令会自动执行STI
这次估计大家是真的看不懂了吧。不过只要仔细读一遍,还是能看懂个大概的。开头的PUSHAD是将值存到应用程序的栈中,因此使用操作系统栈的hrb_api无法读取这些值,我们需要特地把它们复制过来。因为怕麻烦,这次我们把FS和GS的设置给省略了。
这回总该改好了吧?别急,还差一点。大家已经不耐烦了吧?下面我们来修改中断的部分。在应用程序运行中也会产生中断请求,中断产生后会调用_inthandler20等操作系统内部的C语言函数,因此,我们还得对DS和SS进行切换才行。
本次的naskfunc.nas节选
_asm_inthandler20:
PUSH ES
PUSH DS
PUSHAD
MOV AX,SS
CMP AX,1*8
JNE .from_app
; 当操作系统活动时产生中断的情况和之前差不多
MOV EAX,ESP
PUSH SS ; 保存中断时的SS
PUSH EAX ; 保存中断时的ESP
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler20
ADD ESP,8
POPAD
POP DS
POP ES
IRETD
.from_app:
; 当应用程序活动时发生中断
MOV EAX,1*8
MOV DS,AX ; 先仅将DS设定为操作系统用
MOV ECX,[0xfe4] ; 操作系统的ESP
ADD ECX,-8
MOV [ECX+4],SS ; 保存中断时的SS
MOV [ECX ],ESP ; 保存中断时的ESP
MOV SS,AX
MOV ES,AX
MOV ESP,ECX
CALL _inthandler20
POP ECX
POP EAX
MOV SS,AX ; 将SS设回应用程序用
MOV ESP,ECX ; 将ESP设回应用程序用
POPAD
POP DS
POP ES
IRETD
这样总算可以运行了,asm_inthandler21和asm_inthandler2c也和上面大同小异。
如果你看不懂本节中这些汇编语言的程序也不要紧,忽略它们就是了。现在大家可能看得云里雾里的,但其实这个程序在今天之内就会消失不见的。为什么会这样呢?因为CPU实际上本身就有自动进行这种复杂段切换的功能,最终我们还是要用CPU本身的功能来实现。
有人可能会说,有那么方便的功能,为什么不一开始就用呢?笔者是想让大家先体验一下,如果不用CPU的功能实现起来有多么麻烦,先苦后甜嘛。话说回来,任务切换我们一开始就是用TSS来实现的,其实不用TSS也可以,差不多就像现在这样麻烦。由此看来,x86考虑到操作系统开发者的需求,提供了各种方便的功能,还真得感谢它呢。
这次我们还使用了以句点(.)开头的标签名,这是一种被称为本地标签的特殊标签。它基本上和普通的标签功能一样,区别在于即使标签名和其他函数中的标签重复,系统也能将它们区分开来。
■■■■■
好,现在我们应该可以运行hello.hrb这些应用程序了。虽说运行应用程序这种功能我们早就实现了,不过之前应用程序栈和系统栈是混在一起的,而现在我们将它们清晰地区分开了,虽然这种改变表面上看不出来,但却是相当了不起的。之所以这么说是因为只有写这么深奥的汇编语言代码才能把它们区分开,就冲这一点也相当了不起……你说是不是呢?(笑)
总之,我们先来“make run”试试。看,运行成功了。
成功运行了,不过表面上看不出什么区别
那么,我们在这里运行crack1.hrb会怎么样呢?大概电脑会出问题吧。因为我们虽然可以阻止这些有问题的程序,但还没有实现强制其终止的功能。不过“make run”试验一下,却发现电脑运行正常,输入dir也能正常列出文件,看起来防御是成功了。其实QEMU没有出错应该是QEMU本身的bug,因此大家千万别在真机上尝试哦。
![]() | ![]() | |
bug导致crack1.hrb正常结束了 | 不过dir还是正常的 |