7 保护操作系统(4)(harib18g)

这次之所以会中招,是因为应用程序擅自向DS存入了操作系统用的段地址。那么我们只要想个办法,让应用程序无法使用操作系统的段地址不就好了吗?

大家可能会想:“说起来容易,但具体应该怎么做呢?”其实我们的x86架构正好有这样的功能。

在段定义的地方,如果将访问权限加上0x60的话,就可以将段设置为应用程序用。当CS中的段地址为应用程序用段地址时,CPU会认为“当前正在运行应用程序”,这时如果存入操作系统用的段地址就会产生异常。

本次的console.c节选

  1. int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
  2. {
  3. (中略)
  4. char name[13], *p, *q;
  5. struct TASK *task = task_now(); /*这里!*/
  6. (中略)
  7. if (finfo != 0) {
  8. /*找到文件的情况*/
  9. (中略)
  10. set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60); /*从此开始*/
  11. set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) q, AR_DATA32_RW + 0x60);
  12. (中略)
  13. start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0)); /*到此结束*/
  14. (中略)
  15. }
  16. /*没有找到文件的情况*/
  17. return 0;
  18. }

如果使用这次的方法,就必须在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节选

  1. _start_app: ; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
  2. PUSHAD ; 32位寄存器的值全部保存下来
  3. MOV EAX,[ESP+36] ; 应用程序用EIP
  4. MOV ECX,[ESP+40] ; 应用程序用CS
  5. MOV EDX,[ESP+44] ; 应用程序用ESP
  6. MOV EBX,[ESP+48] ; 应用程序用DS/SS
  7. MOV EBP,[ESP+52] ; tss.esp0的地址
  8. MOV [EBP ],ESP ; 保存操作系统用ESP
  9. MOV [EBP+4],SS ; 保存操作系统用SS
  10. MOV ES,BX
  11. MOV DS,BX
  12. MOV FS,BX
  13. MOV GS,BX
  14. ; 下面调整栈,以免用RETF跳转到应用程序
  15. OR ECX,3 ; 将应用程序用段号和3进行OR运算
  16. OR EBX,3 ; 将应用程序用段号和3进行OR运算
  17. PUSH EBX ; 应用程序的SS
  18. PUSH EDX ; 应用程序的ESP
  19. PUSH ECX ; 应用程序的CS
  20. PUSH EAX ; 应用程序的EIP
  21. RETF
  22. ; 应用程序结束后不会回到这里

关于将应用程序的段号和3进行OR运算的部分,是为用RETF调用应用程序而使用的一个小技巧,这里就先不详细讲解了。

这次由于我们并不是通过far-CALL来调用应用程序,因此应用程序也无法用RETF的方式结束并返回,后面我们得想别的办法来替代。

■■■■■

接受API调用的_asm_hrb_api也需要进行修改,改完之后比之前的版本还短。

本次的naskfunc.nas节选

  1. _asm_hrb_api:
  2. STI
  3. PUSH DS
  4. PUSH ES
  5. PUSHAD ; 用于保存的PUSH
  6. PUSHAD ; 用于向hrb_api传值的PUSH
  7. MOV AX,SS
  8. MOV DS,AX ; 将操作系统用段地址存入DSES
  9. MOV ES,AX
  10. CALL _hrb_api
  11. CMP EAX,0 ; EAX不为0时程序结束
  12. JNE end_app
  13. ADD ESP,32
  14. POPAD
  15. POP ES
  16. POP DS
  17. IRETD
  18. end_app:
  19. ; EAXtss.esp0的地址
  20. MOV ESP,[EAX]
  21. POPAD
  22. RET ; 返回cmd_app

怎么样,短了不少吧?现在的代码长度已经和我们尚未准备应用程序用的段之前差不多了,这多亏了CPU来帮我们自动执行那些麻烦的栈切换操作。

当hrb_api返回0时继续运行应用程序,当返回非0的值时则当作tss.esp0的地址来处理,强制结束应用程序。之所以需要这样的设计,是因为我们打算做一个结束程序用的API。这次我们不是用far-CALL来启动应用程序,自然也无法用RETF来结束,因此作为替代方案,我们需要做一个用于结束程序的API。

程序结束API分配到EDX = 4,修改后的hrb_api如下。

本次的console.c节选

  1. int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
  2. {
  3. int cs_base = *((int *) 0xfe8);
  4. struct TASK *task = task_now(); /*这里!*/
  5. struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
  6. if (edx == 1) {
  7. cons_putchar(cons, eax & 0xff, 1);
  8. } else if (edx == 2) {
  9. cons_putstr0(cons, (char *) ebx + cs_base);
  10. } else if (edx == 3) {
  11. cons_putstr1(cons, (char *) ebx + cs_base, ecx);
  12. } else if (edx == 4) { /*这里!*/
  13. return &(task->tss.esp0); /*这里!*/
  14. }
  15. return 0; /*这里!*/
  16. }

没有什么特别的难点,接下来我们照这样把inthandler0d也修改一下。

本次的console.c节选

  1. int *inthandler0d(int *esp)
  2. {
  3. struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
  4. struct TASK *task = task_now(); /*这里!*/
  5. cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
  6. return &(task->tss.esp0); /*让程序强制结束*/ /*这里!*/
  7. }

■■■■■

中断处理的部分又要修改了,说是修改,其实也只不过是改回之前的版本而已。

本次的naskfunc.nas节选

  1. _asm_inthandler20:
  2. PUSH ES
  3. PUSH DS
  4. PUSHAD
  5. MOV EAX,ESP
  6. PUSH EAX
  7. MOV AX,SS
  8. MOV DS,AX
  9. MOV ES,AX
  10. CALL _inthandler20
  11. POP EAX
  12. POPAD
  13. POP DS
  14. POP ES
  15. IRETD

