1. 程式人生 > >Mac OS X 核心Rootkit開發指南

Mac OS X 核心Rootkit開發指南

1.引言 

1.1 背景介紹 

        困 擾著不同作業系統的Rootkit已經由來已久,Linux,Windiws,還有各種類BSD等系統都受到了Rootkit的極大危害。目前廣泛使用的 一類“核心Rootkit”,是原來“檔案轉移Rootkit”的衍生和發展。這種發展趨勢的必然性,來源於Rootkit和Osiris、 Tripwire等安全軟體之間的競爭——後者的出現使得Rootkit開發者不得不在核心空間中尋找更加隱祕的途徑,以達到滲透和顛覆系統的目的。 
        Rootkit 是以後門(backdoor)或者嗅探程式(sniffer)等形式存在的惡意程式碼,其基本行為表現為篡改標準工具和命令的行為與輸出。就像電腦保安領 域的其他分支一樣,Rootkit與Anti-Rootkit之間總存在著“你高一尺,我高一丈”的對立競爭關係,而且隨著技術的發展,這場競爭已經愈演 愈烈。 

        在本文中,以向讀者引導和介紹在一個特定系統上實現Rootkit的具體方法為目的,我們將在Apple的Mac OS X作業系統上實現一個執行時核心補丁,完成一個核心級的Rootkit。Apple的Mac OS X系統支援兩種不同的CPU架構,即Intel和PowerPC體系。我們實現的Rootkit是體系結構無關的,大部分程式碼在兩種架構下都可以相容運 行。 

1.2 Rootkit基礎 

        Rootkit的目的之一是隱藏自身,因此核心級的Rootkit一般都具有隱藏檔案、程序和網路套接字通訊的能力,而高階一些的甚至具有後門和和鍵盤嗅探的功能。 
        當 一個程式,如“/bin/ls”需要列出一個目錄下的所有檔案時,它會呼叫核心中的系統呼叫函式。隨著getdirentries()的函式執行,控制流 程從使用者空間轉移到核心空間,由核心完成使用者的請求操作。最終getdirentries()再將特定目錄下的檔案列表資訊返回到使用者空間,呈現在使用者面 前。 

        為了達到從getdirentires()返回的資訊中隱藏特定檔案的目的,我們需要修改系統呼叫返回的檔案資訊,並在 其到達使用者空間之前將特定的條目刪除。要實現這個功能有多種選擇,一是修改檔案系統的處理層,例如虛擬檔案系統(VFS)等;二是直接修改目標所指的 getdirentires()函式。相對前者,修改系統呼叫要簡單一些,這也是我們所傾向採用的方法。 

1.3 系統呼叫基礎 

        當 使用者空間的程式需要呼叫核心空間的函式時,它就要喚起(invoke)一個系統呼叫。系統呼叫可以看作是提供了特定核心服務的API函式,如檔案讀寫,打 開關閉網路連線等等。每一個系統呼叫都有唯一的編號,稱為系統呼叫號,在喚起時就是通過編號來判斷呼叫的具體函式。 

        當一個用 戶空間的程序需要呼叫核心函式時,總是先呼叫一個在libc庫中的包裝函式,由它產生軟體中斷,將控制流從使用者空間轉移到核心。核心在一個稱作“系統呼叫 表”的地方,儲存了一份可用的系統呼叫函式列表,每一個入口項都有一個函式指標,指向編號所對應的系統呼叫的函式位置。屆時,核心將在系統呼叫表中查詢編 號所指的函式入口,並交由後者來處理使用者空間的請求。完整的系統呼叫列表可以在/usr/include/sys/syscall.h檔案中找到。如果 Rootkit想要隱藏什麼檔案,只需關注下面幾個系統呼叫函式就可: 

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時,為了向端使用者提供統一的輸出口徑,這其中的每一個函式都要被替換掉。 
        為了實現修改函式輸出的功能,設計一個能夠替換原始函式的包裝函式是十分必要的。包裝函式首先呼叫原始的函式,搜尋其輸出,做必要的修改和驗證之後再返回到使用者空間。 

