上一篇文章中,我們介紹了 ret2libc 的攻擊手法,我們藉由 Leak 出實際地址來繞過 ASLR 保護,成功使用 ROP 獲得一個 Shell 並 Pwn 下目標。在過程中,讀者可能注意到一個關鍵點,就是 Leak Address 時程式必須要有輸出函數(如puts
)與 pop rdi; ret;
這個 Gadget,藉此操控輸出函數的第一個參數,讓他輸出 GOT 存的實際位置。
然而如果程式達不到這些條件怎麼辦呢?本篇文章將介紹即使無法 Leak Address 也能獲得控制權的攻擊手法,稱為 ret2dlresolve。
為什麼此攻擊手法不用 Leak Address 就能執行一個 Shell 呢?
此攻擊的概念為:去記憶體的可寫區段寫一些假資料,當動態載入器要參考資料去解析某個函數的真實位址時,參考成我們寫的假資料,導致它被假資料欺騙把某個函數的真實位址解析成 system()
等我們需要的函數的真實位址,因此即使不用 Leak Address 我們也能執行 system()
了。
為了要欺騙動態載入器,我們必須先了解它是如何解析出函數的真實位址的。
註:x86 架構與 x86-64 架構在這部份的實作有些微差異,因此最後攻擊的 Payload 也會因架構不同有些許變化,本篇文章僅以 x86-64 架構說明。
我們再看一次前一篇文章介紹延遲綁定的例子,當時有提到,當程式第一次呼叫 printf() 這個外部函式時,它會透過 .plt
Section 中的對應項目來跳轉到一段動態連結的程式碼,這段程式碼會啟動動態連結器,負責解析 printf() 的實際位址。現在,我們實際追進 printf@plt
看看。
可以看到 printf@plt
只有三行。第一行的 jmp
其實是跳到 GOT,如果已經呼叫過 printf() 這個 GOT 存的值就會是 printf() 的實際位址,也就是上一篇文章 Leak 出的內容。但是,如果 printf() 沒有被呼叫過,GOT 的內容即為 printf@plt
的第二行(0x401036 <printf@plt+6> push 0
)位址。
我們前面為了方便解釋,都稱呼它為 GOT,然而其實 GOT 是由 .got
和 .got.plt
這兩段 ELF Sections 所組成。說明如下:
.got
:存全域變數的實際位址.got.plt
:初始時,存 .plt
的第二行位址,首次函數解析之後,會更新為函數的實際位址後續將會精確的稱呼他為 .got.plt
。
由於我們現在是第一次呼叫 printf(),因此現在 .got.plt
內容為 .plt
的第二行位址,所以現在會看起來像沒跳轉一樣繼續往下執行:
可以看到他最後跳轉到一個名為 _dl_runtime_resolve_xsavec()
的函數。
補充:此段程式是以 x86 架構的組合語言撰寫的,由於之前都是在 x86-64 架構(64bits)中練習,這是第一次遇到 x86 架構,除了暫存器能處理的 bits 不同以外,他的呼叫慣例也與 x86-64 不同。差別主要在於傳參數的方式:
- x86-64:函數的參數會依序存放在 rdi、rsi、rdx、rcx、r8、r9 等暫存器中,如果參數超過6個,從第7個參數開始會存放在 Stack 中
- x86:參數使用 Stack 儲存,呼叫者會從右到左依序將參數 Push 進 Stack 中,也就是說第一個參數會在 Stack 最上層,被呼叫者會再依序將參數 Pop 出來
由於此段程式是以 x86 寫的,所以這段指令是把兩個參數給 _dl_runtime_resolve_xsavec(link_map, index)
(只有此段是以 x86,進入函式後都還是 x86-64 架構)
其中,第一個參數 link_map
是一個結構指標,此結構儲存許多動態載入時需要參考的資訊,這些資訊中最重要的就是 ELF 檔案中名為 .dynamic
的 Section。
我們使用 readelf -d
來看一下 .dynamic
這個 Section 究竟存了什麼資訊:
每一行都代表一種類別的資料表,其中跟尋找函數實際位址有關的資料表,我們需要知道的只有圈起來的三種:
printf()
的 printf、system()
的 system了解上面三種資料表的用途後,我們就能說明 _dl_runtime_resolve_xsavec(link_map, index)
具體做了什麼將實際位址填入 got.plt
:
link_map
獲得上面三種表的位址index
,由重定位表找到符號表.got.plt
下篇文章將會依據這個流程說明如何操作 ret2dlresolve