轉自:https://hardenedlinux.github.io/system-security/2016/06/01/NX-analysis.html
NX(No-eXecute)的實現分析
Shawn: GNU/Linux系統級攻防在歷史上曾經停留在用戶空間很長的時間,經歷了NX/COOKIE/PIE/ASLR/RELRO的進化后后0ldsk00l以及security “researcher”們已經無法通過用戶空間觸及到“上帝寶藏”(-_root_-),sgrakkyu和twzi在Phrack Issue 64中的Attacking the Core標志着這個領域正式進入了內核層面的對抗,10年過去了,在新的時代性背景下(Android/IoT/TEE),人們意識到安全應該是一個整體(again?WTH),而單純依賴於內核層面的攻防無法解決很多老問題,傳統的mitigation技術再次在某些場景化的方案中受到重視,NX(armv6中是XN)是其中之一,棧的不可執行最早是由PaX team實現的PAGEEXEC和SEGEXEC,后來Intel CPU在硬件上支持NX后Ingo Molnar給出了硬件NX的第一版實現給Fedora的用戶嘗鮮,后來則進入了Linux mainline。這篇文檔詳細的分析了GCC/ld/kernel三個層面的NX的工作路線圖。Enjoy it!
NX(No-eXecute)的實現分析
@(mitigation)[NX|gcc|binutils|kernel] –zet
00 導引
以下的代碼分析僅限linux kernel/gcc/GNU binutils-as/GNU binutils-ld/ELF.
在計算機安全領域一個很經典的話題就是緩沖區溢出(Buffer Overflow).緩沖區溢出一般時 候伴隨着攻擊者的篡改堆棧里保存的返回地址,然后執行注入到stack中的shellcode,攻擊者 可以發揮想象力仔細編寫shellcode進行下一步的攻擊,直到完全控制了計算機.這種攻擊之 所以能夠成功主要原因就是因為stack里的shellcode的可執行.所以主要的防御手段 (mitigation)就是禁止stack里數據的執行(noexecstack).
Noexecstack的實現主要出現在兩個地方: compiler-assembler-linker(這里表示一個生成 binary的過程: 編譯->匯編->鏈接器)里和kernel里.在compiler-assembler-linker里的實 現基本上的純粹的軟件實現,結果是在elf的一個stack的section里置位不可以執行.但是捕 獲違反stack不可執行這個問題是在kernel里.
在kernel里的實現,隨着處理器在(頁模式)paging處理過程中涉及到功能寄存器中引入 No-eXecute的配置位,所以實際上kernel在實現NX的時候是在相關的寄存器里置NX的位,在 CPU操作的時候由硬件來做是否可以執行的檢查.
本文的描述描述順序是先描述NX在gcc/binutils里的實現,然后再描述在kernel里的實現.
本文的分析對應的gcc版本是6.1.0,binutils版本是2.26,linux kernel的版本是4.6
01 NX在gcc/binutils里面的實現
在gcc/ld里面有NX相關的選擇,gcc/ld都是-z execstack/noexecstack,在gcc 6.1 manual里 跟-z相關的內容如下:
3.14 Options for Linking: -z keyword -z is passed directly on to the linker along with the keyword keyword. See the section in the documentation of your linker for permitted values and their meanings. –gcc 6.1 manual
也就是說gcc將參數-z execstack/noexecstack直接傳給了ld(linker).
gcc -### -z execstack test.c
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
Target: x86_64-linux-gnu
/usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 -quiet -imultiarch x86_64-linux-gnu
test.c -quiet -dumpbase test.c "-mtune=generic" "-march=x86-64" -auxbase test
-fstack-protector -Wformat -Wformat-security -o /tmp/ccgX6EXC.s
// 調用as
as --64 -o /tmp/ccVl7H5u.o /tmp/ccgX6EXC.s
// 調用collect2
/usr/lib/gcc/x86_64-linux-gnu/4.8/collect2 "--sysroot=/" --build-id
--eh-frame-hdr -m elf_x86_64 "--hash-style=gnu" --as-needed -dynamic-linker
/lib64/ld-linux-x86-64.so.2 -z relro
// 傳入的參數
-z execstack
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/4.8
-L/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu
-L/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../lib -L/lib/x86_64-linux-gnu
-L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib
-L/usr/lib/gcc/x86_64-linux-gnu/4.8/../../.. /tmp/ccVl7H5u.o -lgcc --as-needed
-lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed
/usr/lib/gcc/x86_64-linux-gnu/4.8/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crtn.o
接下來將會詳細描述gcc將-z execstack這一參數怎樣傳給ld(linker)以及這一參數對生成 的ELF文件產生怎樣的影響,最后將會分析這樣的影響是如何導致stack被執行在kernel里捕 獲的.
NX在gcc里的處理
由上面的gcc -###輸出可知道gcc只是一個外殼管理程序,嚴格來說是一個 driver,根據傳入的參數來控制各個compile/assemble/link過程.在gcc實現里 compile過程由cc1來完成,assemble由GNU as完成,link由 GNU ld來完成(GNU社區里有一個備選的鏈接器:gold).
當gcc遇到-z execstack這樣的選項時的處理代碼如下:
在我的機器上gcc的目錄是$HOME/github/gcc
export $SRC=$HOME/github/gcc
// $SRC/gcc/gcc-main.c
// gcc driver的入口代碼
int
main (int argc, char **argv)
{
driver d (false, /* can_finalize */
false); /* debug */
return d.main (argc, argv);
}
// src/gcc/gcc.c
/* driver::main is implemented as a series of driver:: method calls. */
int
driver::main (int argc, char **argv)
{
bool early_exit;
set_progname (argv[0]); // 對調用gcc時指定的名字做處理,刪掉前面的目錄名,不需
要過多研究expand_at_files (&argc, &argv); // 對參數做一些擴展處理
decode_argv (argc, const_cast <const char **> (argv)); // 下面分析
// 下面的這些函數都是在做gcc的常規處理,跟本文主題關系不大
global_initializations ();
build_multilib_strings ();
set_up_specs ();
putenv_COLLECT_GCC (argv[0]);
maybe_putenv_COLLECT_LTO_WRAPPER ();
maybe_putenv_OFFLOAD_TARGETS ();
handle_unrecognized_options ();
if (!maybe_print_and_exit ())
return 0;
early_exit = prepare_infiles ();
if (early_exit)
return get_exit_code ();
do_spec_on_infiles (); // 這里會調用cc1和as
maybe_run_linker (argv[0]); // 這里會調用ld
final_actions ();
return get_exit_code ();
}
由上面的代碼注釋可知,我們需要研究3個main入口里的函數,下面依次進行分析.由於代碼過 多,下面的分析會刪掉跟本文主題無關的代碼,函數調用關系由->表示.如果不做申明,源 代碼位於src/gcc/gcc.c這個文件里.
在分析之前需要說清楚另外一個問題,那就是-z這個參數的定義問題.
cd $HOME/github/gcc
mkdir build
cd build
../configure --prefix="$HOME/bin" --disable-nls --enable-languages=c,c++
make -j8
make install
編譯完成之后,會在build/gcc里有兩個跟本文的主題有關的文件: options.c/options.h.這兩個文件跟gcc的編譯選項處理有很大關系,這兩個文件的 生成是幾個$SRC/gcc里的awk腳本共同作用的結果.
// Makefile.in
// 注意這里的輸入是$(ALL_OPT_FILES)
optionlist: s-options ; @true
s-options: $(ALL_OPT_FILES) Makefile $(srcdir)/opt-gather.awk
$(AWK) -f $(srcdir)/opt-gather.awk $(ALL_OPT_FILES) > tmp-optionlist
$(SHELL) $(srcdir)/../move-if-change tmp-optionlist optionlist
$(STAMP) s-options
options.c: optionlist $(srcdir)/opt-functions.awk $(srcdir)/opt-read.awk \
$(srcdir)/optc-gen.awk
$(AWK) -f $(srcdir)/opt-functions.awk -f $(srcdir)/opt-read.awk \
-f $(srcdir)/optc-gen.awk \
-v header_name="config.h system.h coretypes.h options.h tm.h" < $< > $@
options.h: s-options-h ; @true
s-options-h: optionlist $(srcdir)/opt-functions.awk $(srcdir)/opt-read.awk \
$(srcdir)/opth-gen.awk
$(AWK) -f $(srcdir)/opt-functions.awk -f $(srcdir)/opt-read.awk \
-f $(srcdir)/opth-gen.awk \
< $< > tmp-options.h
$(SHELL) $(srcdir)/../move-if-change tmp-options.h options.h
$(STAMP) $@
// options.h/options.c輸入文件,也就時生成gcc選項處理代碼的配置文件.加選項只需要
// 修改這些配置文件,很方便.
# All option source files
ALL_OPT_FILES=$(lang_opt_files) $(extra_opt_files)
// 編譯選項配置文件
lang_opt_files=@lang_opt_files@ $(srcdir)/c-family/c.opt $(srcdir)/common.opt
由上面的代碼可以知道編譯選項的生成過程是輸入配置文件,然后awk腳本處理配置文件,然 后輸出options.h/options.c
// $SRC/gcc/common.opt里關於-z的內容如下:
// 注意z底下的三個配置,Driver表示這是一個driver處理的選項(考慮一些debug的配置選
項),Joined/Separate表示-z與跟-z本身相對於的參數(在本文中當然是指execstack/
noexecstack)之間需不需要空白符隔開.
z
Driver Joined Separate
相應的生成代碼是:
OPT_z = 1251, /* -z */
接着進行gcc對參數處理的分析
// 處理調用gcc時的參數存入decoded_options數組里
decode_argv()->decode_cmdline_options_to_array()->decode_cmdline_option()
static unsigned int
decode_cmdline_option (const char **argv, unsigned int lang_mask,
struct cl_decoded_option *decoded)
{
// awk處理參數配置文件時會將這些參數存進一個數組里cl_options[]
// 這里argv[0] + 1的值是'z',由此來找到-z在cl_options數組里的索引值
opt_index = find_opt (argv[0] + 1, lang_mask);
// const struct cl_option *option
option = &cl_options[opt_index];
// -z在配置文件里的定義是Joined Separate,所以會進入這個代碼塊
if (joined_arg_flag)
{
// 注意下面的+1,arg的值會是"-z"里z之后的下一個字符,是'\0'
arg = argv[extra_args] + cl_options[opt_index].opt_len + 1 + adjust_len;
//cl_missing_ok表示-z后面不接參數是否可以,顯然是不行
if (*arg == '\0' && !option->cl_missing_ok)
{
// -z的另外一個配置:Separate
if (separate_arg_flag)
{
// 這里arg的值應該是"execstack"
arg = argv[extra_args + 1];
result = extra_args + 2;
if (arg == NULL)
result = extra_args + 1;
else
have_separate_arg = true;
}
else
/* Missing argument. */
arg = NULL;
}
}
// 刪掉無關代碼
// 這個結構是要傳會給調用者的
decoded->opt_index = opt_index; // OPT_z在cl_options[]里的索引
decoded->arg = arg; // "execstack"
decoded->value = value;
decoded->errors = errors;
decoded->warn_message = warn_message;
// 后面的代碼會處理別的參數,在本文的例子里就是待編譯的文件:test.c
gcc處理完參數之后會進行對輸入文件的各種處理.當然在上面的分析中可知輸入文件也是 被處理參數的代碼處理的,只不過decoded->opt_index表示這就是輸入文件.
gcc driver對cc1/as/ld的調用都是通過一個spec文件來進行的,也是一種配置文件. spec配置的語法定義於 [gcc manual: 3.19 Specifying Subprocesses and the Switches to Pass to Them] (https://gcc.gnu.org/onlinedocs/gcc/Spec-Files.html), 與本文涉及到的比較重要的語法如下:
%a 處理as的相關調用. 默認的spec文件叫: asm
%A 處理as相關的調用,默認的spec文件是: asm_final
%(name) 類似於宏替換,將之前定義的name在這里展開
%{S} 當選項S給出時,用-S替換S,注意這里的S是元字符
%{S:X} 對X進行替換操作,當選項-S給出時
%{!S:X} 對X進行替換操作,當選項-S沒有給出時
gcc driver對可以調用的子工具的存儲在一個統一的數組里.其中compiler->spec就是 相應工具默認的調用參數.
/* The default list of file name suffixes and their compilation specs. */
static const struct compiler default_compilers[] =
{
// 只留下部分代表性的數據
{".cc", "#C++", 0, 0, 0}, {".cxx", "#C++", 0, 0, 0},
{".cpp", "#C++", 0, 0, 0}, {".cp", "#C++", 0, 0, 0},
{".c++", "#C++", 0, 0, 0}, {".C", "#C++", 0, 0, 0},
{".CPP", "#C++", 0, 0, 0}, {".ii", "#C++", 0, 0, 0},
{".ads", "#Ada", 0, 0, 0}, {".adb", "#Ada", 0, 0, 0},
{".go", "#Go", 0, 1, 0},
/* Next come the entries for C. */
{".c", "@c", 0, 0, 1},
// 這里是cc1的spec文件
{"@c",
"%{E|M|MM:%(trad_capable_cpp) %(cpp_options) %(cpp_debug_options)}\
%{!E:%{!M:%{!MM:\
%{traditional:\
%eGNU C no longer supports -traditional without -E}\
%{save-temps*|traditional-cpp|no-integrated-cpp:%(trad_capable_cpp) \
%(cpp_options) -o %{save-temps*:%b.i} %{!save-temps*:%g.i} \n\
cc1 -fpreprocessed %{save-temps*:%b.i} %{!save-temps*:%g.i} \
%(cc1_options)}\
%{!save-temps*:%{!traditional-cpp:%{!no-integrated-cpp:\
cc1 %(cpp_unique_options) %(cc1_options)}}}\
// 注意這里!fsyntax-only:%(invoke_as)表示,如果-fsyntax-only沒有指定,那
// 么就調用as(invoke_as),invoka_as 將會在這里在這里展開,invoke_as定
// 義見下面.
%{!fsyntax-only:%(invoke_as)}}}}", 0, 0, 1},
{"-",
"%{!E:%e-E or -x required when input is from standard input}\
%(trad_capable_cpp) %(cpp_options) %(cpp_debug_options)", 0, 0, 0},
// 當gcc -S時
{".s", "@assembler", 0, 0, 0},
{"@assembler",
"%{!M:%{!MM:%{!E:%{!S:as %(asm_debug) %(asm_options) %i %A }}}}", 0, 0, 0},
#include "specs.h"
/* Mark end of table. */
{0, 0, 0, 0, 0}
};
// 當gcc編譯完輸入文件之后,調用as時的driver spec定義.
static const char *invoke_as =
#ifdef AS_NEEDS_DASH_FOR_PIPED_INPUT
"%{!fwpa*:\
%{fcompare-debug=*|fdump-final-insns=*:%:compare-debug-dump-opt()}\
// 在下面可以看到很明顯的as調用.
%{!S:-o %|.s |\n as %(asm_options) %|.s %A }\
}";
#else
"%{!fwpa*:\
%{fcompare-debug=*|fdump-final-insns=*:%:compare-debug-dump-opt()}\
%{!S:-o %|.s |\n as %(asm_options) %m.s %A }\
}";
#endif
接着將會是gcc driver調用相應的工具程序處理輸入源文件.由上面的spec可以看到默 認的cc1/as調用以輸出匯編代碼.
/* 處理輸入源文件,根據相應的工具程序的spec輸出匯編代碼*/
void
driver::do_spec_on_infiles () const
{
size_t i;
for (i = 0; (int) i < n_infiles; i++)
{
// 根絕輸入文件的后綴查找編譯器,就是找到上面的default_compilers[]里面的一個
input_file_compiler
= lookup_compiler (infiles[i].name, input_filename_length,
infiles[i].language);
if (input_file_compiler) {
if (input_file_compiler->spec[0] == '#')
;
else {
int value;
// 根據spec文件來調用相應的工具程序,這里會輸出匯編
value = do_spec (input_file_compiler->spec);
infiles[i].compiled = true;
}
}
}
最后將是gcc調用linker來處理匯編代碼,在這里本文將會研究-z execstack的傳遞過程.
driver::maybe_run_linker() -> do_spec(link_command_spec);
#define link_command_spec LINK_COMMAND_SPEC
#define LINK_COMMAND_SPEC "\
%{!fsyntax-only:%{!c:%{!M:%{!MM:%{!E:%{!S:\
// linker在這里定義為collect2,其實只是一個GNU ld的包裝
%(linker) " \
LINK_PLUGIN_SPEC \
"%{flto|flto=*:%<fcompare-debug*} \
%{flto} %{fno-lto} %{flto=*} %l " LINK_PIE_SPEC \
"%{fuse-ld=*:-fuse-ld=%*} " LINK_COMPRESS_DEBUG_SPEC \
"%X %{o*} %{e*} %{N} %{n} %{r}\
// 這里第4個就是本文關注的-z選項
%{s} %{t} %{u*} %{z} %{Z} %{!nostdlib:%{!nostartfiles:%S}} \
%{static:} %{L*} %(mfwrap) %(link_libgcc) " \
VTABLE_VERIFICATION_SPEC " " SANITIZER_EARLY_SPEC " %o " CHKP_SPEC " \
%{fopenacc|fopenmp|%:gt(%{ftree-parallelize-loops=*:%*} 1):\
%:include(libgomp.spec)%(link_gomp)}\
%{fcilkplus:%:include(libcilkrts.spec)%(link_cilkrts)}\
%{fgnu-tm:%:include(libitm.spec)%(link_itm)}\
%(mflib) " STACK_SPLIT_SPEC "\
%{fprofile-arcs|fprofile-generate*|coverage:-lgcov} " SANITIZER_SPEC " \
%{!nostdlib:%{!nodefaultlibs:%(link_ssp) %(link_gcc_c_sequence)}}\
%{!nostdlib:%{!nostartfiles:%E}} %{T*} \n%(post_link) }}}}}}"
接着本文將會分析當遇到LINK_COMMAND_SPEC里的%{z}時進行的操作.
do_spec()->do_spec_2()->do_spec_1()
static int
do_spec_1 (const char *spec, int inswitch, const char *soft_matched_part) {
// 在這里本文關注的spec將會是%{z}
const char *p = spec;
while ((c = *p++))
switch (inswitch ? 'a' : c) {
case '%':
switch (c = *p++)
case '{':
p = handle_braces (p);
break;
do_spec_1()->handle_braces()
static const char *
handle_braces (const char *p) {
// 標記"z}"的起始和結束
atom = p;
while (ISIDNUM (*p) || *p == '-' || *p == '+' || *p == '='
|| *p == ',' || *p == '.' || *p == '@')
p++;
end_atom = p;
// p當前的值應該是'}'
switch (*p) {
case '&': case '}':
/**
struct switchstr {
const char *part1;
const char **args;
unsigned int live_cond;
bool known;
bool validated;
bool ordering;
};
struct switchstr switches[];
在這里的時候switches[]里面存儲的是調用gcc時候的參數,其中有一項是{"z", &"execstack",}
根據'z'在switches[]里面置part1是"z"的這一項的ordering為1 */
mark_matching_switches (atom, end_atom, a_is_starred);
if (*p == '}')
process_marked_switches ();
break;
}
}
do_spec_1()->handle_braces()->process_marked_switches()
static inline void
process_marked_switches (void) {
int i;
for (i = 0; i < n_switches; i++)
// 根據上面的ordering的標記調用give_switch (i, 0)
if (switches[i].ordering == 1) {
switches[i].ordering = 0;
give_switch (i, 0);
}
}
do_spec_1()->handle_braces()->process_marked_switches()->give_switch()
static void
give_switch (int switchnum, int omit_first_word) {
if (!omit_first_word) {
do_spec_1 ("-", 0, NULL);
// 這里的part1是"z",這個函數最終的處理會將"z"壓入一個類似於C++ STL vertor
// 的容器argbuf里
do_spec_1 (switches[switchnum].part1, 1, NULL);
}
if (switches[switchnum].args != 0) {
const char **p;
for (p = switches[switchnum].args; *p; p++) {
const char *arg = *p;
// 這里arg的值將會是"execstack",這個函數會將execstack壓入argbuf里,到
// 這里argbuf的值已經是"z execstack"了,由上面的link_command_spec定義
// 中的%{z}和spec文件的相關語義,gcc最終對linker的調用將會是:
// collect2 -z execstack ... 這個樣子的
do_spec_1 (arg, 1, NULL);
}
}
// ...
}
NX在ld里面的處理
下面將會分析linker遇到-z execstack時進行怎樣的處理.對生成的ELF文件產生怎樣的 影響.
在binutils/include/elf/common.h里與execstack/noexecstack相關的定義如下:
#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
由於篇幅所限,下面僅僅分析當ld被調用時與-z execstack相關的代碼.
/**
bfd_link_info里分別有兩個位域:
unsigned int execstack: 1;
unsigned int noexecstack: 1; */
struct bfd_link_info link_info;
// GUN linker的入口函數
int
main (int argc, char **argv) {
// 給bfd_link_info賦一些默認值
// 處理參數,不過-z execstack的處理代碼是在binutils里架構相關的結構里定義的
parse_args (argc, argv);
// 做一些分配地址之前的准備工作
lang_process ();
// 生成一個elf文件
ldwrite ();
// ...
}
main()->parse_args()->ldemul_handle_option()->ld_emulation->handle_option()
/** ld_emulation是一個跟架構相關的結構,binutils里面根據后端的不同分開定義是為了
移植和feature的方便,在項目里是很常見的工程設計.*/
// EMULATION_NAME是一個類似於elf_x86_64這樣的名字
static bfd_boolean
gld${EMULATION_NAME}_handle_option (int optc) {
switch (optc) {
case 'z':
if (strcmp (optarg, "execstack") == 0) {
// 在link_info里保存置相關的位
link_info.execstack = TRUE;
link_info.noexecstack = FALSE;
} else if (strcmp (optarg, "noexecstack") == 0) {
link_info.noexecstack = TRUE;
link_info.execstack = FALSE;
}
// ...
}
}
main()->lang_process ()->ldemul_before_allocation()->ld_emulation->before_allocation()
// ld_emulation的初始化定義為:
struct ld_emulation_xfer_struct ld_${EMULATION_NAME}_emulation =
{
gld${EMULATION_NAME}_before_parse,
syslib_default,
hll_default,
after_parse_default,
after_open_default,
after_allocation_default,
set_output_arch_default,
ldemul_default_target,
// ld_emulation->before_allocation()的調用會是這個函數
gld${EMULATION_NAME}_before_allocation,
//...
};
gld${EMULATION_NAME}_before_allocation()->bfd_elf_size_dynamic_sections()
bfd_boolean
bfd_elf_size_dynamic_sections (bfd *output_bfd,
const char *soname,
const char *rpath,
const char *filter_shlib,
const char * const *auxiliary_filters,
struct bfd_link_info *info,
asection **sinterpptr,
struct bfd_elf_version_tree *verdefs) {
// 根據參數的分析結果,也就是bfd_link_info結構中的execstack/noexecstack來置位
// stack_flags
if (info->execstack)
elf_tdata (output_bfd)->stack_flags = PF_R | PF_W | PF_X;
else if (info->noexecstack)
elf_tdata (output_bfd)->stack_flags = PF_R | PF_W;
// ...
}
ld_write()將會是linker的最后一步,正確執行完將會得到一個目標文件(比如說ELF 格式的可執行文件)
main->ld_write()->bfd_final_link()->bfd_elf_final_link()
->_bfd_elf_compute_section_file_positions()
->assign_file_positions_except_relocs()->assign_file_positions_for_segments()
->map_sections_to_segments()
// 這個結構描述section到segment的對應關系
struct elf_segment_map
{
/* Next program segment. */
struct elf_segment_map *next;
unsigned long p_type;
unsigned long p_flags;
bfd_vma p_paddr;
unsigned int p_flags_valid : 1;
unsigned int p_paddr_valid : 1;
unsigned int includes_filehdr : 1;
unsigned int includes_phdrs : 1;
/* 這個segment包含的section數目*/
unsigned int count;
/* Sections*/
asection *sections[1];
};
map_sections_to_segments() {
// 刪掉跟本文無關的代碼
struct elf_segment_map *m;
if (elf_tdata (abfd)->stack_flags) {
amt = sizeof (struct elf_segment_map);
m = bfd_zalloc (abfd, amt);
if (m == NULL)
goto error_return;
m->next = NULL;
m->p_type = PT_GNU_STACK;
/** 根據stack_flags來置位segment的p_flags,最終這個值就是ELF文件的
Program Header里的p_flag的值,在ELF1.2標准里定義的可選值是:
PF_X 0x1
PF_W 0x2
PF_R 0x4
*/
m->p_flags = elf_tdata (abfd)->stack_flags;
m->p_flags_valid = 1;
*pm = m;
pm = &m->next;
}
}
NX在kernel里的捕獲
上面已經介紹了gcc/ld里對-z execstack的處理,總共的影響就是在ELF文件里對應的 program header里置相關的位.下面將會描述在ELF文件里這樣的置位前提下,如果違反了 訪問規則,kernel如何捕獲非法訪問.
一般來說kernel執行一個binary的時候會進行下面的代碼調用鏈:
do_execve()->search_binary_handler()->linux_binfmt.load_binary()
// elf文件的情況下load_binary()實際是就是調用load_elf_binary()
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) {
int executable_stack = EXSTACK_DEFAULT;
// bprm->buf里存儲的就是elf文件的二進制流,讀入elf header
loc->elf_ex = *((struct elfhdr *)bprm->buf);
// e_phnum表示program header的數目,這里分配的存儲是為了容納elf里的
// program header在進程地址空間里
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
retval = -ENOMEM;
elf_phdata = kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
// 讀入elf文件program header進入地址空間
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
(char *)elf_phdata, size);
// ...
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
if (elf_ppnt->p_type == PT_GNU_STACK) {
// 在ld里置位的p_flags
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
}
//...
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
load_elf_binary()->setup_arg_pages()
// 處理加載的elf對應的進程的初始stack對應的vm_area_struct
int setup_arg_pages(struct linux_binprm *bprm,
unsigned long stack_top,
int executable_stack) {
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma = bprm->vma;
unsigned long vm_flags;
// ...
if (unlikely(executable_stack == EXSTACK_ENABLE_X))
vm_flags |= VM_EXEC;
else if (executable_stack == EXSTACK_DISABLE_X)
vm_flags &= ~VM_EXEC;
vm_flags |= mm->def_flags;
vm_flags |= VM_STACK_INCOMPLETE_SETUP;
// vm_flags里的位會加入到vma里
ret = mprotect_fixup(vma, &prev, vma->vm_start, vma->vm_end,
vm_flags);
// ...
注意上面建立的vm_area_struct只是存在於進程的虛擬地址空間里.並沒有映射實際的RAM, 當這個ELF對stack進行訪問時就會進入page fault,處理代碼就是do_page_fault()
void do_page_fault(struct pt_regs *regs, unsigned long error_code) {
// 忽略掉跟本文討論無關的一系列kernel的檢查過程
// 對於我們剛剛映射的stack VMA來說會執行到這里
good_area:
// access_error()會對vma進行常規檢查
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}
// ...
}
do_page_fault()->access_error()
static inline int
access_error(unsigned long error_code, struct vm_area_struct *vma)
{
if (error_code & PF_WRITE) {
/* write, present and write, not present: */
if (unlikely(!(vma->vm_flags & VM_WRITE)))
return 1;
return 0;
}
/* read, present: */
if (unlikely(error_code & PF_PROT))
return 1;
// 假如調用gcc編譯elf文件時給出的參數是-z noexecstack,那么stack
// vma->flags的VM_EXEC位肯定是清除了的.如果是這種情況,那么access_error()
// 返回1,do_page_fault()也會返回上一級,對應的必將是kernel的報錯.
if (unlikely(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE))))
return 1;
return 0;
}
NX軟件實現小結
總結一下前面的內容,就是當調用gcc -z execstack test.c時,gcc將參數打包處理傳給ld, 由於參數的影響,ld會在生成的ELF文件stack對應的program header里置位p_flags的PF_X值, 當ELF文件執行時,由於RAM需要分配就會觸發page fault,然后處理do_page_fault()函數里 調用access_error()以捕獲到stack的執行權限錯誤.
02 NX在kernel/CPU里的實現:
NX在CPU里面的實現跟硬件有很大的關系.所以下面的描述先從硬件相關的寄存器開始描述, 然后進行kernel層面的描述.
NX相關的寄存器
Intel® 64 and IA-32 Architectures Developer’s Manual - System Programming Guide 2.2.1 Extended Feature Enable Register(EFER) IA32_EFER MSR提供了一些IA32e模式相關的使能配置位,還有另外一些位是跟page-access權 限相關的. typedef struct IA32_EFER { long SYSCALL_Enable : 1; // Enables SYSCALL/SYSRET instructions in 64-bit mode long Reserved : 7; // Reserved long IA-32e_Mode_Enable : 1; // Enables IA-32e mode operation long Reserved : 1; // Reserved long IA-32e_Mode_Active : 1; // Indicates IA-32e mode is active when set. long Execute_Disable_Bit_Enable : 1 // Enables page access restriction by preventing instruction // fetches from PAE pages with the XD bit set. // 我們感興趣的這一位,也就是第12位,這一位也叫作NXE long : 0; } IA32_EFER;
IA32_EFER.NXE僅僅對PAE和IA-32e模式起作用(因為只有PAE/IA-32e模式下的paging單元(頁 表項/頁目錄表項)是64位的).如果IA32_EFER.NXE = 1,從某一線性地址處的指令預取將會被 禁止,即使這一線性地址處的數據訪問是允許的.
如果CPUID.80000001H:EDX.NX [bit 20] = 1, IA32_EFER.NXE才能夠被設置為1,不支持 CPUID.80000001H的處理器IA32_EFER.NXE不能被設置為1.
4.4.2 Linear-Address Translation with PAE Paging 在PAE paging中,如果IA32_EFER.NXE = 0且PDE/ PTE的P是1, 則XD(63位)是保留的.
(PAE/PTE).63 (XD)跟IA32_EFER.NXE的功能是類似的,只不過存在於頁表寄存器/頁目錄表 寄存器的最高位.
由上面的內容可以知道,要想在MMU層面使用NX,首先需要檢測 CPUID.80000001H:EDX.NX [bit 20]是否為1,如果是1進行IA32_EFER.NXE的置位使能,然后按 照需在PAE/PTE里使能第63位(XD).
NX在kernel里的實現
// 在arch/x86/mm/Setup_nx.c有如下的代碼:
static int disable_nx;
/*
* noexec = on|off
*
* Control non-executable mappings for processes.
*
* on Enable
* off Disable
*/
static int __init noexec_setup(char *str)
{
if (!str)
return -EINVAL;
if (!strncmp(str, "on", 2)) {
disable_nx = 0;
} else if (!strncmp(str, "off", 3)) {
disable_nx = 1;
}
x86_configure_nx();
return 0;
}
// 注冊到kernel的啟動組件里,可以在boot參數里配置是否啟用noexec
early_param("noexec", noexec_setup);
void x86_configure_nx(void)
{
if (boot_cpu_has(X86_FEATURE_NX) && !disable_nx)
__supported_pte_mask |= _PAGE_NX;
else
__supported_pte_mask &= ~_PAGE_NX;
}
// __supported_pte_mask的初始定義
pteval_t __supported_pte_mask __read_mostly = ~0;
// _PAGE_NX的定義與PAE/PTE的第63位(XD)對應
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_PAE)
#define _PAGE_NX 1 << 63
MMU實現NX的在三層的paging結構中是類似的.下面的描述以PTE為代表來進行.
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
// 創建一個能進入PTE的項,對於本文的NX描述來說,這個值肯定是64位的.
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
massage_pgprot(pgprot));
}
static inline pgprotval_t massage_pgprot(pgprot_t pgprot)
{
pgprotval_t protval = pgprot_val(pgprot);
if (protval & _PAGE_PRESENT)
// 這里對每一個實際作用的訪問(_PAGE_PRESENT置位),pte_t(最終是要寫入
// PTE項的)的值的產生都要經過__supported_pte_mask,這個變量里按需存儲
// 了是否使用_PAGE_NX的信息.最終的pte_t的值會寫入PTE項.
protval &= __supported_pte_mask;
return protval;
}
寫入PAT/PTE之后,就是CPU自己的操作了,paging之前CPU會檢查相應的置位,以決定是否預取 指令.
03 總結
上面我們詳細描述了NX在整個系統層的實現細節.在系統安全領域由於stack作為一個可以寫 的存儲區所以很容易作為攻擊者的目標,stack overflow作為經典而且古老的攻擊方式給 stack植入shellcode然后因為stack的可執行性,讓攻擊的門檻非常低.后來引入了canary的 機制,但是canary容易被bypass,真正解決這個問題就是NX的引入,所以NX其實是stack overflow攻擊的最重要的解決方案.
live long and prosper