第21天 保護操作系統

第21天 保護操作系統

2020.4.24

1. 攻克難題——字符串顯示API(harib18a)

  • 在harib17h中hello.hrb運行出現異常。應該是內存段的鍋。

  • 顯示單個字符時,用[CS:ECX]的方式特意指定了CS(代碼段寄存器),因此可以成功讀取msg的內容。但是在顯示字符串時,由於無法指定段地址,程序誤以爲是DS從而從完全錯誤的內存地址中讀取了內容,碰巧讀出的內容是0,於是什麼也沒顯示。

  • 因此,需要修改API,使API能夠將應用程序傳遞的地址解釋成爲代碼段內的地址

  • 執行應用程序的過程:

  • hrb_api並不知道代碼段的起始位置(指針p)位於哪個內存地址,但是cmd_app知道。沒有辦法從cmd_app向hrb_api直接傳遞數據,因此,將內存地址0xfe8存放指針p。 此前,0xfec存放着cons。

  • 修改cmd_app:

    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; /*將指針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);
            farcall(0, 1003 * 8);
            memman_free_4k(memman, (int) p, finfo->size);
            cons_newline(cons);
            return 1;
        }
        /* 沒有找到文件的情況 */
        return 0;
    }
    
  • 修改hrb_api:

    void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
    {
        int cs_base = *((int *) 0xfe8); /*從特定內存地址讀出指針p指向的地址*/
        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;
    }
    
    • 在執行應用程序時,CS存放的值其實就是指針p。
    • 沒有從CS中直接取出p。而是通過上述方式將應用程序傳遞的地址解釋成爲代碼段內的地址。
    • ebx內存放的是指向字符串的指針(存放一個內存地址),char可以將其強制類型轉換。
  • make後用VMware運行:

    • 果然是內存段的鍋!

2. 用C語言編寫應用程序(harib18b)

  • 使用C語言編寫應用程序a.c:

    void api_putchar(int c);
    
    void HariMain(void)
    {
        api_putchar('A');
        return;
    }
    
    • 注意,這裏的HariMain和bootpack.c中的HariMain不是一回事。用C語言編寫程序時,開始執行的入口就是HariMain。bootpack.c和a.c是完全獨立編譯的。
    • 需要編寫api_putchar函數(這個函數是應用程序的函數,不是OS的)。
    • api_putchar函數需要使用彙編語言編寫,以C語言來調用。
    • api_putchar函數的功能是:向EDX和AL賦值,並調用INT 40。
  • 編寫api_putchar(a_nask.nas):

    [FORMAT "WCOFF"]				; 生成對象文件的模式	
    [INSTRSET "i486p"]				; 表示使用486兼容指令集
    [BITS 32]						; 生成32位模式機器語言
    [FILE "a_nask.nas"]				; 源文件名信息
    
            GLOBAL	_api_putchar
    
    [SECTION .text]
    
    _api_putchar:	; void api_putchar(int c);
            MOV		EDX,1
            MOV		AL,[ESP+4]		; c
            INT		0x40
            RET
    
    • api_putchar需要和a.c的編譯結果進行連接,因此使用對象文件模式。
  • 修改Makefile:

    a.bim : a.obj a_nask.obj Makefile
        $(OBJ2BIM) @$(RULEFILE) out:a.bim map:a.map a.obj a_nask.obj
    
    a.hrb : a.bim Makefile
        $(BIM2HRB) a.bim a.hrb 0
    
    • 這裏借鑑了生成bootpack.hrb的方式。至於爲什麼使用這種方式,以後會講到。
  • make後生成一個72字節的a.hrb(命名只顯示1個字符,卻比顯示5個字符的hello.hrb還要大,這也見怪不怪,畢竟是C語言寫的嘛)。

    • 用VMware運行試試:
      • BOOM,出錯了。
  • 修改BUG方法:

    • 用二進制編輯器打開a.hrb:
    • 將開頭的6個字節換成E8 16 00 00 00 CB
  • 原理:

    • E8 16 00 00 00 CB這6個字節相當於下面代碼經過nask彙編之後的結果:
      [BITS 32]  
              CALL    0x1b
              RETF
      
      • 0x1b相當於.hrb文件中的HariMain的地址。
      • 聯想到在asmhead.nas中,最後調用bootpack.hrb時有一句:
        JMP     DWORD   2*8:0x0000001b
        
      • 這個0x1b是一樣的。
    • 執行完畢以後,需要一個far-RET,來返回命令行。
  • 然後make用VMware運行:

    • 成功了!
  • 趁熱打鐵,編寫應用程序hello3.c:

    void api_putchar(int c);
    
    void HariMain(void)
    {
        api_putchar('h');
        api_putchar('e');
        api_putchar('l');
        api_putchar('l');
        api_putchar('o');
        return;
    }
    
    • 相應地也需要修改Makefile,做法同a.c。這裏不再贅述。
    • make後hello3.hrb剛好100字節。
  • 每次都要修改應用程序可執行文件.hrb的前6字節的數據很是麻煩。因此,修改cmd_app函數更改.hrb寫入內存中的前6字節的數據

    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;
            file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
            set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
            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;   
            }
            farcall(0, 1003 * 8);
            memman_free_4k(memman, (int) p, finfo->size);
            cons_newline(cons);
            return 1;
        }
        /* 未找到文件 */
        return 0;
    }
    
    • 凡是通過bim2hrb生成的hrb文件,其4~7字節一定是Hari
  • make後用VMware運行:

