高效 ccpp 调试第六章 进程镜像

 

6.2 运行期加载和链接

ELF 文件呈现为静态视图.

程序执行时系统将 ELF 动态加载或映射到内存, 从而创建程序的实例. 随后, 系统调用运行时链接器来解析所有加载模块间的符号引用, 包括可执行文件以及共享库中的输入和输出函数及变量.

在生成新的进程时, 系统加载器与链接器会按以下顺序执行操作:

  1. 为可执行文件创建内存段并将其内容映射到进程的地址空间.
  2. 为所有与可执行文件有依赖关系的共享库创建内存段, 并将它们映射到进程中
  3. 对可执行文件及其依赖的共享库进行重定位
  4. 执行可执行文件及其所有直接或间接依赖的库的初始化代码, 首先执行依赖库的初始化代码
  5. 将程序的控制转移到程序的入口点(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 了.

猜测:

  1. 是不是还没有找到原始的 malloc, 还未将其初始化到malloc_stub里面, 就调用? 也就是说, 全局变量初始化 后面的数组影响了全局变量之后 对于函数有影响吗, 是使用时读取还是已经在初始化时候写死了

疑惑:

  1. 18 是怎么来的, 看起来 lpMalloc 位置的字节很多, 不止 18 字节