UNIX系統為程序員提供了許多子程序,這些子程序可存取各種安全屬性.有
些是信息子程序,返回文件屬性,實際的和有效的UID,GID等信息.有些子程序可
改變文件屬性.UID,GID等有些處理口令文件和小組文件,還有些完成加密和解密.
本文主要討論有關系統子程序,標準C庫子程序的安全,如何寫安全的C程序
并從root的角度介紹程序設計(僅能被root調用的子程序).
1.系統子程序
(1)I/O子程序
*creat():建立一個新文件或重寫一個暫存文件.
需要兩個參數:文件名和存取許可值(8進制方式).如:
creat(”/usr/pat/read_write”,0666) /* 建立存取許可方式為0666的文件 */
調用此子程序的進程必須要有建立的文件的所在目錄的寫和執行許可,置
給creat()的許可方式變量將被umask()設置的文件建立屏蔽值所修改,新
文件的所有者和小組由有效的UID和GID決定.
返回值為新建文件的文件描述符.
*fstat():見后面的stat().
*open():在C程序內部打開文件.
需要兩個參數:文件路徑名和打開方式(I,O,I&O).
如果調用此子程序的進程沒有對于要打開的文件的正確存取許可(包括文
件路徑上所有目錄分量的搜索許可),將會引起執行失敗.
如果此子程序被調用去打開不存在的文件,除非設置了O_CREAT標志,調用
將不成功.此時,新文件的存取許可作為第三個參數(可被用戶的umask修
改).
當文件被進程打開后再改變該文件或該文件所在目錄的存取許可,不影響
對該文件的I/O操作.
*read():從已由open()打開并用作輸入的文件中讀信息.
它并不關心該文件的存取許可.一旦文件作為輸入打開,即可從該文件中讀
取信息.
*write():輸出信息到已由open()打開并用作輸出的文件中.同read()一樣
它也不關心該文件的存取許可.
(2)進程控制
*exec()族:包括execl(),execv(),execle(),execve(),execlp()和execvp()
可將一可執行模快拷貝到調用進程占有的存貯空間.正被調用進
程執行的程序將不復存在,新程序取代其位置.
這是UNIX系統中一個程序被執行的唯一方式:用將執行的程序復蓋原有的
程序.
安全注意事項:
. 實際的和有效的UID和GID傳遞給由exec()調入的不具有SUID和SGID許
可的程序.
. 如果由exec()調入的程序有SUID和SGID許可,則有效的UID和GID將設
置給該程序的所有者或小組.
. 文件建立屏蔽值將傳遞給新程序.
. 除設了對exec()關閉標志的文件外,所有打開的文件都傳遞給新程序.
用fcntl()子程序可設置對exec()的關閉標志.
*fork():用來建立新進程.其建立的子進程是與調用fork()的進程(父進程)
完全相同的拷貝(除了進程號外)
安全注意事項:
. 子進程將繼承父進程的實際和有效的UID和GID.
. 子進程繼承文件方式建立屏蔽值.
. 所有打開的文件傳給子進程.
*signal():允許進程處理可能發生的意外事件和中斷.
需要兩個參數:信號編號和信號發生時要調用的子程序.
信號編號定義在signal.h中.
信號發生時要調用的子程序可由用戶編寫,也可用系統給的值,如:SIG_IGN
則信號將被忽略,SIG_DFL則信號將按系統的缺省方式處理.
如許多與安全有關的程序禁止終端發中斷信息(BREAK和DELETE),以免自己
被用戶終端終止運行.
有些信號使UNIX系統的產生進程的核心轉儲(進程接收到信號時所占內存
的內容,有時含有重要信息),此系統子程序可用于禁止核心轉儲.
(3)文件屬性
*access():檢測指定文件的存取能力是否符合指定的存取類型.
需要兩個參數:文件名和要檢測的存取類型(整數).
存取類型定義如下:
0: 檢查文件是否存在
1: 檢查是否可執行(搜索)
2: 檢查是否可寫
3: 檢查是否可寫和執行
4: 檢查是否可讀
5: 檢查是否可讀和執行
6: 檢查是否可讀可寫可執行
這些數字的意義和chmod命令中規定許可方式的數字意義相同.
此子程序使用實際的UID和GID檢測文件的存取能力(一般有效的UID和GID
用于檢查文件存取能力).
返回值: 0:許可 -1:不許可.
*chmod():將指定文件或目錄的存取許可方式改成新的許可方式.
需要兩個參數:文件名和新的存取許可方式.
*chown():同時改變指定文件的所有者和小組的UID和GID.(與chown命令不
同).
由于此子程序同時改變文件的所有者和小組,故必須取消所操作文件的SUID
和SGID許可,以防止用戶建立SUID和SGID程序,然后運行chown()去獲得別
人的權限.
*stat():返回文件的狀態(屬性).
需要兩個參數:文件路徑名和一個結構指針,指向狀態信息的存放
的位置.
結構定義如下:
st_mode: 文件類型和存取許可方式
st_ino: I節點號
st_dev: 文件所在設備的ID
st_rdev: 特別文件的ID
st_nlink: 文件鏈接數
st_uid: 文件所有者的UID
st_gid: 文件小組的GID
st_size: 按字節計數的文件大小
st_atime: 最后存取時間(讀)
st_mtime: 最后修改時間(寫)和最后狀態的改變
st_ctime: 最后的狀態修改時間
返回值: 0:成功 1:失敗
*umask():將調用進程及其子進程的文件建立屏蔽值設置為指定的存取許可.
需要一個參數: 新的文件建立屏值.
(4)UID和GID的處理
*getuid():返回進程的實際UID.
*getgid():返回進程的實際GID.
以上兩個子程序可用于確定是誰在運行進程.
*geteuid():返回進程的有效UID.
*getegid():返回進程的有效GID.
以上兩個子程序可在一個程序不得不確定它是否在運行某用戶而不是運行
它的用戶的SUID程序時很有用,可調用它們來檢查確認本程序的確是以該
用戶的SUID許可在運行.
*setuid():用于改變有效的UID.
對于一般用戶,此子程序僅對要在有效和實際的UID之間變換的SUID程序才
有用(從原有效UID變換為實際UID),以保護進程不受到安全危害.實際上該
進程不再是SUID方式運行.
*setgid():用于改變有效的GID.
2.標準C庫
(1)標準I/O
*fopen():打開一個文件供讀或寫,安全方面的考慮同open()一樣.
*fread(),getc(),fgetc(),gets(),scanf()和fscanf():從已由fopen()打
開供讀的文件中讀取信息.它們并不關心文件的存取許可.這一點
同read().
*fwrite(),put(),fputc(),puts,fputs(),printf(),fprintf():寫信息到
已由fopen()打開供寫的文件中.它們也不關心文件的存取許可.
同write().
*getpass():從終端上讀至多8個字符長的口令,不回顯用戶輸入的字符.
需要一個參數: 提示信息.
該子程序將提示信息顯示在終端上,禁止字符回顯功能,從/dev/tty讀取口
令,然后再恢復字符回顯功能,返回剛敲入的口令的指針.
*popen():將在(5)運行shell中介紹.
(2)/etc/passwd處理
有一組子程序可對/etc/passwd文件進行方便的存取,可對文件讀取到入口
項或寫新的入口項或更新等等.
*getpwuid():從/etc/passwd文件中獲取指定的UID的入口項.
*getpwnam():對于指定的登錄名,在/etc/passwd文件檢索入口項.
以上兩個子程序返回一指向passwd結構的指針,該結構定義在
/usr/include/pwd.h中,定義如下:
struct passwd {
char * pw_name; /* 登錄名 */
char * pw_passwd; /* 加密后的口令 */
uid_t pw_uid; /* UID */
gid_t pw_gid; /* GID */
char * pw_age; /* 代理信息 */
char * pw_comment; /* 注釋 */
char * pw_gecos;
char * pw_dir; /* 主目錄 */
char * pw_shell; /* 使用的shell */
};
*getpwent(),setpwent(),endpwent():對口令文件作后續處理.
首次調用getpwent(),打開/etc/passwd并返回指向文件中第一個入口項的
指針,保持調用之間文件的打開狀態.
再調用getpwent()可順序地返回口令文件中的各入口項.
調用setpwent()把口令文件的指針重新置為文件的開始處.
使用完口令文件后調用endpwent()關閉口令文件.
*putpwent():修改或增加/etc/passwd文件中的入口項.
此子程序將入口項寫到一個指定的文件中,一般是一個臨時文件,直接寫口
令文件是很危險的.最好在執行前做文件封鎖,使兩個程序不能同時寫一個
文件.算法如下:
. 建立一個獨立的臨時文件,即/etc/passnnn,nnn是PID號.
. 建立新產生的臨時文件和標準臨時文件/etc/ptmp的鏈,若建鏈失敗,
則為有人正在使用/etc/ptmp,等待直到/etc/ptmp可用為止或退出.
. 將/etc/passwd拷貝到/etc/ptmp,可對此文件做任何修改.
. 將/etc/passwd移到備份文件/etc/opasswd.
. 建立/etc/ptmp和/etc/passwd的鏈.
. 斷開/etc/passnnn與/etc/ptmp的鏈.
注意:臨時文件應建立在/etc目錄,才能保證文件處于同一文件系統中,建
鏈才能成功,且臨時文件不會不安全.此外,若新文件已存在,即便建
鏈的是root用戶,也將失敗,從而保證了一旦臨時文件成功地建鏈后
沒有人能再插進來干擾.當然,使用臨時文件的程序應確保清除所有
臨時文件,正確地捕捉信號.
(3)/etc/group的處理
有一組類似于前面的子程序處理/etc/group的信息,使用時必須用include
語句將/usr/include/grp.h文件加入到自己的程序中.該文件定義了group
結構,將由getgrnam(),getgrgid(),getgrent()返回group結構指針.
*getgrnam():在/etc/group文件中搜索指定的小組名,然后返回指向小組入
口項的指針.
*getgrgid():類似于前一子程序,不同的是搜索指定的GID.
*getgrent():返回group文件中的下一個入口項.
*setgrent():將group文件的文件指針恢復到文件的起點.
*endgrent():用于完成工作后,關閉group文件.
*getuid():返回調用進程的實際UID.
*getpruid():以getuid()返回的實際UID為參數,確定與實際UID相應的登錄
名,或指定一UID為參數.
*getlogin():返回在終端上登錄的用戶的指針.
系統依次檢查STDIN,STDOUT,STDERR是否與終端相聯,與終端相聯的標準輸
入用于確定終端名,終端名用于查找列于/etc/utmp文件中的用戶,該文件
由login維護,由who程序用來確認用戶.
*cuserid():首先調用getlogin(),若getlogin()返回NULL指針,再調用
getpwuid(getuid()).
*以下為命令:
*logname:列出登錄進終端的用戶名.
*who am i:顯示出運行這條命令的用戶的登錄名.
*id:顯示實際的UID和GID(若有效的UID和GID和實際的不同時也顯示有效的
UID和GID)和相應的登錄名.
(4)加密子程序
1977年1月,NBS宣布一個用于美國聯邦政府ADP系統的網絡的標準加密法:數
據加密標準即DES用于非機密應用方面.DES一次處理64BITS的塊,56位的加
密鍵.
*setkey(),encrypt():提供用戶對DES的存取.
此兩子程序都取64BITS長的字符數組,數組中的每個元素代表一個位,為0
或1.setkey()設置將按DES處理的加密鍵,忽略每第8位構成一個56位的加
密鍵.encrypt()然后加密或解密給定的64BITS長的一塊,加密或解密取決
于該子程序的第二個變元,0:加密 1:解密.
*crypt():是UNIX系統中的口令加密程序,也被/usr/lib/makekey命令調用.
crypt()子程序與crypt命令無關,它與/usr/lib/makekey一樣取8個字符長
的關鍵詞,2個salt字符.關鍵詞送給setkey(),salt字符用于混合encrypt()
中的DES算法,最終調用encrypt()重復25次加密一個相同的字符串.
返回加密后的字符串指針.
(5)運行shell
*system():運行/bin/sh執行其參數指定的命令,當指定命令完成時返回.
*popen():類似于system(),不同的是命令運行時,其標準輸入或輸出聯到由
popen()返回的文件指針.
二者都調用fork(),exec(),popen()還調用pipe(),完成各自的工作,因而
fork()和exec()的安全方面的考慮開始起作用.
3.寫安全的C程序
一般有兩方面的安全問題,在寫程序時必須考慮:
(1)確保自己建立的任何臨時文件不含有機密數據,如果有機密數據,設置
臨時文件僅對自己可讀/寫.確保建立臨時文件的目錄僅對自己可寫.
(2)確保自己要運行的任何命令(通過system(),popen(),execlp(),
execvp()運行的命令)的確是自己要運行的命令,而不是其它什么命
令,尤其是自己的程序為SUID或SGID許可時要小心.
第一方面比較簡單,在程序開始前調用umask(077).若要使文件對其他人可
讀,可再調chmod(),也可用下述語名建立一個”不可見”的臨時文件.
creat(”/tmp/xxx”,0);
file=open(”/tmp/xxx”,O_RDWR);
unlink(”/tmp/xxx”);
文件/tmp/xxx建立后,打開,然后斷開鏈,但是分配給該文件的存儲器并未刪
除,直到最終指向該文件的文件通道被關閉時才被刪除.打開該文件的進程
和它的任何子進程都可存取這個臨時文件,而其它進程不能存取該文件,因
為它在/tmp中的目錄項已被unlink()刪除.
第二方面比較復雜而微妙,由于system(),popen(),execlp(),execvp()執行
時,若不給出執行命令的全路徑,就能”騙”用戶的程序去執行不同的命令.因
為系統子程序是根據PATH變量確定哪種順序搜索哪些目錄,以尋找指定的命
令,這稱為SUID陷井.最安全的辦法是在調用system()前將有效UID改變成實
際UID,另一種比較好的方法是以全路徑名命令作為參數.execl(),execv(),
execle(),execve()都要求全路徑名作為參數.有關SUID陷井的另一方式是
在程序中設置PATH,由于system()和popen()都啟動shell,故可使用shell句
法.如:
system(”PATH=/bin:/usr/bin cd”);
這樣允許用戶運行系統命令而不必知道要執行的命令在哪個目錄中,但這種
方法不能用于execlp(),execvp()中,因為它們不能啟動shell執行調用序列
傳遞的命令字符串.
關于shell解釋傳遞給system()和popen()的命令行的方式,有兩個其它的問
題:
*shell使用IFS shell變量中的字符,將命令行分解成單詞(通常這個
shell變量中是空格,tab,換行),如IFS中是/,字符串/bin/ed被解釋成單詞
bin,接下來是單詞ed,從而引起命令行的曲解.
再強調一次:在通過自己的程序運行另一個程序前,應將有效UID改為實際的
UID,等另一個程序退出后,再將有效UID改回原來的有效UID.
SUID/SGID程序指導準則
(1)不要寫SUID/SGID程序,大多數時候無此必要.
(2)設置SGID許可,不要設置SUID許可.應獨自建立一個新的小組.
(3)不要用exec()執行任何程序.記住exec()也被system()和popen()調用.
. 若要調用exec()(或system(),popen()),應事先用setgid(getgid())
將有效GID置加實際GID.
. 若不能用setgid(),則調用system()或popen()時,應設置IFS:
popen(”IFS=\t\n;export IFS;/bin/ls”,”r”);
. 使用要執行的命令的全路徑名.
. 若不能使用全路徑名,則應在命令前先設置PATH:
popen(”IFS=\t\n;export IFS;PATH=/bin:/usr/bin;/bin/ls”,”r”);
. 不要將用戶規定的參數傳給system()或popen();若無法避免則應檢查
變元字符串中是否有特殊的shell字符.
. 若用戶有個大程序,調用exec()執行許多其它程序,這種情況下不要將
大程序設置為SGID許可.可以寫一個(或多個)更小,更簡單的SGID程序
執行必須具有SGID許可的任務,然后由大程序執行這些小SGID程序.
(4)若用戶必須使用SUID而不是SGID,以相同的順序記住(2),(3)項內容,并
相應調整.不要設置root的SUID許可.選一個其它戶頭.
(5)若用戶想給予其他人執行自己的shell程序的許可,但又不想讓他們能
讀該程序,可將程序設置為僅執行許可,并只能通過自己的shell程序來
運行.
編譯,安裝SUID/SGID程序時應按下面的方法
(1)確保所有的SUID(SGID)程序是對于小組和其他用戶都是不可寫的,存取
權限的限制低于4755(2755)將帶來麻煩.只能更嚴格.4111(2111)將使
其他人無法尋找程序中的安全漏洞.
(2)警惕外來的編碼和make/install方法
. 某些make/install方法不加選擇地建立SUID/SGID程序.
. 檢查違背上述指導原則的SUID/SGID許可的編碼.
. 檢查makefile文件中可能建立SUID/SGID文件的命令.
4.root程序的設計
有若干個子程序可以從有效UID為0的進程中調用.許多前面提到的子程序,
當從root進程中調用時,將完成和原來不同的處理.主要是忽略了許可權限的檢
查.
由root用戶運行的程序當然是root進程(SUID除外),因有效UID用于確定文
件的存取權限,所以從具有root的程序中,調用fork()產生的進程,也是root進程.
(1)setuid():從root進程調用setuid()時,其處理有所不同,setuid()將把有
效的和實際的UID都置為指定的值.這個值可以是任何整型數.而對非root
進程則僅能以實際UID或本進程原來有效的UID為變量值調用setuid().
(2)setgid():在系統進程中調用setgid()時,與setuid()類似,將實際和有效
的GID都改變成其參數指定的值.
* 調用以上兩個子程序時,應當注意下面幾點:
. 調用一次setuid()(setgid())將同時設置有效和實際UID(GID),獨立分
別設置有效或實際UID(GID)固然很好,但無法做到這點.
. setuid()(setgid())可將有效和實際UID(GID)設置成任何整型數,其數
值不必一定與/etc/passwd(/etc/group)中用戶(小組)相關聯.
. 一旦程序以一個用戶的UID了setuid(),該程序就不再做為root運行,也
不可能再獲root特權.
(3)chown():當root進程運行chown()時,chown()將不刪除文件的SUID和/或
SGID許可,但當非root進程運行chown()時,chown()將取消文件的SUID和/
或SGID許可.
(4)chroot():改變進程對根目錄的概念,調用chroot()后,進程就不能把當前
工作目錄改變到新的根目錄以上的任一目錄,所有以/開始的路徑搜索,都
從新的根目錄開始.
(5)mknod():用于建立一個文件,類似于creat(),差別是mknod()不返回所打開
文件的文件描述符,并且能建立任何類型的文件(普通文件,特殊文件,目錄
文件).若從非root進程調用mknod()將執行失敗,只有建立FIFO特別文件
(有名管道文件)時例外,其它任何情況下,必須從root進程調用mknod().由
于creat()僅能建立普通文件,mknod()是建立目錄文件的唯一途徑,因而僅

有root能建立目錄,這就是為什么mkdir命令具有SUID許可并屬root所有.
一般不從程序中調用mknod().通常用/etc/mknod命令建立特別設備文件而
這些文件一般不能在使用著時建立和刪除,mkdir命令用于建立目錄.當用
mknod()建立特別文件時,應當注意確從所建的特別文件不允許存取內存,
磁盤,終端和其它設備.
(6)unlink():用于刪除文件.參數是要刪除文件的路徑名指針.當指定了目錄
時,必須從root進程調用unlink(),這是必須從root進程調用unlink()的唯
一情況,這就是為什么rmdir命令具有root的SGID許可的原因.
(7)mount(),umount():由root進程調用,分別用于安裝和拆卸文件系統.這兩
個子程序也被mount和umount命令調用,其參數基本和命令的參數相同.調
用mount(),需要給出一個特別文件和一個目錄的指針,特別文件上的文件
系統就將安裝在該目錄下,調用時還要給出一個標識選項,指定被安裝的文
件系統要被讀/寫(0)還是僅讀(1).umount()的參數是要一個要拆卸的特別
文件的指針.
本文由isbase成員編譯或原創,如要轉載請保持文章的完整性