3. 保護操作系統(1)(harib18c)

  • OS的應用程序,可能是OS的開發者編寫的,也有可能是其他人編寫的。這些應用程序有善意的,也有惡意的。

  • 應用程序不論善意還是惡意,不可避免有BUG,而某些BUG會對OS造成破壞。比如,擅自刪除重要文件,使其他任務運行產生異常,造成OS司機而不得不重啓等。

  • 因此,需要花點功夫讓OS變得更加安全。x86架構的CPU爲我們提供了保護OS的功能。只要運用這些功能完事操作系統的安全性,就可以最大限度地保護OS。目標:沒有漏洞

  • 製作一個惡意應用程序crack1.c:

    void HariMain(void)
    {
        *((char *) 0x00102600) = 0;
        return;
    }
    
    • 向內存地址0x00102600寫入0。0x00102600是:
      struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);
      
      • finfo指向的地址是0x00102600。
      • 因此將無法獲取磁盤文件列表。
  • make後移用VMware運行:

    • 輸入crack1後,貌似沒有反應。
    • 接着輸入dir,結果出BUG了。
    • 再輸入一次crack1,顯示“Bad command.”。
    • 這在意料之中。
  • 重啓一下系統就恢復了。現在,只有重啓能解決這個問題。因此,對於這種程序,我們必須在它造成破壞之前強行終止它才行。

