本篇的内容主要是操作系统的保护,涉及到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语言编写应用程序。敬请期待。