在上一篇文章中,我們提到延遲綁定時 dl-resolve(link_map, index)
將實際地址填入 .got.plt
的流程。
現在我們將繼續說明具體要如何操作此攻擊手法。此篇文章架構如下:
dl-resolve(link_map, index)
dl-resolve(link_map, index)
上篇文章的結尾有提到整個程式的流程如下:
為了更清楚了解攻擊流程,接下來我們將逐步搭配程式來詳細說明每個步驟的細節。
我們之前透過 readelf -d
看過 ELF 檔案中的 .dynamic
Section。實際上我們現在看到的內容在 ELF 檔中是以名為 Elf64_Dyn 的 Struct 的形式儲存,如下:
struct Elf64_Dyn {
Elf64_Sxword d_tag
union {
Elf64_Xword d_val
Elf64_Addr d_ptr
} d_un
};
readelf 的內容對應下來如下
所以第一步 dl-resolve
函數會使用 link_map
找到 Elf64_Dyn Struct 中 d_tag 為 0x5、0x6、0x17 的 d_ptr,來獲取這三個表(等同於上圖黃色的 Section)的位址。
註:使用跟上一篇相同的例子說明,即需要延遲綁定的函數為
printf()
。
第二個參數 index
,代表 printf()
的重定位表實際上是重定位表第幾組,例如:index
當初是被設為 0,因此他現在是重定位表中的第一組。
如果呼叫 printf 之後又呼叫另一個外部函數,例如 scanf,就會像下面這樣:index
被設為 1,且可以看到位於重定位表的第二組。
所以現在我們得知真正的重定位表位址為:JMPPEL(0x17) + index
*一組的大小,我們稱呼它為 reloc
。
我們來看一下 reloc
的內容,重定位表的內容在 ELF 檔中以名為 Elf64_Rel 的 Struct 的形式儲存,如下:
struct Elf64_Rel {
Elf64_Addr r_offset;
Elf64_Xword r_info;
};
其中 reloc -> r_offset
為 .got.plt
的位址,而 .got.plt
的位址上會儲存最後拿到的記憶體實際位址。reloc -> r_info
的前 8 bytes 用於儲存符號表的索引值,後 8 bytes 代表類型。可以透過 readelf -r
查看,如下:
可以看到 Offset 即 reloc -> r_offset
就是.got.plt
的位址,而 Info 即為 reloc -> r_info
,前 8 bytes 為 0x00000002(前面補上4個0)、後 8 為 0x00000007,代表 R_X86_64_JUMP_SLO 類型。
我們可以看到上面的資訊表示 printf()
的符號表位於符號表中 Index 為 2 的位置,如下:
從剛剛的 reloc
已經找到 printf()
的符號表實際上位於索引值 2 號,我們稱呼這個實際上的符號表為 sym
。
來看一下 sym
的內容,它以名為 Elf64_Sym 的 Struct 的形式儲存
struct Elf64_Sym {
Elf64_Word st_name; // Symbol name (index into string table)
unsigned char st_info; // Symbol's type and binding attributes
unsigned char st_other; // Must be zero; reserved
Elf64_Half st_shndx; // Which section (header tbl index) it's defined in
Elf64_Addr st_value; // Value or address associated with the symbol
Elf64_Xword st_size; // Size of the symbol
}
其中 sym -> st_name
儲存字串表的位移,也就是說字串表的開頭 STRTAB(0x5) + sym -> st_name
= "printf" 的位址。
最後 dl-resolve
會使用這個字串去尋找實際位址,然後儲存至 reloc -> r_offset
即 .got.plt
的位址。
依據上述的流程,可以看出我們只要篡改字串表的內容,例如直接把 printf 改為 system,當程式在延遲綁定 printf()
時,實際上會將 system()
的位址寫入到 .got.plt
。這樣,雖然原始的指令是呼叫 printf()
,但由於綁定過程中相應位置的字串表被篡改,程式會執行 system()
,從而劫持控制流程並執行任意命令。
所以既然是藉由篡改內容而造成的攻擊,那就讓那部份的記憶體位址不能被寫就好了,所以就有了 RELRO(ReLocation Read-Only) 的機制,除了 .got.plt
設為可寫,讓實際地址可以寫上以外,其他所有相關 Sections,像是 .dynamic
、.got
都只可讀。這樣攻擊者就不能篡改字串了。
那不能改字串表就不能攻擊了嗎?只要能控制輸入,就可能發動攻擊,還記得我們提到的第二個參數 index
嗎?
如果我們控制 index
,讓程式認為重定位表位於一個可寫區段,並且我們在此可寫區段寫一個假的 reloc
、sym
以及字串表,是不是就能達成攻擊了呢?
所以整個攻擊思路如下:
index
,使得 reloc
位於可寫入位址。reloc
,使得 reloc -> r_info
能自己控制,因此能於此創建假的 sym
。sym -> st_name
寫入需要的字串,如 system
。如此一來,我們就能成功獲取控制權了。
從上面可以發現,此攻擊的根本在於延遲綁定的機制,因此 Full RELRO 保護機制是直接取消延遲綁定這個功能,程式一開始就直接全部綁定了。
因此阻絕了所有利用延遲綁定功能的攻擊,例如 ret2dlresolve。