linux中是不可以依附別的進程的,更不可能建立遠程線程,然而一種不太正規的方式卻可以做到這一點,這就是ptrace介面,ptrace可以依附任何使用者進程,用特殊的參數甚至可以更改任何進程的寄存器和記憶體映射,這個功力和建立遠程線程不相上下,甚至比其更加靈活,如果理解了elf映像在記憶體的布局便可以通過ptrace修改被調試進程的任意記憶體。然而有一個限制就是ptrace介面只能調試屬於自己使用者的進程,也就是說它不能調試別的使用者的進程,一種顯而易見的限制就是普通使用者的ptrace不能調試root進程,但是2.6.29核心有一個明顯的漏洞使得普通使用者可以提升本地許可權。
簡單來講就是在調用exec的時候需要經曆一系列的uid,euid的計算,特別是exec具有suid的程式的時候,這種計算將變得更加繁複,在ptrace的時候需要進行一系列uid,euid的判斷,那麼如果處理不好這二者之間的同步,必然會導致ptrace的誤判,就是說ptrace調試了一個根本不屬於自己的進程,甚至可能是root進程,如果ptrace的調用進程更改了被調用的具有suid的進程的記憶體,使之exec了一個shell,那麼該shell將會是屬於root的,因為exec的調用進程也就是suid進程的euid是0.
早期的核心心漏洞更加明顯,看看2.4.9的核心中的ptrace_attach
int ptrace_attach(struct task_struct *task)
{
task_lock(task);
...
if(((current->uid != task->euid) ||
(current->uid != task->suid) ||
(current->uid != task->uid) ||
(current->gid != task->egid) ||
(current->gid != task->sgid) ||
(!cap_issubset(task->cap_permitted, current->cap_permitted)) ||
(current->gid != task->gid)) && !capable(CAP_SYS_PTRACE))
goto bad;
...
task->ptrace |= PT_PTRACED;
task_unlock(task);
write_lock_irq(&tasklist_lock);
if (task->p_pptr != current) {
REMOVE_LINKS(task);
task->p_pptr = current;
SET_LINKS(task);
}
write_unlock_irq(&tasklist_lock);
send_sig(SIGSTOP, task, 1);
return 0;
bad:
task_unlock(task);
return -EPERM;
}
這個早期的核心顯得很整潔,因為都到了很少量的鎖,鎖的粒度自然很粗,不過還好,看看第一行就鎖住了這個進程,然後進行一系列的判斷,鎖住進程的目的就是阻止別的執行緒在判斷期間更改進程的屬性,這個想法很好,但是再看一個函數:
void compute_creds(struct linux_binprm *bprm)
{
kernel_cap_t new_permitted, working;
int do_unlock = 0;
new_permitted = cap_intersect(bprm->cap_permitted, cap_bset);
working = cap_intersect(bprm->cap_inheritable,
current->cap_inheritable);
new_permitted = cap_combine(new_permitted, working);
if (bprm->e_uid != current->uid || bprm->e_gid != current->gid ||
!cap_issubset(new_permitted, current->cap_permitted)) {
current->mm->dumpable = 0;
lock_kernel(); //僅僅鎖住了kernel而沒有鎖住task,只要沒有執行緒和該執行緒競爭kernel鎖,那麼誰也不會等待
if (must_not_trace_exec(current) //注意這個函數,見下面。該位置設為A
|| atomic_read(¤t->fs->count) > 1
|| atomic_read(¤t->files->count) > 1
|| atomic_read(¤t->sig->count) > 1) {
if(!capable(CAP_SETUID)) { //千萬別通過這個if語句,否則弊大於利,攻擊成功的可能性就小了
bprm->e_uid = current->uid;
bprm->e_gid = current->gid;
}
...
}
do_unlock = 1;
}
...//注意,以下就應該設定新進程的各種ID了。下面的位置設定為B。
current->suid = current->euid = current->fsuid = bprm->e_uid;
current->sgid = current->egid = current->fsgid = bprm->e_gid;
if(do_unlock)
unlock_kernel();
current->keep_capabilities = 0;
}
static inline int must_not_trace_exec(struct task_struct * p)
{
return (p->ptrace & PT_PTRACED) && !cap_raised(p->p_pptr->cap_effective, CAP_SYS_PTRACE);
}
注意,在A和B之間並沒有進行task本身的保護,最起碼沒有和ptrace互斥,ptrace為了安全起見被設計為不能跟蹤自己使用者id之外的進程,比如一個普通使用者進程不能跟蹤suid進程,但是看看2.4.9的代碼,如果在A和B之間,ptrace闖了進來要跟蹤正在exec的進程,此時被跟蹤的正在 exec的進程尚未設定好新的euid和egid之類的id,那麼ptrace進程很容易就得逞了,通過了ptrace_attach中的那麼長的判斷,從而成功實現一個普通使用者進程進程跟蹤一個suid進程,以後可以通過ptrace介面修改被跟蹤的suid進程的記憶體實現注入,如果注入一個root的 shell,那麼一切就成功了!在後來的核心中,在compute_creds也用了task_lock(task)將進程鎖住,那麼exec和 ptrace就不能同時進入到競爭態了,如果exec先進去,那麼ptrace_attach的一系列判斷會將有企圖的進程趕出去,如果ptrace先進去,那麼must_not_trace_exec會將exec新進程的euid設定成current的uid從而降低了它的許可權,仔細看 must_not_trace_exec發現它也有漏洞,如果suid程式本身有漏洞,那麼!cap_raised(p->p_pptr->cap_effective, CAP_SYS_PTRACE)這個條件就有可能被利用,從而繞過bprm->e_uid = current->uid,因此後來的版本將該函數的後一個條件去掉了,只要一個進程被跟蹤了,那麼就儘可能不讓它獲得特權,除非它的調用者本身就是特權程式,皇帝自殺誰也擋不住啊。
雖然後來的版本在兩個地方加上了鎖,但是一下子就鎖一個進程未免粒度過大,於是2.6的後期核心將鎖的粒度減小,引入了ptrace和exec進行互斥的專用鎖,這樣核心的效率會更高,和這二者無關的操作不會因為鎖而被阻攔。