深入考察解釋型語言背后隱藏的攻擊面,Part 2(三)
接上文:
- 《深入考察解釋型語言背后隱藏的攻擊面,Part 1(上)》
- 《深入考察解釋型語言背后隱藏的攻擊面,Part 1(下)》
- 《深入考察解釋型語言背后隱藏的攻擊面,Part 2(一)》
- 《深入考察解釋型語言背后隱藏的攻擊面,Part 2(二)》
在本文中,我們將深入地探討,在通過外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫“粘合”到解釋語言的過程中,安全漏洞是如何產(chǎn)生的。
決定最終的漏洞利用策略
為了弄清楚如何在觸發(fā)我們的堆內(nèi)存覆蓋之前對data_數(shù)組進行最佳定位,我們需要檢查一下堆的狀態(tài)。到目前為止,我們有兩個感興趣的目標:png_ptr結(jié)構(gòu)體和運行時解析器正在使用的動態(tài)鏈接器數(shù)據(jù)。
如果我們檢查png_ptr結(jié)構(gòu)體數(shù)據(jù)所在的堆分塊,我們就會發(fā)現(xiàn),它是一個大小為0x530的main arena分塊。
- Thread 1 "node" hit Breakpoint 2, 0x00007ffff40309b4 in png_read_row () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
- gef? i r rdi
- rdi 0x2722ef0 0x2722ef0
- gef? heap chunk $rdi
- Chunk(addr=0x2722ef0, size=0x530, flags=PREV_INUSE)
- Chunk size: 1328 (0x530)
- Usable size: 1320 (0x528)
- Previous chunk size: 25956 (0x6564)
- PREV_INUSE flag: On
- IS_MMAPPED flag: Off
- NON_MAIN_ARENA flag: Off
- gef?
此前,我們已經(jīng)研究了png_ptr結(jié)構(gòu)體本身,以及如何利用它來顛覆node進程,現(xiàn)在,讓我們仔細考察_dl_fixup,以及在解析器代碼中發(fā)生崩潰的具體原因。
當我們觸發(fā)崩潰時,我們注意到:
- 0x00007ffff7de2fb2 in _dl_fixup (l=0x2722a10, reloc_arg=0x11d) at ../elf/dl-runtime.c:69
- 69 const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
- gef? p *l
- $5 = {
- l_addr = 0x4141414141414141,
- ...
- l_info = {0x4141414141414141
- ...
- }
- gef? p l
- $6 = (struct link_map *) 0x2722a10
- gef?
這意味著,我們已經(jīng)破壞了用于解析png-img庫函數(shù)的linkmap。實際上,linkmap是一個數(shù)據(jù)結(jié)構(gòu),用于保存動態(tài)鏈接器執(zhí)行運行時解析和重定位所需的所有信息。
下面,我們來看一下linkmap堆塊和數(shù)據(jù)結(jié)構(gòu)未被破壞之前的樣子:
- gef? heap chunk 0x2722a10
- Chunk(addr=0x2722a10, size=0x4e0, flags=PREV_INUSE)
- Chunk size: 1248 (0x4e0)
- Usable size: 1240 (0x4d8)
- Previous chunk size: 39612548531313 (0x240703e24471)
- PREV_INUSE flag: On
- IS_MMAPPED flag: Off
- NON_MAIN_ARENA flag: Off
- gef? p *l
- $7 = {
- l_addr = 0x7ffff400f000,
- l_name = 0x2718010 "/home/anticomputer/node_modules/png-img/build/Release/png_img.node",
- l_ld = 0x7ffff4271c40,
- l_next = 0x0,
- l_prev = 0x7ffff7ffd9f0
- l_real = 0x2722a10,
- l_ns = 0x0,
- l_libname = 0x2722e88,
- l_info = {0x0, 0x7ffff4271c70, 0x7ffff4271d50, 0x7ffff4271d40, 0x0, 0x7ffff4271d00, 0x7ffff4271d10, 0x7ffff4271d80, 0x7ffff4271d90, 0x7ffff4271da0, 0x7ffff4271d20, 0x7ffff4271d30, 0x7ffff4271c90, 0x7ffff4271ca0, 0x7ffff4271c80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271d60, 0x0, 0x0, 0x7ffff4271d70, 0x0, 0x7ffff4271cb0, 0x7ffff4271cd0, 0x7ffff4271cc0, 0x7ffff4271ce0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271dc0, 0x7ffff4271db0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271de0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff4271dd0, 0x0
- ...
- }
- gef?
當我們檢查png_ptr chunk和linkmap chunk的地址和大小時,注意到它們所在的內(nèi)存不僅是相鄰的,并且是連續(xù)內(nèi)存。png_ptr chunk位于地址0x2722ef0處,而大小為0x4e0的linkmap chunk則位于它之前的地址0x2722a10處。由于是連續(xù)內(nèi)存,因此,兩者之間不存在分塊。
當從攻擊者的角度評估堆狀態(tài)時,我們總是同時考慮連續(xù)內(nèi)存布局和邏輯內(nèi)存布局(例如鏈表)。
因為linkmap和png_ptr的內(nèi)存在我們開始影響目標node進程之前分配的,并且在漏洞利用過程中都處于使用狀態(tài),所以,我們似乎不太可能在這兩個分塊之間挪動我們的data_ chunk,以暢通無阻地破壞png_ptr數(shù)據(jù)。此外,我們貌似可以通過例如PNG文件大小來影響早期的堆狀態(tài),但這似乎無法得到可靠的結(jié)果。
這意味著我們將必須對linkmap進行破壞,以獲取對node進程的控制權(quán)。

攻擊運行時解析器
作為攻擊者,我們經(jīng)常要從系統(tǒng)代碼中提煉出非預(yù)期的、但有用的行為。這里的挑戰(zhàn)是:不要被那些我們不關(guān)心的東西所干擾,而要專注于那些在特定的漏洞利用場景中可資利用的行為。
那么,運行時解析器代碼的哪些行為可能對攻擊者有用呢?
要回答這個問題,我們必須了解運行時解析器是如何使用linkmap的。簡而言之,它將從linkmap中抓取已加載的庫的基地址,然后檢查各種二進制段,以確定從庫的基地址到要解析的函數(shù)起始地址的正確偏移量。一旦計算出這個偏移量,把它加到庫的基地址上,用解析函數(shù)地址更新函數(shù)的GOT條目,然后跳轉(zhuǎn)到解析函數(shù)的起始地址即可。
作為攻擊者,我們從中提煉出以下有用的原語:向動態(tài)鏈接器的運行時解析器提供一個精心設(shè)計的linkmap,將它們加在一起,然后將執(zhí)行流重定向到生成的地址處。加法的第一個操作數(shù)是直接從linkmap中得到的,而加法的第二個操作數(shù)可以通過linkmap中提供的指針從二進制段中獲取。我們注意到,根據(jù)包含在某個解除引用的二進制段中的數(shù)據(jù),在執(zhí)行被重定向之前,解析的值將被寫入一個內(nèi)存位置。
實際上,通過破壞動態(tài)鏈接器來發(fā)動攻擊并不是一個新主意,其中,所謂的“ret2dlresolve”攻擊就是一種流行的方式,它可以在不知道libc本身在內(nèi)存中的位置的情況下,將執(zhí)行重定向到所需的libc函數(shù)中。在Nergal發(fā)布在Phrack上的“The advanced return-into-lib(c) exploits: PaX case study”一文中,就公開討論過這個概念。
當PLT處于目標二進制文件的已知位置時,就像非PIE二進制文件的情況一樣,ret2dlresolve攻擊就是一個非常有吸引力的選擇,它可以將執(zhí)行重定向到任意庫偏移處,而無需知道所需的目標庫實際加載到內(nèi)存的具體位置。這是因為解析器代碼會替我們完成所有繁重的工作。
濫用運行時解析器的主流方法,通常會假設(shè)攻擊者已經(jīng)能夠重定向進程的執(zhí)行流,并通過PLT返回到解析器代碼中,以便為_dl_runtime_resolve提供攻擊者控制的參數(shù)。因此,這種方法被稱為“ret2dlresolve”(即return to dl resolve的縮寫形式)。他們的想法是,隨后可以利用解析器與現(xiàn)有的或精心制作的linkmap數(shù)據(jù)和重定位數(shù)據(jù)的交互,推導(dǎo)出攻擊者控制的偏移量,即到達內(nèi)存中的現(xiàn)有指針值的偏移。例如,他們可以欺騙解析器,讓解析器將攻擊者控制的偏移量應(yīng)用到一個已經(jīng)建立的libc地址上,以便從那里偏移到一個任意的libc函數(shù)上,比如system(3)。在不知道libc基地址且無法直接返回libc的情況下,上面這種方法的一個變體是使用解析器邏輯來解析libc函數(shù)。
當然,這個技術(shù)還存在其他變體,例如在內(nèi)存中的已知位置提供一個完全精心制作的linkmap,用相對尋址來偽造重定位和符號數(shù)據(jù)。這里的目標同樣是濫用運行時解析器,從已知的內(nèi)存位置偏移到攻擊者想要轉(zhuǎn)移執(zhí)行的位置。
然而,雖然在我們的例子中,我們能夠提供一個精心制作的linkmap,但我們并不能控制運行時解析器的參數(shù)。此外,我們也還沒有掌握執(zhí)行控制權(quán),而是旨在“策反”運行時解析器,通過我們精心制作的linkmap數(shù)據(jù),以繞過ASLR機制并實現(xiàn)執(zhí)行重定向。由于堆的基地址是隨機的,而且我們是通過PNG文件來攻擊進程的,所以,我們沒有辦法泄露linkmap的位置,因此我們只能基于非PIE節(jié)點二進制文件來進行內(nèi)存布局和內(nèi)容假設(shè)。
為了更好地了解如何實現(xiàn)攻擊者的目標,讓我們來看看_dl_fixup的工作原理。在這里,所有的代碼引用都來自glibc-2.27。
- elf/dl-runtime.c:
- #ifndef reloc_offset
- # define reloc_offset reloc_arg
- # define reloc_index reloc_arg / sizeof (PLTREL)
- #endif
- /* This function is called through a special trampoline from the PLT the
- first time each PLT entry is called. We must perform the relocation
- specified in the PLT of the given shared object, and return the resolved
- function address to the trampoline, which will restart the original call
- to that address. Future calls will bounce directly from the PLT to the
- function. */
- DL_FIXUP_VALUE_TYPE
- attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
- _dl_fixup (
- # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
- ELF_MACHINE_RUNTIME_FIXUP_ARGS,
- # endif
- struct link_map *l, ElfW(Word) reloc_arg)
- {
- const ElfW(Sym) *const symtab
- = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
- const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
- [1]
- const PLTREL *const reloc
- = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
- [2]
- const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
- const ElfW(Sym) *refsym = sym;
- [3]
- void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
- lookup_t result;
- DL_FIXUP_VALUE_TYPE value;
- /* Sanity check that we're really looking at a PLT relocation. */
- assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
- /* Look up the target symbol. If the normal lookup rules are not
- used don't look in the global scope. */
- if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
- {
- const struct r_found_version *version = NULL;
- if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
- {
- const ElfW(Half) *vernum =
- (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
- ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
- version = &l->l_versions[ndx];
- if (version->hash == 0)
- version = NULL;
- }
- /* We need to keep the scope around so do some locking. This is
- not necessary for objects which cannot be unloaded or when
- we are not using any threads (yet). */
- int flags = DL_LOOKUP_ADD_DEPENDENCY;
- if (!RTLD_SINGLE_THREAD_P)
- {
- THREAD_GSCOPE_SET_FLAG ();
- flags |= DL_LOOKUP_GSCOPE_LOCK;
- }
- #ifdef RTLD_ENABLE_FOREIGN_CALL
- RTLD_ENABLE_FOREIGN_CALL;
- #endif
- result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
- version, ELF_RTYPE_CLASS_PLT, flags, NULL);
- /* We are done with the global scope. */
- if (!RTLD_SINGLE_THREAD_P)
- THREAD_GSCOPE_RESET_FLAG ();
- #ifdef RTLD_FINALIZE_FOREIGN_CALL
- RTLD_FINALIZE_FOREIGN_CALL;
- #endif
- /* Currently result contains the base load address (or link map)
- of the object that defines sym. Now add in the symbol
- offset. */
- value = DL_FIXUP_MAKE_VALUE (result,
- sym ? (LOOKUP_VALUE_ADDRESS (result)
- + sym->st_value) : 0);
- }
- else
- {
- /* We already found the symbol. The module (and therefore its load
- address) is also known. */
- value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
- result = l;
- }
- /* And now perhaps the relocation addend. */
- value = elf_machine_plt_value (l, reloc, value);
- if (sym != NULL
- && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
- value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
- /* Finally, fix up the plt itself. */
- if (__glibc_unlikely (GLRO(dl_bind_not)))
- return value;
- return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
- }
復(fù)雜,但我們只需要關(guān)注以下幾點:_dl_fixup是如何通過與我們控制的linkmap中的三個主要指針進行交互,來解析和重定位函數(shù)地址的,所有這些指針都是從linkmap的l_info數(shù)組中提取的:
- l_info[DT_SYMTAB],指向符號表的.dynamic條目的指針。
- l_info[DT_STRTAB],指向字符串表的.dynamic條目的指針。
- l_info[DT_JMPREL],指向PLT重定位記錄數(shù)組的.dynamic條目的指針。
Elf二進制文件中的.dynamic段用于保存解析器需要獲取的各個段的信息。在我們的例子中,.dynstr (STRTAB)、.dynsym (SYMTAB)和.rela.plt (JMPREL)段都是解析和重定位函數(shù)所需要的。
動態(tài)條目(Dynamic entry)的結(jié)構(gòu)如下所示:
- typedef struct
- {
- Elf64_Sxword d_tag; /* Dynamic entry type */
- union
- {
- Elf64_Xword d_val; /* Integer value */
- Elf64_Addr d_ptr; /* Address value */
- } d_un;
- } Elf64_Dyn;
用于訪問l_info條目的D_PTR宏定義為:
- /* All references to the value of l_info[DT_PLTGOT],
- l_info[DT_STRTAB], l_info[DT_SYMTAB], l_info[DT_RELA],
- l_info[DT_REL], l_info[DT_JMPREL], and l_info[VERSYMIDX (DT_VERSYM)]
- have to be accessed via the D_PTR macro. The macro is needed since for
- most architectures the entry is already relocated - but for some not
- and we need to relocate at access time. */
- #ifdef DL_RO_DYN_SECTION
- # define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr)
- #else
- # define D_PTR(map, i) (map)->i->d_un.d_ptr
- #endif
請注意,在大多數(shù)情況下,D_PTR只是從.dynamic段條目中獲取d_ptr字段,以檢索相關(guān)段的運行時重定位地址。例如,const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);將按照提供的指向.dynstr (STRTAB)段的.dynamic條目的指針,在l_info數(shù)組的索引DT_STRTAB處,獲取上述條目的d_ptr字段。
在指針方面,這里比較讓人頭疼,但我們只要記住一點就行了:我們并不是通過控制linkmap中的l_info數(shù)組來提供直接指向解析器所需要的各個段的指針,而是指向(假定的).dynamic條目的指針,這些條目在偏移量+8處應(yīng)該包含一個指向相關(guān)段的指針。

前面,我們介紹了如何通過精心制作的linkmap數(shù)據(jù)為解析器提供偽造的二進制段,接下來,我們快速了解一下_dl_fixup中的實際解析和重定位邏輯。
在我們的測試平臺上,重定位記錄的定義如下所示:
- elf.h:
- typedef struct
- {
- Elf64_Addr r_offset; /* Address */
- Elf64_Xword r_info; /* Relocation type and symbol index */
- Elf64_Sxword r_addend; /* Addend */
- } Elf64_Rela;
在我們的測試平臺上,這些符號的定義如下所示:
- elf.h:
- typedef struct
- {
- Elf64_Word st_name; /* Symbol name (string tbl index) */
- unsigned char st_info; /* Symbol type and binding */
- unsigned char st_other; /* Symbol visibility */
- Elf64_Section st_shndx; /* Section index */
- Elf64_Addr st_value; /* Symbol value */
- Elf64_Xword st_size; /* Symbol size */
- } Elf64_Sym;
我們再來回顧一下_dl_fixup的代碼,注意在[1]處,_dl_fixup的reloc_arg參數(shù)重定位記錄表的索引,來讀取重定位記錄。這個重定位記錄提供了一個reloc->r_info字段,該字段通過宏分為高32位的符號表索引和低32位的重定位類型。
在[2]處,_dl_fixup利用reloc->r_info索引從符號表中獲取相應(yīng)的符號條目,在reloc->r_info處的ELF_MACHINE_JMP_SLOT類型斷言和sym->st_other處的符號查找范圍檢查之前,實際的函數(shù)解析以一種非常簡單的方式進行。首先,通過將linkmap中的l->l_addr字段和符號表項的sym->st_value字段相加來解析函數(shù)地址。然后將解析后的值寫入rel_addr中,rel_addr是在[3]處計算出來的,也就是l->l_addr和reloc->r_offset相加的結(jié)果。
linkmap中的l->l_addr字段是用來存放加載庫的基地址,任何解析的偏移值都會被加入其中。
綜上所述,sym->st_value + l->l_addr是解析函數(shù)的地址,l->l_addr + reloc->r_offset是重定位目標,也就是GOT條目,將用解析函數(shù)地址進行更新。
所以,從我們攻擊者的角度來看,既然我們控制了l->l_addr,以及指向符號表和重定位記錄的.dynamic段的指針,我們就可以將執(zhí)行重定向到對我們有利的地方。
小結(jié)
在本文中,我們將深入地探討,在通過外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫“粘合”到解釋語言的過程中,安全漏洞是如何產(chǎn)生的。由于篇幅過長,我們將分為多篇進行介紹,更多精彩內(nèi)容敬請期待!
本文翻譯自:https://securitylab.github.com/research/now-you-c-me-part-two