在這個文章的開頭部分,我們先討論一下編碼的機制,加固的概念,尤其是棧的加固技巧。這裡解釋的一些概念也將在下面的部分中使用到,所以你可能至少要閱讀前幾段。
可執行區域保護
如第一部分所述,滲透常常會通過向程式中注入程式碼的方法,來使資料結構溢位,比如char
暫存器。接下來執行的程式碼就會跳轉到攻擊者可以進一步使用利用的記憶體位置,例如請求ROOT許可權的shell指令碼,(因此常稱作“shell
code”)。注意,即使程式碼是儲存在程式的資料記憶體空間中(棧或是堆),它仍然可以被執行。可執行區域保護通過將這些記憶體頁標記為需要它的可執行檔案來更改此操作。這在大多數現代處理器和大多數作業系統上都有支援。不同廠家的名稱各不相同,但基本概念保持不變。
尤其是Linux系統中,解決的措施如下:
(1)當程式被載入到記憶體中,只有這些記憶體頁中的程式碼才允許執行。載入器會識別ELF頭部。這些部分包含了一下程式碼:
1) .init 和 .fini:這是在初始化和清除程式是執行的程式碼;
2) .plt 和.plt.got: 用於訪問位於其他共享庫中的函式的Trampoline程式碼;
3) .text: 其他的,也就是程式中實現功能的程式碼;
(2)堆沒有執行許可權。
(3)ELF檔案的可執行原始碼和共享庫也包含了GNU_STACK程式頭部,也就意味著有執行棧記憶體頁面的許可權。預設情況下,不會設定執行標誌,而堆疊只具有讀/寫許可權。
有三個例外:
1)當連結到一個可執行檔案或共享庫,-z execstack聯結器會生成一個明確的標誌;
2)
至少有一個目標檔案是由彙編程式生成的。在這種情況下,還不知道堆疊是否可以在沒有執行許可權的情況下進行對映。需要一個明確的聲 明: .section .note.GNU-stack,"",@progbits;
3) 當你使用巢狀函式,有一個GNU C擴充套件(在GNU C++不可用);
可執行區域保護是很重要,幸運的是許多漏洞已經被處理了。接下來要做的是正確的gnu_stack設定,因此我們將重點放在這。
Linux使用最簡化的GNU_STACK,
這就意味著你的共享庫中的程式塊只要有一個被標記為需要一個可執行棧,那麼整個程式都會以這樣的方式執行。即使是 dlopen()庫也是一樣的--核心將在執行時更改許可權。!
從安全的角度,這樣會很不方便:你必須非常小心,不能在配置中將庫錯誤設定。不涉及彙編源程式情況下,當你自己把所有東西都建好,那麼應該就沒問題。可以從分佈伺服器載入的共享庫應該始終正確配置此標誌。也就是說,我建議做兩個測試:
1. 對於所有的可執行程式碼和共享庫,檢查他們的輸出readelf -l ,這gnu_stack入口只有RW設定標誌。應該像這樣的:
$ readelf -l libm.so.6
...
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
...
它不應該是這樣(注意E(執行)現在出現在這個標誌中)
$ readelf -l unprotected.so
...
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 10
...
2. 在啟動你的程式時可以測試一下,然後匯入所有的動態連結庫。檢查在/proc/PID/maps(其中pid由執行程式的數字程式ID替換)路徑下,[stack]是否正確對映。就如上面所提到的,此測試將排除某些庫在執行時更改堆疊許可權。執行的過程應該是這樣:
$ cat /proc/11684/maps
...
7ffe3401a000-7ffe3403c000 rw-p 00000000 00:00 0 [stack]
...
而不是這樣(注意:x(執行)現在存在):
$ cat /proc/11687/maps
...
7ffea187d000-7ffea189d000 rwxp 00000000 00:00 0 [stack]
...
注意:在JIT編碼器中存在一個漏洞,需要在執行時生成程式碼。程式設計人員通常會注意將程式碼對映到只寫/不可執行得記憶體空間,然後切換到只執行/防寫的記憶體空間。但一旦能夠成功誘導JIT編譯器生成程式碼時,這種做法也就失效了。
隨機地址分配機制
通過預防可執行程式碼被注入到程式中,可以消除很大部分的攻擊媒介。但是假如一些對攻擊者十分有用的程式碼已經儲存在程式中了要怎麼辦?例如,所有的程式都連結到libc中,這樣可以通過呼叫system()來啟動SHELL。攻擊者只要能覆蓋棧指標中的函式返回地址,讓指標指向system()的地址。這是記憶體地址佈局隨機化的本質:分為許多記憶體對映儘可能使系統無法預知的。在現代Linux系統中,這些區域受到影響:
主程式中可執行的程式碼
共享庫的程式碼
堆和棧
mmap_base
VDSO 檔案
部分核心本身
ASLR是在Linux核心中用到的。一般是在64位系統中使用,因為在可用的地址空間中隨機儲存會使地址範圍大得多,極大地降低了遍歷攻擊的機會。如果擁有ROOT許可權可以禁用ASLR:
# echo 0 > /proc/sys/kernel/randomize_va_space
這對除錯來說很有用,但是它不應該被永遠禁用。. gdb本身會預設禁用ASLR,讓接下來的除錯對話方塊中得地址都是恆定的。
堆和棧的隨機分佈是自動的,所以在當前這樣的系統不需要執行什麼命令。 為了能夠在隨機地址中載入主可執行檔案和共享庫,它們的程式碼必須是位置無關的。這實現如下:
共享庫
使用-fpic 或-fPIC編譯。-fpic在一些結構中是使用一個範圍有限的GOT(全域性偏置表)。 當地址溢位時,聯結器就會提醒你。這時你就要轉換到-fPIC ,這可能會有一些開銷。在X86系統中,這兩者是沒差別的;
連結到-shared,指定和編譯期間使用的相同選項(-fpicor-fPIC).;
只要有一個檔案不是以 -fpic/-fPIC生成的,連結就會失敗。共享庫必須是與地址無關,否則,不同庫的指標就可能會指向相同的記憶體地址,產生衝突。
可執行檔案
使用 -fpieor-fPIE編譯( 和編譯共享庫相同的區別,見上面 );
連結到-shared,指定和編譯期間使用的相同選項(-fpicor-fPIC).;
不幸的是,在一些比較熱門的編譯系統中,為連結到可執行檔案或共享庫中的原始碼設定不同的編譯選項是很麻煩的。所以,你在編譯時還是會用-fpic/ -fPIC。唯一不同的是符號將會被覆蓋,但這隻會在你使用LD_PRELOAD,會有影響,因為只有這些程式會在主函式之前被載入。在至少有一個GCC編譯器的情況下也推薦這樣做。
下面是一個程式連結到共享庫的完整例子,地址無關程式碼:
main.cpp
double pi();
int main()
{
return (int)pi();
}
shared.cpp
double pi()
{
return 3.14f;
}
Building with full ASLR support:
# Build and link shared library
$ g++ -c -fPIC shared.cpp -o shared.o
$ g++ -shared -fPIC shared.o -o libshared.so
# Build and link main executable
$ g++ -c -fPIE main.cpp -o main.o
$ g++ -pie -fPIE main.o -L. -lshared -o main
main 是一個可執行檔案, 但因為完全被重定向了,所以它生成的檔案和共享庫一樣。
$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.0.0, BuildID[sha1]=9fabc139c49f30
為了對比, 沒有-pie選項:
$ g++ main.o -L. -lshared -o main_no_pie
$ file main_no_pie
main_no_pie: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.0.0, BuildID[sha1]=8d7792
這有兩篇著名的對ASLR攻擊論文
Jump over ASLR: Attacking branch predictors to bypass ASLR
ASLR⊕Cache
也就是說,這些攻擊比攻擊未受保護的二進位制檔案要付出更多代價。ASLR的開銷非常低,所以沒有理由不採用。
有了gcc7,就多加了一個-static-pie 選項。這樣的可執行檔案,不依賴於其他共享庫,並可以在任意地址載入。
本文由看雪翻譯小組 南極小蝦 編譯,來源fireeye@Nick Harbour 轉載請註明來自看雪社群