2010年12月28日 星期二

解決"sudo: must be setuid root"的問題

今天在自己的Linux桌機 (Ubuntu 10.10) 幹了一件傻事:
$ sudo chown -R adrian.adrian /usr/
結果導致使用sudo會有底下的問題:
$ sudo su
sudo: must be setuid root
這下可high了, Ubuntu在安裝的時候並沒有讓管理者設定root密碼, 所以無法使用root登入, 沒有root權限什麼事都不能做啊! 底下解決方式:

  1. 重新啟動Ubuntu,在grub選單中選擇有recovery mode的選項, Ex: "Ubuntu, with Linux 2.6.35-23-server (recovery mode)"
  2. 以recovery模式開機後, 會出現"Recovery Menu"選單, 選擇"root Drop to root shell prompt"選項就可進入root shell prompt。
  3. 利用ls -l觀看sudo資訊,所以是擁有此程式使用者的問題。
    -rwxr-xr-x 2 adrian adrian 147872 2010-09-01 04:39 /usr/bin/sudo
  4. 執行底下指令便可大功告成:
    # chwon root:root /usr/bin/sudo
    # chmod 4755 /usr/bin/sudo
    # reboot
  5. 最後還是乖乖地把/usr/重新設定回root。
    $ sudo chown root.root /usr/

【Reference】
[1] Ubuntu forum

2010年12月21日 星期二

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (七) 利用Call Gate與TSS (Task-State Segment)實現特權等級的轉換

上篇文章僅介紹如何利用Call Gate,文中並未提及如何實現特權等級的轉換,也就是從低特權等級進入高特權等級,就如同Linux的user space (Privilege 3或稱Ring 3)進入kernel space (Privilege 0或稱Ring 0)。

簡介TSS (Task-State Segment)
本文僅簡單說明TSS在本文被使用的地方,如欲詳盡說明請參考[2]之第七章。

Task Structure
Figure 1為一個工作任務 (Task)的結構圖,一個工作任務分為兩大部份: 1. 工作任務的執行空間 (Task Execution Space),其中包含CS、SS以及若干個DS,也就是Figure 1右上角的三個區段。2. TSS: 由工作任務執行空間與一個儲存空間 (用來儲存工作任務的狀態)所組成。Figure 1又說明另一件事: TSS的Segment Selector必須儲存在Task Register。因此在使用TSS之前,必須使用LTR指令將TSS之Segment Selector載入至Task Register。

Figure 1 Structure of a Task

TSS Memory Layout

Figure 2為32位元TSS記憶體空間配置圖,如下所述:
  • ESP0、SS0、ESP1、SS1、ESP2與SS2: 分別為特權等級0、1與2的堆疊區段與堆疊指標,也就是Figure 1右下角三個 "Stack Seg. Priv. Level x"。本文利用ESP0與SS0實現特權等級的轉換 (Ring 3轉換至Ring 0),因此作者僅設定TSS的這兩個欄位,其它欄位則設為0。
  • 其它欄位在此不進一步探討,有興趣的網友可以參考[2]。

Figure 2 TSS Memory Layout

原始碼下載
由於篇幅的關係,往後該系列文章將不張貼程式碼,將提供連結下載方式,原始碼下載點


作業系統程式碼說明
Figure 3為作業系統程式碼解說圖,此圖說明GDT與LDT的記憶體配置圖與程式流程圖,其流程圖並非以傳統方式表示。取而代之,改以紅色圈圈的數字代表程式流程並搭配GDT與LDT記憶體配置圖,如此更能清楚地明白程式執行流程。底下將針對每一步驟 (紅色圈圈的數字)做詳盡的解釋:

