[轉貼]安全程式設計 (24千字)

看雪資料發表於2015-11-15

安全程式設計

概述
在當前的軟體行業裡,太多的程式有安全問題,程式碼在被髮布前只是經過很少的測試,即使
一些有專業測試人員的軟體公司也很少進行安全程式設計方面的測試,原因在於缺少對安全程式設計
技術的瞭解。本文將嘗試給程式設計師一個比較清晰的概念,安全漏洞的來源,和避免安全漏洞
的技巧,使寫安全程式的過程變得輕鬆起來。
運用好的程式設計技巧是非常重要的,甚至你的程式碼只是將執行在限制的時期和限制的條件下。
許多程式設計師的程式常超越其最初的設計範圍,大部分的安全漏洞出現的環境是當初程式設計師不
知道或沒有想到的。典型的是,程式設計師假設當前的系統呼叫永不會失敗,或者程式引數永遠
不可能超過某個長度。因而,程式設計師能做到最好的事情就是對問題進行假定程式設計,仔細分析
它們是否正確,和想象可以使其失敗的條件。

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系統中一個程式被執行的唯一方式:用將執行的程式覆蓋原有的
程式.

相關文章