_dl_fixup 机制研究记录

搭建 Glibc Debug 调试环境

Build Glibc

$ wget -q -O- https://ftp.gnu.org/gnu/glibc/glibc-2.38.tar.xz | tar -Jxv -C  ./
$ cd glibc-2.38
$ ./configure --enable-debug --prefix=/home/ihexon/glibc_bin
$ make ; make install

glibc 不支持 clang 编译,或者说暂时不支持。因为 glibc 本身使用了大量GCC独占的特性。但开源社区也有尝试用 Clang 构建 Glibc

必须指定安装位置 --prefix=/home/ihexon/glibc_bin,不然 glibc 就安装到 /usr/local/lib 中去了,跨版本升级底层C库这显然会让系统会发生异常。

如果手抖覆盖了系统的 /usr/lib/aarch64-linux-gnu/libc.so.6 库,整个系统就崩了。 [color=red]

使用 readelf -S printf_nopic 命令验证编译出的动态库是否带有 Debug symbol:

$ readelf --wide -S glibc_bin/lib/libc.so.6 | grep debug
  [58] .debug_aranges    PROGBITS        0000000000000000 183e80 016390 00      0   0 16
  [59] .debug_info       PROGBITS        0000000000000000 19a210 5106dc 00      0   0  1
  [60] .debug_abbrev     PROGBITS        0000000000000000 6aa8ec 0e5ca5 00      0   0  1
  [61] .debug_line       PROGBITS        0000000000000000 790591 13056f 00      0   0  1
  [62] .debug_str        PROGBITS        0000000000000000 8c0b00 02ccf1 01  MS  0   0  1
  [63] .debug_line_str   PROGBITS        0000000000000000 8ed7f1 00adb5 01  MS  0   0  1
  [64] .debug_loclists   PROGBITS        0000000000000000 8f85a6 16042f 00      0   0  1
  [65] .debug_rnglists   PROGBITS        0000000000000000 a589d5 0230b8 00      0   0  1

测试 Glibc 是否正常工作

写一个小型的 C 程序:

#include <stdio.h>
int main(int argc, char const* argv[])
{
  printf("%s","What a nice day MTF !");
  return 0;
}

编译出的 ELF 文件需要链接到新的 ` /home/ihexon/glibc_bin/lib/libc.so.6 上,当然 ELF 的链接器也要设定为 /home/ihexon/glibc_bin/lib/ld-linux-aarch64.so.1`

$ gcc printf.c  -o printf_nopic \
        -g -no-pie -Wl,-z,norelro \
        -Wl,-rpath=/home/ihexon/glibc_bin/lib/ \
        -Wl,-dynamic-linker=/home/ihexon/glibc_bin/lib/ld-linux-aarch64.so.1

使用 ldd 验证:

$ ldd printf_nopic_fuck
        linux-vdso.so.1 (0x0000007f8ed98000)
        libc.so.6 => /home/ihexon/glibc_bin/lib/libc.so.6 (0x0000007f8ebb0000)
        /home/ihexon/glibc_bin/lib/ld-linux-aarch64.so.1 => /lib/ld-linux-aarch64.so.1 (0x0000007f8ed5f000)

开始 Debug

在开始 Debug之前还有些细致的活需要做:

  1. 如果 Debug 是在 Docker 内,需要在 ~/.gdbinit 内写入 set disable-randomization off,因为 Docker 内没权限让 gdb 关闭内核的 address randomization。
  2. 由于 gdb 的安全性设定禁止加载外部的 libs,需要配置 auto-load safe-path 路径否则 gdb 无法加载到外部库如 libthread_db.so.1 等。写入这条语句到 ~/.gdbinit 中:set auto-load safe-path /home/ihexon/glibc_bin/lib
  3. 还可以设定 set verbose on 来让 gdb 在调试时打印更多信息。当然着有时候会比较影响注意力。

尝试断点 print_nopic 的 main 函数:

