ksh 與 bash 的擴展與相容性
模式匹配的擴展
bash 要先啟用 extglob 選項才支援此功能
正則的 ^
與 $
在 Shell 的模式匹配沒有等同物,我們可以想像,在 Shell 的模式匹配中, ^
與 $
已經自動帶在前後文了,若在模式前後加上 *
則可以停用此功能
1 2 3 4 5 6 7 8 |
$ ls biff bob frederick shishkabob $ shopt -s extglob # 啟用 bash 的模式匹配 $ echo @(dave|fred|bob) bob $ echo *@(dave|fred|bob)* # 停用隱含的 ^ 與 $ 前後文 bob frederick shishkabob |
括弧展開
源自於 csh 的功能,ksh 跟 bash 都支持此用法,可以巢狀化
1 2 3 |
$ echo cpp-{args,l{e,o}x,parse}.c cpp-args.c cpp-lex.c cpp-lox.c cpp-parse.c |
進程替換 (Process Substitution)
可以讓使用者開啟多個進程數據流,再把它們餵給單一程序處理,程序會依序處理每個數據流,不管數據是否來自多個不同來源
範例:用於輸入的進程替換
1 2 |
awk '...' <(generate_data) <(generate_more_data) # 注意圓括弧也是語法的一部分 |
範例:用於輸出的進程替換
須與 tee
命令配合
索引式陣列
Shell 會自動評估表達式產生索引,但只支援一維陣列
可以當成是一個整數輸入變量的數學函數,會回傳一個對應的值,屬於「number-dominate」的資料結構
有三種方式可以將值指定給陣列元素:
- 使用標準 Shell 變量指定,指定的值都會被視為字串:
names[1]=bob
- 使用 set 命令:
set -A names bob rob jacob
(注意 bash 不支援此語法) - (建議使用)複合指定:
names=(bob rob jacob)
要取出索引陣列中的值,語法為 ${names[i]}
,索引 i 可以是算術表達式。如果在索引處使用 *
或 @
則會是以空格隔開所有元素,。省略索引的話,等同是指定索引為 0
可以透過 "${names[@]}"
(使用雙引號)保留陣列元素內的任何空白字符,類似我們會用 "$@"
而不是 $*
或 "$*"
可以透過 ${#names[*]}
求出陣列中的元素數量,*
是必須的,因為忽略索引會解釋成第 0 個元素(此用法有可移植性的問題,但 bash、ksh 皆支持)
算式展開的循環用法
POSIX 把表達式 $((...))
定義作算術展開,且有離開狀態(0 或 1),因此能在 if 和 while 語句下使用
1 2 3 4 5 |
while ((x != 42)) do ... done |
1 2 3 4 5 |
for ((i=1; i<=limit; i++)) # ksh, bash 支援 ++ 與 -- 操作 do ... done |
即席字符串(here strings)
即席字符串使用 <<<
再接上字符串,字符串即成為相關命令的標準輸入,不用再建立額外的進程
1 2 |
tr ... <<< "$myvar1 $myvar2" | ... |
Shell 初始化與終止
因應客製化,Shell 會在啟動、終止時,讀取某些特定文件,每個 Shell 各自有不同的慣例模式
如果腳本是處理一般性的功能,且希望能相容其他的 Shell,就不應該依賴 Shell 客製化的功能,相反的,應該在腳本中設置它們自己的環境(例如 $PATH
的值)
Shell 的行為模式取決於它是否為 login Shell,可以檢查 $0
的值來判斷,以-
開頭即為 login Shell,連字號並不是暗示有個文件叫 /bin/-bash
,而是當父進程執行 exec() 啟動 Shell 時,是用它來設置第 0 個參數
Bourne Shell (sh)
是登錄 Shell 時,起始會嘗試讀取 /etc/profile
及 $HOME/.profile
,這兩個檔案不一定要存在
在離開時,不管是否為登錄 Shell,皆不會讀取標準的終結文件(要達到此效果,可以在初始化的腳本額外設置 trap 指令如: trap '. $HOME/.logout' EXIT
),但不保證終結的腳本一定會被執行
由於沒有終結文件、不支援 history 指令等限制,Bourne Shell 不常在交互模式下被使用
Korn Shell (ksh)
是登錄 Shell 時,起始會嘗試讀取 /etc/profile
及 $HOME/.profile
是交互模式的話,在啟動時讀取 $ENV
定義的文件
Bourne-Again Shell (bash)
很多系統上的 /bin/sh 僅為 /bin/bash 的連結(尤其是 GNU/Linux)
是登錄 Shell 時,起始會依序嘗試讀取 /etc/profile
、$HOME/.bash_profile
及 $HOME/.bash_login
,最後才是 $HOME/.profile
;離開時會讀取 $HOME/.bash_logout
不是登錄 Shell 且是交互模式時,起始會嘗試讀取 $HOME/.bashrc
不是登錄 Shell 且不是交互模式時,起始會嘗試讀取 $BASH_ENV
定義的文件
Shell 的安全性
- 不要將當前目錄(
.
)置於$PATH
中:可執行程序應該只放在標準的系統目錄,避免木馬攻擊 - 確保
$PATH
的每個目錄都只有該目錄擁有者可以寫入 - 不要信任傳進來的環境變量:在變量被命令使用前(例如
TZ
、PATH
、IFS
),請檢查或重設為已知的值 - 保留紀錄:使用 logger 或建立函數來保存日誌
- 不要對輸入的參數使用
eval
- 使用輸入參數時,一定要以雙引號引用:避免執行超出範圍的計算
- 文件可能是連結:在
chmod
文件或編輯文件時,檢查它是否真的是一個文件,而非連結到某個關鍵系統文件 - 用
setgid
而不要用setuid
:若非必要,兩者最好都不要在 Shell 中使用 - 使用新的使用者而不是 root:如果必須使用 setuid 訪問文件,請考慮用新建的使用者而非 root
受限制的 Shell
嚴格限制文件寫入與移動的環境,使用的對象通常是訪客 (guest) 帳號,常見的有 rksh
、rbash
常見的限制有:
- 不允許變更工作目錄
- 不允許重定向輸出到文件
- 不允許指定新值給特定的環境變量
- 只能執行 $PATH 可以找到的命令
木馬的運作機制
在 bin 目錄疏忽保護的情況下,依芙在愛麗絲的 bin 目錄下建立一個 grep
腳本:
1 2 3 4 5 6 7 |
/bin/grep "#@" case $(whoami) in root) nasty stuff here rm ~/alice/bin/grep ;; esac |
當愛麗絲以自己身份做事時,這個腳本不會有危險。一旦愛麗絲切換身份到 root(假設她有權限),su
命令會繼承當前 PATH
的設置,導致 PATH
感染了 ~/alice/bin/grep
當惡意腳本執行結束,腳本將自己刪除,不留下任何證據
setuid 的漏洞
進程牽涉到兩個 User ID:
- real user ID: 表示進程由「誰」控制,也就是
ps
命令列出的USER
欄位 - effective user ID: 表示進程使用「誰」的權限
setuid
是文件的一個設置權限的位元:當一個可執行的文件啟用 setuid
,在執行時,effective user ID 會轉換成文件擁有者的 user ID,也就是「以文件擁有者的權限來執行」
可以想見,如果程序的擁有者是 root
又啟用了setuid
,駭客就有機會以與 root 相同權限執行任何命令
以下範例的代碼可以搭配木馬來攻擊,當 root 感染了此木馬,當駭客就能以交互模式執行 affected_sh,並以 root 權限做任何事了:
1 2 3 4 |
cp /bin/sh ~badguy/bin/affected_sh # 複製一份 shell chown root ~badguy/bin/affected_sh # 將此文件擁有者設為 root chmod u+s ~badguy/bin/affected_sh # 啟用 setuid |
我們常見到的 Shell 腳本,會在開頭(Shebang 加上 shell 文件)後方帶上 -
,也是為了防止類似(將開啟 setuid 的 Shell 轉成交互式的 Shell)的攻擊:
1 2 3 4 5 6 |
#!/bin/sh # 此處若有帶上 `-` 做結尾,則 `-i` 參數就會被視為某個「文件/命令」,而非「開啟交互模式」 cd /tmp ln /etc/some_setuid_shell -i PATH=. -i |