7 保护操作系统(4)(harib18g)
这次之所以会中招,是因为应用程序擅自向DS存入了操作系统用的段地址。那么我们只要想个办法,让应用程序无法使用操作系统的段地址不就好了吗?
大家可能会想:“说起来容易,但具体应该怎么做呢?”其实我们的x86架构正好有这样的功能。
在段定义的地方,如果将访问权限加上0x60的话,就可以将段设置为应用程序用。当CS中的段地址为应用程序用段地址时,CPU会认为“当前正在运行应用程序”,这时如果存入操作系统用的段地址就会产生异常。
本次的console.c节选
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
(中略)
char name[13], *p, *q;
struct TASK *task = task_now(); /*这里!*/
(中略)
if (finfo != 0) {
/*找到文件的情况*/
(中略)
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60); /*从此开始*/
set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) q, AR_DATA32_RW + 0x60);
(中略)
start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0)); /*到此结束*/
(中略)
}
/*没有找到文件的情况*/
return 0;
}
如果使用这次的方法,就必须在TSS中注册操作系统用的段地址和ESP,因此,我们在start_app中增加了用于传递注册地址的代码。
■■■■■
用上面的方法的话,在启动应用程序的时候我们需要让“操作系统向应用程序用的段执行far-CALL”,但根据x86的规则,是不允许操作系统CALL应用程序的(如果强行CALL的话会产生异常)。可能有人会想如果CALL不行的话JMP总可以吧,但在x86中“操作系统向应用程序用的段进行far-JMP”也是被禁止的。
那我们该怎么办呢?可以使用RETF。就像是被应用程序CALL过之后那样,事先将地址PUSH到栈中,然后执行RETF,这样就可以成功启动应用程序了。
可能有人会问:“为什么不可以从操作系统CALL/JMP应用程序呢!”笔者也搞不明白,还是打电话问英特尔公司的大叔吧——难道说这样设计可以减轻CPU的负担吗?
不过,从应用程序CALL操作系统是可以的(只是需要通过一些设置才能实现),这应该是为API而设计的。
之前我们一直讲RETF是当far-CALL调用后进行返回的指令,其实即便没有被CALL调用,也可以进行RETF。说穿了,RETF的本质就是从栈中将地址POP出来,然后JMP到该地址而已。因此正如这次我们所做的一样,可以用RETF来代替far-JMP的功能。
本次的naskfunc.nas节选
_start_app: ; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
PUSHAD ; 将32位寄存器的值全部保存下来
MOV EAX,[ESP+36] ; 应用程序用EIP
MOV ECX,[ESP+40] ; 应用程序用CS
MOV EDX,[ESP+44] ; 应用程序用ESP
MOV EBX,[ESP+48] ; 应用程序用DS/SS
MOV EBP,[ESP+52] ; tss.esp0的地址
MOV [EBP ],ESP ; 保存操作系统用ESP
MOV [EBP+4],SS ; 保存操作系统用SS
MOV ES,BX
MOV DS,BX
MOV FS,BX
MOV GS,BX
; 下面调整栈,以免用RETF跳转到应用程序
OR ECX,3 ; 将应用程序用段号和3进行OR运算
OR EBX,3 ; 将应用程序用段号和3进行OR运算
PUSH EBX ; 应用程序的SS
PUSH EDX ; 应用程序的ESP
PUSH ECX ; 应用程序的CS
PUSH EAX ; 应用程序的EIP
RETF
; 应用程序结束后不会回到这里
关于将应用程序的段号和3进行OR运算的部分,是为用RETF调用应用程序而使用的一个小技巧,这里就先不详细讲解了。
这次由于我们并不是通过far-CALL来调用应用程序,因此应用程序也无法用RETF的方式结束并返回,后面我们得想别的办法来替代。
■■■■■
接受API调用的_asm_hrb_api也需要进行修改,改完之后比之前的版本还短。
本次的naskfunc.nas节选
_asm_hrb_api:
STI
PUSH DS
PUSH ES
PUSHAD ; 用于保存的PUSH
PUSHAD ; 用于向hrb_api传值的PUSH
MOV AX,SS
MOV DS,AX ; 将操作系统用段地址存入DS和ES
MOV ES,AX
CALL _hrb_api
CMP EAX,0 ; 当EAX不为0时程序结束
JNE end_app
ADD ESP,32
POPAD
POP ES
POP DS
IRETD
end_app:
; EAX为tss.esp0的地址
MOV ESP,[EAX]
POPAD
RET ; 返回cmd_app
怎么样,短了不少吧?现在的代码长度已经和我们尚未准备应用程序用的段之前差不多了,这多亏了CPU来帮我们自动执行那些麻烦的栈切换操作。
当hrb_api返回0时继续运行应用程序,当返回非0的值时则当作tss.esp0的地址来处理,强制结束应用程序。之所以需要这样的设计,是因为我们打算做一个结束程序用的API。这次我们不是用far-CALL来启动应用程序,自然也无法用RETF来结束,因此作为替代方案,我们需要做一个用于结束程序的API。
程序结束API分配到EDX = 4,修改后的hrb_api如下。
本次的console.c节选
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int cs_base = *((int *) 0xfe8);
struct TASK *task = task_now(); /*这里!*/
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + cs_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + cs_base, ecx);
} else if (edx == 4) { /*这里!*/
return &(task->tss.esp0); /*这里!*/
}
return 0; /*这里!*/
}
没有什么特别的难点,接下来我们照这样把inthandler0d也修改一下。
本次的console.c节选
int *inthandler0d(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now(); /*这里!*/
cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
return &(task->tss.esp0); /*让程序强制结束*/ /*这里!*/
}
■■■■■
中断处理的部分又要修改了,说是修改,其实也只不过是改回之前的版本而已。
本次的naskfunc.nas节选
_asm_inthandler20:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler20
POP EAX
POPAD
POP DS
POP ES
IRETD
由于我们把麻烦的栈切换全部交给CPU来处理,因此程序又完全恢复到之前的样子了。我们把_asm_inthandler21和_asm_inthandler2c也改回去了。
中断处理的部分中,只有负责处理异常中断的_asm_inthandler0d没有完全改回之前的样子。
本次的naskfunc.nas节选
_asm_inthandler0d:
STI
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler0d
CMP EAX,0 ; 只有这里不同
JNE end_app ; 只有这里不同
POP EAX
POPAD
POP DS
POP ES
ADD ESP,4 ; 在INT 0x0d中需要这句
IRETD
不同的地方只有两处,像API一样,我们添加了用于强制结束的代码。
接下来,还要修改一下IDT的设置。在我们已经清晰地区分操作系统段和应用程序段的情况下,当应用程序试图调用未经操作系统授权的中断时,CPU会认为“这家伙乱用奇怪的中断号,想把操作系统搞坏,是坏人”,并产生异常。因此,我们需要在IDT中将INT 0x40设置为“可供应用程序作为API来调用的中断”。
本次的dsctbl.c节选
void init_gdtidt(void)
{
(中略)
/* IDT设置*/
set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60); /*这里!*/
return;
}
我们所谓的设置,就是将访问权编码加上0x60而已。至于其他的中断,并不是用于应用程序对操作系统的调用,而是用于键盘、鼠标等外部设备的控制,以及异常处理用的中断,因此禁止应用程序调用。
对了,应用程序也需要修改一下,因为已经不能通过RETF来结束程序了。
本次的hello.nas
[INSTRSET "i486p"]
[BITS 32]
MOV ECX,msg
MOV EDX,1
putloop:
MOV AL,[CS:ECX]
CMP AL,0
JE fin
INT 0x40
ADD ECX,1
JMP putloop
fin:
MOV EDX,4 ; 这里!
INT 0x40 ; 这里!
msg:
DB "hello",0
本次的hello2.nas
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2
MOV EBX,msg
INT 0x40
MOV EDX,4 ; 这里!
INT 0x40 ; 这里!
msg:
DB "hello",0
本次的a.c
void api_putchar(int c);
void api_end(void); /*这里!*/
void HariMain(void)
{
api_putchar('A');
api_end(); /*这里!*/
}
本次的a_nask.nas
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "a_nask.nas"]
GLOBAL _api_putchar
GLOBAL _api_end ; 这里!
[SECTION .text]
_api_putchar: ; void api_putchar(int c);
MOV EDX,1
MOV AL,[ESP+4] ; c
INT 0x40
RET
_api_end: ; void api_end(void); ; 从此开始
MOV EDX,4
INT 0x40 ; 到此结束
本次的hello3.c
void api_putchar(int c);
void api_end(void); /*这里!*/
void HariMain(void)
{
api_putchar('h');
api_putchar('e');
api_putchar('l');
api_putchar('l');
api_putchar('o');
api_end(); /*这里!*/
}
本次的crack1.c
void api_end(void); /*这里!*/
void HariMain(void)
{
*((char *) 0x00102600) = 0;
api_end(); /*这里!*/
}
本次的crack2.nas
[INSTRSET "i486p"]
[BITS 32]
MOV EAX,1*8 ; 操作系统用段号
MOV DS,AX ; 将其存入DS
MOV BYTE [0x102600],0
MOV EDX,4 ; 这里!
INT 0x40 ; 这里!
好,这样就大功告成了!
■■■■■
我们来“make run”,先来看看以前能运行的程序现在还能不能正常运行。不错不错,貌似很顺利,太好了!
运行情况正常
下面运行一下做坏事的程序看看怎么样。哦哦,crack2.hrb貌似被强制结束了,我们的系统好厉害!
破坏行为被阻止了
话说,这次QEMU对异常的处理貌似又正常了,不错。
啊,不过crack1.hrb还是会正常结束,这是QEMU的bug,不过dir命令可以正常运行说明系统成功防御了攻击。那么我们用“make install”在真机环境下试验一下吧……很好很好,在真机环境下两个crack程序都产生了一般保护异常。
今天就到这里吧,大家晚上做个好梦,明天还要继续加油哦。