4. 保護操作系統(2)(harib18d)

  • crack1所做的事情,無非就是訪問(修改)了本該由OS來管理的內存空間。

  • 解決方法:

    • 爲應用程序提供專用的內存空間,並且禁止其訪問其他內存空間
  • 具體做法:

    • 爲應用程序創建應用程序專用的數據段,並在應用程序運行期間,將DS和SS指向該段地址
      • SS: 棧段寄存器(stack segment)
      • DS: 數據段寄存器(data segment)
    • 段寄存器資料:
      • 代碼段寄存器CS(Code Segment):存放當前正在運行的程序代碼所在段的段基址,表示當前使用的指令代碼可以從該段寄存器指定的存儲器段中取得,相應的偏移量則由IP提供。
      • 數據段寄存器DS(Data Segment):指出當前程序使用的數據所存放段的最低地址,即存放數據段的段基址。
      • 堆棧段寄存器SS(Stack Segment):指出當前堆棧的底部地址,即存放堆棧段的段基址。
      • 附加段寄存器ES(Extra Segment):指出當前程序使用附加數據段的段基址,該段是串操作指令中目的串所在的段。
  • 規定:

    • OS用代碼段:2*8
    • OS用數據段:1*8
    • 應用程序用代碼段:1003*8
    • 應用程序用數據段:1003*4
    • (3*8~1002*8爲TSS所使用的段)
      • 這裏可以聯想到asmhead.nas(第8天)中除了CS外的段寄存器都賦值1*8,CS賦值2*8。
  • 修改console.c中的cmd_app:

    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); /*調用函數start_app*/
            memman_free_4k(memman, (int) p, finfo->size);
            memman_free_4k(memman, (int) q, 64 * 1024);
            cons_newline(cons);
            return 1;
        }
        /* 未找到文件 */
        return 0;
    }
    
    • 暫時先爲應用程序數據段專用空間分配64KB的內存空間吧。
    • 用start_app函數代替原先的farcall。這個start_app是用來啓動應用程序的函數。原先的farcall只是執行了far-CALL,現在的start_app需要設置ESP、DS和SS。
  • 添加start_app(naskfunc.nas):

    _start_app:		; void start_app(int eip, int cs, int esp, int ds);
            PUSHAD		; 將32位寄存器的值全部保存起來
            MOV		EAX,[ESP+36]	; 應用程序用EIP
            MOV		ECX,[ESP+40]	; 應用程序用CS
            MOV		EDX,[ESP+44]	; 應用程序用ESP
            MOV		EBX,[ESP+48]	; 應用程序用DS/SS
            MOV		[0xfe4],ESP		; OS用ESP
            CLI			; 在切換過程中禁止中斷請求
            MOV		ES,BX
            MOV		SS,BX
            MOV		DS,BX
            MOV		FS,BX
            MOV		GS,BX
            MOV		ESP,EDX         ; 注意,此時ESP已經改變位置,下面的PUSH是跟着ESP的。
            STI			; 切換完成後恢復中斷請求
            PUSH	ECX				; 用於far-CALL的PUSH(CS)
            PUSH	EAX				; 用於far-CALL的PUSH(EIP)
            CALL	FAR [ESP]		; 執行far-CALL調用應用程序
    
    ;	應用程序結束後返回此處
    
            MOV		EAX,1*8			; OS用的DS/SS
            CLI			; 從應用程序切換回OS
            MOV		ES,AX
            MOV		SS,AX
            MOV		DS,AX
            MOV		FS,AX
            MOV		GS,AX
            MOV		ESP,[0xfe4]
            STI			; 恢復中斷
            POPAD	; 恢復之前的寄存器值
            RET
    
    • 在向SS和DS賦值的同時,也向ES、FS和GS賦值,以防萬一。
    • 將OS用的ESP保存在0xfe4這個地址(0xfec:cons;0xfe8:指針p),以便程序返回時OS正確使用。
    • 注意PUSH是根據當前ESP來的,ESP永遠指向當前棧的棧頂。
  • 應用程序調用API時會產生中斷,進而調用asm_hrb_api,進而調用hrb_api。調用asm_hrb_api時,需要將段地址設回OS用的段。因此修改asm_hrb_api:

    _asm_hrb_api:
            ; INT 40.現在就是處於禁止中斷
            PUSH	DS
            PUSH	ES
            PUSHAD		; 用於保存寄存器的值的PUSH
            MOV		EAX,1*8
            MOV		DS,AX			; 先只將DS設爲OS用
            MOV		ECX,[0xfe4]		; 取出OS用的ESP
            ADD		ECX,-40
            MOV		[ECX+32],ESP	; 保存應用程序的ESP
            MOV		[ECX+36],SS		; 保存應用程序的SS
    
    ; 將PUSH後的值複製到系統棧
            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			; 將剩餘的段寄存器也設爲OS用
            MOV		SS,AX
            MOV		ESP,ECX
            STI			; 恢復中斷請求
    
            CALL	_hrb_api        ; 有8個參數
    
            MOV		ECX,[ESP+32]	; 取出應用程序的ESP
            MOV		EAX,[ESP+36]	; 取出應用程序的SS
            CLI         ; 切換回應用程序
            MOV		SS,AX
            MOV		ESP,ECX
            POPAD
            POP		ES
            POP		DS
            IRETD		; 這個命令會自動執行STI
    
    • PUSHAD是將值複製到應用程序的棧中,因此OS的系統棧的hrb_api無法讀取這些值,需要把他們複製過來。
    • 這次,將FS和GS的設置省略了。
  • 應用程序運行中也會產生中斷請求(比如鼠標,鍵盤等中斷請求),中斷產生後悔調用asm_inthandler20等函數,這些函數也是屬於OS。因此,的需要對DS和SS進行切換。

  • 修改asm_inthandler20函數:

    _asm_inthandler20:
            PUSH	ES
            PUSH	DS
            PUSHAD
            MOV		AX,SS
            CMP		AX,1*8
            JNE		.from_app
    ;	當OS活動產生中斷時的情況和之前差不多
            MOV		EAX,ESP
            PUSH	SS				; 保存中斷時的SS
            PUSH	EAX				; 保存中斷時的ESP
            MOV		AX,SS
            MOV		DS,AX
            MOV		ES,AX
            CALL	_inthandler20
            ADD		ESP,8           ; 按照順序畫一下棧,就可以知道爲什麼+8了
            POPAD
            POP		DS
            POP		ES
            IRETD
    .from_app:
    ;	當應用程序活動時發生中斷
            MOV		EAX,1*8
            MOV		DS,AX			; 先只將DS設定爲OS用
            MOV		ECX,[0xfe4]		; 取出OS的ESP
            ADD		ECX,-8
            MOV		[ECX+4],SS		; 保存中斷時的SS
            MOV		[ECX  ],ESP		; 保存中斷時的ESP
            MOV		SS,AX
            MOV		ES,AX
            MOV		ESP,ECX
            CALL	_inthandler20   ; inthandler20需要一個參數esp
            POP		ECX
            POP		EAX
            MOV		SS,AX			; 將SS設迴應用程序用
            MOV		ESP,ECX			; 將ESP設迴應用程序用
            POPAD
            POP		DS
            POP		ES
            IRETD
    
    • .開頭的標籤名,這是一種被稱爲本地標籤的特殊標籤,基本上和普通標籤一致,區別在於即使標籤名和其他函數中的標籤名重複,系統也能將它們區分開來。
  • 修改asm_inthandler21和asm_inthandler2c,跟上面的asm_inthandler20大同小異。

  • 以上彙編語言程序懂個大概即可,因爲CPU實際上本身有自動進行這種複雜切換的功能,最終還是要使用CPU本身的功能來實現

  • 現在的harib18d是應用程序棧和系統棧是分開的。

  • make後用VMware運行:

    • 運行應用程序crack1.hrb:
      • 很不幸,這次的BUG直接讓OS崩潰了。
      • 這大概是因爲我們區分了系統棧和應用程序棧,當應用程序crack1強行訪問由OS管理的內存段時,CPU發現不對勁,給終止了。
      • 因此,接下來應該實現,當發現不對勁時,強制結束crack1。這就是harib18e的工作了。

