Linux系統(tǒng)中的init進(jìn)程(pid=1)是除了idle進(jìn)程(pid=0傀蓉,也就是init_task)之外另一個(gè)比較特殊的進(jìn)程察纯,它是Linux內(nèi)核開始建立起進(jìn)程概念時(shí)第一個(gè)通過kernel_thread產(chǎn)生的進(jìn)程淑蔚,其開始在內(nèi)核態(tài)執(zhí)行必尼,然后通過一個(gè)系統(tǒng)調(diào)用骗随,開始執(zhí)行用戶空間的/sbin/init程序,期間Linux內(nèi)核也經(jīng)歷了從內(nèi)核態(tài)到用戶態(tài)的特權(quán)級轉(zhuǎn)變艺挪,/sbin/init極有可能產(chǎn)生出了shell不翩,然后所有的用戶進(jìn)程都有該進(jìn)程派生出來(目前尚未閱讀過/sbin/init的源碼)…
目前我們至少知道在內(nèi)核空間執(zhí)行用戶空間的一段應(yīng)用程序有兩種方法:
1. call_usermodehelper
2. kernel_execve
它們最終都通過int $0x80在內(nèi)核空間發(fā)起一個(gè)系統(tǒng)調(diào)用來完成兵扬,這個(gè)過程我在《深入Linux設(shè)備驅(qū)動(dòng)程序內(nèi)核機(jī)制》第9章有過詳細(xì)的描述,對它的討論最終結(jié)束在 sys_execve函數(shù)那里口蝠,后者被用來執(zhí)行一個(gè)新的程序∑髦樱現(xiàn)在一個(gè)有趣的問題是,在內(nèi)核空間發(fā)起的系統(tǒng)調(diào)用妙蔗,最終通過sys_execve來執(zhí)行用戶 空間的一個(gè)程序傲霸,比如/sbin/myhotplug,那么該應(yīng)用程序執(zhí)行時(shí)是在內(nèi)核態(tài)呢還是用戶態(tài)呢眉反?直覺上肯定是用戶態(tài)昙啄,不過因?yàn)閏pu在執(zhí)行 sys_execve時(shí)cs寄存器還是__KERNEL_CS,如果前面我們的猜測是真的話寸五,必然會(huì)有個(gè)cs寄存器的值從__KERNEL_CS到 __USER_CS的轉(zhuǎn)變過程梳凛,這個(gè)過程是如何發(fā)生的呢?下面我以kernel_execve為例梳杏,來具體討論一下其間所發(fā)生的一些有趣的事情韧拒。
start_kernel在其最后一個(gè)函數(shù)rest_init的調(diào)用中,會(huì)通過kernel_thread來生成一個(gè)內(nèi)核進(jìn)程十性,后者則會(huì)在新進(jìn)程環(huán)境下調(diào) 用kernel_init函數(shù)叛溢,kernel_init一個(gè)讓人感興趣的地方在于它會(huì)調(diào)用run_init_process來執(zhí)行根文件系統(tǒng)下的 /sbin/init等程序:
1
2
3
4
5
6
7
8
9
10staticnoinlineintinit_post(void)
{
...
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
run_init_process的核心調(diào)用就是kernel_execve,后者的實(shí)現(xiàn)代碼是:
1
2
3
4
5
6
7
8
9
10intkernel_execve(constchar*filename,
constchar*constargv[],
constchar*constenvp[])
{
long__res;
asm volatile("int $0x80"
:"=a"(__res)
:"0"(__NR_execve),"b"(filename),"c"(argv),"d"(envp):"memory");
return__res;
}
里面是段內(nèi)嵌的匯編代碼劲适,代碼相對比較簡單楷掉,核心代碼是int $0x80,執(zhí)行系統(tǒng)調(diào)用霞势,系統(tǒng)調(diào)用號(hào)__NR_execve放在AX里烹植,當(dāng)然系統(tǒng)調(diào)用的返回值也是在AX中,要執(zhí)行的用戶空間應(yīng)用程序路徑名稱保存在 BX中支示。int $0x80的執(zhí)行導(dǎo)致代碼向__KERNEL_CS:system_call轉(zhuǎn)移(具體過程可參考x86處理器中的特權(quán)級檢查及Linux系統(tǒng)調(diào)用的實(shí)現(xiàn)一帖). 此處用bx,cx以及dx來保存filename, argv以及envp參數(shù)是有講究的刊橘,它對應(yīng)著struct pt_regs中寄存器在棧中的布局鄙才,因?yàn)榻酉聛砭蜁?huì)涉及從匯編到調(diào)用C函數(shù)過程颂鸿,所以匯編程序在調(diào)用C之前,應(yīng)該把要傳遞給C的參數(shù)在棧中準(zhǔn)備好攒庵。
system_call是一段純匯編代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ENTRY(system_call)
RING0_INT_FRAME# can't unwind into user space anyway
pushl_cfi%eax# save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl$_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl$(nr_syscalls),%eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl%eax,PT_EAX(%esp)# store the return value
syscall_exit:
...
restore_nocheck:
RESTORE_REGS4# skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN#iret instruction for x86_32
system_call首先會(huì)為后續(xù)的C函數(shù)的調(diào)用在當(dāng)前堆棧中建立參數(shù)傳遞的環(huán)境(x86_64的實(shí)現(xiàn)要相對復(fù)雜一點(diǎn)嘴纺,它會(huì)將系統(tǒng)調(diào)用切換到內(nèi)核棧 movq PER_CPU_VAR(kernel_stack),%rsp),尤其是接下來對C函數(shù)sys_execve調(diào)用中的struct pt_regs *regs參數(shù)浓冒,我在上面代碼中同時(shí)列出了系統(tǒng)調(diào)用之后的后續(xù)操作syscall_exit栽渴,從代碼中可以看到系統(tǒng)調(diào)用int $0x80最終通過iret指令返回,而后者會(huì)從當(dāng)前棧中彈出cs與ip稳懒,然后跳轉(zhuǎn)到cs:ip處執(zhí)行代碼闲擦。正常情況下,x86架構(gòu)上的int n指 令會(huì)將其下條指令的cs:ip壓入堆棧,所以當(dāng)通過iret指令返回時(shí)墅冷,原來的代碼將從int n的下條指令繼續(xù)執(zhí)行纯路,不過如果我們能在后續(xù)的C代碼中改變r(jià)egs->cs與regs->ip(也就是int n執(zhí)行時(shí)壓入棧中的cs與ip),那么就可以控制下一步代碼執(zhí)行的走向寞忿,而 sys_execve函數(shù)的調(diào)用鏈正好利用了這一點(diǎn)驰唬,接下來我們很快就會(huì)看到。SAVE_ALL宏的最后為將ds, es, fs都設(shè)置為__USER_DS腔彰,但是此時(shí)cs還是__KERNEL_CS.
核心的調(diào)用發(fā)生在call *sys_call_table(,%eax,4)這條指令上叫编,sys_call_table是個(gè)系統(tǒng)調(diào)用表,本質(zhì)上就是一個(gè)函數(shù)指針數(shù)組霹抛,我們這里的系 統(tǒng)調(diào)用號(hào)是__NR_execve=11, 所以在sys_call_table中對應(yīng)的函數(shù)為:
1
2
3
4
5
6
7
8
9
10
11
12ENTRY(sys_call_table)
.longsys_restart_syscall/* 0 - old "setup()" system call, used for restarting */
.longsys_exit
.longptregs_fork
.longsys_read
.longsys_write
.longsys_open/* 5 */
.longsys_close
...
.longsys_unlink/* 10 */
.longptregs_execve//__NR_execve
...
ptregs_execve其實(shí)就是sys_execve函數(shù):
1
#define ptregs_execve sys_execve
而sys_execve函數(shù)的代碼實(shí)現(xiàn)則是:
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/*
* sys_execve() executes a new program.
*/
longsys_execve(constchar__user *name,
constchar__user *const__user *argv,
constchar__user *const__user *envp,structpt_regs *regs)
{
longerror;
char*filename;
filename=getname(name);
error=PTR_ERR(filename);
if(IS_ERR(filename))
returnerror;
error=do_execve(filename,argv,envp,regs);
#ifdef CONFIG_X86_32
if(error==0){
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
#endif
putname(filename);
returnerror;
}
注意這里的參數(shù)傳遞機(jī)制搓逾!其中的核心調(diào)用是do_execve,后者調(diào)用do_execve_common來干執(zhí)行一個(gè)新程序的活,在我們這個(gè)例子中要執(zhí) 行的新程序來自/sbin/init上炎,如果用file命令看一下會(huì)發(fā)現(xiàn)它其實(shí)是個(gè)ELF格式的動(dòng)態(tài)鏈接庫恃逻,而不是那種普通的可執(zhí)行文件,所以 do_execve_common會(huì)負(fù)責(zé)打開藕施、解析這個(gè)文件并找到其可執(zhí)行入口點(diǎn)寇损,這個(gè)過程相當(dāng)繁瑣,我們不妨直接看那些跟我們問題密切相關(guān)的代 碼裳食,do_execve_common會(huì)調(diào)用search_binary_handler去查找所謂的binary formats handler矛市,ELF顯然是最常見的一種格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14intsearch_binary_handler(structlinux_binprm *bprm,structpt_regs *regs)
{
...
for(try=0;try<2;try++){
read_lock(&binfmt_lock);
list_for_each_entry(fmt,&formats,lh){
int(*fn)(structlinux_binprm *,structpt_regs *)=fmt->load_binary;
...
retval=fn(bprm,regs);
...
}
...
}
}
代碼中針對ELF格式的 fmt->load_binary即為load_elf_binary, 所以fn=load_elf_binary, 后續(xù)對fn的調(diào)用即是調(diào)用load_elf_binary,這是個(gè)非常長的函數(shù)诲祸,直到其最后浊吏,我們才找到所需要的答案:
1
2
3
4
5
6staticintload_elf_binary(structlinux_binprm *bprm,structpt_regs *regs)
{
...
start_thread(regs,elf_entry,bprm->p);
...
}
上述代碼中的elf_entry即為/sbin/init中的執(zhí)行入口點(diǎn), bprm->p為應(yīng)用程序新棧(應(yīng)該已經(jīng)在用戶空間了),start_thread的實(shí)現(xiàn)為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void
start_thread(structpt_regs *regs,unsignedlongnew_ip,unsignedlongnew_sp)
{
set_user_gs(regs,0);
regs->fs=0;
regs->ds=__USER_DS;
regs->es=__USER_DS;
regs->ss=__USER_DS;
regs->cs=__USER_CS;
regs->ip=new_ip;
regs->sp=new_sp;
/*
* Free the old FP and other extended state
*/
free_thread_xstate(current);
}
在這里,我們看到了__USER_CS的身影腋妙,在x86 64位系統(tǒng)架構(gòu)下说莫,該值為0x33. start_thread函數(shù)最關(guān)鍵的地方在于修改了regs->cs= __USER_CS, regs->ip= new_ip,其實(shí)就是人為地改變了系統(tǒng)調(diào)用int $0x80指令壓入堆棧的下條指令的地址弧呐,這樣當(dāng)系統(tǒng)調(diào)用結(jié)束通過iret指令返回時(shí),代碼將從這里的__USER_CS:elf_entry處開始執(zhí) 行,也就是/sbin/init中的入口點(diǎn)漆改。start_thread的代碼與kernel_thread非常神似,不過它不需要象 kernel_thread那樣在最后調(diào)用do_fork來產(chǎn)生一個(gè)task_struct實(shí)例出來了准谚,因?yàn)槟壳爸恍枰诋?dāng)前進(jìn)程上下文中執(zhí)行代碼挫剑,而不是創(chuàng)建一個(gè)新進(jìn)程。關(guān)于kernel_thread柱衔,我在本版曾有一篇帖子分析過樊破,當(dāng)時(shí)基于的是ARM架構(gòu)愉棱。
所以我們看到,start_kernel在最后調(diào)用rest_init哲戚,而后者通過對kernel_thread的調(diào)用產(chǎn)生一個(gè)新進(jìn)程(pid=1)羽氮,新進(jìn)程在其kernel_init()–>init_post()調(diào)用鏈中將通過run_init_process來執(zhí)行用戶空間的/sbin /init,run_init_process的核心是個(gè)系統(tǒng)調(diào)用惫恼,當(dāng)系統(tǒng)調(diào)用返回時(shí)代碼將從/sbin/init的入口點(diǎn)處開始執(zhí)行档押,所以雖然我們知道 post_init中有如下幾個(gè)run_init_process的調(diào)用:
1
2
3
4run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
但是只要比如/sbin/init被成功調(diào)用,run_init_process中的kernel_execve函數(shù)將無法返回祈纯,因?yàn)樗鼒?zhí)行int $0x80時(shí)壓入堆棧中回家的路徑被后續(xù)的C函數(shù)調(diào)用鏈給改寫了令宿,這樣4個(gè)run_init_process只會(huì)有一個(gè)有機(jī)會(huì)被成功執(zhí)行,如果這4個(gè)函數(shù)都失敗 了腕窥,那么內(nèi)核將會(huì)panic. 所以內(nèi)核設(shè)計(jì)時(shí)必須確保用來改寫int $0x80壓入棧中的cs和ip的start_thread函數(shù)之后不會(huì)再有其他額外的代碼導(dǎo)致整個(gè)調(diào)用鏈的失敗粒没,否則代碼將執(zhí)行非預(yù)期的指令,內(nèi)核進(jìn)入不穩(wěn)定狀態(tài)簇爆。
最后癞松,我們來驗(yàn)證一下,所謂眼見為實(shí)入蛆,耳聽為虛响蓉。再者,如果驗(yàn)證達(dá)到預(yù)期哨毁,也是很鼓舞人好奇心的極佳方法枫甲。驗(yàn)證的方法我打算采用“Linux設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)” 中的路線,通過call_usermodehelper來做扼褪,因?yàn)樗蚹ernel_execve本質(zhì)上都是一樣的想幻。我們自己寫個(gè)應(yīng)用程序,在這個(gè)應(yīng)用程序里讀取cs寄存器的值话浇,程序很簡單:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include
#include
#include
#include
intmain()
{
unsignedshortucs;
asm(
"movw %%cs, %0\n"
:"=r"(ucs)
::"memory");
syslog(LOG_INFO,"ucs = 0x%x\n",ucs);
return0;
}
然后把這個(gè)程序打到/sys/kernel/uevent_help上面(參照Linux設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)一文)脏毯,之后我們往電腦里插個(gè)U盤,然后到/var/log/syslog文件里看輸出(在某些distribution上幔崖,syslog的輸出可能會(huì)到/var/log/messages中):
Mar 10 14:20:23 build-server main:ucs = 0x33
0x33正好就是x86 64位系統(tǒng)(我實(shí)驗(yàn)用的環(huán)境)下的__USER_CS.
所以第一個(gè)內(nèi)核進(jìn)程(pid=1)通過執(zhí)行用戶空間程序食店,期間通過cs的轉(zhuǎn)變(從__KERNEL_CS到__USER_CS)來達(dá)到特權(quán)級的更替。
官方微博:http://weibo.com/codingke
官方QQ群:148715490
官方QQ:2337862882