ihexon@raspberrypi ~/l/DynamicLink> gdb printf_nopic
Reading symbols from printf_nopic...
(gdb) b main
Breakpoint 1 at 0x4005d4: file printf.c, line 4.
(gdb) run
Starting program: /home/ihexon/learnassembly/DynamicLink/printf_nopic
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/home/ihexon/glibc_bin/lib/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fc2507698) at printf.c:4
4         printf("%s","ssss");

此时可以断点感兴趣的 glibc 内部函数如 _dl_fixup,(/home/ihexon/glibc/elf/dl-runtime.c),并跳入glibc 内部函数运行:

(gdb) b _dl_fixup
Breakpoint 2 at 0x7fa0137200: file dl-runtime.c, line 47.
(gdb) c
Continuing.

Breakpoint 2, _dl_fixup (l=0x7fa0162350, reloc_arg=72) at dl-runtime.c:47
47        const ElfW(Sym) *const symtab
(gdb) next
48          = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
(gdb) next
79        return !(l->l_ld_readonly || DL_RO_DYN_SECTION);
(gdb) next
49        const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

查看 _dl_fixup 函数的所属的源文件位置:

(gdb) info source
Current source file is dl-runtime.c
Compilation directory is /home/ihexon/glibc/elf
Located in /home/ihexon/glibc/elf/dl-runtime.c
Contains 348 lines.
Source language is c.
Producer is GNU C11 11.4.0 -mlittle-endian -mabi=lp64 -g -O2 -std=gnu11 -fgnu89-inline -fmerge-all-constants -frounding-math -fno-common -fmath-errno -fPIC -fno-stack-protector -fexceptions -ftls-model=initial-exec -fasynchronous-unwind-tables -fstack-clash-protection.
Compiled with DWARF 5 debugging format.
Does not include preprocessor macro info.

查看局部变量:

(gdb) info locals
symtab = 0x400298
strtab = <optimized out>
pltgot = <optimized out>
reloc = <optimized out>
sym = 0x7fa01372f4 <_dl_fixup+244>
refsym = <optimized out>
rel_addr = <optimized out>
result = <optimized out>
value = 548146323600
__PRETTY_FUNCTION__ = "_dl_fixup"

不废话了….

其他工具

  • pwngdb
  • objdump
  • readelf
  • elixir.bootlin.com 可以到 elixir.bootlin.com 翻看源码,这个网站对 glibc 整个源码生成了 Tags,可以实现对任意一个 symbol 在整个源码树下的 Definition/Reference/Implementation 跳转。

等玩6了之后就可以尝试使用这些搞基工具让自己更加搞基

  • radare2 (一个逆向工程框架)
  • unicorn
  • capston
  • qemu

_dl_fixup 函数

注释如下:

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.

Function Signature:

function _dl_fixup
→ Elf64_Addr (aka unsigned long)
Parameters:

struct link_map * l
Elf64_Word reloc_arg (aka unsigned int)


Elf64_Addr _dl_fixup(struct link_map *l, Elf64_Word reloc_arg)

它需要 link_map *,和 reloc_arg (int 类型)。

_dl_fixup 是怎么找到 printf@got[plt]

感觉和这行代码有关:

局部变量探索

当执行到 _dl_lookup_symbol_x 卡一下,

      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
				    version, ELF_RTYPE_CLASS_PLT, flags, NULL);

function Signature 如下:

lookup_t (aka struct link_map *) # 返回类型

参数列表: 

const char * undef
struct link_map * undef_map
const Elf64_Sym ** sym
struct r_scope_elem ** symbol_scope
const struct r_found_version * version
int type_class
int flags
struct link_map * skip_map

看看局部变量是什么情况,此时局部变量如下:

sym

sym 变量是 section .dynstr 的地址 sym 本质上是 const Elf64_Sym 结构体, 结构体成员如下:

 

reloc

指向 .rela 内的一个大小为24字节的条目。指向的槽位内的地址又指向了 got.plt 内的地址,如果 got.plt 没有填充正确的 printf 地址,则这个槽位又指向了 .PLT 开始处,如果填充了正确的地址,那么就直接跳转到 printf 的地址中运行。

