Linux 系统中的动态链接虽然是一个基础话题,近二十年来有许多书籍和文章讨论,但是很多内容较为繁杂。本次,我们以一个案例来说明动态链接和寻址过程,贯穿动态链接的主要生命周期,从一个不一样的视角,带你入门二进制 Patch、ELF 感染,深入体验 ELF 的动态链接过程。
可执行文件 hello
调用动态库 myfun.so
函数。编译阶段,链接器将 myfun.so
写入到 hello
的 .dynamic
节,运行阶段,链接器 ld.so
就会装载 myfun.so
,遍历 .rela.plt
节中的重定位符号,填充对应的 .got.plt
节的实际符号地址。这样 hello
在运行时,就能跳转到对应函数的真实地址。整个动态链接生命周期如下所示,结合后文内容,你就能明白动态链接的符号如何实现定位功能。
编写共享库函数 myfun.c
相应的头文件 myfun.h
编译为动态共享库
主程序代码 hello.c
调用动态库的 func1
编译可执行程序
上面的程序很简单,我们从整个 ELF 文件动态链接生命周期的每个阶段尝试通过函数劫持的方式,使得原本调用 getchar()
改为调用 func1
或者 func2
。
将恶意代码插入到 ELF 二进制文件,通常被称为 “ELF 二进制文件感染”。高质量的 ELF 二进制文件感染通常涉及使用特定的感染算法,这些算法针对 ELF 文件的不同用例。
通过了解动态链接过程,有助于后续将函数流劫持到我们注入的恶意代码,更好的实现感染效果。本次主要跟大家分享一下,如何通过动态链接生命周期中的每个阶段,改变程序执行流。
使用 IDA 反编译 hello
IDA 反汇编窗口识别到的汇编指令是已经优化过的,事实上操作系统并不直接感知函数跳转的符号,对于系统来说,只有地址才是真实的。这一点,可以从 call _getchar
原始指令看出。
call 操作数是一个偏移地址,跳转地址 = 当前指令地址 + 偏移地址。ReverseWidget 的反汇编引擎 Capstone 也进行了优化,当我们设置好当前指令的地址,就可以直接得出 call
指令的跳转地址。0X1040
指向 .plt
节的代码。因此,如果要将 call _getchar
改为 call _func1
,需要将 call 0x1040
改为 0x1030
,也就是将 e4
改为 d4
。
IDA Patch 以后,执行 hello
,发现原本不打印的 getchar
已被改为 func1
。
.plt 也是一段代码,但是使用的是 jump
而非 call
这时候,Capstone 似乎并不能直接算出跳转地址
跳转地址 = 当前 DIP + 偏移地址(当前指令的下一条指令地址)
分析得出,.plt
跳转到 .got.plt
因此,如果要将 call _getchar
改为 call _func1
,需要将 .plt
跳转到 got
表中的 func1
。仍然使用 IDA Patch
也就是将 c2
改为 ba
执行 hello
,getchar
已被修改为 func1
。
CTF 比赛中描述的 got
表劫持,通常指的就是 .got.plt
节中的函数指针,是缓冲区溢出常见的漏洞利用方式,也被称为 ret2got
。这是一种动态行为,动态链接器会在每次加载 so 时,将 .got.plt
地址修改为真实的外部函数地址,这就是所谓的重定位(延迟绑定)。ELF Patch 或者说感染,是在程序运行前,是一种静态行为,这不同于漏洞利用中的动态行为。因此,如果只是静态修改地址,是无法实现函数劫持的。
那么 .got.plt 存放的地址指针如何重定位?这是由 GNU libc 动态链接器/加载器 ld.so
在 ELF 文件加载到进程空间以后,程序运行之前完成的。动态链接器遍历 .rela.plt
节,完成函数地址的重定位
在此节中,可以直接修改符号下标,实现函数替换。
此时我们发现重定位节的符号下标已被成功修改
.rela.plt
节表示了需要重定位的符号,该节中的 Info
成员高 8 位,表示在 .dynsym
动态符号表的下标,从而定位到我们在 IDA 看到的符号 getchar
,这就是一个完整的重定位符号流程。
因此,如果要将 call _getchar
改为 call _func2
,需要将动态符号表 getchar
改为 func2
结果显示,函数 getchar
已被劫持为 func2
。
除上述方法以外,还可以修改 myfun.so
的动态符号表,达到相同的函数劫持效果。
如今的 Linux 二进制世界,动态链接随处可见,理解了动态链接的过程,也就理解了函数的执行流,控制执行流是漏洞利用必不可少的一环。通过上述实际案例有助于我们更好的理解一个二进制如何找到外部符号的地址,也能告诉我们,可以在动态链接生命周期的任何一环实现函数劫持。
#include <stdio.h>
#include "myfun.h"
void
func1() {
printf
(
"Hello, this is func1.\n"
);
}
void
func2() {
printf
(
"Hello, this is func2.\n"
);
}
#include <stdio.h>
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-11-23 15:45
被magicsong编辑
,原因: 更新图片和描述