接上文:
- 《深入考察解释型语言背后隐藏的攻击面,Part 1(上)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 1(下)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 2(一)》
- 《深入考察解释型语言背后隐藏的攻击面,Part 2(二)》
在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。
决定最终的漏洞利用策略
为了弄清楚如何在触发我们的堆内存覆盖之前对data_数组进行最佳定位,我们需要检查一下堆的状态。到目前为止,我们有两个感兴趣的目标:png_ptr结构体和运行时解析器正在使用的动态链接器数据。
如果我们检查png_ptr结构体数据所在的堆分块,我们就会发现,它是一个大小为0x530的main arena分块。
Thread1"node"hitBreakpoint2,0x00007ffff40309b4inpng_read_row()from/home/anticomputer/node_modules/png-img/build/Release/png_img.node gef?irrdi rdi0x2722ef00x2722ef0 gef?heapchunk$rdi Chunk(addr=0x2722ef0,size=0x530,flags=PREV_INUSE) Chunksize:1328(0x530) Usablesize:1320(0x528) Previouschunksize:25956(0x6564) PREV_INUSEflag:On IS_MMAPPEDflag:Off NON_MAIN_ARENAflag:Off gef?
此前,我们已经研究了png_ptr结构体本身,以及如何利用它来颠覆node进程,现在,让我们仔细考察_dl_fixup,以及在解析器代码中发生崩溃的具体原因。
当我们触发崩溃时,我们注意到:
0x00007ffff7de2fb2in_dl_fixup(l=0x2722a10,reloc_arg=0x11d)at../elf/dl-runtime.c:69 69constchar*strtab=(constvoid*)D_PTR(l,l_info[DT_STRTAB]); gef?p*l $5={ l_addr=0x4141414141414141, ... l_info={0x4141414141414141 ... } gef?pl $6=(structlink_map*)0x2722a10 gef?
这意味着,我们已经破坏了用于解析png-img库函数的linkmap。实际上,linkmap是一个数据结构,用于保存动态链接器执行运行时解析和重定位所需的所有信息。
下面,我们来看一下linkmap堆块和数据结构未被破坏之前的样子:
gef?heapchunk0x2722a10 Chunk(addr=0x2722a10,size=0x4e0,flags=PREV_INUSE) Chunksize:1248(0x4e0) Usablesize:1240(0x4d8) Previouschunksize:39612548531313(0x240703e24471) PREV_INUSEflag:On IS_MMAPPEDflag:Off NON_MAIN_ARENAflag: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的地址和大小时,注意到它们所在的内存不仅是相邻的,并且是连续内存。png_ptr chunk位于地址0x2722ef0处,而大小为0x4e0的linkmap chunk则位于它之前的地址0x2722a10处。由于是连续内存,因此,两者之间不存在分块。
当从攻击者的角度评估堆状态时,我们总是同时考虑连续内存布局和逻辑内存布局(例如链表)。
因为linkmap和png_ptr的内存在我们开始影响目标node进程之前分配的,并且在漏洞利用过程中都处于使用状态,所以,我们似乎不太可能在这两个分块之间挪动我们的data_ chunk,以畅通无阻地破坏png_ptr数据。此外,我们貌似可以通过例如PNG文件大小来影响早期的堆状态,但这似乎无法得到可靠的结果。
这意味着我们将必须对linkmap进行破坏,以获取对node进程的控制权。
攻击运行时解析器
作为攻击者,我们经常要从系统代码中提炼出非预期的、但有用的行为。这里的挑战是:不要被那些我们不关心的东西所干扰,而要专注于那些在特定的漏洞利用场景中可资利用的行为。
那么,运行时解析器代码的哪些行为可能对攻击者有用呢?
要回答这个问题,我们必须了解运行时解析器是如何使用linkmap的。简而言之,它将从linkmap中抓取已加载的库的基地址,然后检查各种二进制段,以确定从库的基地址到要解析的函数起始地址的正确偏移量。一旦计算出这个偏移量,把它加到库的基地址上,用解析函数地址更新函数的GOT条目,然后跳转到解析函数的起始地址即可。
作为攻击者,我们从中提炼出以下有用的原语:向动态链接器的运行时解析器提供一个精心设计的linkmap,将它们加在一起,然后将执行流重定向到生成的地址处。加法的第一个操作数是直接从linkmap中得到的,而加法的第二个操作数可以通过linkmap中提供的指针从二进制段中获取。我们注意到,根据包含在某个解除引用的二进制段中的数据,在执行被重定向之前,解析的值将被写入一个内存位置。
实际上,通过破坏动态链接器来发动攻击并不是一个新主意,其中,所谓的“ret2dlresolve”攻击就是一种流行的方式,它可以在不知道libc本身在内存中的位置的情况下,将执行重定向到所需的libc函数中。在Nergal发布在Phrack上的“The advanced return-into-lib(c) exploits: PaX case study”一文中,就公开讨论过这个概念。
当PLT处于目标二进制文件的已知位置时,就像非PIE二进制文件的情况一样,ret2dlresolve攻击就是一个非常有吸引力的选择,它可以将执行重定向到任意库偏移处,而无需知道所需的目标库实际加载到内存的具体位置。这是因为解析器代码会替我们完成所有繁重的工作。
滥用运行时解析器的主流方法,通常会假设攻击者已经能够重定向进程的执行流,并通过PLT返回到解析器代码中,以便为_dl_runtime_resolve提供攻击者控制的参数。因此,这种方法被称为“ret2dlresolve”(即return to dl resolve的缩写形式)。他们的想法是,随后可以利用解析器与现有的或精心制作的linkmap数据和重定位数据的交互,推导出攻击者控制的偏移量,即到达内存中的现有指针值的偏移。例如,他们可以欺骗解析器,让解析器将攻击者控制的偏移量应用到一个已经建立的libc地址上,以便从那里偏移到一个任意的libc函数上,比如system(3)。在不知道libc基地址且无法直接返回libc的情况下,上面这种方法的一个变体是使用解析器逻辑来解析libc函数。
当然,这个技术还存在其他变体,例如在内存中的已知位置提供一个完全精心制作的linkmap,用相对寻址来伪造重定位和符号数据。这里的目标同样是滥用运行时解析器,从已知的内存位置偏移到攻击者想要转移执行的位置。
然而,虽然在我们的例子中,我们能够提供一个精心制作的linkmap,但我们并不能控制运行时解析器的参数。此外,我们也还没有掌握执行控制权,而是旨在“策反”运行时解析器,通过我们精心制作的linkmap数据,以绕过ASLR机制并实现执行重定向。由于堆的基地址是随机的,而且我们是通过PNG文件来攻击进程的,所以,我们没有办法泄露linkmap的位置,因此我们只能基于非PIE节点二进制文件来进行内存布局和内容假设。
为了更好地了解如何实现攻击者的目标,让我们来看看_dl_fixup的工作原理。在这里,所有的代码引用都来自glibc-2.27。
elf/dl-runtime.c: #ifndefreloc_offset #definereloc_offsetreloc_arg #definereloc_indexreloc_arg/sizeof(PLTREL) #endif /*ThisfunctioniscalledthroughaspecialtrampolinefromthePLTthe firsttimeeachPLTentryiscalled.Wemustperformtherelocation specifiedinthePLTofthegivensharedobject,andreturntheresolved functionaddresstothetrampoline,whichwillrestarttheoriginalcall tothataddress.FuturecallswillbouncedirectlyfromthePLTtothe function.*/ DL_FIXUP_VALUE_TYPE attribute_hidden__attribute((noinline))ARCH_FIXUP_ATTRIBUTE _dl_fixup( #ifdefELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, #endif structlink_map*l,ElfW(Word)reloc_arg) { constElfW(Sym)*constsymtab =(constvoid*)D_PTR(l,l_info[DT_SYMTAB]); constchar*strtab=(constvoid*)D_PTR(l,l_info[DT_STRTAB]); [1] constPLTREL*constreloc =(constvoid*)(D_PTR(l,l_info[DT_JMPREL])+reloc_offset); [2] constElfW(Sym)*sym=&symtab[ELFW(R_SYM)(reloc->r_info)]; constElfW(Sym)*refsym=sym; [3] void*constrel_addr=(void*)(l->l_addr+reloc->r_offset); lookup_tresult; DL_FIXUP_VALUE_TYPEvalue; /*Sanitycheckthatwe'rereallylookingataPLTrelocation.*/ assert(ELFW(R_TYPE)(reloc->r_info)==ELF_MACHINE_JMP_SLOT); /*Lookupthetargetsymbol.Ifthenormallookuprulesarenot useddon'tlookintheglobalscope.*/ if(__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other),0)==0) { conststructr_found_version*version=NULL; if(l->l_info[VERSYMIDX(DT_VERSYM)]!=NULL) { constElfW(Half)*vernum= (constvoid*)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; } /*Weneedtokeepthescopearoundsodosomelocking.Thisis notnecessaryforobjectswhichcannotbeunloadedorwhen wearenotusinganythreads(yet).*/ intflags=DL_LOOKUP_ADD_DEPENDENCY; if(!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG(); flags|=DL_LOOKUP_GSCOPE_LOCK; } #ifdefRTLD_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); /*Wearedonewiththeglobalscope.*/ if(!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG(); #ifdefRTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif /*Currentlyresultcontainsthebaseloadaddress(orlinkmap) oftheobjectthatdefinessym.Nowaddinthesymbol offset.*/ value=DL_FIXUP_MAKE_VALUE(result, sym?(LOOKUP_VALUE_ADDRESS(result) +sym->st_value):0); } else { /*Wealreadyfoundthesymbol.Themodule(andthereforeitsload address)isalsoknown.*/ value=DL_FIXUP_MAKE_VALUE(l,l->l_addr+sym->st_value); result=l; } /*Andnowperhapstherelocationaddend.*/ 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,fixupthepltitself.*/ if(__glibc_unlikely(GLRO(dl_bind_not))) returnvalue; returnelf_machine_fixup_plt(l,result,refsym,sym,reloc,rel_addr,value); }
复杂,但我们只需要关注以下几点:_dl_fixup是如何通过与我们控制的linkmap中的三个主要指针进行交互,来解析和重定位函数地址的,所有这些指针都是从linkmap的l_info数组中提取的:
- l_info[DT_SYMTAB],指向符号表的.dynamic条目的指针。
- l_info[DT_STRTAB],指向字符串表的.dynamic条目的指针。
- l_info[DT_JMPREL],指向PLT重定位记录数组的.dynamic条目的指针。
Elf二进制文件中的.dynamic段用于保存解析器需要获取的各个段的信息。在我们的例子中,.dynstr (STRTAB)、.dynsym (SYMTAB)和.rela.plt (JMPREL)段都是解析和重定位函数所需要的。
动态条目(Dynamic entry)的结构如下所示:
typedefstruct { Elf64_Sxwordd_tag;/*Dynamicentrytype*/ union { Elf64_Xwordd_val;/*Integervalue*/ Elf64_Addrd_ptr;/*Addressvalue*/ }d_un; }Elf64_Dyn;
用于访问l_info条目的D_PTR宏定义为:
/*Allreferencestothevalueofl_info[DT_PLTGOT], l_info[DT_STRTAB],l_info[DT_SYMTAB],l_info[DT_RELA], l_info[DT_REL],l_info[DT_JMPREL],andl_info[VERSYMIDX(DT_VERSYM)] havetobeaccessedviatheD_PTRmacro.Themacroisneededsincefor mostarchitecturestheentryisalreadyrelocated-butforsomenot andweneedtorelocateataccesstime.*/ #ifdefDL_RO_DYN_SECTION #defineD_PTR(map,i)((map)->i->d_un.d_ptr+(map)->l_addr) #else #defineD_PTR(map,i)(map)->i->d_un.d_ptr #endif
请注意,在大多数情况下,D_PTR只是从.dynamic段条目中获取d_ptr字段,以检索相关段的运行时重定位地址。例如,const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);将按照提供的指向.dynstr (STRTAB)段的.dynamic条目的指针,在l_info数组的索引DT_STRTAB处,获取上述条目的d_ptr字段。
在指针方面,这里比较让人头疼,但我们只要记住一点就行了:我们并不是通过控制linkmap中的l_info数组来提供直接指向解析器所需要的各个段的指针,而是指向(假定的).dynamic条目的指针,这些条目在偏移量+8处应该包含一个指向相关段的指针。
前面,我们介绍了如何通过精心制作的linkmap数据为解析器提供伪造的二进制段,接下来,我们快速了解一下_dl_fixup中的实际解析和重定位逻辑。
在我们的测试平台上,重定位记录的定义如下所示:
elf.h: typedefstruct { Elf64_Addrr_offset;/*Address*/ Elf64_Xwordr_info;/*Relocationtypeandsymbolindex*/ Elf64_Sxwordr_addend;/*Addend*/ }Elf64_Rela;
在我们的测试平台上,这些符号的定义如下所示:
elf.h: typedefstruct { Elf64_Wordst_name;/*Symbolname(stringtblindex)*/ unsignedcharst_info;/*Symboltypeandbinding*/ unsignedcharst_other;/*Symbolvisibility*/ Elf64_Sectionst_shndx;/*Sectionindex*/ Elf64_Addrst_value;/*Symbolvalue*/ Elf64_Xwordst_size;/*Symbolsize*/ }Elf64_Sym;
我们再来回顾一下_dl_fixup的代码,注意在[1]处,_dl_fixup的reloc_arg参数重定位记录表的索引,来读取重定位记录。这个重定位记录提供了一个reloc->r_info字段,该字段通过宏分为高32位的符号表索引和低32位的重定位类型。
在[2]处,_dl_fixup利用reloc->r_info索引从符号表中获取相应的符号条目,在reloc->r_info处的ELF_MACHINE_JMP_SLOT类型断言和sym->st_other处的符号查找范围检查之前,实际的函数解析以一种非常简单的方式进行。首先,通过将linkmap中的l->l_addr字段和符号表项的sym->st_value字段相加来解析函数地址。然后将解析后的值写入rel_addr中,rel_addr是在[3]处计算出来的,也就是l->l_addr和reloc->r_offset相加的结果。
linkmap中的l->l_addr字段是用来存放加载库的基地址,任何解析的偏移值都会被加入其中。
综上所述,sym->st_value + l->l_addr是解析函数的地址,l->l_addr + reloc->r_offset是重定位目标,也就是GOT条目,将用解析函数地址进行更新。
所以,从我们攻击者的角度来看,既然我们控制了l->l_addr,以及指向符号表和重定位记录的.dynamic段的指针,我们就可以将执行重定向到对我们有利的地方。
小结
在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。由于篇幅过长,我们将分为多篇进行介绍,更多精彩内容敬请期待!
本文翻
转载请注明:IT运维空间 » 安全防护 » 深入考察解释型语言背后隐藏的攻击面,Part 2(三)
发表评论