6.2 运行期加载和链接
ELF 文件呈现为静态视图.
程序执行时系统将 ELF 动态加载或映射到内存, 从而创建程序的实例. 随后, 系统调用运行时链接器来解析所有加载模块间的符号引用, 包括可执行文件以及共享库中的输入和输出函数及变量.
在生成新的进程时, 系统加载器与链接器会按以下顺序执行操作:
- 为可执行文件创建内存段并将其内容映射到进程的地址空间.
- 为所有与可执行文件有依赖关系的共享库创建内存段, 并将它们映射到进程中
- 对可执行文件及其依赖的共享库进行重定位
- 执行可执行文件及其所有直接或间接依赖的库的初始化代码, 首先执行依赖库的初始化代码
- 将程序的控制转移到程序的入口点(Entry)
ASLR: 地址空间随机布局
Linux为了提高安全性, 开启这个功能. 这会使得链接器随机选择地址来加载共享库, 但这个功能可以关闭
动态库
- PIC: 位置无关代码, 使用相对地址定位. 依赖于下面的两个表
- GOT: 全局偏移表, 计算真正的地址. 存储函数和变量的地址, 而且其位置相对于库代码是固定的.
- PLT: 过程链接表, 进一步优化, 用于延迟函数的绑定, 意味着函数只在第一次被调用时才解析其地址, 这样不会浪费时间去解析那些从未被调用的函数的地址 . 这些指令配合 GOT 来确定或解析函数的地址.
通过实际例子理解 GOT 和 PLT
## gcc -g -m32 plt_got.c
## gdb a.out
(gdb) l
1 #include<stdlib.h>
2
3 int main(){
4 char* p1 = getenv("HOME");
5 char* p2 = getenv("PATH");
6 return 0;
7 }
(gdb) disas main
Dump of assembler code for function main:
0x0000119d <+0>: lea 0x4(%esp),%ecx
0x000011a1 <+4>: and $0xfffffff0,%esp
0x000011a4 <+7>: push -0x4(%ecx)
0x000011a7 <+10>: push %ebp
0x000011a8 <+11>: mov %esp,%ebp
0x000011aa <+13>: push %ebx
0x000011ab <+14>: push %ecx
0x000011ac <+15>: sub $0x10,%esp
0x000011af <+18>: call 0x10a0 <__x86.get_pc_thunk.bx>
0x000011b4 <+23>: add $0x2e24,%ebx
0x000011ba <+29>: sub $0xc,%esp
0x000011bd <+32>: lea -0x1fd0(%ebx),%eax
0x000011c3 <+38>: push %eax
0x000011c4 <+39>: call 0x1050 <getenv@plt>
0x000011c9 <+44>: add $0x10,%esp
0x000011cc <+47>: mov %eax,-0x10(%ebp)
0x000011cf <+50>: sub $0xc,%esp
0x000011d2 <+53>: lea -0x1fcb(%ebx),%eax
0x000011d8 <+59>: push %eax
0x000011d9 <+60>: call 0x1050 <getenv@plt>
0x000011de <+65>: add $0x10,%esp
0x000011e1 <+68>: mov %eax,-0xc(%ebp)
0x000011e4 <+71>: mov $0x0,%eax
0x000011e9 <+76>: lea -0x8(%ebp),%esp
0x000011ec <+79>: pop %ecx
0x000011ed <+80>: pop %ebx
0x000011ee <+81>: pop %ebp
0x000011ef <+82>: lea -0x4(%ecx),%esp
0x000011f2 <+85>: ret
End of assembler dump.
(gdb) x/3i 0x1050
0x1050 <getenv@plt>: jmp *0x10(%ebx)
0x1056 <getenv@plt+6>: push $0x8
0x105b <getenv@plt+11>: jmp 0x1030
直接替换函数
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<dlfcn.h>
#include<sys/mman.h>
#define PAGE_SZ 0x1000ul
extern "C" typedef void*(*MALLOC_FUNC) (size_t);
unsigned char malloc_stub[PAGE_SZ];
MALLOC_FUNC default_malloc = (MALLOC_FUNC) &malloc_stub[0];
extern "C" void* my_malloc(size_t sz) {
fprintf(stderr, "malloc request %ld bytes\n", sz);
return default_malloc(sz);
}
void InjectJumpInstr(unsigned char* target, unsigned char* new_func) {
unsigned char jmp_code[14] = {
// jmpq *0(%rip)
0xff, 0x25, 0x00, 0x00, 0x00, 0x00,
// absolute jmp address 8 bytes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
*(unsigned long*) (jmp_code + 6) = (unsigned long ) new_func;
// r->rw
char* pageaddr = (char*) ((unsigned long) target & ~(PAGE_SZ - 1));
if (mprotect(pageaddr, PAGE_SZ, PROT_READ |PROT_WRITE|PROT_EXEC)) {
printf("failed to change protection mode\n");
exit(-1);
}
memcpy(target, jmp_code, sizeof(jmp_code));
if (mprotect(pageaddr, PAGE_SZ, PROT_READ |PROT_EXEC)) {
printf("failed to recover protection mode\n");
exit(-1);
}
}
int main(){
unsigned char* lpMalloc = (unsigned char*) dlsym(RTLD_DEFAULT, "malloc");
unsigned char* lpMyMalloc = (unsigned char*) &my_malloc;
memcpy(&malloc_stub[0], lpMalloc, 18);
InjectJumpInstr(&malloc_stub[18], lpMalloc+18);
// change dest func
InjectJumpInstr(lpMalloc, lpMyMalloc);
// test malloc
void* parray[16];
for (int i=0;i < 16;i++)
parray[i] = malloc(i*8);
for (int i=0;i < 16;i++)
free(parray[i]);
return 0;
}
不知道问题出在了哪里, sf 了.
猜测:
- 是不是还没有找到原始的 malloc, 还未将其初始化到malloc_stub里面, 就调用? 也就是说, 全局变量初始化 后面的数组影响了全局变量之后 对于函数有影响吗, 是使用时读取还是已经在初始化时候写死了
疑惑:
- 18 是怎么来的, 看起来 lpMalloc 位置的字节很多, 不止 18 字节