组译器与连结器 (下)

本文目标

  • 了解连结器与常见的连结方式
  • Lazy-binding
  • 建立对 elf file format 的基本认知

连结器的用途

连结器让我们能够对各个独立文件进行编译与组译,这样的好处显而易见: 当一个专案有多个档案时,如果仅修改一个档案,我们不需要重新编译全部的程序码,而是编译更动的程序再做连结即可。

静态连结与动态连结

上图取自该网站

静态连结

多数人对於程序执行的第一个步骤便是执行 main() 函式,实际上却不是这麽一回事。
假设有一支程序是以静态连结的方式编译,该程序被执行後的大致步骤如下:

./program -> fork() -> execve("./program", *argv[], *envp[])

执行 execve() 後,执行绪会从作业系统的 user_mode 切换至 kernel_mode 继续执行:

sys_execve() -> do_execve() -> search_binary_handler() -> load_elf_binary()

载入执行档的 binary 资料後,再切换回 user_mode 继续执行:

_start -> main

关於系统呼叫 fork() 以及 execve() ,可以参考作业系统章节以更深入的了解它,至於本篇的重点则会聚焦在执行档 elf 身上。

静态连结的缺陷

上面所提到的方法便是静态连结,这个方式会在程序运行之前将所有的 Library 进行连结与载入,如果有多个程序都使用到某一个很大的 Library ,便会产出不小的效能开销。
此外,若使用静态连结绑定的 Library 被发现有设计错误,即使该 Library 的作者已经将他更新,使用静态连结的程序中仍是绑定旧有的 Library 。

动态连结

为了解决上述的问题,现今的系统会采用 Dynamic linking 的设计,这样做会有以下优点:

  • 动态连结的 Library 只有在第一次载入时会产生动态开销,之後都会采取 Fast linking 的方式做载入。
  • 如果有多个程序依赖同一个 Library ,那这个动态连结库也只会被加载一次。

补充:
使用动态连结产生的程序码与传统的方式没有太大的差异,最大的差别是,跳转的目标并不是实际的函式,而是带有三条指令的 Stub function 。
Stub function 会查询主记忆体中的 Table 找出实际函式的位置再进行跳转。
也因为第一次呼叫函式时,该函式并没有被载入到主记忆体中 (Table 中找不到实体位置),所以在第一次调用函式时会产生额外的开销。

./program -> fork() -> execve("./program", *argv[], *envp[])

执行 execve() 後,执行绪会从作业系统的 user_mode 切换至 kernel_mode 继续执行:

sys_execve() -> do_execve() -> search_binary_handler() -> load_elf_binary()

载入执行档的 binary 资料後,再切换回 user_mode 继续执行:

ld.so -> _start -> libc_start_main() -> _init -> main

上面的 ld.so 便是动态连结器,它会负责按照可执行档案运作时的需要载入与连结 shared library 。

Lazy-binding

当程序是利用 Dynamic linking 的方式做连结时,其函式位址会在执行周期才确定。这样做的好处显而易见: 程序引入的 library 的函式有千百个,但在执行周其中并不会都使用到,当函式被呼叫时再去载入它,就可以大幅提升执行效率。

判别是否为 Lazy-binding 的方法:
当我们利用逆向工具查看组合语言时,如果有发现 call function 的形式如 call puts@plt ,就代表该函式会在执行期间才做载入。

  • GOT: Global Offset Table

    GOT 其实就是一个存放函式指标的阵列。

    用来记录在 ELF file 中用到的 Shared library 中符号的绝对地址, GOT 主要涵盖以下内容:

    • .dynamic
      动态连结的资讯。

    • .got
      储存全域变数的位址。

    • .got.plt

      Name Description
      address of .dynamic 指向 GOT 的 .dynamic
      link_map 一个链结串列,用来纪录用到的 Library
      dl_runtime_resolve 找出函式的位址
    • .data

  • PLT: Procedure Linkage Table

流程

  • 程序码中呼叫了 func
  • 执行 func@plt
  • func@plt 会跳到 GOT 的 .got.plt 寻找 func 的位置
  • func 的 id 推入 Stack 中。
  • 由於 func 是第一次呼叫,所以没办法顺利在 .got.plt 找到函式位址,这时系统就会把 func 的位址写进 .got.plt 当中。
  • 如此一来,等到 func 第二次被呼叫时,就可以直接找到其位址。

AIS3 2021 - Write me