由于我们把麻烦的栈切换全部交给CPU来处理,因此程序又完全恢复到之前的样子了。我们把_asm_inthandler21和_asm_inthandler2c也改回去了。

中断处理的部分中,只有负责处理异常中断的_asm_inthandler0d没有完全改回之前的样子。

本次的naskfunc.nas节选

  1. _asm_inthandler0d:
  2. STI
  3. PUSH ES
  4. PUSH DS
  5. PUSHAD
  6. MOV EAX,ESP
  7. PUSH EAX
  8. MOV AX,SS
  9. MOV DS,AX
  10. MOV ES,AX
  11. CALL _inthandler0d
  12. CMP EAX,0 ; 只有这里不同
  13. JNE end_app ; 只有这里不同
  14. POP EAX
  15. POPAD
  16. POP DS
  17. POP ES
  18. ADD ESP,4 ; INT 0x0d中需要这句
  19. IRETD

不同的地方只有两处,像API一样,我们添加了用于强制结束的代码。

接下来,还要修改一下IDT的设置。在我们已经清晰地区分操作系统段和应用程序段的情况下,当应用程序试图调用未经操作系统授权的中断时,CPU会认为“这家伙乱用奇怪的中断号,想把操作系统搞坏,是坏人”,并产生异常。因此,我们需要在IDT中将INT 0x40设置为“可供应用程序作为API来调用的中断”。

本次的dsctbl.c节选

  1. void init_gdtidt(void)
  2. {
  3. (中略)
  4. /* IDT设置*/
  5. set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
  6. set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
  7. set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
  8. set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
  9. set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
  10. set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60); /*这里!*/
  11. return;
  12. }

我们所谓的设置,就是将访问权编码加上0x60而已。至于其他的中断,并不是用于应用程序对操作系统的调用,而是用于键盘、鼠标等外部设备的控制,以及异常处理用的中断,因此禁止应用程序调用。

对了,应用程序也需要修改一下,因为已经不能通过RETF来结束程序了。

本次的hello.nas

  1. [INSTRSET "i486p"]
  2. [BITS 32]
  3. MOV ECX,msg
  4. MOV EDX,1
  5. putloop:
  6. MOV AL,[CS:ECX]
  7. CMP AL,0
  8. JE fin
  9. INT 0x40
  10. ADD ECX,1
  11. JMP putloop
  12. fin:
  13. MOV EDX,4 ; 这里!
  14. INT 0x40 ; 这里!
  15. msg:
  16. DB "hello",0

本次的hello2.nas

  1. [INSTRSET "i486p"]
  2. [BITS 32]
  3. MOV EDX,2
  4. MOV EBX,msg
  5. INT 0x40
  6. MOV EDX,4 ; 这里!
  7. INT 0x40 ; 这里!
  8. msg:
  9. DB "hello",0

本次的a.c

  1. void api_putchar(int c);
  2. void api_end(void); /*这里!*/
  3. void HariMain(void)
  4. {
  5. api_putchar('A');
  6. api_end(); /*这里!*/
  7. }

本次的a_nask.nas

  1. [FORMAT "WCOFF"]
  2. [INSTRSET "i486p"]
  3. [BITS 32]
  4. [FILE "a_nask.nas"]
  5. GLOBAL _api_putchar
  6. GLOBAL _api_end ; 这里!
  7. [SECTION .text]
  8. _api_putchar: ; void api_putchar(int c);
  9. MOV EDX,1
  10. MOV AL,[ESP+4] ; c
  11. INT 0x40
  12. RET
  13. _api_end: ; void api_end(void); ; 从此开始
  14. MOV EDX,4
  15. INT 0x40 ; 到此结束

本次的hello3.c

  1. void api_putchar(int c);
  2. void api_end(void); /*这里!*/
  3. void HariMain(void)
  4. {
  5. api_putchar('h');
  6. api_putchar('e');
  7. api_putchar('l');
  8. api_putchar('l');
  9. api_putchar('o');
  10. api_end(); /*这里!*/
  11. }

本次的crack1.c

  1. void api_end(void); /*这里!*/
  2. void HariMain(void)
  3. {
  4. *((char *) 0x00102600) = 0;
  5. api_end(); /*这里!*/
  6. }

本次的crack2.nas

  1. [INSTRSET "i486p"]
  2. [BITS 32]
  3. MOV EAX,1*8 ; 操作系统用段号
  4. MOV DS,AX ; 将其存入DS
  5. MOV BYTE [0x102600],0
  6. MOV EDX,4 ; 这里!
  7. INT 0x40 ; 这里!

好,这样就大功告成了!

■■■■■

我们来“make run”,先来看看以前能运行的程序现在还能不能正常运行。不错不错,貌似很顺利,太好了!

7 保护操作系统(4)(harib18g) - 图1

运行情况正常

下面运行一下做坏事的程序看看怎么样。哦哦,crack2.hrb貌似被强制结束了,我们的系统好厉害!

7 保护操作系统(4)(harib18g) - 图2

破坏行为被阻止了

话说,这次QEMU对异常的处理貌似又正常了,不错。

啊,不过crack1.hrb还是会正常结束,这是QEMU的bug,不过dir命令可以正常运行说明系统成功防御了攻击。那么我们用“make install”在真机环境下试验一下吧……很好很好,在真机环境下两个crack程序都产生了一般保护异常。

今天就到这里吧,大家晚上做个好梦,明天还要继续加油哦。