【读书笔记-《30天自制操作系统》-20】Day21

本篇的内容主要是操作系统的保护,涉及到x86 CPU的一些机制,以及操作系统的异常处理。

在这里插入图片描述

1. 字符显示API问题解决

首先来解决一下上一篇内容中字符串显示API没有生效的问题。

void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	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);
	} else if (edx == 3) {
		cons_putstr1(cons, (char *) ebx, ecx);
	}
	return;
}

在显示单个字符的时候,用[CS:ECX]的方式指定了CS的值,可以成功读取到显示的内容;而显示字符串时,无法指定段地址,传入的ebx地址,程序按照DS从错误的地址中读取了内容,恰好内容为0,因此什么都没有显示出来。

从cmd_app到hrb_api的数据传递,仍然需要借助内存。这里我们用了0xfe8这个地址:

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
……
	if (finfo != 0) {
		/* 找到文件名的情况 */
		p = (char *) memman_alloc_4k(memman, finfo->size);
		*((int *) 0xfe8) = (int) p; //传递段地址到0xfe8
		file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
		set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
		farcall(0, 1003 * 8);
		memman_free_4k(memman, (int) p, finfo->size);
		cons_newline(cons);
		return 1;
	}
	return 0;
}

void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	int cs_base = *((int *) 0xfe8);
	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);//加上段地址
	}
	return;
}

这样修改之后再运行程序,就可以正常显示字符串了。

在这里插入图片描述
2. 操作系统保护

操作系统上运行着很多应用程序。有些应用程序可能会因为bug而对操作系统造成影响,有的恶意应用则会故意对操作系统进行破坏,造成运行异常等。对此操作系统需要提供足够的保护,防止这些有意无意的破坏。实际上x86架构的CPU就提供了保护操作系统的功能。

2.1 破坏程序1:直接篡改操作系统的内存

首先来模拟一种破坏方式——在应用程序中直接篡改操作系统的内存内容。

void HariMain(void)
{
	*((char *) 0x00102600) = 0;
	return;
}

将以上代码编译为crack1应用程序并进行运行。可以看到输入crack1后运行,虽然没有任何输出,但接下来输入dir也无法正常响应了。

在这里插入图片描述
这里应用程序直接操作了属于操作系统管理的内存空间。操作系统和应用程序之间可谓是泾渭分明,应用程序只能通过允许的接口调用操作系统功能。对于这种非法的操作,必须要在其生效之前强行终止。我们需要为应用程序创建专用的数据段,在应用程序运行期间将DS和SS指向该地址段,而不允许应用程序访问其他的内存空间。

  • 操作系统用代码段 2*8
  • 操作系统用数据段 1*8
  • 应用程序用代码段 1003*8
  • 应用程序用数据段 1004*8

(3*8 - 1002*8为TSS所使用的段)

	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);
		if (finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0) {
			p[0] = 0xe8;
			p[1] = 0x16;
			p[2] = 0x00;
			p[3] = 0x00;
			p[4] = 0x00;
			p[5] = 0xcb;
		}
		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。

接下来作者用一大段汇编语言程序来展示了上面提到的需求。老实说确实没太理解,不过这些汇编语言的功能已经由x-86的CPU实现了,这里跳过也不影响后面的内容。

通过汇编语言增加了以上功能,虽然可以阻止有问题的程序,但还没有实现强制终止其功能。虽然在QEMU上执行已经正常(可能是QEMU自身的bug),但在真机上运行仍然会有问题。

对于这种破坏性的操作,我们应该在程序生效之前强行结束,就是异常处理。
在x86架构中,当应用程序试图破坏操作系统或违背操作系统设置时,会自动产生0xd中断。这样我们只需在中断号0xd注册一个中断处理函数,强制结束程序即可。

_asm_inthandler0d函数如下:

_asm_inthandler0d:
		STI
		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	_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			; 先仅将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
		STI
		CALL	_inthandler0d
		CLI
		CMP		EAX,0
		JNE		.kill
		POP		ECX
		POP		EAX
		MOV		SS,AX			; 将SS恢复为应用程序用
		MOV		ESP,ECX			; 将ESP恢复为应用程序用
		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
int inthandler0d(int *esp)
{
	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
	cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
	return 1; /* 返回1,强制结束程序 */
}

在_asm_inthandler0d中,首先通过段号来判断中断是操作系统产生的还是应用程序产生的。如果是应用程序产生的,则跳转到.from_app。from_app中,首先保存寄存器的值,然后调用_inthandler0d函数。在_inthandler0d函数中,会显示一段提示信息"General Protected Exception.\n",即一般保护异常,然后返回1。
在from_app接下来的处理中,判断如果EAX不为0,则跳转到.kill,kill强制返回到了执行应用程序前的start_app。

2.1 破坏程序2:篡改DS寄存器

不能直接篡改操作系统管理的内存了,但在操作系统运行应用程序时,会指定应用程序使用的DS。破坏者可以通过篡改DS寄存器为操作系统的段地址,间接篡改操作系统的内存,如下:

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

运行一下crack2,同样发现dir命令没有响应了。

在这里插入图片描述

防止这种破坏也很简单,只需要让应用程序无法使用操作系统的段地址就可以了。而x86的CPU恰好实现了这一功能。

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

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
……
	char name[18], *p, *q;
	struct TASK *task = task_now(); // 获取当前任务
……
	if (finfo == 0 && name[i - 1] != '.') {
		name[i    ] = '.';
		name[i + 1] = 'H';
		name[i + 2] = 'R';
		name[i + 3] = 'B';
		name[i + 4] = 0;
		finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
	}

	if (finfo != 0) {
		/* 找到文件的情况 */
……
		// 设置应用程序段的权限加0x60
		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));
		memman_free_4k(memman, (int) p, finfo->size);
		memman_free_4k(memman, (int) q, 64 * 1024);
		cons_newline(cons);
		return 1;
	}
	/* 没有找到文件的情况 */
	return 0;
}

这样的话就需要在TSS中注册操作系统用的段地址和ESP,因此在start_app中增加传入了注册的地址。

接下来还需要在操作系统运行应用程序的时候执行far-CALL。但在x-86 CPU中,操作系统向应用程序用的段执行far-CALL或far-JMP都是被禁止的。(跟Intel的设计有关) 这样我们只能使用RETF来实现这一功能。事先将地址PUSH到栈中,再执行RETF,就可以实现启动应用程序了。

_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
;	应用程序结束后不会回到这里

由于不是使用far-CALL指令调用应用程序,应用程序无法以RETF方式结束并返回,需要通过其他方式来替代。

接受API调用的_asm_hrb_api也需要进行修改:

_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

因为不能用RETF来结束应用程序,这里做了一个结束应用程序的API。结束程序的API分配到EDX=4,对应修改后的hrb_api如下:

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;
}

还需要修改一下IDT的设置。我们已经清楚地区分了操作系统段和应用程序段,这时候如果应用程序试图调用未经操作系统授权的中断时,CPU会产生异常。这里需要将INT 0x40设置为"可供应用程序作为API来调用的中断",其实也就是在注册到IDT时在访问权限编码上加上0x60。

……
	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;

这样我们需要修改应用程序中原来使用RETF的地方,改为调用api_end来结束程序。
运行crack2,可以成功抛出异常:
在这里插入图片描述
下一篇内容中将进入用C语言编写应用程序。敬请期待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值