Figure 3 High Level Perspective of OS code

  1. 使用ljmp指令由真實模式轉換至32位元保護模式 (Ring 0)。其片段程式碼如下所示:
    
      /* Jump to protected-mode OS code        *
       * ljmpl prototype:                      *
       *   ljmpl segment_selector_of_CS offset */
      ljmpl     $SegSelectorCode32, $0

    此區段程式碼將Msg1輸出至螢幕,並將LDT的segment selector載入至LDTR,接著使用lcall指令跳至LDT的CS,如下所示:
        /* Load LDT selector */
       mov     $(SegSelectorLDT), %ax
    
       /* Load LDT selector in GDT to LDT register */
       lldt     %ax
    
       /* Jump to code segment in LDT */
       lcall     $(SegSelectorLDTCode32), $0
    
  2. 如第1點所述。
  3. 如第1點所述。
  4. 輸出Msg2,隨即使用lret指令返回LABEL_GDT_CODE。
  5. 通常,call與ret指令必須配合使用。執行call指令時,處理器會將SS、ESP、CS與EIP推入(Push) 該執行空間的堆疊 (Stack)。接著,執行ret指令時,處理器會將EIP、CS、ESP與SS從堆疊取出 (Pop)。為了實現從Ring 0返回Ring 3,底下程式碼模擬處理器的工作: 將Ring 3的SS、ESP、CS與EIP推入(Push) 該執行空間的堆疊 (Stack)並使用lret指令返回Ring 3。
       pushl    $(SegSelectorStackR3)
      pushl    $(TopOfStackR3)
      pushl    $(SegSelectorCodeR3)
      pushl    $0
      lret
    
  6. 從Ring 0 (LABEL_GDT_CODE)返回 Ring 3 (LABEL_GDT_CODE_R3)。
  7. 此Ring 3程式區段將Msg1_R3輸出至螢幕,利用call gate並搭配TSS成功地返回Ring 0程式區段。因此,必須設定TSS之ESP0與SS0欄位,如下所示:
    LABEL_TSS:
     .4byte  0                         /* Previous Task Link   */
     .4byte  TopOfStackR0              /* ESP0                 */
     .4byte  SegSelectorStackR0        /* SS0                  */
     .4byte  0                         /* ESP1                 */
     .4byte  0                         /* SS1                  */
     .4byte  0                         /* ESP2                 */
     .4byte  0                         /* SS2                  */
     .4byte  0                         /* CR3 (PDBR)           */
     .4byte  0                         /* EIP                  */
     .4byte  0                         /* EFLAGS               */
     .4byte  0                         /* EAX                  */
     .4byte  0                         /* ECX                  */
     .4byte  0                         /* EDX                  */
     .4byte  0                         /* EBX                  */
     .4byte  0                         /* ESP                  */
     .4byte  0                         /* EBP                  */
     .4byte  0                         /* ESI                  */
     .4byte  0                         /* EDI                  */
     .4byte  0                         /* ES                   */
     .4byte  0                         /* CS                   */
     .4byte  0                         /* SS                   */
     .4byte  0                         /* DS                   */
     .4byte  0                         /* FS                   */
     .4byte  0                         /* GS                   */
     .4byte  0                         /* LDT Segment Selector */
     .2byte  0                         /* Reserved             */
     .2byte  (. - LABEL_TSS + 2)       /* I/O Map Base Address */
    .set TSSLen, (. - LABEL_TSS)
    
    在此,特別對特權等級轉換做進一步解釋,以便讓讀者了解為什麼只要設定ESP0與SS0欄位即可。當存取目的程式區段 (Destination Code Segment)是被允許的,處理器便會根據Call Gate Descriptor的Segment Selector欄位找出對應的程式區段,如果又涉及特權等級轉換,處理器會將原本使用中的堆疊切換至目標特權等級的堆疊。舉例來說,假設目前程式區段運行於Ring 3,處理器便使用Ring 3的堆疊,當使用Call Gate欲轉換至Ring 0的程式區段。如果此要求是被允許的,則處理器會跳至Ring 0的程式區段並使用Ring 0的堆疊,因此程式設計員必須先設定Ring 0堆疊 (ESP0與SS0),這就是為什麼必須設定TSS的ESP與SS0欄位的原因。
  8. 跳至GDT之Call Gate的描述子 (Descriptor)。
  9. 根據Call Gate描述子的Segment Selector欄位找出對應的CS。
  10. 將Msg3輸出至螢幕並返回。
  11. 返回Ring 3程式區段,並執行一無窮迴圈。
QEMU測試結果

【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31

2010年12月8日 星期三

[打造簡易作業系統 - 以GNU Assembler組合語言撰寫] (六) 簡介Call Gate