#include <stdlib.h>
#include <stdio.h>
int main()
{
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    void *systemgot = 0x404028;
    void *scanfgot = 0x404040;
    //void *systemgot = (void *)((long long)(*(int *)(systemptr+2))+(long long)(systemptr+6));
    *(long long *)systemgot = (long long)0x0;

    printf("Address: ");
    void *addr;
    long long v;
    scanf("%ld",&addr);
    printf("Value: ");
    scanf("%ld",&v);
    *(long long *)addr = (long long)v;
    *(long long *)scanfgot = (long long)0x0;
    printf("OK! Shell for you :)\n");
    system("/bin/sh");
    return 0;
}

这一题将 GOT 中的 system() 清掉了,所以我们需要将它重新指向 PLT ,输入:

Address: 4210728 (0x404028)
Value: 4198480 (0x401050)

即可获得 Flag 。
至於为何是输入 4198480 呢?我们先使用 IDA 打开 elf 档案并查看 GOT :

_got_plt        segment qword public 'DATA' use64
.got.plt:0000000000404000                 assume cs:_got_plt
.got.plt:0000000000404000                 ;org 404000h
.got.plt:0000000000404000 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got.plt:0000000000404008 qword_404008    dq 0                    ; DATA XREF: sub_401020↑r
.got.plt:0000000000404010 qword_404010    dq 0                    ; DATA XREF: sub_401020+6↑r
.got.plt:0000000000404018 off_404018      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404020 off_404020      dq offset __stack_chk_fail
.got.plt:0000000000404020                                         ; DATA XREF: ___stack_chk_fail+4↑r
.got.plt:0000000000404028 off_404028      dq offset system        ; DATA XREF: _system+4↑r
.got.plt:0000000000404028                                         ; main+5D↑o
.got.plt:0000000000404030 off_404030      dq offset printf        ; DATA XREF: _printf+4↑r
.got.plt:0000000000404038 off_404038      dq offset setvbuf       ; DATA XREF: _setvbuf+4↑r
.got.plt:0000000000404040 off_404040      dq offset __isoc99_scanf
.got.plt:0000000000404040                                         ; DATA XREF: ___isoc99_scanf+4↑r
.got.plt:0000000000404040                                         ; main+65↑o
.got.plt:0000000000404040 _got_plt        ends

对照基指可以得知 system() 会喷进 PLT 的第三个位址:

plt:0000000000401026 ; ---------------------------------------------------------------------------
.plt:000000000040102D                 align 10h
.plt:0000000000401030 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401030. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040103F                 align 20h
.plt:0000000000401040 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401040. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040104F                 align 10h
.plt:0000000000401050 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401050. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040105F                 align 20h
.plt:0000000000401060 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401060. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040106F                 align 10h
.plt:0000000000401070 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401070. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040107F                 align 20h
.plt:0000000000401080 ; [0000000F BYTES: COLLAPSED FUNCTION sub_401080. PRESS CTRL-NUMPAD+ TO EXPAND]
.plt:000000000040108F                 align 10h
.plt:000000000040108F _plt            ends
.plt:000000000040108F

Lazy-loading 造成的安全性问题

Lazy-binding 虽能够大幅度的提升程序的执行效率,但也因为该机制需要 GOT 能够被写入,所以如果有有心人士将 PLT 的对应位置改成 system call 的位置,那呼叫 plt function 时便会变呼叫 system call ,这点需要特别注意。

浅谈目的档

a.out 是旧版类 Unix 系统中用於执行档、目的码和後来系统中的函式库的一种档案格式,这个名称的意思是组译器输出。
-- 维基百科

a.out 最早可以追朔到第一版 UNIX 作业系统上。对!就是那个搭载在 PDP-7 与 PDP-11 的 UNIX 作业系统。
随着 UNIX 与 UNIX like 被越来越多人使用,我们可以在这些作业系统上看到他的身影。

a.out 的结构

a.out 档案主要包含(最多)七个部分,参考 C 语言定义:

/*
 * Header prepended to each a.out file.
 */
struct exec {
    long      a_magic;   /* magic number */
    unsigned  a_text;    /* size of text segment */
    unsigned  a_data;    /* size of initialized data */
    unsigned  a_bss;     /* size of uninitialized data */
    unsigned  a_syms;    /* size of symbol table */
    unsigned  a_entry;   /* entry point */
    unsigned  a_trsize;  /* size of text relocation */
    unsigned  a_drsize;  /* size of data relocation */
};

先不考虑 a_magic ,以下为七个区块的解说:

  • header (必备)

包含核心将二进位档案载入入记忆体并执行所需的参数,也包含对动态连结器 ld 的指引。

以 C 语言程序为例,经过 gcc 编译为 a.out 档案後,会在 header 纪录各个区块所需要的大小。
Memory Layout

  • text section
    Text section 存放程序执行时被载入记忆体的机器码和相关资料。

  • data section
    已初始化的资料。

    int a = 0;
    
  • text relocation
    包含连结编辑器在合并二进位档案时修改文字段指标的记录。

  • data relocation
    与文字重定位一节类似,但是给资料段指标用的。

  • symbol table
    Symbol table 包含 linker editor 用於交叉参照不同二进位档案中变数和函式 (符号)。

    The symbol table is an array of nlist.
    -- FreeBSD Manual Pages

    至於 nlist 的结构,我们也可以参考 Linux 中的实作:

    struct nlist {
      union {
        char *n_name;
        struct nlist *n_next;
        long n_strx;
      } n_un;
      unsigned char n_type;
      char n_other;
      short n_desc;
      unsigned long n_value;
    };
    
  • string table
    包含对应於符号表的字串。

属於 a.out 的魔术

a.out 有以下多种变体:

  • OMAGIC
    OMAGIC 除了必备的 header 以外,其後紧随了 text section 和 data section , Kernel 会将这两个部分读入可读写的记忆体当中。
  • NMAGIC
    NMAGIC 与 OMAGIC 类似,差别在 data section 出现在 text section 结束後的下一页,并且 text section 在 NMAGIC 格式为唯读状态。
  • ZMAGIC
    ZMAGIC 格式加入了对按需分页的支援, text section 和 data section 的长度需要是页宽的整数倍。
  • QMAGIC
    Binary file 通常会被载入到虚拟位址池的底端,以通过段错误撷取对空指标的解除参照。 a.out 的 Header 与 text section 的第一页合并,通常会省下一页的记忆体。
  • CMAGIC
    旧版的 Linux 使用此格式来存放核心转储。

有些专业术语用中文表示会让原意跑掉,如果对 MAGIC 真的很感兴趣的话可以参考 Stackoverflow 上的问答串

如何编译出 a.out 档案

若没有指定 gcc 的输出 -o 选项,在预设情况下, gcc 会直接把 C 语言编译为 a.out 档案。

可执行与可链结格式

a.out 的构造非常简易,也因为这个特性, a.out 无法支援较为复杂的功能,如: 动态连结与载入等。
目前,主流的 UNIX Like 都已改采 .elf 格式作为标准的目的档格式。

可执行与可链结格式 (Executable and Linkable Format) 简称为 elf 格式,我们通常会在编译 C 语言程序时看到 .elf 档案。

ELF

  • .text: 放置已编译的程序码 (组合语言)
  • .rodata: ro 表示 read-only ,该段落会放置常数
  • .data: 放置已初始化的全域变数或是静态的区域变数
  • .bss: 未初始化的全域变数或是静态的区域变数
  • .debug: 此段放置除错资讯,可以帮助我们更顺利的进行程序分析 (GDB)

使用 Binutils 查看 ELF

使用 --help 选项查看 readelf 有哪些功能可用:

readelf --help

一般来说, readelf 提供了以下功能:

  • -a, --all: 等同於 -h -l -s -S -r -d -V -A -I
  • -h, --file-header: 查看 ELF 文件的档头
  • -l, --program-headers, --segments: 显示 Program headers
  • -S, --section-headers: 显示 section's header

使用 Binutils 对 ELF 反组译

使用 --help 选项查看 objdump 有哪些功能可用:

objdump --help

objdump 仅有两大功能:

  • -D: 反组译
  • -S: 将 ELF 反组译并与 C Source code 混合输出

透过上面的介绍可以了解到 elf 档案在执行周期与连结时期时会用不同的观点存取档案,也因为 elf 档案非常复杂,读者有兴趣的话可以参考陈锺诚老师的教学文章

总结

最初规划文章的时候本来想要用一篇讲完组译器与连结器,後来越写越多乾脆连可执行文件与 lazy-binding 也一起补上了,希望能帮助到正在观看文章的你 : )

Reference


<<:  [Day 18] 针对网页的单元测试(四)

>>:  Day 4:透过 npm、Hexo 指令在本机端安装你的 Hexo 部落格

【第二十五天 - XSS Lab(2)-3】

题目:https://alf.nu/alert1 TI(S)M 题目: function esca...

用 Python 畅玩 Line bot - 28:Line Notify(一)

Line bot 如果使用轻用量方案,每个月的免费主动推送次数只会有500则(一封讯息主动推送给 n...

伸缩自如的Flask [day 28] Flask-Mail

在有必要的时候,我们可能需要使用寄信来通知使用者、寄信给公会小姐、 把AI数据视觉化的资料寄给老板。...

Google Recaptcha v3 使用心得

简单说一下,纪录今天的成果 使用Google Recaptcha v3 一开始的想法是在送给後端前先...

[Python]Login, Search, Download

https://github.com/KaliChen/SearchAndDownload Inst...