深入考察解释型语言背后隐藏的攻击面,Part 2(三) - 嘶吼 RoarTalk – 回归最本质的信息安全,互联网安全新媒体,4hou.com

深入考察解释型语言背后隐藏的攻击面,Part 2(三)

fanyeee 资讯 2021-01-05 09:49:21
收藏

导语:在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。

在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。

深入考察解释型语言背后隐藏的攻击面,Part 2(一)

深入考察解释型语言背后隐藏的攻击面,Part 2(二)

决定最终的漏洞利用策略

为了弄清楚如何在触发我们的堆内存覆盖之前对data_数组进行最佳定位,我们需要检查一下堆的状态。到目前为止,我们有两个感兴趣的目标:png_ptr结构体和运行时解析器正在使用的动态链接器数据。

如果我们检查png_ptr结构体数据所在的堆分块,我们就会发现,它是一个大小为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?

此前,我们已经研究了png_ptr结构体本身,以及如何利用它来颠覆node进程,现在,让我们仔细考察_dl_fixup,以及在解析器代码中发生崩溃的具体原因。

当我们触发崩溃时,我们注意到:

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?

这意味着,我们已经破坏了用于解析png-img库函数的linkmap。实际上,linkmap是一个数据结构,用于保存动态链接器执行运行时解析和重定位所需的所有信息。

下面,我们来看一下linkmap堆块和数据结构未被破坏之前的样子:

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的地址和大小时,注意到它们所在的内存不仅是相邻的,并且是连续内存。png_ptr chunk位于地址0x2722ef0处,而大小为0x4e0的linkmap chunk则位于它之前的地址0x2722a10处。由于是连续内存,因此,两者之间不存在分块。

当从攻击者的角度评估堆状态时,我们总是同时考虑连续内存布局和逻辑内存布局(例如链表)。

因为linkmap和png_ptr的内存在我们开始影响目标node进程之前分配的,并且在漏洞利用过程中都处于使用状态,所以,我们似乎不太可能在这两个分块之间挪动我们的data_ chunk,以畅通无阻地破坏png_ptr数据。此外,我们貌似可以通过例如PNG文件大小来影响早期的堆状态,但这似乎无法得到可靠的结果。

这意味着我们将必须对linkmap进行破坏,以获取对node进程的控制权。

1.png

 

攻击运行时解析器

作为攻击者,我们经常要从系统代码中提炼出非预期的、但有用的行为。这里的挑战是:不要被那些我们不关心的东西所干扰,而要专注于那些在特定的漏洞利用场景中可资利用的行为。

那么,运行时解析器代码的哪些行为可能对攻击者有用呢?

要回答这个问题,我们必须了解运行时解析器是如何使用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:
 
 
#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);
}

虽然这段代码乍看之下可能非常复杂,但我们只需要关注以下几点:_dl_fixup是如何通过与我们控制的linkmap中的三个主要指针进行交互,来解析和重定位函数地址的,所有这些指针都是从linkmap的l_info数组中提取的:

1. l_info[DT_SYMTAB],指向符号表的.dynamic条目的指针。

2. l_info[DT_STRTAB],指向字符串表的.dynamic条目的指针。

3. l_info[DT_JMPREL],指向PLT重定位记录数组的.dynamic条目的指针。

Elf二进制文件中的.dynamic段用于保存解析器需要获取的各个段的信息。在我们的例子中,.dynstr (STRTAB)、.dynsym (SYMTAB)和.rela.plt (JMPREL)段都是解析和重定位函数所需要的。

动态条目(Dynamic entry)的结构如下所示:

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

请注意,在大多数情况下,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处应该包含一个指向相关段的指针。

1.png

前面,我们介绍了如何通过精心制作的linkmap数据为解析器提供伪造的二进制段,接下来,我们快速了解一下_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参数重定位记录表的索引,来读取重定位记录。这个重定位记录提供了一个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++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。由于篇幅过长,我们将分为多篇进行介绍,更多精彩内容敬请期待!

本文翻译自:https://securitylab.github.com/research/now-you-c-me-part-two如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论