Call Gate主要目的用來將一程式的特權等級 (Privilege Level) 轉換至另一個特權等級。舉例來說,Linux使用者利用ioctl()系統呼叫從user space (Privilege 3)進入kernel space (Privilege 0)。

Note
本篇範例程式僅示範如何使用call gate,並如何利用call gate呼叫所對應的程式碼片段,所有的程式碼都運行於privilege 0。對於特權等級的轉換 (Privilege 3 -> Privilege 0),留待往後的文章再詳加探討。本篇文章僅簡單地介紹Call Gate,詳盡介紹請參考Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A之5.8節。

Call-Gate Descriptor之介紹
參考Figure 1,其欄位如下所述:
  1. Offset: 代表所指定的程式區段 (Code Segment)進入點 (Entry Point),通常都設為0。
  2. Segment Selector: 此區段選擇器指定所要存取的程式區段 (Code Segment)。如此說明或許有點模糊,請參考Figure 2,此圖紅色數字方塊展示出設定Call Gate的必要程序,因此設定Call Gate需要三道步驟:A. 設定Call Gate Descriptor相關欄位,其欄位設定值如Table 1所示。B. 設定對應之程式區段。C. 設定Call-Gate之區段選擇器。
  3. Param. Count: 指定呼叫程序 (Calling Procedure)欲傳遞幾個參數給被呼叫的程序 (Called Procedure)。
  4. DPL (Descriptor Privilege Level): 此區段描述子之特權等級,其DPL與RPL特權等級之檢查請參考Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A之Figure 5-11。
  5. P: 此Call-Gate描述子是否有有效 (有效: P=1, 無效: P=0)。

Figure 1. Call-Gate Descriptor


Figure 2. Steps for Configuring Call Gate


Table 1. Field Value Configuration for Call-Gate Descriptor

Boot Loader程式碼
請參考此篇文章的"Boot Loader 程式碼"。

作業系統程式碼
Figure 3為作業系統程式碼,此作業系統程式碼運行於32位元保護模式,程式說明如下:

  1. 定義七個Segment Descriptor (LABEL_GDT_NULL、LABEL_GDT_CODE、LABEL_GDT_DATA、LABEL_GDT_VIDEO、LABEL_GDT_LDT、LABEL_GDT_CG_CODE與LABEL_GDT_CG)。其中VIDEO的基底位址為0xB8000,詳情請參考Printing to Screen。接著定義GDT的長度、定義Code、Data、VIDEO與LDT的segment selector、定義輸出的字串、定義GDTPtr與定義LDT表。
  2. LABEL_GDT_CG為Call Gate Descriptor,其儲存在GDT。此設定步驟為Figure 2的1號紅色方塊。
  3. LABEL_GDT_CG_CODE為Call Gate Descriptor欄位Segment Selector所指定的地方,也就是Call Gate所對應的區段程式碼。此設定步驟為Figure 2的2號紅色方塊。
/* os.S
*
*/

#include "pm.h"

.code16
.text
jmp os_main

