寶貝王然最近十分想搞嫁禍於人的惡作劇!
一台linux主機上建立了一個使用者wangran,一共有兩個人王然和王其知道使用者wangran的密碼pw,其中王其還知道root密碼而王然不知道,為了安全起見,遠端使用者都不得用root直接登入主機,也就是王然和王其只能通過wangran這個使用者來登入,王然自認為是linux高手,她當然知道他們遠程登入的終端是/dev/pts/n,n從0開始遞增,而且她也知道/dev/pts/n的許可權就是通過終端登入的使用者也就是wangran的許可權,任何su的使用者都不會改變終端檔案的許可權,由於她不知道root密碼,然而她又想做一些XX,於是她想出了一個嫁禍於人的辦法,王然開始工作,她的最終目的是在王其登入的終端上執行root命令,設想王其登入的終端是/dev/pts/y,而王然的登入終端是/dev/pts/x,王然想一定能在/dev/pts/y上執行命令,因為她有權操作/dev/pts/y,畢竟他倆都是通過一個username登入的。
起初,王然把事情想簡單了,她首先來了一個用過linux的人都會的方式:
wangran@DROP:~$echo ls > /dev/pts/y #當然不能用halt命令來測試了,所以用ls。
為了得知結果,王然讓王其看螢幕有何變化,王其很鬱悶,不解為何突然螢幕上被寫了一個ls,而王然只說這隻是一個惡作劇,以顯示自己水平的高超,她是斷然不會把最終目的告訴王其的,於是接下來的工作是王其在幫著王然完成的。唯寫了一個ls,沒有什麼用,是不是沒有寫斷行符號符號呢,於是精通vt100終端的王然又來了一個:
wangran@DROP:~$echo -e 'pwd^M' > /dev/pts/y #行倒是換了,可是還是沒有執行ls
於是王然拿出了strace,讓王其在螢幕執行:strace bash,王其被出來的一大堆看不懂的資訊給搞懵了,於是離開了座位,把兩個終端全部讓給了王然,此時王然當然可以敲入passwd命令修改root密碼,可是作為一名駭客,這樣做不雅。
在王其的終端上:strace bash正在運行中:
...
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "l", 1) = 1
write(2, "l", 1l) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "s", 1) = 1
write(2, "s", 1s) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "/r", 1) = 1
write(2, "/n", 1
) = 1
rt_sigprocmask(SIG_BLOCK, [INT], [], 8) = 0
...
stat64(".", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
...
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x4005e968) = 8396
...
waitpid(-1, ...(ls命令的結果)...
...
只要你敲入一個字元,strace就會先從標準輸入read出來,在從標準輸出write回去,如果在別的終端上通過echo的方式往另一個終端寫東西,無論如何都是不能被該目的終端解釋為鍵盤輸入的,因為echo只是將字元寫入了輸出緩衝區,最終將送往螢幕,所以你只能看到你寫入的資訊,而你無法將字元從另一個終端送入該終端的輸入緩衝區,在tty終端以及真實的終端只有鍵盤的敲擊才能做到往輸入緩衝區送資訊,而在諸如/dev/pts/n或者串口之類的終端,你必須遵循其“線路規程”才可以,比如telnet終端,你在遠程敲入了字母l,該字母l將會按照傳輸線路以及應用協議的規則被編碼,然後主機在接收到該編碼後會同樣按照線路規程和應用協議將資料解碼,最終將字母l送入終端的輸入緩衝區。這個過程是複雜的,可愛的王然決定將駭客精神發揚光大,因此她將這個過程呈現出來。
以在windows機器上ssh一台linux主機為例,我們執行strace -p pid(隨意一個sshd進程號),縮減的結果如下(以#標識注釋):
...
read(4, "/3166/213/310/..."..., 16384) = 5 #從檔案標識符4讀取資料(加密資料)
select(11, [4 6 9 10], [8], NULL, NULL) = 1 (out [8]) #檔案8可寫
...
write(8, "l", 1) = 1 #寫入檔案8字元"l"
...
select(11, [4 6 9 10], [], NULL, NULL) = 1 (in [9]) #檔案9可讀
...
read(9, "l", 16384) = 1 #檔案9中讀取字元l
select(11, [4 6 9 10], [4], NULL, NULL) = 1 (out [4])
...
write(4, "a/313/3732/..."..., 36) = 36
select(11, [4 6 9 10], [], NULL, NULL) = 1 (in [4])
...
read(4, "/3./364/352/2..."..., 16384) = 52
select(11, [4 6 9 10], [8], NULL, NULL) = 1 (out [8])
...
write(8, "s", 1) = 1
...
select(11, [4 6 9 10], [], NULL, NULL) = 1 (in [9])
...
read(9, "s", 16384) = 1
select(11, [4 6 9 10], [4], NULL, NULL) = 1 (out [4])
...
write(4, "/20zSMS/215..."..., 36) = 36
select(11, [4 6 9 10], [], NULL, NULL
...
以上是strace的輸出,可是檔案描述符4,8,9到底是什麼呢?這還得需要lsof命令幫忙,下面是lsof -p pid(sshd的pid)的縮減結果
-----------------------------lsof sshd
...:/usr/src/linux# lsof -p 8793
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
...
sshd 8793 zyw 3u unix 0xc33c7ac0 12870 socket
sshd 8793 zyw 4u IPv6 12857 TCP host1:ssh->host2 #4是一個通訊端
sshd 8793 zyw 5u unix 0xf413be40 12872 socket
sshd 8793 zyw 6r FIFO 0,7 12873 pipe
sshd 8793 zyw 7w FIFO 0,7 12873 pipe
sshd 8793 zyw 8u CHR 5,2 23921462 /dev/ptmx #8和9都是終端
sshd 8793 zyw 9u CHR 5,2 23921462 /dev/ptmx
...
知道了檔案描述符代表的檔案,下一步就是分析sshd的執行過程了,可以從strace的輸出看到,首先從網路讀取了一段加密的資料,然後將之寫入了/dev/ptmx,然後將又從相同的檔案讀出了剛剛寫入的相同字元,就像回顯一樣,究竟sshd將資料寫到了哪裡?如果輸入l和s之後敲斷行符號,那麼目前的目錄的檔案就會顯示出來,這又是為什嗎?
簡單說來,終端是一個古老的東西,大體上分為兩類,第一類是真實的終端,比如通過串口串連到主機的終端或者slip線路終端等等,這類終端其實很簡單,本質上就是一條網線,你可以將其線路規程理解成物理層協議,看一段slip的tty線路規程代碼就知道了,每個線路規程都有一個receive_buf方法,負責從驅動接收資料並且交給線路規程來處理:slip_receive_buf即是其slip的receive_buf方法,其中調用netif_rx將資料按照線路規程解碼後交給網路通訊協定棧。;第二類終端當然就是偽終端了,其中又分為兩個小類,一類是console終端,也就是/dev/ttyn(n是一個數)之類的,它們更簡單,這類終端的讀緩衝區和鍵盤之類的輸入裝置相聯絡,而寫緩衝區和螢幕或者印表機之類的輸出裝置相聯絡,只要你敲擊了鍵盤,資料就會進入tty系列的讀緩衝,而往tty寫資料,則會顯示到輸出裝置上,另一類網路偽終端,它們都是成對的,即/dev/ptmx和/dev/pts/n(n是一個數),因此如果你往終端檔案寫一個字元,那麼該字元將從另一端被讀出,反之亦然,因此,王然告訴大家,sshd將資料寫入了ptmx,然後資料就進入了pts/n的讀緩衝區,那麼誰來讀取呢?答案是shell,同樣的執行一下lsof -p pid(一個ssh終端bash的pid),就會發現其標準輸入,輸出都是/dev/pts/n,然後strace -p pid(一個ssh終端bash的pid),就會發現bash先從pts/n讀取資料,然後再往其寫入相同的資料,顯然讀取的資料是sshd寫入ptmx的,而將相同的資料寫入pts/n,sshd則可以從ptmx讀取,然後通過通訊端回顯到遠端windows終端之上,整個過程是sshd-ptmx和bash-pts/n在配合,windows方面只是一個顯示作用。
王然的解說結束了,應該還算詳細,說歸說,王然還是總忘不了她的最終任務,因此使出了最後一招,那就是寫一個c檔案,main函數如下:
int fd = open("/dev/pts/0", O_RDWR);
ioctl(fd, TIOCSTI, "l ");
ioctl(fd, TIOCSTI, "s ");
ioctl(fd, TIOCSTI, "/n ");
return 0;
以wangran使用者執行之,得到了令人失望的許可權錯誤,看了核心代碼,TIOCSTI的ioctl會調用tiocsti,在tiocsti函數中有:
static int tiocsti(struct tty_struct *tty, char __user *p)
{
char ch, mbz = 0;
struct tty_ldisc *ld;
if ((current->signal->tty != tty) && !capable(CAP_SYS_ADMIN))
return -EPERM; //就是在此出的許可權錯誤
...
ld->receive_buf(tty, &ch, &mbz, 1); //如果是root使用者或者使用當前終端,那麼就可以將資料放入讀緩衝區了。
...
}
可是王然畢竟不是王其,怎麼辦?除了放置木馬就徹底沒有辦法了,到頭來,還是沒有找到linux的漏洞啊!可是,且慢!將許可權檢查放到核心這樣好嗎?王然覺得很不好,為何不在使用者執行su other的時候將其對應終端的許可權修改成other的許可權呢?答案在哪?答案在寫核心的人對寫shell的人不信任?王然覺得是這樣的,可是不信任的代價太大了,機制和策略不再分離...明顯是使用者空間的問題為何非要核心來做呢?於是王然修改了核心源碼,將tiocsti中那個判斷去掉,同時修改了bash的源碼,一旦使用者su成了別的使用者,其終端也就成了別的使用者權限,如果su成了root,則wangran使用者根本就無許可權操縱pts/n,核心不必再參與使用者空間的授權事務了,正如核心不管使用者密碼的保管一樣,如果一個shell在su的時候沒有改變終端的許可權,那麼有一種需求是使用者希望使用該shell在別的終端上執行類似QQ或者MSN遠程協助的功能。
最後,王然要總結一下在/dev/pts/m通過echo ls > /dev/pts/n為何不能在pts/n上執行ls,當她在windows上的一個ssh終端執行echo ls > /dev/pts/n的時候,整個命令列通過ssh協議傳輸到了linux主機的sshd進程,然後sshd進程將之寫入到/dev/ptmx,最終/dev/pts/m上的bash進程的read返回sshd寫入ptmx的資料,也就是整個命令列,然後bash執行之,最終將輸出重新定向到了/dev/pts/n,寫入pts/n就意味著資料可以從ptmx中被讀出,確實地,資料被和pts/n相關的sshd進程從ptmx中讀出了,然後通過網路將ls+/n這些字元顯示到了windows上和/dev/pts/n相關的ssh終端上,可見至始至終,/dev/pts/n上的bash並沒有讀取任何資料,而若想使之執行ls命令就必然要使得bash讀出ls/n,最終寫入/dev/pts/n的資料僅僅顯示到了windows機器相應的ssh終端而已。我們知道,一般終端輸入都和輸入裝置相關,ssh終端雖然拉遠了linux主機和終端的距離,但是不可否認若想在/dev/pts/n上執行ls,則此ls必然需要在windows機器鍵盤輸入,並且是和/dev/pts/n相關的ssh終端視窗的輸入,只有這樣,資料才能一步步經過windows機器->linux的sshd->linux的/dev/ptmx->/dev/pts/n->bash執行後再通過bash->/dev/pts/n->linux的/dev/ptmx->linux的sshd->windows機器回顯過來。