1.4 使用者空間與核心空間 

        就 像使用者空間的程序有其私有獨立的記憶體地址一樣,核心也是在相對獨立的地址空間中執行的。這同時意味著在核心中想要自由、不受約束的讀寫記憶體地址是不可能實 現的了。當核心空間的程式想要修改使用者空間的地址,如拷貝資料到使用者空間時,就需要遵守特定的協議和處理例程。好在為了完成特定的任務,有相當數量的輔助 函式可以參考和借鑑,在這裡我們就可以使用copyin和copyout這兩個函式。 



2 XNU核心介紹 

        Mac OS X作業系統的核心叫做XNU,其核心是基於Mach微核心與FreeBSD 5而實現的。系統在Mach層,負責核心執行緒、程序、多工排程、訊息傳遞、虛擬記憶體管理以及控制檯IO等多項任務;接著Mach之上的是BSD層,提供 了與POSIX標準相相容的API,網路功能,以及檔案系統等等。XNU核心採用了一個稱之為“I/O Kit”的面向物件框架來實現裝置驅動程式的載入和解除安裝,它既可以將不同的技術糅合在一起為同樣的目的服務,也為修改作業系統提供了一條的簡便途徑。除此 之外,XNU核心還有一個有趣的事情,核心和使用者空間都使用各種獨立的4GB的地址空間,和我們在其他作業系統中見到的好像不太一樣。^_^ 

2.1 OS X核心Rootkit的歷史 

        目 前已知的在Mac OS X系統上釋出的最早核心Rootkit是WeaponX,由nemo開發,出現於2004年11月份。它採用了和大多數Rootkit一樣的核心擴充套件(可 載入核心模組,LKM)技術,提供了核心Rootkit的各項基本功能。然而WeaponX的相容性不是很好,隨著後來Mac OS X核心的調整,在新系統上也就不能再正常工作了。 
        在最近釋出的幾個Mac OS X的版本中,Apple作了很多工作,加強了核心防護,讓系統滲透變得困難了許多。更令人感到沮喪的是,系統呼叫表等重要的核心結構,都不再向開發者公開其具體細節,此時開發Rootkit的工作更是顯得難上加難。 

2.2 尋找系統呼叫表 

        版 本號為10.4的OS X系統已經沒有匯出的核心符號表的存在,這意味著編譯器將無法自動確定系統呼叫表在記憶體中的存放位置了。此時,可以迂迴的,採用在記憶體空間中強力搜尋,或 者尋找其他參照物的方法來解決這個問題。Landon Fuller發現的一條簡便途徑,系統的輸出符號之一nsysent(系統呼叫表中的入口數目)就位於和系統呼叫表臨近的某個位置,用特殊的程式就將其找 到並返回一個指向系統呼叫表的指標,具體細節可以參http://landonf.bikemonkey.org/code/macosx/Leopard_PT_DENY_ATTACH.20080122.html 。最終,我們得到了系統呼叫表入口項的資料結構如下: 

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”指標了,它指出了系統呼叫處理函式的實際位置,同時也提示了我們待會兒準備HOOK的目標。說到HOOK 的過程,原理上其實相當簡單,只需將“sy_call”指標指向記憶體中我們自己提供的處理函式就可以了。 