reloc 由多个宏运算与inline函数运算后得到:

展开后就是下面一大串:

  • reloc 本质上是结构体 Elf64_Rela。
    pwndbg> whatis reloc
    type = const Elf64_Rela * const
    
  • 结构体 Elf64_Rela 大小为 24 字节。
    pwndbg> print sizeof(Elf64_Rela)
    $20 = 24
    
  • 每个 Elf64_Rela 都是一个在 .rela 内的一个大小为24字节的条目。

结构体 Elf64_Rela 的字段如下:

pwndbg> ptype Elf64_Rela
type = struct {
    Elf64_Addr r_offset;
    Elf64_Xword r_info;
    Elf64_Sxword r_addend;
}

打印 reloc 中的成员变量:

pwndbg> print -- /x *reloc
$2 = {
  r_offset = 0x20028,
  r_info = 0xb00000402,
  r_addend = 0x0
}

各成员大小为

pwndbg> print sizeof(reloc.r_offset)
$27 = 8
pwndbg> print sizeof(reloc.r_info)
$28 = 8
pwndbg> print sizeof(reloc.r_addend)
$29 = 8

每个 Elf64_Rela 都是一个在 section .rela 内的一个大小为24字节的条目。

如果不想那么抽象的话,当前 reloc 的地址为 ` 0x5567fa0698,这个地址在就是 .rela.plt 0x5567fa0698 = 0x5567fa0620 + 0x78`,大小为 24 字节:

