在這個文章的開頭部分,我們先討論一下編碼的機制,加固的概念,尤其是棧的加固技巧。這裡解釋的一些概念也將在下面的部分中使用到,所以你可能至少要閱讀前幾段。
可執行區域保護
如第一部分所述,滲透常常會通過向程序中注入代碼的方法,來使數據結構溢出,比如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 選項。這樣的可執行文件,不依賴於其他共享庫,並可以在任意地址加載。
每天會更新論文和視頻,還有如果想學習c++知識在晚上8.30免費觀看這個直播:https://ke.qq.com/course/131973#tuin=b52b9a80
閱讀更多 IT布丁老師 的文章