2.3 未公開的核心結構 

        在 10.4版的OS X中,Apple修改了核心結構,以求更好的核心API穩定性。因此即使是在核心調整之後,核心擴充套件設施仍然能夠正常工作。然而正是由於Apple所做的 這些修改,才隱藏了API內部的大量實現細節和關鍵資料結構,只將某些部分有選擇的公開給開發者。 
        這裡有一個未公開結構的例子,標識程序的資料結構“proc”。該結構從使用者和核心空間都可以訪問,使用者空間的結構定義在/usr/include/sys/proc.h檔案,如文中的程式碼所示(恕不列出)。 
        內 核中的“proc”結構定義可以從XNU的原始碼包中獲得,位於xnu-xxx/bsd/sys/proc_internal.h檔案,其內部的資料域要 比使用者空間中的豐富很多。如果我們回到10.3版本去看一下同樣使用者空間的proc結構,如下面的程式碼,也會發現原來的具有更多的資料成員,如文中的程式碼 所示(恕不列出)。 
        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開發者來說,已經成了一個不可迴避的問題,一方面核心結構是未輸出和未公開文件化的,另一方面系統自身版本的演 進也加快了結構調整的步伐。不過仍然讓我們感到幸運的是,核心程式碼目前都是開源的,從Apple處可以自由下載。從某種意義上說,這為我們從原始碼中提取 需要的資料結構打開了方便之門。 

2.4 I/O Kit框架 

        Mac OS X為建立裝置驅動程式提供了一個完整的實現框架,包括多種庫、工具和資源等各項元件,就是我們前面提到的“I/O Kit”。I/O Kit在Mac OS X中為上層提供了一個硬體裝置的抽象檢視,簡化了設計過程,也節省了開發時間。整個框架是以面向物件的原則,採用了一種裁剪過的C++語言來實現的,保證 了框架結構的清晰,也提高了程式碼的重用效率。 
        I/O Kit在核心空間中執行,並且和實際的硬體相互動,所以用來編寫鍵盤記錄程式keylogger是再合適不過的了。drspringfield寫的 “logKext”就是這方面一個比較典型的例子,它利用I/O Kit框架來記錄使用者的擊鍵事件。I/O Kit還有其他很多方面的用途,在實現Mac OS X的核心Rootkit時藉助它的幫忙可以省去很多不必要的麻煩。 



3 Mac OS X下的核心開發 

        Mac OS X下的核心開發可有多條途徑,最簡便的就是將“改進”的功能作為核心驅動載入上去。驅動程式可以BSD子層核心擴充套件,或者面向物件的I/O Kit驅動的方式新增。而這裡最簡單的核心擴充套件程式開發方式就要數專門為“Generic Kernel Extension”而設計的XCode-templates了。開啟Xcode程式,在“File”選單中選擇“New Project”新建一個專案,在“Kernel Extension”下從可用的模板列表中選擇“Generic Kernel Extension”,取一個合適的名字,如“rootkit 0.1”,最後單擊“Finish”,就成功的建立了一個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許可權,之後這些函式都是在核心空間中執行,對整個作業系統有著 完全的控制權。因此這就要求在編寫這些函式時要慎之又慎,一不小心就有可能導致系統的崩潰。這裡借用Apple《Kernel Program Guide》中的一句話,“核心程式設計是一項黑色藝術,應該避免所有的可能,確保萬無一失!”,以此來說明核心程式設計工作的危險性是再合適不過的了。 
        一般來說,在start()函式中對核心做出的任何修改都應該在stop()函式中恢復回來。函式,變數,還有其他形式的本地物件等等都應該在模組解除安裝時析構,否則後續對其的引用可能引發系統的錯誤行為,嚴重時將導致系統崩潰。 
        構 建自己的專案只需要點選“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註冊後,也可以下載獲得最新版本的開發工具集合。而XNU核心的原始碼包可以http://opensource.apple.com/darwinsource/ 處下載,在開發時最好保留一份以便快速參考。 
        使 用核心擴充套件API的最大好處就是,kextload命令接管了連線和載入時的所有操作。這意味著整個Rootkit可以用C語言編寫,不用關心之外的繁瑣 操作。C語言編寫的程式效率較高,可移植性也不錯,在Mac OS X支援的兩種CPU架構上都可適用。 

3.1 核心版本依賴性 

