本文目标
连结器让我们能够对各个独立文件进行编译与组译,这样的好处显而易见: 当一个专案有多个档案时,如果仅修改一个档案,我们不需要重新编译全部的程序码,而是编译更动的程序再做连结即可。
上图取自该网站。
多数人对於程序执行的第一个步骤便是执行 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 的设计,这样做会有以下优点:
补充:
使用动态连结产生的程序码与传统的方式没有太大的差异,最大的差别是,跳转的目标并不是实际的函式,而是带有三条指令的 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 。
当程序是利用 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
第二次被呼叫时,就可以直接找到其位址。#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-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 档案主要包含(最多)七个部分,参考 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
,以下为七个区块的解说:
包含核心将二进位档案载入入记忆体并执行所需的参数,也包含对动态连结器 ld 的指引。
以 C 语言程序为例,经过 gcc 编译为 a.out 档案後,会在 header 纪录各个区块所需要的大小。
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
有以下多种变体:
有些专业术语用中文表示会让原意跑掉,如果对 MAGIC 真的很感兴趣的话可以参考 Stackoverflow 上的问答串。
若没有指定 gcc 的输出 -o
选项,在预设情况下, gcc 会直接把 C 语言编译为 a.out 档案。
a.out
的构造非常简易,也因为这个特性, a.out
无法支援较为复杂的功能,如: 动态连结与载入等。
目前,主流的 UNIX Like 都已改采 .elf 格式作为标准的目的档格式。
可执行与可链结格式 (Executable and Linkable Format) 简称为 elf 格式,我们通常会在编译 C 语言程序时看到
.elf
档案。
ro
表示 read-only
,该段落会放置常数使用 --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使用 --help
选项查看 objdump
有哪些功能可用:
objdump --help
objdump 仅有两大功能:
-D
: 反组译-S
: 将 ELF 反组译并与 C Source code 混合输出透过上面的介绍可以了解到 elf
档案在执行周期与连结时期时会用不同的观点存取档案,也因为 elf
档案非常复杂,读者有兴趣的话可以参考陈锺诚老师的教学文章。
最初规划文章的时候本来想要用一篇讲完组译器与连结器,後来越写越多乾脆连可执行文件与 lazy-binding
也一起补上了,希望能帮助到正在观看文章的你 : )
>>: Day 4:透过 npm、Hexo 指令在本机端安装你的 Hexo 部落格
题目:https://alf.nu/alert1 TI(S)M 题目: function esca...
Line bot 如果使用轻用量方案,每个月的免费主动推送次数只会有500则(一封讯息主动推送给 n...
在有必要的时候,我们可能需要使用寄信来通知使用者、寄信给公会小姐、 把AI数据视觉化的资料寄给老板。...
简单说一下,纪录今天的成果 使用Google Recaptcha v3 一开始的想法是在送给後端前先...
https://github.com/KaliChen/SearchAndDownload Inst...