「30天自制操作系统」Day21 - 保护操作系统

用 C 语言编写应用程序

现在我们考虑用 C 语言来写应用程序,还是从显示单个字符的 API 开始。C 语言的程序如下:

1
2
3
4
5
6
void api_putchar(int c);

void HariMain(void) {
api_putchar('A');
return;
}

我们需要在应用程序方面创建一个 api_putchar 函数,功能是向 EDX 和 AL 赋值,并调用 INT 0x40。程序如下(注意该函数的位置,不是创建在操作系统中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "a_nask.nas"]

GLOBAL _api_putchar

[SECTION .text]

_api_putchar: ; void api_putchar(int c);
MOV EDX, 1
MOV AL, [ESP+4]
INT 0x40
RET

这里的 api_putchar 要与 a.c 的编译结果进行连接,生成 a.hrb,方法类似之前生成 bootpack.hrb 的方法。

但是这样还无法正常运行,我们需要把 a.hrb 的前 6 个字节替换成 E8 16 00 00 00 CB,这 6 个字节就是以下三行代码汇编后的结果:

1
2
3
[BITS 32]
CALL 0x1b
RETF

这里的 0x1b 其实就是 .hrb 文件中 HariMain 的地址,RETF 是为了程序结束后返回命令行。

这样我们程序就可以运行了,运行效果如下:

为了避免每次都要手动修改 .hrb 文件,我们添加一个处理,只要第 4 ~ 7 字节为 Hari,就表明是通过 bim2hrb 生成的文件,就将读取的数据先修改后运行。这样就不用每次手动修改了。

保护操作系统

为了防止病毒或某些应用程序的 bug 对操作系统造成破坏,我们需要添加保护操作系统的功能。

我们先来做一个捣乱的程序:

1
2
3
4
5
void HariMain(void)
{
*((char *) 0x00102600) = 0;
return;
}

运行一下看看会发生什么:

出现错误是因为 crack1.hrb 擅自访问本该由操作系统来管理的内存空间。因此,我们需要为应用程序提供专用的内存空间,我们创建应用程序专用的数据段,在引用程序运行期间,将 DS 和 SS 指向该段地址。

我们先为应用程序分配 64KB 的专用内存空间,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
_start_app:		; void start_app(int eip, int cs, int esp, int ds);
PUSHAD ; 将 32 位寄存器全部保存起来
MOV EAX,[ESP+36]
MOV ECX,[ESP+40]
MOV EDX,[ESP+44]
MOV EBX,[ESP+48]
MOV [0xfe4],ESP
CLI
MOV ES,BX
MOV SS,BX
MOV DS,BX
MOV FS,BX
MOV GS,BX
MOV ESP,EDX
STI
PUSH ECX
PUSH EAX
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

注意到这里讲操作系统栈的 ESP 保存在 0xfe4 这个地址,以便应用程序返回操作系统时使用。

当使用 API 时应用程序需要调用 hrb_api,但该函数是用 C 语言编写的操作系统程序,如果不将段地址设回操作系统用的段就无法正常工作。我们修改 _asm_hrb_api 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
_asm_hrb_api:
PUSH DS
PUSH ES
PUSHAD
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

下面再修改中断部分,中断产生后会调用 _inthandler20 等操作系统内部的 C 语言函数,因此我们还得对 DS 和 SS 进行切换。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
_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
MOV [ECX ],ESP
MOV SS,AX
MOV ES,AX
MOV ESP,ECX
CALL _inthandler20
POP ECX
POP EAX
MOV SS,AX
MOV ESP,ECX
POPAD
POP DS
POP ES
IRETD

对异常的支持

现在我们阻止了 crack1.hrb,但还没有实现强制其终止的功能。要想强制结束程序,只要在中断号 0x0d 中注册一个函数即可(在 x86 架构规范中,0x0d 中断被称为「异常」)。

我们来实现一个 _asm_inthandler0d 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
_asm_inthandler0d:
STI
PUSH ES
PUSH DS
PUSHAD
MOV AX,SS
CMP AX,1*8
JNE .from_app