如 前所述,Landon Fuller已經注意到在10.4版OS X上找到nsysent變數就可以取得系統呼叫表的地址。然而隨著核心發行版本的不同,參考目標之間的相對位置也在發生著或多或少的變化。因此,核心發行 版本間的差異使得核心依賴性的配置操作在核心擴充套件程式的設計過程中也顯得尤為重要。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版本的核心聯絡在一起,程式每一次主版本號和次版本號的更新,都有必要將程式碼重新編譯一遍。核心的依賴配置操作,是保證核心擴充套件執行時安全的必要手段之一,系統由此將拒絕載入非匹配版本的核心擴充套件程式。 



4第一個OS X核心Rootkit 

4.1 替換系統呼叫 

        要 想快速的在核心中開闢出一片屬於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”和監控所有執行程序的操作都要用到系統呼叫sysctl。我們知道,sysctl是一個多動能的、和核心多種功能互動的通用目的 API,既可以用來列舉執行程序,也可以執行開啟網路連線等各項操作。現在準備擷取和修改系統的程序列表,那滲透sysctl系統呼叫當然就是我們不二的 選擇了。 
        擷取sysctl系統呼叫的方法和前面getuid()的一樣,但是需要特別注意的是這裡呼叫的引數情況。 Apple為了支援大端序和小端序兩種記憶體資料的組織形式,使用了資料填充的巨集PADL和PADR。它們也帶了一些副作用,使得程式的引數結構看上去非常 怪異,不易理解。在使用這些引數結構時建議直接從XNU的原始碼包中拷貝相關結構體的定義部分到目標檔案,免得資料填充時引起莫名的混淆和錯誤。 
sysctl 通過一個char型別陣列“name”傳遞功能命令,該命令是按照層次組織的,並且經常包括一些子命令,子命令也會附帶自己的引數等等。sysctl及其 子命令的詳細說明可以參考“/usr/include/sys/sysctl.h”檔案,這其中有CTL_KERN->KERN_PROC的命令請 求,將系統的執行程序列表拷貝到使用者提供的緩衝區中。從Rootkit的角度來看,這引入了一個問題——我們意圖在資料返回到使用者之前修改其輸出,但它卻 直接將資料返回到了使用者提供的緩衝區裡。不過幸運的是,我們此時仍然有辦法在返回到使用者應用程式之前成功的篡改資料,只要將資料從使用者空間先拷貝到核心空 間的緩衝區,修改完成後再複製回去即可。 
        首先為了拷貝資料,需要用MALLOC巨集分配必要的記憶體空間;接著用copyin函 數將使用者空間的資料拷貝到核心中來;然後是對資料的篩選和驗證過程,留下不重要的,刪去那些敏感的資訊,將緩衝區中的內容覆蓋掉就可以去除某個程序的相關 條目。覆蓋操作可以用bcopy函式完成,一旦有資料被刪除,還應該調整緩衝區的長度資訊,長度縮短以後,將資料拷貝回到使用者空間。 

/* 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函式拷貝回到使用者空間的緩衝區。在本例中,用到了兩個相關的拷貝函式,suulong拷貝少量的資料到使用者空間,copyout則將整個資料緩衝區都拷貝回去。 

/* Copy back the length to userspace */ 
suulong(uap->oldlenp,oldlen); 

/* Copy the data back to userspace */ 
copyout(mem,uap->old, oldlen); 

        資料被修改之後,在緩衝區的尾部可能會殘留著原來最後一個程序條目的相關資訊,作為檢測記憶體篡改的依據。為了確保篡改不被發現,有必要將緩衝區的空餘部分都設定為0。 

4.3 隱藏檔案 

        如 前所述,和隱藏檔案有關的三個系統呼叫分別是SYS_getdirentries,SYS_getdirentriesattr和 SYS_getdirentries64。它們都使用共享sysctl的方式填充所提供的資料緩衝區,並接收返回的資料長度計數值。由於其中各結構變數的 尺寸不同,資料轉換時需要進行準確的指標運算。然而有過C語言程式設計經驗的人都知道,指標算術是最容易犯錯誤的領域之一,在系統核心的範圍之內,稍不注意更 是有可能造成嚴重後果。而且要做到隱藏檔案的一致性,getdirent族的三個系統呼叫都有修改的必要。 
        隱藏檔案的過程和隱藏程序非常類似,先呼叫原始的函式,將返回資料從使用者空間拷貝到核心空間,修改之後再拷貝回去就可以了,具體細節可以參考文章附件裡的程式碼。 

