编译器驱动程序过程

以c语言为例子

  1. 首先将头文件预处理等展开,将.c文件转化为.i文件,.i文件就是头文件展开过后的文件
  2. 编译器驱动将.i文件转化为.sASCII码的汇编文件
  3. .s的汇编文件编译成.o的二进制目标文件
  4. 由链接器将.o文件链接,形成二进制可执行文件

GOT与PLT

首先说下这两个单词的全称。

plt 全称为过程链接表,英文单词为Procedure Linkage Table

got全称为全局偏移量表,英文名称为Global Offset Table

got表的前三项存放的都是固定的东西。got[0]got[1]包含动态链接器在解析函数地址时会使用的地址,got[3]存放ld-linux.so的模块入口点,其他位置就存放需要动态绑定的函数。如下图所示

plt表前两项是特殊的,plt[0]跳转到动态链接器中,plt[1]调用__libc_start_main函数。

用一个测试文件先看看情况,代码如下。编译命令为gcc -g -o init.c init

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <malloc.h>

int main() {
int *p = malloc(32);
free(p);
p = malloc(32);
free(p);

return 0;
}

在第一次调用malloc时,跟进看汇编代码,两个jmp一个push,在通过内存看看跳转的地址,其实就是跳转到第二条指令。根据书上所说的,每次调用一个进入一个没有绑定函数的plt中的代码,他的第一条指令总是跳转到第二条指令处。

在将malloc函数的id压入栈后,跳转到plt[0]处,指令就如下两条代码,第一条汇编代码将got[1]内容压入栈中,然后通过got[2]间接的跳转到动态链接器中,也就是函数_dl_runtime_resolve_xsavec,这样就完成了与got表的绑定。

image-20220311003712041

当再次调用malloc函数时,plt的第一条指令就直接跳转到malloc函数处

image-20220311003957713

上面就简单的介绍了延迟绑定的细节(要记住plt -> got -> func),做后在用书上的一副图,感觉这附图画得是真的很详细了

库打桩

这个技术也算是hook技术的一种,一共有三种形式:编译时打桩,链接时打桩和运行时打桩。

统一的测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
//init.c
#include <stdio.h>
#include <malloc.h>

int main() {
int *p = malloc(32);
free(p);
p = malloc(32);
free(p);

return 0;
}

运行打桩

依赖于LD_PRELOAD这个环境变量,动态链接器ld-linux.so会首先搜寻LD_PRELOAD这个库,将下面源代码编译成so文件,在在程序运行时设置LD_PRELOAD这个环境变量就可以进行打桩了(个人认为这种技术是平时用的最多的一种)

测试代码如下,编译命令为gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// mymalloc.c
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

void *malloc(size_t size) {
void *(*mallocp)(size_t size);
char *error;

mallocp = dlsym(RTLD_NEXT, "malloc");
if((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}

void free(void *ptr) {
void (*freep)(void *) = NULL;
char *error;
if (!ptr)
return;
freep = dlsym(RTLD_NEXT, "free");
if((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr);
printf("free(%p)\n", ptr);
}
#endif

在使用时加上环境变量命令LD_PRELOAD="./mymalloc.so"

链接打桩

测试文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// mymalloc.c
#ifdef LINKTIME
#include <stdio.h>

void *__real_malloc(size_t size);
void __real_free(void *ptr);

void *__wrap_malloc(size_t size) {
void *ptr = __real_malloc(size);
printf("malloc(%d) = %p\n", (int)size, ptr);

return ptr;
}

void __wrap_free(void *ptr) {
__real_free(ptr);
printf("free(%p)\n", ptr);
}
#endif

编译命令为

1
2
3
gcc -DLINKTIME  -c mymalloc.c
gcc -c init.c
gcc -WI,--warp,malloc -WI,--warp,free -o init init.o mymalloc.o

-WI命令告诉编译器将后面的逗号都转换为空格

编译打桩

编译打桩需要自己写一个malloc.h头文件,测试代码与头文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// mymalloc.c
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>

void *mymalloc(size_t size) {
void *ptr = malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);

return ptr;
}

void myfree(void *ptr) {
free(ptr);
printf("free(%p)\n", ptr);

}
#endif
1
2
3
4
5
6
//malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)

void *mymalloc(size_t size);
void myfree(void *ptr);

使用如下命令编译

1
2
gcc -DCOMPILETIME -c mymalloc.c
gcc -I. -o init init.c mymalloc.o

-I.这个参数的意思为在预处理编译器在搜索系统目录之前,首先搜索当前目录的malloc.h文件

关于打桩技术我看得迷迷糊糊的,只是知道如何使用,但是具体的过程不清楚(猜测可能和编译器有关系),后面需要将这个坑填上。里面的知识点个人认为在安全方面比较常用。比如各种劫持技术等,其中里面的__real_malloc__real_free这两个函数在打pwn时经常遇到,没有弄明白过,后面需要抽时间好好探究下这个问题。