5. 對異常的支持(harib18e)

  • 實現強制結束應用程序的功能。

  • 要想強制結束程序,只要在中斷號0x0d中註冊一個函數即可。在x86架構規範中,當應用程序試圖破壞OS,或者違背OS的設置時,就會自動產生0x0d中斷。該中斷也被稱爲異常

  • 編寫asm_inthandler0d函數:

    _asm_inthandler0d:
            STI
            PUSH	ES
            PUSH	DS
            PUSHAD
            MOV		AX,SS
            CMP		AX,1*8
            JNE		.from_app
    ;	當OS活動時產生中斷的情況和之前差不多
            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設爲OS用
            MOV		ECX,[0xfe4]		; 取出OS用的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			; OS用的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
    
    • 這個函數與asm_inthandler20的主要區別在於增加了STI/CLI這樣的控制中斷請求禁止、恢復的指令和根據inthandler0d的結果來執行強制結束應用程序的操作。
    • 在強制結束時,儘管中斷處理完成了,但卻沒有使用IRETD指令,而且還把棧強制恢復到start_app時的狀態,使程序運行返回到cmd_app。
    • 這種奇怪的做法是沒有問題的。
  • 編寫中斷處理程序inthandler0d(console.c):

    int inthandler0d(int *esp)
    {
        struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
        cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
        return 1; /* 強制結束程序 */
    }
    
    • 只要應用程序運行過程中產生INT 0x0d中斷,inthandler0d函數一定會返回1,那麼應用程序一定會被強制結束。
    • 但是OS產生INT 0x0d中斷時,只是打印提示信息。
    • 顯示信息是“General Protection Exception”,意思是一般保護異常。處理一般保護異常,還有一些其他特殊的異常,這些特殊的異常並不是由應用程序的bug和破壞行爲所引發的,而是其他種類的異常情況,特殊的異常會由INT 0x0d以外的中斷來處理。
  • 將asm_inthandler0d註冊到IDT中:

    void init_gdtidt(void)
    {
        ……
        /* IDT的設置 */
        set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32); /*將asm_inthandler0d中斷處理程序註冊到0x0d號中斷*/
        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);
    
        return;
    }
    
  • make後用VMware運行:

    • 執行可執行文件crack1.hrb:
    • 繼續輸入指令dir:
    • 顯然,應用程序crack1企圖破壞操作系統,由於harib18e是在harib18d的基礎上添加了INT 0x0d中斷以強制結束應用程序,所以,當CPU發現應用程序企圖破壞OS時(訪問不屬於它管理的內存空間),立即產生了0x0d中斷,然後強制回到cmd_app中。