4.4 隱藏核心擴充套件程式 

        平時用kextstat命令就可以列舉出系統內的所有核心模組,如果Rootkit的模組也被顯示出來,那Rootkit將毫無任何隱蔽性可言。Nemo在WeaponX的實現時,想出了一個簡單的方法來克服這個問題。 

extern kmod_info_t *kmod; 

void activate_cloaking() 

kmod_info_t *k; 
k = kmod; 
kmod = k->next; 


        上 面的程式碼搜尋可載入核心模組的連結串列,簡單的將Rootkit的模組從中刪除。kextstat命令在執行時會遍歷該連結串列,輸出模組資訊。現在 Rootkit的模組沒有了,自然也就銷聲匿跡了。不過在kextunload的時候,由於找不到模組,執行也會以失敗而告終,這也算是獲得隱蔽性所換來 的代價吧。 

4.5 在核心空間執行使用者空間的程式 

        Mac OS X中有一種特殊的API,叫做KUNC(Kernel-User Notification Center),用來從核心向用戶顯示一條通知資訊,或者在使用者空間執行程式或者命令。 
        KUNC API中有在使用者空間執行程式的命令KUNCExecute(),用於從核心在使用者空間執行程式的目的。該函式的定義在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” 是要執行的程式路徑。“openAsUser”標誌指出執行程式所屬的使用者,既可以是“kOpenAppAsConsoleUser”,屬於當前登入用 戶,也可以是“kOpenAppAsRoot”,作為root使用者執行。最後的“pathExecutionType”也是一個程式標誌,指出執行程式的 型別,有以下幾種取值: 

kOpenApplicationPath                按照絕對路徑定位可執行程式 
kOpenPreferencePanel                優先定位/System/Library/PreferencePanes目錄下的可執行程式 
kOpenApplication                        優先定位/Applications目錄下的可執行程式 

此時如果要執行的是“/var/tmp/mybackdoor”,只需編寫下面的函式呼叫即可: 

KUNCExecute("/var/tmp/mybackdoor", kOpenAppAsRoot, kOpenApplicationPath); 

        KUNCExecute 函式在某些觸發器程式上有著廣泛的應用,例如勾掛TCP處理函式之後,在使用者空間向源IP地址回發一個標誌報文,觸發源IP端的某種響應功能就可以用到 它。有時,我們也可以勾掛SYS_open函式,根據特殊的標誌執行KUNCExecute呼叫,擴大後門程式的本地許可權。由此觀之,利用 KUNCExecute為我們的Rootkit所帶來的可能性是無窮無盡的。 

4.6 從使用者空間控制Rootkit 

        一旦合適的系統呼叫和核心函式都被替換過之後,新的函式就可以隱藏檔案、程序,甚至開啟系統的網路連線通訊了。通常,要觸發Rookit開始工作就是勾掛特定的系統呼叫函式和匹配特定的訊號。該過程在實現上比較簡單,不需要額外的工具支援就可以做到。 
        當 隱藏程序時,可以勾掛fork()和exec()等函式族,根據引數傳入的特定標誌隱藏單個程序或者整個程序樹。而隱藏檔案和套接字通訊時,則更具技巧一 些。因為此時沒有類似前者使用的標誌那樣的東西,可以通知Rootkit需要在何時何地隱藏什麼,所以我們轉而想辦法要建立一些新的系統調用出來。建立新 的系統呼叫並不是件困難的事情,勾掛原始的,再加上一個特殊的引數,能夠觸發通訊的隱蔽通道就可以了。不過,這需要使用者空間特殊工具的支援,藉助它