本章的内容比较杂。首先构建了几个攻击程序,尝试破坏操作系统,并依据测试结果,强化操作系统保护机制的过程。其次描述了.hrb文件的开头结构,并依据改文件格式,来用C语言编写应用程序。最后还顺带提了一下创建显示窗口、以及绘制字符和方块的API。
一 测试操作系统
几个测试,证明了某些情况下,异常保护的有效性:
【1】第一招,在定时器上做手脚。
但是实际操作系统会出现异常报错,原因在于当以应用程序模式运行时,执行 IN 或者 OUT 指令,都会产生一般保护异常:
[INSTRSET "i486p"]
[BITS 32]
MOV AL,0x34
OUT 0x43,AL
MOV AL,0xff
OUT 0x40,AL
MOV AL,0xff
OUT 0x40,AL
; 上述代码的功能与下面代码相当
; io_out8(PIT_CTRL, 0x34);
; io_out8(PIT_CNT0, 0xff);
; io_out8(PIT_CNT0, 0xff);
MOV EDX,4
INT 0x40
【2】第二招,先CLI再HLT。
但是实际操作系统会出现异常报错,原因在于当以应用程序模式运行时,执行 CLI 、STI 和 HLT 这些指令,都会产生一般保护异常:
[INSTRSET "i486p"]
[BITS 32]
CLI
fin:
HLT
JMP fin
【3】第三招,如果不能直接执行 CLI 命令,那么far-CALL这个函数是不是可以。
但是实际操作系统会出现异常报错,原因在于除了设置好的地址之外,禁止应用程序CALL其他的地址。比如在前文中,应用程序要调用操作系统只能采取 INT 0x40 的方法。
[INSTRSET "i486p"]
[BITS 32]
CALL 2*8:0xac1 ;编译出来的map文件中有 “0x00000AC1 : _io_cli” 这么一段
MOV EDX,4
INT 0x40
【4】第四招,里应外合。要求操作系统首先提供破坏性的API,外部应用程序调用即可。如果真有操作系统的设计人员这么做,那也无话可说了。
二 加强保护----堆栈回溯
准备强化一下系统保护。首先考虑在系统报错的时候能够进行堆栈回溯。
【1】这边先写一个数组写越界:
void api_putchar(int c);
void api_end(void);
void HariMain(void)
{
char a[100];
a[10] = 'A'; /*这句当然没有问题*/
api_putchar(a[10]);
a[102] = 'B'; /*这句就有问题了*/
api_putchar(a[102]);
a[123] = 'C'; /*这句也有问题了*/
api_putchar(a[123]);
api_end();
}
【2】由于数组a是保存在栈中的,因此需要一个函数来处理栈异常,栈异常的中断号是 0x0c,由此入手(根据CPU说明书,从0x00到0x1f都是异常所使用的中断,因此,IRQ的中断号都是从0x20之后开始,其他一些比较有用的异常有0x00号除零异常和0x06号非法指令异常):
_asm_inthandler0c:
STI
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler0c
CMP EAX,0
JNE _asm_end_app
POP EAX
POPAD
POP DS
POP ES
ADD ESP,4 ; 在INT 0x0c中也需要这句
IRETD
【3】另外,编写函数 inthandler0c 并在IDT中注册一下,这个处理函数与 inthandler0d 类似,区别只是报错信息:
int *inthandler0c(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
return &(task->tss.esp0); /*强制结束程序*/
}
void init_gdtidt(void)
{
/* 中略 */
set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 * 8, AR_INTGATE32); /* 这里 */
set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
/* 中略 */
}
【4】根据下表中栈寄存器ESP中的元素,可以将对应的地址显示出来(关于中断中寄存器的入栈顺序,可以参考【操作系统】30天自制操作系统--(5)分割编译与中断处理中的第二节内容):
要想知道指令寄存器 EIP 的地址,显示栈寄存器 ESP 中的第11个元素即可:
int *inthandler0c(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
char s[30]; /* 这里! */
cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
sprintf(s, "EIP = %08X\n", esp[11]); /* 这里!打印ESP第11个元素EIP的内容 */
cons_putstr0(cons, s); /* 这里! */
return &(task->tss.esp0); /* 强制结束程序 */
}
【5】实际测试如下(作者提供的qemu模拟器在异常处理上面有BUG,所以这边在Vmware虚拟机上测试),果然发生异常了,显示指令地址位于0x00000042:
这个地址指向哪里呢?我们来看bug1.map,其中显示_HariMain的地址是0x00000024,下一句 _api_putchar的地址已经是0x00000052了,所以可以确定0x00000042位于 HariMain 中:
这边需要深入到 HariMain 中看,查看 bug1.lst 文件。这边的_HariMain是00000000,因为 HariMain 的地址需要实际链接之后才能确定(链接后的 HariMain 位于map中,地址为0x00000024),那么相对的,0x0000001e 位置,就相当于map中的0x00000042位置(0x00000024+0x0000001e=0x00000042)
显然,下面的这一句话就是对应的源文件中将a[123]赋值为“C”的指令(“C”为0x43,即67):
22 0000001E C6 45 0B 43 MOV BYTE [11+EBP],67
综上,我们便通过在异常处理中断0x0c中加入对 EIP 地址的打印,追踪到异常触发的具体位置。
【注】:这边补充一点,为啥“C”会被判定为异常,而“B”就会被放过去呢,按理说两者都是越界啊?
因为a[102]虽然超过了数组的边界,但却没有超出为应用程序分配的数据段的边界,因此虽然是个bug,CPU也不会产生异常。另一方面,a[123]所在的地址已经超出了数据段的边界,因此CPU马上就发现并产生了异常。
所以 ,越界程度不同,最终的故障表现也会有差异。大面积的写穿可能导致堆栈回溯困难,故障表现千奇百怪。我们不能仅仅依赖内核的保护,coders应当为自己的代码负责。
三 加强保护----强制结束应用程序
类似于windows的ALT+F4,或者安卓的滑屏--扔掉操作,我们需要一个能够强制关掉应用的操作。这边使用的是 SHIFT+F1 :
if (i == 256 + 0x3b && key_shift != 0 && task_cons->tss.ss0 != 0) { /* Shift+F1 */
cons = (struct CONSOLE *) *((int *) 0x0fec);
cons_putstr0(cons, "\nBreak(key) :\n");
io_cli(); /*不能在改变寄存器值时切换到其他任务*/
task_cons->tss.eax = (int) &(task_cons->tss.esp0);
task_cons->tss.eip = (int) asm_end_app;
io_sti();
}
上述程序的工作原理是,当按下强制结束键时,改写命令行窗口任务的的寄存器值,并goto到asm_end_app,仅此而已。
另外,还要确认 task_cons->tss.ss0 不为 0 时才能继续处理,为此,我们还要经行一些修改,使得当有应用程序在运行时,该值一定不为 0,而当应用程序没有运行时,该值一定为 0:
_asm_end_app:
; EAX为tss.esp0的地址
MOV ESP,[EAX]
MOV DWORD [EAX+4],0
POPAD
RET ; 返回cmd_app
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.iomap = 0x40000000;
task->tss.ss0 = 0; /* 这里! */
return task;
}
}
return 0;
}
创建一个bug3.hrb,该程序负责不断显示字符”a“:
void api_putchar(int c);
void api_end(void);
void HariMain(void)
{
for (;;) {
api_putchar('a');
}
}
实际中使用 SHIFT+F1 关闭它并打印:
四 用C语言编写应用程序
使用C来调用API:
_api_putstr0: ; void api_putstr0(char *s)
PUSH EBX
MOV EDX,2
MOV EBX,[ESP+8] ; S
INT 0X40
POP EBX
RET
void api_putstr0(char *s);
void api_end(void);
void HariMain(void) {
api_putstr0("Hello World\n");
api_end();
}
运行失败,寻找原因。
可以看到,hello4.hrb 只有 113 字节的大小。但实际加打印测试中, 基址寄存器 EBX 的值是0x0400。这是因为链接了 .obj 文件的 bim2hrb 认为 Hello World 就是在 0x0400 这个位置上。
所以说作者设计的 .hrb 文件实际上是由两个部分组成的:
(1)代码部分
(2)数据部分
有使用字符串和外部变量
.hrb文件的数据部分会在应用程序启动时被传送到应用程序用的数据段中,而.hrb文件中数据部分的位置则存放在代码部分的开头一块区域中
.hrb 文件的结构式这样的,开头的36个字节不是程序,而是存放了下列 这些信息
- 第一行存放的是数据段的大小
- 第二行的 Hari 是标记
- 第三行是数据段预备的空间大小,目前还没用
- 第四行中存放的是应用程序启动时ESP寄存器的初始值,在这个数值之前的部分会被当作栈来使用
- 第五行存放的是向数据段传送的部分的大小(字节数)
- 第六行存放的是要向数据段传送的部分在 .hrb 文件中的位置
- 第七行存放的是0xe900 0000这个数值,这个数在内存中存放的时候形式为“00 00 00 E9”,E9是JMP指令的机器语言编码,和后面的四个字节合起来就是 JMP 到应用程序运行的入口地址
- 第八行存放的是应用程序运行入口地址减去0x20后的值,因为我们在0x0018(其实是 0x001b)写了一个JMP指令,通过这样的处理,只要 JMP 到 0x001b这个地址程序就会开始执行了
- 第九行存放的是将来编写应用程序用malloc函数时要使用的地址
根据上面的内容改写 console.c :
if (finfo != 0) {
/*找到文件的情况*/
p = (char *) memman_alloc_4k(memman, finfo->size);
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
// meet .hrb spec
segsiz = *((int *) (p + 0x0000));
esp = *((int *) (p + 0x000c));
datsiz = *((int *) (p + 0x0010));
dathrb = *((int *) (p + 0x0014));
q = (char *) memman_alloc(memman, segsiz);
*((int *) 0x0fe8) = (int) q;
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
set_segmdesc(gdt + 1004, segsiz - 1, (int) q, AR_DATA32_RW + 0x60);
for (i = 0; i < datsiz; i++) {
q[esp + i] = p[dathrb + i];
}
start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));
memman_free_4k(memman, (int) q, segsiz);
} else {
cons_putstr0(cons, ".hrb file format error.\n");
}
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
这边作者做了三件事情:
- 文件中找不到标识
Hari
就报错 - 数据段的大小根据
.hrb
文件中指定的值分配 - 将
.hrb
文件中的数据部分先复制到数据段后再开始启动程序
至此可以成功用C语言编写应用程序了:
五 构建显示窗口的的API
设计显示窗口、显示字符和显示方块的API如下:
API函数:
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int ds_base = *((int *) 0xfe8);
struct TASK *task = task_now();
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct SHTCTL *shtctl = (struct SHTCTL *) *((int *) 0x0fe4);
struct SHEET *sht;
int *reg = &eax + 1; /* eax后面的地址*/
/*强行改写通过PUSHAD保存的值*/
/* reg[0] : EDI, reg[1] : ESI, reg[2] : EBP, reg[3] : ESP */
/* reg[4] : EBX, reg[5] : EDX, reg[6] : ECX, reg[7] : EAX */
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + ds_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) {
return &(task->tss.esp0);
} else if (edx == 5) { /* 窗口API */
sht = sheet_alloc(shtctl);
sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
sheet_slide(sht, 100, 50);
sheet_updown(sht, 3); /*背景层高度3位于task_a之上*/
reg[7] = (int) sht;
}
else if (edx == 6) { /* 字符API */
sht = (struct SHEET *) ebx;
putfonts8_asc(sht->buf, sht->bxsize, esi, edi, eax, (char *) ebp + ds_base);
sheet_refresh(sht, esi, edi, esi + ecx * 8, edi + 16);
} else if (edx == 7) { /* 放块API */
sht = (struct SHEET *) ebx;
boxfill8(sht->buf, sht->bxsize, ebp, eax, ecx, esi, edi);
sheet_refresh(sht, eax, ecx, esi + 1, edi + 1);
}
return 0;
}
编写应用程序调用上述API,与打印字符的应用程序大同小异,这边不再赘述,效果如下: