skip to content
z10 blog

PAWNYABLE kernel userfaultfd 笔记

/ 9 min read

Table of Contents

glibc2.36

引入 (lazy binding)

下文的分析将以如下代码为例

# include<stdio.h>
int main(){
char c;
c = getchar();
printf("c: %c\n", c);
return 0;
}
//gcc -g dl.c -no-pie -o dl

在 gdb 中下断点 b 6 使函数停 printf 执行前

image-20230721150114090

一步步执行可以看见跳转到了 printf@plt 所在的地方,这里是一段跳转的代码,将要跳转到 got.plt 中存放的地址。不过此时 got 还未初始化,存放的不是真实的地址

而是将 printf 的偏移(0)入栈,再跳转到 0x401020 (这是什么固定的地址吗?),将 link_map 入栈,最后跳转到 _dl_runtime_resolve_xsavec

image-20230721150706486

通过以上的调试过程可以知道

  • plt

    是过程链接表,存放跳转时候的代码

  • got

    完成初始化之后存放真实地址,未初始化时是 plt 附近的地址

后续 _dl_runtime_resolve_xsavec 将会调用 _dl_fixup 查找真正的函数地址并写入 got 表,具体的过程结合源码分析。

结构

节表

可以用 readelf -d dl 命令查看所有节表

image-20230721171524209

其中几个比较关键

  • STRTAB

    字符串表

  • SYMTAB

    符号表,可以用 readelf -s dl 查看

  • PLTGOT

    就是常说的 got 表

  • JUMPREL

    函数重定位表,可以用 readelf -r dl 查看

    Terminal window
    Relocation section '.rela.plt' at offset 0x508 contains 2 entries:
    Offset Info Type Sym. Value Sym. Name + Addend
    000000404018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
    000000404020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0

结构体

这是链接器(linker)为每一共享库创建一个 link_map 结构体,所有 link_map 组织成链表。定义在 inlude/link.h 中,太长了所以只摘取了部分。

struct link_map
{
//libc 基址
ElfW(Addr) l_addr;
...
//dynamic section
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
...
//version
struct r_found_version *l_versions;

可以在 gdb 中查看这个链结构,具体做法是把 dl-runtime.c 文件放在同一个目录下后(如果编译时没有保留 -g 选项要导入一下调试信息)就可以在 gdb 看见源码

image-20230721180231981

当执行到结构体附近的代码时可以打印结构体信息,这里有一个小技巧,set print pretty on 能打印详细的结构体信息。

image-20230721180556828

也可以用 -> 访问结构体成员,比如下一个 link_map 的名字是

image-20230721180814583

Elf64_Rel

每一个需要重定位的函数都有一个 Elf64_Rela 结构体,存储了重定位所需要的信息,这些 Elf64_Rela 组成了 JMPREL 。

  • r_offset

    它包含解析符号的地址将存储的位置(在GOT中)。

  • r_info

    指示重新定位类型并充当符号表索引。它将用于在 DYNSYM 部分中查找相应的Elf64_Sym结构。

image-20230721181014860

printf 对应的 reloc 为,我们可以在前文 JUMPREL 中看见同样的结构。

image-20230721181153309

Elf64_Dyn

记录 dynamic table 信息

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;

Elf64_Sym

描述每个符号的结构体

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

完整代码

_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]);
const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL])
+ reloc_offset (pltgot, reloc_arg));
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
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,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
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);
}

可以看到,这个函数接收两个参数,分别是我们在引入的小节中 push 的参数,link_map l 存放 libc 的信息,reloc_arg 是在 JUPREL 用于查找的序号。

symtab&strtab&pltgot&reloc

函数首先从 link_map 中找到以上节表的地址,我们先来看第一条解析 symtab 的语句。

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

这里赋值了一个指向常量 ElfW(Sym) 类型的常量指针 symtab。根据宏,类型ELFw(Sym) 可以解析成 Elf64_sym

#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t) // 64 位 w = 64
#define _ElfW_1(e,w,t) e##w##t // ## 用于连接成符号

至于 symtab 是如何得到的呢?在 elf/elf.h 中定义了DT_SYMTAB 为常量 6,即 l_info[6] 表示了第六个 ”Dynamic section entry“ 不会翻译(

#define DT_PLTGOT 3 /* Processor defined value */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_RELA 7 /* Address of Rela relocs */

而 D_PTR 的宏如下

# define D_PTR(map, i) (map)->i->d_un.d_ptr

因此 D_PTR (l, l_info[DT_SYMTAB]) 表示的是

D_PTR (l, l_info[DT_SYMTAB])
= l-> l_info[5]->d_un.d_ptr

Terminal window
pwndbg> p/x *l->l_info[6]
$57 = {
d_tag = 0x6,
d_un = {
d_val = 0x4003d0,
d_ptr = 0x4003d0
}
}
pwndbg> p/x l->l_info[6]->d_un->d_ptr
$58 = 0x4003d0

其值正好是 SYMTAB。获得 strtab&pltgot&reloc 变量的值的过程于此相似,不再赘述。

随后,根据 reloc (Elf64_Rel 结构体)函数重定位的信息 r_offset,r_offset 得到 got 表真实地址和符号信息

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
image-20230721181153309

根据宏 #define ELF64_R_SYM(i) ((i) >> 32) 可以得知,这是取 r_info 前四字节,因此 sym 指向 symtab 的序号 2 的结构体。

l->l_addr 表示程序的加载基址,这里关了 pie 所以是 0,得到了 got 表地址 rel_addr。

两个检查

第一个检查是检查 r_info 的后一字节为 0x7

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF_MACHINE_JMP_SLOT R_X86_64_JUMP_SLOT
#define R_X86_64_JUMP_SLOT 7 /* Create PLT entry */
//等价于
assert ((reloc->r_info & 0xff) == 0x7);

第二个检查是符号的 visibility 如果不满足 check 的话就表示 normal lookup rules are not used

if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o)
#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)

其次计算 version

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;
}

寻找地址、填入 got

在 libc 中定位函数地址

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

加上 libc 的基址

value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));

写入 got 表

return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

ret2dl_resolve

再说吧

后话

clion 怎么跳转到定义

https://syst3mfailure.io/ret2dl_resolve/

https://xz.aliyun.com/t/5722

clion不支持代码跳转 不同文件夹之间clion无法代码跳转? - 知乎用户的回答 - 知乎 https://www.zhihu.com/question/50223053/answer/752977021