# Segment descritors for GDT
LABEL_GDT_NULL: SEG_DESC 0, 0, 0
LABEL_GDT_CODE: SEG_DESC 0, (PECode32Len - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
LABEL_GDT_DATA: SEG_DESC 0, (DataLen - 1), (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_VIDEO: SEG_DESC 0xB8000, 0xFFFF, (DESC_ATTR_TYPE_CD_RW)
LABEL_GDT_LDT: SEG_DESC 0, (LDTLen - 1), (DESC_ATTR_TYPE_LDT)
LABEL_GDT_CG_CODE: SEG_DESC 0, (CG_CODE32_LEN - 1), (DESC_ATTR_TYPE_CD_ER | DESC_ATTR_D)
LABEL_GDT_CG: CALL_GATE SegSelectorCGCODE, 0, 0, (GATE_CG_ATTR)

# The length of GDT
.set GdtLen, (. - LABEL_GDT_NULL)

# Segment selectors
.set SegSelectorCode32, (LABEL_GDT_CODE - LABEL_GDT_NULL)
.set SegSelectorData, (LABEL_GDT_DATA - LABEL_GDT_NULL)
.set SegSelectorVideo, (LABEL_GDT_VIDEO - LABEL_GDT_NULL)
.set SegSelectorLDT, (LABEL_GDT_LDT - LABEL_GDT_NULL)
.set SegSelectorCGCODE, (LABEL_GDT_CG_CODE - LABEL_GDT_NULL)
.set SegSelectorCG, (LABEL_GDT_CG - LABEL_GDT_NULL)

# data segment
LABEL_DATA:
Msg1: .ascii "Welcome to Protect mode in GDT.\0"
Msg2: .ascii "Welcome to Protect mode in LDT.\0"
Msg3: .ascii "Welcome to Protect mode through Call Gate.\0"

.set Msg1Offset, (Msg1 - LABEL_DATA)
.set Msg2Offset, (Msg2 - LABEL_DATA)
.set Msg3Offset, (Msg3 - LABEL_DATA)

.set DataLen, (. - LABEL_DATA)

# GDTR pointer
LABEL_GDTR:
.2byte (GdtLen - 1) # Limit field
.4byte 0 # base field

# LDT information
LABEL_LDT:
LABEL_LDT_ENTRY: SEG_DESC 0, (LDT_CODE32_LEN - 1), (DESC_ATTR_TYPE_CD_E | DESC_ATTR_D)

# length of LDT
.set LDTLen, (. - LABEL_LDT)

# LDT selector
.set SegSelectorLDTCode32, (LABEL_LDT_ENTRY - LABEL_LDT + SA_TIL)


# real-mode OS code
os_main:
mov %cs, %ax
mov %ax, %ds
mov %ax, %ss
mov %ax, %es


/* Set gdt for code segment */
InitSegDescriptor LABEL_PE_CODE32, LABEL_GDT_CODE
InitSegDescriptor LABEL_DATA, LABEL_GDT_DATA
InitSegDescriptor LABEL_LDT, LABEL_GDT_LDT
InitSegDescriptor LABEL_PE_LDT_CODE32, LABEL_LDT_ENTRY
InitSegDescriptor LABEL_PE_CG_CODE32, LABEL_GDT_CG_CODE

/* Set GDTR */
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $LABEL_GDT_NULL, %eax
movl %eax, (LABEL_GDTR + 2)

/* Enable A20 line */
xor %ax, %ax
in $0x92, %al
or $2, %al
out %al, $0x92

cli

/* Load the GDT base address and limit from memory into the GDTR register */
lgdt LABEL_GDTR

/* Enable protect mode */
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0

/* Jump to protected-mode OS code */
ljmp $SegSelectorCode32, $0

LABEL_PE_CG_CODE32:
.code32
mov $(SegSelectorData), %ax
mov %ax, %ds
mov $(SegSelectorVideo), %ax
mov %ax, %gs

xorl %esi, %esi
xorl %edi, %edi
movl $Msg3Offset, %esi
movl $((80 * 11 + 0) * 2), %edi
movb $0xC, %ah

cg_dump_str:
lodsb
andb %al, %al
jz cg_fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp cg_dump_str

cg_fin:
lret

.set CG_CODE32_LEN, (. - LABEL_PE_CG_CODE32)

LABEL_PE_LDT_CODE32:
.code32
# invoke a procedure call throught a call-gate.
lcall $(SegSelectorCG), $0

mov $(SegSelectorData), %ax
mov %ax, %ds
mov $(SegSelectorVideo), %ax
mov %ax, %gs

xorl %esi, %esi
xorl %edi, %edi
movl $Msg2Offset, %esi
movl $((80 * 13 + 0) * 2), %edi
movb $0xC, %ah

ldt_dump_str:
lodsb
andb %al, %al
jz ldt_fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp ldt_dump_str

ldt_fin:
jmp .

.set LDT_CODE32_LEN, (. - LABEL_PE_LDT_CODE32)

# protected-mode OS code in GDT
LABEL_PE_CODE32:
.code32
/* Load data segment selector */
mov $(SegSelectorData), %ax
mov %ax, %ds

/* Load Video segment selector */
mov $(SegSelectorVideo), %ax
mov %ax, %gs

/* Output the data */
xorl %esi, %esi
xorl %edi, %edi
movl $Msg1Offset, %esi
movl $((80 * 10 + 0) * 2), %edi
movb $0xC, %ah

dump_str:
lodsb
andb %al, %al
jz fin
mov %ax, %gs:(%edi)
addl $2, %edi
jmp dump_str

fin:
/* Load LDT selector */
mov $(SegSelectorLDT), %ax

/* Load LDT selector in GDT to LDT register */
lldt %ax

/* Jump to code segment in LDT */
ljmp $(SegSelectorLDTCode32), $0

.set PECode32Len, (. - LABEL_PE_CODE32)

.ascii "Welcome to OS context!"
.byte 0

.org 0x400, 0x41 # fill characters with 'A'. Sector 2


Figure 3. Operating System Code


pm.h標頭檔
/* pm.h
*
* Adrian Huang (adrianhuang0701@gmail.com)
*/
.macro SEG_DESC Base, Limit, Attr
.2byte (\Limit & 0xFFFF)
.2byte (\Base & 0xFFFF)
.byte ((\Base >> 16) & 0xFF)
.2byte ((\Attr & 0xF0FF) | ((\Limit >> 8) & 0x0F00))
.byte ((\Base >> 24) & 0xFF)
.endm

.macro InitSegDescriptor OFFSET GDT_SEG_ADDR
xor %ax, %ax
mov %cs, %ax
shl $4, %eax
addl $(\OFFSET), %eax
movw %ax, (\GDT_SEG_ADDR + 2)
shr $16, %eax
movb %al, (\GDT_SEG_ADDR + 4)
movb %ah, (\GDT_SEG_ADDR + 7)

.endm

.macro CALL_GATE SegSelector, Offset, ParamCount, Attr
.2byte (\Offset & 0xFFFF)
.2byte (\SegSelector)
.byte (\ParamCount)
.byte (\Attr)
.2byte ((\Offset >> 16) & 0xFFFF)
.endm

.set DESC_ATTR_TYPE_LDT, 0x82 /* LDT Segment */
.set DESC_ATTR_TYPE_CG, 0x8C /* Call-Gate Segment */
.set DESC_ATTR_TYPE_CD_ER, 0x9A /* Code segment with Execute/Read */
.set DESC_ATTR_TYPE_CD_E, 0x98 /* Code segment with Execute Only */
.set DESC_ATTR_TYPE_CD_RW, 0x92 /* Data segment with R/W */
.set DESC_ATTR_D, 0x4000 /* 32-bit segment */

/* Selector Attribute */
.set SA_TIL, 0x4
.set SA_RPL0, 0x0
.set SA_RPL1, 0x1
.set SA_RPL2, 0x2
.set SA_RPL3, 0x3

/* The attribute of call gate */
.set GATE_CG_ATTR, 0x8C



編譯程式碼
下圖為編譯的Makefile。
LD=ld
CC=gcc

all: boot_loader.bin

boot_loader.bin: boot_loader.o os.o
${LD} -Ttext=0x7C00 -s $< -o $@ --oformat binary ${LD} -Ttext=0x0 -s os.o -o os.bin --oformat binary cat os.bin >> $@

boot_loader.o:
${CC} -c boot_loader.S

os.o:
${CC} -c os.S

clean:
rm -f boot_loader.o os.o os.bin boot_loader.bin

其編譯訊息如下所示:
adrian@adrian-desktop:~/working/build_os/my_ex/blog/pe-call-gate-same-priv$ make clean all
rm -f boot_loader.o os.o os.bin boot_loader.bin
gcc -c boot_loader.S
gcc -c os.S
ld -Ttext=0x7C00 -s boot_loader.o -o boot_loader.bin --oformat binary
ld -Ttext=0x0 -s os.o -o os.bin --oformat binary
ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
cat os.bin >> boot_loader.bin
adrian@adrian-desktop:~/working/build_os/my_ex/blog/pe-call-gate-same-priv$




QEMU測試結果



【Reference】
[1] Solrex - 使用開源軟體-自己動手寫作業系統
[2] Intel 64 and IA-32 Architectures. Software Developer's Manual. Volume 3A
[3] 30天打造OS!作業系統自作入門
[4] Jserv's Blog
[5] X86 開機流程小記
[6] Linux assemblers: A comparison of GAS and NASM
[7] linux-source-2.6.31