Contents of section .rela.plt:
 0620 00000200 00000000 02040000 03000000  ................
 0630 00000000 00000000 08000200 00000000  ................
 0640 02040000 05000000 00000000 00000000  ................
 0650 10000200 00000000 02040000 06000000  ................
 0660 00000000 00000000 18000200 00000000  ................
 0670 02040000 07000000 00000000 00000000  ................
 0680 20000200 00000000 02040000 09000000   ...............
 0690 00000000 00000000 [28000200 00000000  ........(.......
 06a0 02040000 0b000000 00000000 00000000]  ................

reloc.r_offset == 0x20028 # [28000200 00000000]
r_info = 0xb00000402      # [02040000 0b000000]
r_addend = 0x0            # [00000000 00000000]

reloc.r_offset (0x20028) 是一个地址,指向 got.plt:

Contents of section .got.plt:
 1ffe8 00000000 00000000 00000000 00000000  ................
 1fff8 00000000 00000000 d0060000 00000000  ................
 20008 d0060000 00000000 d0060000 00000000  ................
 20018 d0060000 00000000 d0060000 00000000  ................
 20028 [d0060000 00000000]

这个槽位的值为 d0060000 00000000,这是 plt 最开始的一小段代码,使用 objdump 可以得到汇编代码:

$ objdump -d a.out
00000000000006d0 <.plt>:
 6d0:   a9bf7bf0        stp     x16, x30, [sp, #-16]!
 6d4:   f00000f0        adrp    x16, 1f000 <__GNU_EH_FRAME_HDR+0x1e6a4>
 6d8:   f947fe11        ldr     x17, [x16, #4088]
 6dc:   913fe210        add     x16, x16, #0xff8
 6e0:   d61f0220        br      x17
 6e4:   d503201f        nop
 6e8:   d503201f        nop
 6ec:   d503201f        nop

注意 0x20028 是未被装载之前的地址也就是ELF内的偏移量,使用 pwndbg 可以得到 VMA 内的 got.plt 地址。

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
[0x5567fc0028] printf@GLIBC_2.17 -> 0x5567fa06d0 ◂— stp x16, x30, [sp, #-0x10]!

0x20028 在装载时的地址为 0x5567fc0028。而 0x5567fa06d0 也是 6d0 在VMA内的地址。显然他们对应的都是 .PLT 开头的那一小段代码。这一小段代码就是地址寻找 printf 在 libs.so.6 中的地址旅程的大门。

当然所有的痛苦也由此开始:)

reloc 的计算过程如下:

pwndbg> macro expand (D_PTR (l, l_info[DT_JMPREL])
expands to: (((l)->l_info[23]->d_un.d_ptr + (dl_relocate_ld (l) ? 0 : (l)->l_addr))
pwndbg> p /x (l)->l_info[23]->d_un.d_ptr
$10 = 0x5567fa0620

前一部分的地址为 0x5567fa0620,后一部分 (dl_relocate_ld (l) ? 0 : (l)->l_addr)) 是一个 inline 函数,单纯返回 pltn 参数:

pltn 就是 _dl_fixup 的第二个参数:reloca_args

pwndbg> info args
l = 0x7fb6bad360
reloc_arg = 120
pwndbg> print /x 120
$12 = 0x78

所以 reloc 的值为

pwndbg> print /x 0x5567fa0620+0x78
$14 = 0x5567fa0698

pwndbg> p reloc # 验证计算是否正确
$15 = (const Elf64_Rela * const) 0x5567fa0698

rel_addr

rel_addr 指向的地址是 0x00410b38 ,即为 got.plt 中的第 7 项 0x410b38] printf@GLIBC_2.17, rel_addr 在这里被赋值:

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

l->l_addr 为 0,reloc->r_offset == 0x410b38 ,0x410b38 指向了 .rela.plt 中的某地址,这个地址是 0x00410b38

0x00410b38 是 got(准确来说是 got.plt)的第 7 项(为什么是第 7 项,因为 .got.plt 前三项是固定的,后面是 ),也就是 printf@GLIBC_2.17

0x00410b38 是需要被修正的地址,等 _dl_fixup 完事后 0x00410b38 就会指向 printf 的在 libc.so.6 中的真正地址,也就是 0x7fb35cc0f0 <__printf>

_dl_lookup_symbol_x 做了什么

_dl_lookup_symbol_x 执行完得到一个指针 0x7fa090c000并赋值给 result。

   95       result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
    96                                     version, ELF_RTYPE_CLASS_PLT, flags, NULL);

result == 0x7fa0730000 。 实际上 result 的类型为 lookup_t

pwndbg> whatis result
type = lookup_t

从 Language Server 后端 clangd 对的代码上下文语境分析得到,lookup_t 实际上指向了 结构体 link_map :

type-alias lookup_t
provided by "/home/ihexon/glibc/sysdeps/generic/ldsodefs.h"

Type: struct link_map *
Result of the lookup functions and how to retrieve the base address.

typedef struct link_map *lookup_t

result (aka link_map *)会在 DL_FIXUP_MAKE_VALUE 宏修正 got.plt 时被用到。因为 result.l_addr 成员变量中里存储了 libs.so.6 在虚拟内存中的基地址:

pwndbg> xinfo result.l_addr
Extended information for virtual address 0x7fa0730000:

  Containing mapping:
      0x7fa0730000       0x7fa08af000 r-xp   17f000      0 /home/ihexon/glibc_bin/lib/libc.so.6

  Offset information:
         Mapped Area 0x7fa0730000 = 0x7fa0730000 + 0x0
         File (Base) 0x7fa0730000 = 0x7fa0730000 + 0x0
      File (Segment) 0x7fa0730000 = 0x7fa0730000 + 0x0
         File (Disk) 0x7fa0730000 = /home/ihexon/glibc_bin/lib/libc.so.6 + 0x0

TODO: _dl_lookup_symbol_x 是怎么得到 libc.so.6 的基地址呢?

得到了 libc.so.6 后还要加上 printf 在 libc.so.6 中的偏移量才能得到 printf 的地址并修正给 got.plt 中的 [0x410b38] printf@GLIBC_2.17。 实际上修正 .got.plt 是在 DL_FIXUP_MAKE_VALUE 宏内完成的:

 ► 109       value = DL_FIXUP_MAKE_VALUE (result,
   110                                    SYMBOL_ADDRESS (result, sym, false));

展开:

(((sym) == ((void *)0) ? 0
                       : (__builtin_expect(((sym)->st_shndx == 0xfff1), 0)
                              ? 0
                              : ((0) || (result) ? (result)->l_addr : 0)) +
  (sym)->st_value))

实际上就是:result.l_addr + (sym)->st_value,也就是 libs.so.6 的基地址(result.l_addr) + printf 在 libc.so.6 中的偏移量((sym)->st_value)。得到0x7fa077c0f0,这就是 printf 的真实的地址。

pwndbg> x result.l_addr + (sym)->st_value
0x7fa077c0f0 <__printf>:        0xfd

sym 变量起到了关键作用,此时 sym 变量变成了 0x7fa0745960,之前不是 0x4002f8 吗 ??

可能是在 _dl_lookup_symbol_x 函数中被修改了吧,因为它的第三个参数传入了 sym 的地址。 好了大概流程就是这样,下面来点细节。

局部变量

  • symtab
     const ElfW(Sym) *const symtab
      = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    

symtab 指向的 .dynsym 的开始,.dynsym 和 .dynstr 是强关联的,比较熟知的是 .dynsym 是一个结构体,如下:

pwndbg> whatis symtab
type = const Elf64_Sym * const

pwndbg> print sizeof(Elf64_Sym)
$11 = 24

pwndbg> print sizeof(Elf64_Sym)
$12 = 24
pwndbg> ptype Elf64_Sym
type = struct {
    Elf64_Word st_name;
    unsigned char st_info;
    unsigned char st_other;
    Elf64_Section st_shndx;
    Elf64_Addr st_value;
    Elf64_Xword st_size;
}

所以 symtab 每一项都是 24 字节的:

0x19 为偏移量,这里我直接说这个是 print 函数名在 .dynstr 的偏移量,0x19==25

pwndbg> p -- /d 0x19
$13 = 25

数25 位试试看:

  • strtab 变量字如其名
  • pltgot 指向 got.plt(为什么这个变量叫 pltgot 而不是 gotplt??)

  • refsym 就更有趣了,st_name = 25 意思就指向了 .symtab 的 print 函数名,告诉 ld.so,我就是要找在 .dynstr 段中偏移量位 25 的函数,这个函数就是 printf 函数
    pwndbg> p *refsym
    $36 = {
    st_name = 25,
    st_info = 18 '\022',
    st_other = 0 '\000',
    st_shndx = 0,
    st_value = 0,
    st_size = 0
    }
    
  • rela_addr 指向了 got.plt 中的 printf@got[plt] 条目 ```bash pwndbg> p rel_addr $38 = (void * const) 0x410b38 <printf@got[plt]>

pwndbg> got [0x410b38] printf@GLIBC_2.17 -> 0x400450 ◂— stp x16, x30, [sp, #-0x10]!




- `reloc`:
```C=
# elf/dl-runtime.c:53
  const PLTREL *const reloc
    = (const void *) (D_PTR (l, l_info[DT_JMPREL])
                      

指向 XXX@got.plt 中的条目的地址,可想而知,当 dl_fixup 走完后,XXX@got.plt 的地址将会被修正,当程序 bl XXX@got.plt 时,不会再走 _dl_runtime_resolve 了。

pwndbg> p reloc
$12 = (const Elf64_Rela * const) 0x400418

reloc 指向的是ELF 内段 .rela.plt 0x400418-0x400418+4 的内容,那么 0x400418-0x400418+4,而 0x400418-0x400418+4 这4字节存储的是380b4100,因为aarch64 是小端架构所以逆序得出 0x410b38, 这就是 .got.plt 中 printf@GLIBC_2.17 条目的地址。

ihexon@raspberrypi ~/l/DynamicLink> objdump -s printf_nopic -j .rela.plt

printf_nopic:     file format elf64-littleaarch64

Contents of section .rela.plt:
 4003d0 200b4100 00000000 02040000 01000000   .A.............
 4003e0 00000000 00000000 280b4100 00000000  ........(.A.....
 4003f0 02040000 02000000 00000000 00000000  ................
 400400 300b4100 00000000 02040000 03000000  0.A.............
 400410 00000000 00000000 [380b4100] 00000000  ........8.A.....
 400420 02040000 04000000 00000000 00000000  ................

0x410b38 又指向 0x400450

pwndbg> got
│[0x410b38] printf@GLIBC_2.17 -> 0x400450 ◂— stp x16, x30, [sp, #-0x10]!

0x400450 开始的地方是一小段代码:

pwndbg> x/10i 0x00400450
   0x400450:    stp     x16, x30, [sp, #-16]!
   0x400454:    adrp    x16, 0x410000
   0x400458:    ldr     x17, [x16, #2840]
   0x40045c:    add     x16, x16, #0xb18
   0x400460:    br      x17

br x17 不说了,直接跳转到 dl_runtime_resolv 里去了。但这不会发送,因为当 _dl_fixup 跑完后, printf@GLIBC_2.17 的地址将会被指向 printf 真正的地址,而不是走 _dl_runtime_resolve。

  • 参数 *l: dl_fixup 的参数 *l 就是一个巨大的结构体变量,里面保存了 ELF 的各个段的偏移地址,这些地址会被频繁用到从而计算出需要的地址,如 rel_addr 就是被计算出来的。
    pwndbg> ptype l
    type = struct link_map {
      Elf64_Addr l_addr;
      char *l_name;
      Elf64_Dyn *l_ld;
      struct link_map *l_next;
      struct link_map *l_prev;
      struct link_map *l_real;
      Lmid_t l_ns;
      struct libname_list *l_libname;
      Elf64_Dyn *l_info[86];
      ,,,,,
    

rel_addr 是这样被计算出来的:

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

还有其他的变量可以通过 info locales 得到,不废话了。

ELFW 是一个宏:

要开始修正 plt 地址了亲!

In file: /home/ihexon/glibc/elf/dl-runtime.c
   154         value = reloc_result->addr;
   155     }
   156 #endif
   157
   158   /* Finally, fix up the plt itself.  */
 ► 159   if (__glibc_unlikely (GLRO(dl_bind_not)))
   160     return value;

到这里拿到了 printf 的真实地址,验证一下:

In file: /home/ihexon/glibc/sysdeps/aarch64/dl-machine.h
   142                        const ElfW(Sym) *refsym, const ElfW(Sym) *sym,
   143                        const ElfW(Rela) *reloc,
   144                        ElfW(Addr) *reloc_addr,
   145                        ElfW(Addr) value)
   146 {
 ► 147   return *reloc_addr = value;
   148 }
   149
   150 /* Return the final value of a plt relocation.  */
   151 static inline ElfW(Addr)
   152 elf_machine_plt_value (struct link_map *map,
``
```bash
pwndbg> p printf
$35 = {int (const char *, ...)} 0x7f85b2c0f0 <__printf>
pwndbg> p -- /x value
$36 = 0x7f85b2c0f0

什么时候修正的

相关函数:


elf_machine_fixup_plt (struct link_map *map, lookup_t t,
		       const ElfW(Sym) *refsym, const ElfW(Sym) *sym,
		       const ElfW(Rela) *reloc,
		       ElfW(Addr) *reloc_addr,
		       ElfW(Addr) value)
{
  return *reloc_addr = value;
}

这里让 reloc_addr = value(printf 的值),还记得吗,reloc_addr 是 got.plt 内地址 0x410b38,讲 value 赋值给这个地址就是修正了这个地址:

pwndbg> got
[0x410b38] printf@GLIBC_2.17 -> 0x7f92f8c0f0 (printf) ◂— stp x29, x30, [sp, #-0x110]!

没修正之前:

pwndbg> got
[0x410b38] printf@GLIBC_2.17 -> 0x400450 ◂— stp x16, x30, [sp, #-0x10]!

操了,头疼….