6. 保護操作系統(3)(harib18f)

  • OS會指定應用程序用的DS,因此應用程序的破壞行爲會發生異常。如果忽略OS指定的DS,而是用彙編語言直接將OS用的段地址存入DS的話,便會破壞OS。

  • 編寫crack2.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            MOV		EAX,1*8			; OS用的段號
            MOV		DS,AX			; 將其存入DS
            MOV		BYTE [0x102600],0
            RETF
    
    • crack2.nas將OS用的數據段寄存器(DS)賦值給自己的DS,因此,crack2.nas就可以訪問地址0x102600了。
  • make後用VMware運行:

    • 運行crakc2.hrb後並輸入dir:
      • 沒有產生異常,且dir命令失效,OS崩潰。

7. 保護操作系統(4)(harib18g)

  • harib18f之所以會被crack2.hrb成功攻擊,是因爲應用程序擅自向自己用的DS存入了OS用的段地址DS

  • 解決方法:讓應用程序無法使用OS用的段地址。

    • x86架構的CPU正好有這樣的功能。
  • 在段定義的地方,如果將訪問權限加上0x60的話,就可以將段設置爲應用程序用。當CS(代碼段寄存器)中的段地址爲應用程序的段地址時,CPU就會認爲“當前正在運行應用程序”,這時如果過訪問OS用的段地址就會產生異常。

  • 修改cmd_app:

    int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
    {
        ……
        char name[18], *p, *q;
        struct TASK *task = task_now(); /*獲取當前任務的指針*/
        ……
        if (finfo != 0) {
            /* 找到文件的情況 */
            ……
            set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60); /*訪問權限加上0x60*/
            set_segmdesc(gdt + 1004, 64 * 1024 - 1,   (int) q, AR_DATA32_RW + 0x60); /*訪問權限加上0x60*/
            ……
            start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0)); /*啓動*/
            ……
        }
        /* 沒有找到文件的情況 */
        return 0;
    }
    
    • 應用程序段的訪問權限加上了0x60,代表使用x86架構的方法。
    • 如果使用上述方法,必須在TSS中註冊OS用的段地址和ESP。 因此,start_app函數增加了一個參數,用於傳遞註冊地址。
  • 用上述方法,在啓動應用程序時,需要讓“OS嚮應用程序用的段執行far-CALL”。但根據x86的規則,是不允許操作系統CALL應用程序的(如果強行CALL會產生異常)。同樣,在x86中,“OS嚮應用程序用的段進行far-JMP”也是被禁止的。(不用上述方法,OS是可以嚮應用程序執行far-CALL的,harib18e就是這麼辦的。)

  • 使用RETF:就像是被應用程序CALL過之後那樣,事先將地址PUSH到棧中,然後執行RETF,這樣就能成功啓動應用程序了。

  • 修改naskfunc.nas中的start_app:

    _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		; 保存OS用的ESP
            MOV		[EBP+4],SS		; 保存OS用的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
    ;	應用程序結束後不會到這裏
    
    • 將應用程序的段號和3進行運算的部分,是爲用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		; 將OS用的段地址存入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
    
    • 多虧了CPU幫我們自動執行那些麻煩的棧切換工作,這裏的asm_hrb_api比harib18e的asm_hrb_api斷了許多。
    • 當hrb_api返回(EAX中存放返回值)0時應用程序繼續運行,當返回值非0時,返回值當做tss.esp0的地址來處理,強制結束應用程序。
    • 這樣設計,是想做一個強制結束應用程序的API。因爲沒有通過far-CALL啓動應用程序,自然也無法使用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); /*返回當前任務的tss.esp0*/
        }
        return 0;
    }
    
    • 返回0代表繼續運行應用程序,返回非0則強制結束應用程序。
  • 修改inthandler0d:

    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);	/* 強制應用程序結束 */
    }
    
    • 只要產生INT 0x0d中斷,應用程序一定會被強制結束。(產生了保護性異常)
  • 由於把麻煩的棧切換全部交給了CPU去處理了,因此中斷處理部分(鍵盤鼠標等)需要修改:

    _asm_inthandler20:
            PUSH	ES
            PUSH	DS
            PUSHAD
            MOV		EAX,ESP
            PUSH	EAX
            MOV		AX,SS
            MOV		DS,AX
            MOV		ES,AX
            CALL	_inthandler20
            POP		EAX
            POPAD
            POP		DS
            POP		ES
            IRETD
    
    • 其實就是將asm_inthandler20改回原來的版本。
    • asm_inthandler21和asm_inthandler2c也改回原來的版本。
  • 負責處理異常中斷的asm_inthandler0d也需要修改:

    _asm_inthandler0d:
            STI
            PUSH	ES
            PUSH	DS
            PUSHAD
            MOV		EAX,ESP
            PUSH	EAX
            MOV		AX,SS
            MOV		DS,AX
            MOV		ES,AX
            CALL	_inthandler0d
            CMP		EAX,0		; 比較EAX和0
            JNE		end_app		; 不同,跳轉到end_app
            POP		EAX
            POPAD
            POP		DS
            POP		ES
            ADD		ESP,4			; INT 0x0d 需要這句
            IRETD
    
    • 添加了強制結束的代碼。
  • 修改IDT設置:

    void init_gdtidt(void)
    {
        ……
        /* IDT設置 */
        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;
    }
    
    • 在我們已經清晰地區分OS系統段和應用程序段的情況下,當應用程序試圖調用未經OS授權的中斷時,CPU會認爲“這傢伙亂用奇怪的中斷號,想把OS搞壞,這是壞蛋”,CPU便會產生異常。因此,需要在IDT設置中,將INT 40設置爲“可供應用程序作爲API來調用的中斷”
    • 將INT 40的訪問屬性編碼加上0x60就完成了上述設置。至於其他中斷,並不是用於應用程序對OS的調用,而是鼠標鍵盤等控制以及對異常的處理,因此禁止應用程序調用這種中斷,倘若應用程序強行調用這些中斷,CPU會自動產生異常中斷0x0d然後結束應用程序。
  • 應用程序運行流程圖解

  • 因爲不能使用RETF結束應用程序了,所以需要修改所有的應用程序:

    • 修改hello.nas:

      [INSTRSET "i486p"]
      [BITS 32]
              MOV		ECX,msg
              MOV		EDX,1
      putloop:
              MOV		AL,[CS:ECX]
              CMP		AL,0
              JE		fin
              INT		0x40
              ADD		ECX,1
              JMP		putloop
      fin:
              MOV		EDX,4
              INT		0x40
      msg:
              DB	"hello",0
      
      • fin調用了4號API。結束程序。
    • 修改hello2.nas:

      [INSTRSET "i486p"]
      [BITS 32]
              MOV		EDX,2
              MOV		EBX,msg
              INT		0x40
              MOV		EDX,4   ; 調用4號API
              INT		0x40
      msg:
              DB	"hello",0
      
    • 修改a.c:

      void api_putchar(int c);
      void api_end(void);
      
      void HariMain(void)
      {
          api_putchar('A');
          api_end();
      }
      
      • api_end函數是調用4號API的。
    • 修改a.nas:

      [FORMAT "WCOFF"]
      [INSTRSET "i486p"]
      [BITS 32]
      [FILE "a_nask.nas"]
      
              GLOBAL	_api_putchar
              GLOBAL	_api_end
      
      [SECTION .text]
      
      _api_putchar:	; void api_putchar(int c);
              MOV		EDX,1
              MOV		AL,[ESP+4]		; c
              INT		0x40
              RET
      
      _api_end:	; void api_end(void);
              MOV		EDX,4
              INT		0x40
      
      • C語言編寫的應用程序需要編譯後和a.nas進行連接。(比如a.c和下面的hello3.c)
    • 修改hello3.c:

      void api_putchar(int c);
      void api_end(void);
      
      void HariMain(void)
      {
          api_putchar('h');
          api_putchar('e');
          api_putchar('l');
          api_putchar('l');
          api_putchar('o');
          api_end();
      }
      
    • 修改crack1.c:

      void api_end(void);
      
      void HariMain(void)
      {
          *((char *) 0x00102600) = 0;
          api_end();
      }
      
    • 修改crack2.nas:

      [INSTRSET "i486p"]
      [BITS 32]
              MOV		EAX,1*8			; OS用的段號
              MOV		DS,AX			; 將其存入DS
              MOV		BYTE [0x102600],0
              MOV		EDX,4           ; 調用4號API
              INT		0x40
      
  • make後用VMware運行:

    • 運行可執行文件hello、hello2、a和hello3:
    • 運行crack2:
    • 運行crack1:
      • crack1和crack2都產生了0x0d中斷。