MOV EAX,ESP
PUSH SS
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler0d
ADD ESP,8
POPAD
POP DS
POP ES
ADD ESP,4 ; 在 INT 0x0d 中需要这句
IRETD
.from_app:
; 当应用程序活动时产生中断
CLI
MOV EAX,1*8
MOV DS,AX
MOV ECX,[0xfe4]
ADD ECX,-8
MOV [ECX+4],SS
MOV [ECX ],ESP
MOV SS,AX
MOV ES,AX
MOV ESP,ECX
STI
CALL _inthandler0d
CLI
CMP EAX,0
JNE .kill
POP ECX
POP EAX
MOV SS,AX
MOV ESP,ECX
POPAD
POP DS
POP ES
ADD ESP,4 ; 在 INT 0x0d 中需要这句
IRETD
.kill:
; 将应用程序强制结束
MOV EAX,1*8 ; 操作系统用的 DS/SS
MOV ES,AX
MOV SS,AX
MOV DS,AX
MOV FS,AX
MOV GS,AX
MOV ESP,[0xfe4] ; 强制返回到 start_app 时的 ESP
STI ; 切换完成后恢复中断请求
POPAD ; 恢复实现保存的寄存器值
RET

然后再来写一个 inthandler0d:

1
2
3
4
5
6
int inthandler0d(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
return 1; /* 强制结束程序 */
}

最后我们将 _asm_inthandler0d 注册到 IDT 中就可以了,运行一下看:



并没有出现异常,不过这是 QEMU 的 bug,并不是我们系统的 bug。

保护操作系统(2)

我们再来写一个 crack2。

1
2
3
4
5
6
[INSTRSET "i486p"]
[BITS 32]
MOV EAX,1*8 ; OS 用的段号
MOV DS,AX ; 将其存入 DS
MOV BYTE [0x102600],0
RETF

我们成功把操作系统给干掉了,是因为应用程序擅自向 DS 存入了操作系统用的段地址。我们只要让应用程序无法使用操作系统的段地址就好了。

在段定义的地方,如果将访问权限加上 0x60 的话,就可以将段设置为应用程序用,CPU 就会认为「当前正在运行应用程序」,此时如果存入操作系统用的段地址就会产生异常。添加的两行代码如下:

1
2
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);

如果使用这次的方法,就必须在 TSS 中注册操作系统用的段地址和 ESP。在启动应用程序的时候我们需要让「操作系统向应用程序用的段执行 far-CALL」,我们可以使用 RETF。

接受 API 调用的 _asm_hrb_api 修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_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

当 hrb_api 返回 0 时继续运行应用程序,当返回非 0 的值时则当作 tss.esp0 的地址来处理,强制结束应用程序。下面我们需要做一个用于结束程序的 API。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 也修改一下:

1
2
3
4
5
6
7
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); /* 让程序强制结束 */
}

然后我们把中断处理的部分改回原来的版本,修改一下 IDT 的设置(将 INT 0x40 设置为「可供应用程序作为 API 来调用的中断」),最后修改一下应用程序。现在我们来运行一下:

下面试一下刚才的两个 crack 是否能够结束。

成功了~

现在我们再实现一个 crack,尝试攻击我们的系统。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[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

然而这段代码我们的系统会检测出异常,因为当以应用程序模式运行时,执行 IN 指令和 OUT 指令都会产生一般保护异常。

再来实现一个 crack4,先执行 CLI 再执行 HLT,代码如下:

1
2
3
4
5
6
[instrset "i486p"]
[bits 32]
cli
fin:
hlt
jmp fin

还是产生了异常。因为当以应用程序模式运行时,执行 CLI、STI、HLT 这些指令都会产生异常。因为中断是由操作系统来管理的,应用程序不可以随便进行控制。

再来实现一个 crack5,这次我们通过 far-CALL 来调用 CLI 函数。程序如下:

1
2
3
4
5
[INSTRSET "i486p"]
[BITS 32]
CALL 2*8:0xac1
MOV EDX,4
INT 0x40

这里的 0xAC1 是我们在 map 文件中找到的 CLI 函数的地址。

结果还是产生了异常,因为 CPU 规定除了设置好的地址以外,禁止应用程序 CALL 其他的地址。也就是说,我们的应用程序要调用操作系统只能采用 INT 0x40 的方法。

那么我们通过调用 API 的方式来试一下,crack6 如下:

1
2
3
4
5
6
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,123456789
INT 0x40
MOV EDX,4
INT 0x40

果然,这下我们的操作系统无能为力了。