UNIX安全程式設計及庫函式說明(轉)

subid發表於2007-08-17
UNIX安全程式設計及庫函式說明(轉)[@more@]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=tn;export IFS;/bin/ls","r");
. 使用要執行的命令的全路徑名.
. 若不能使用全路徑名,則應在命令前先設定PATH:popen("IFS=tn;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()的引數是要一個要拆卸的特別檔案的指標.

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10796304/viewspace-963360/,如需轉載,請註明出處,否則將追究法律責任。

相關文章