這幾天一個叫做"Dirty COW"的linux內核競爭條件漏洞蠻火的,相關公司不但給這個漏洞起了個洋氣的名字,還給它設計了logo(見下圖),首頁,Twitter賬號以及網店。恰逢周末,閑來無事的我也強勢圍觀了一波,在這之前,好好的學習了一下競爭條件以及看了一下近幾年關於競爭條件漏洞。
1:什么是競爭條件以及競爭條件為什么會產生漏洞
競爭條件是系統中的一種反常現象,由於現代Linux系統中大量使用並發編程,對資源進行共享,如果產生錯誤的訪問模式,便可能產生內存泄露,系統崩潰,數據破壞,甚至安全問題。競爭條件漏洞就是多個進程訪問同一資源時產生的時間或者序列的沖突,並利用這個沖突來對系統進行攻擊。一個看起來無害的程序如果被惡意攻擊者利用,將發生競爭條件漏洞。
用簡單代碼講的更清楚:
//myThreadTest
#include <stdio.h> #include <pthread.h> #include <unistd.h> int i = 1; void *mythread1() { if(i == 1){ <a> sleep(3); if(i == 2) <b> printf("hack it!\n"); else printf("you can try again!\n"); } } void *mythread2() { sleep(1); i=2; } int main(int argc, const char *argv[]) { pthread_t id1,id2; pthread_create(&id1, NULL, (void *)mythread1,NULL); pthread_create(&id2, NULL, (void *)mythread2,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); return 0;
}
單獨看mythread1()函數,顯然肯定是輸出 "you can try again!\n",但是,由於i是全局共享的資源可以通過線程的方式來使i值在<a>的之后<b>之前進行改變,可以改變函數的流程
為了使結果清晰明了,所以手動利用sleep()函數進行設置了,實際利用過程中,可能是一些會耗時較長的函數,如memcpy()
流程如下:
這也是競爭條件的核心思想,這也是一個數據競爭引發條件競爭的典型例子。
2:以cve-2014-0496為例
由於Android的崛起起,Linux內核的安全性受到了極大地考驗,很多潛藏的安全也慢慢被安全研究眼發現。這個漏洞的主要成因是因為在pty/tty設備驅動中在訪問某些資源的時候沒有正確的加鎖處理,准確來說,加鎖的范圍較小。首先,先看一下理解這個漏洞必要的兩個數據結構
struct tty_buffer { struct tty_buffer *next; char *char_buf_ptr; unsigned char *flag_buf_ptr; int used; int size; int commit; int read; /* Data points here */ unsigned long data[0]; }; struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; struct tty_bufhead buf; };
tty_buffer是一個動態大小的數據結構,其中,char_buf_ptr指向該對象后面的第一字節,即(char_buf_ptr = tty_buffer+sizeof(tty_buffer)大小為size... flag_buf_ptr=tty_buffer+sizeof(tty_buffer)+size。大小為size。used為已經使用的buffer大小。tty_struct為tty的數據結構,其中ops為tty設備的一些操作函數,如open(),write()。
該漏洞的函數調用為write(pty_fd) in userspace -> sys_write() in kernelspace -> tty_write() -> pty_write() -> tty_insert_flip_string_fixed_flag()。看 tty_insert_flip_string_fixed_flag()函數:
int tty_insert_flip_string_fixed_flag(struct tty_struct *tty, const unsigned char *chars, char flag, size_t size) { int copied = 0; do { int goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE); int space = tty_buffer_request_room(tty, goal); struct tty_buffer *tb = tty->buf.tail; if (unlikely(space == 0)) break; memcpy(tb->char_buf_ptr + tb->used, chars, space); memset(tb->flag_buf_ptr + tb->used, flag, space); tb->used += space; copied += space; chars += space; } while (unlikely(size > copied)); return copied; }
這個函數的大致流程為通過tty_buffer_request_room來申請函數空間,如果沒有,則另外申請。利用memcpy進行復制內存。最后再遞增used。其中memcpy調用時間較長,由於沒有加鎖,存在競爭條件的問題,查看tty_buffer_request_room,看是否能夠利用:
int tty_buffer_request_room(struct tty_struct *tty, size_t size) { struct tty_buffer *b, *n; int left; unsigned long flags; spin_lock_irqsave(&tty->buf.lock, flags); if ((b = tty->buf.tail) != NULL) left = b->size - b->used; else left = 0; if (left < size) { <1> /* This is the slow path - looking for new buffers to use */ if ((n = tty_buffer_find(tty, size)) != NULL) { if (b != NULL) { b->next = n; b->commit = b->used; } else tty->buf.head = n; tty->buf.tail = n; } else size = left; } spin_unlock_irqrestore(&tty->buf.lock, flags); return size; }
這里有一個整數溢出的問題,由於程序員的疏忽size為unsigned,而left為int,根據c語言規則,left為轉化為無符號,在<1>中,如果left為負數,轉化為無符號數,便可以實現size大於left,即需要的空間大於空余的空間,但是仍不進行內存分配,再通過tty_insert_flip_string_fixed_flag函數里的memcpy來溢出,對內存進行讀寫。left = b->size - b->used,其中size不能改變,但是可以改變used的值,通過條件競爭,來對used進行連續多次寫,使used大於size。
詳細流程圖如下:
由於memcpy函數將會持續較長時間,只要對一個tty連續多次利用多線程對該函數進行申請空間,使used > size,這樣就可以在使用memcpy時,進行內存溢出。
這個漏洞,是一個非常典型的競爭條件漏洞。其實也就是上面那個測試代碼也是這個函數的簡化版。
3:結語
在現代操作系統,競爭條件是不可避免的,只要兩個兩個執行線程訪問同一個數據結構,由於我們無法預測linux調度的順序,就會產生混合,就可能產競爭條件。我們如果需要對內核或驅動編寫,應該盡量避免資源的共享,如果不可避免,需要利用鎖來對原子數據進行鎖定。