8. 接下來做幾個有趣的實驗(harib18g_extend)

  • 在harib18g的基礎上修改應用程序代碼,make後用VMware運行。

  • 修改hello.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            MOV		ECX,msg
            MOV		EDX,1
    putloop:
            MOV		AL,[CS:ECX]
            CMP		AL,0
            JE		fin
            INT		0x40
            ADD		ECX,1
            JMP		putloop
    fin:
            
    msg:
            DB	"hello",0
    
    • 上述代碼,假設了一種情況:編寫應用程序hello.nas的程序員忘記調用4號API結束應用程序。
    • 這樣的話,CPU執行到fin會發現應用程序出現了BUG,會自動產生0x0d中斷(情況4)。
    • 驗證一下:
      • 果然和我們預想的一樣。
  • 修改hello2.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            MOV		EDX,2
            MOV		EBX,msg
            INT		0x40
            INT		0x21
    msg:
            DB	"hello",0
    
    • 調用0x21中斷(應用程序禁止調用的中斷):
      • (情況3,和我們預想的一樣)
  • 修改crack1.c:

    void api_end(void);
    
    void HariMain(void)
    {
        *((char *) 0x00102600) = 0;
        //api_end();
    }
    
    • 註釋掉了代碼api_end()。其實有沒有api_end都一樣,因爲在執行*((char *) 0x00102600) = 0;時crack2已經觸發了0x0d中斷,crack1.hrb被強制結束,然後返回cmd_app了。
      • 和我們想的一樣。

9. 寫在現在

  • 現在是2020.4.26 16:51。這篇文檔寫了3天。
  • 4.24休息了一天,可能是第21天的彙編太多,加上之前的勞累,偷懶了一天。
  • 4.25~4.26兩天時間啃完了第21天這根硬骨頭。
  • 第21天的確難,尤其是彙編代碼太多了,晦澀難懂。
  • 最後感慨一句,轉眼4月底了,真是韶華易逝啊。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章