天天看點

UNIX(Linux)系統程式設計常用庫函數說明

UNIX系統為程式員提供了許多子程式,這些子程式可存取各種安全屬性.有些是資訊子程式,傳回檔案屬性,實際的和有效的UID,GID等資訊.有些子程式可改變檔案屬性.UID,GID等有些處理密碼檔案和小組檔案,還有些完成加密和解密.

本文主要讨論有關系統子程式,标準C庫子程式的安全,如何寫安全的C程式并從root的角度介紹程式設計(僅能被root調用的子程式).

1.系統子程式

(1)I/O子程式

*creat():建立一個新檔案或重寫一個暫存檔案.

需要兩個參數:檔案名和存取許可值(8進制方式).如:

creat("/usr/pat/read_write",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;

gid_t pw_gid;

char * pw_age;

char * pw_comment;

char * pw_gecos;

char * pw_dir;

char * pw_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()的參數是要一個要拆卸的特别檔案的指針.

繼續閱讀