一道题浅析LD_PRELOAD文件劫持 /proc/self/status反反调试

这是Geekgame2025的一道逆向题,需要附件联系我

其中里面有打开/proc/self/status的反调试,对于这题来说很简单,我们修改stack或者patch直接让v1为0均可

但是为了学习,可以了解下LD_PRELOAD文件劫持实现反反调试

image-20251025111805942

可以看下调试时的pid

image-20251024212657134

对于本题很简单,ai直接给出脚本即可

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
36
37
38
// hide_tracer.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef FILE *(*fopen_t)(const char *, const char *);

static fopen_t real_fopen = NULL;

/* 生成一个假的 /proc/self/status 内容,其中保证包含 TracerPid: 0 */
static const char fake_status[] =
"Name:\tmyproc\n"
"State:\tR (running)\n"
"Pid:\t1234\n"
"PPid:\t1\n"
"TracerPid:\t0\n" /* <-- 关键:返回 0 表示未被调试 */
"Uid:\t1000\t1000\t1000\t1000\n"
"VmSize:\t0 kB\n"
;

/* 替换 fopen,只在 path == "/proc/self/status" 时返回内存 FILE* */
FILE *fopen(const char *path, const char *mode) {
if (!real_fopen) {
real_fopen = (fopen_t)dlsym(RTLD_NEXT, "fopen");
}
if (path && strcmp(path, "/proc/self/status") == 0) {
/* fmemopen 在 glibc 可用:把字符串当作 FILE* 返回 */
/* 注意:fmemopen 需要可写缓冲区,因此复制一份到 malloc 的区域 */
char *buf = malloc(sizeof(fake_status));
if (!buf) return NULL;
memcpy(buf, fake_status, sizeof(fake_status));
return fmemopen(buf, sizeof(fake_status) - 1, "r");
}
/* 其他文件按真实 fopen 处理 */
return real_fopen(path, mode);
}

要用IDA进行远程调试这里有个坑点就是

远程调试需要用这个命令去设置环境变量启动调试器,在Parameters里设置没有用!!!:

1
2
gcc -shared -fPIC -o hide_tracer.so hide_tracer.c -ldl
LD_PRELOAD=/home/matriy/RE/TSCTF/hide_tracer.so ./linux_server64

此外写出代码很简单,我们也需要了解背后机理:

当设置了 LD_PRELOAD=/path/to/hook.so 时,Linux 动态链接器在加载程序时会优先加载这个库,并把里面的同名函数符号放在最前面

所以当程序调用 fopen() 时,**动态链接器解析符号时先找到库里的 fopen**,就直接调用你写的函数,而不是 libc 的版本。

符号解析顺序(优先级)大致如下:

  1. 主程序(main ELF)
  2. LD_PRELOAD 指定的库(最高优先)
  3. 其他依赖库(按照 ELF 的依赖顺序)
  4. libc.so.6(系统 C 标准库)
  5. ld-linux 自身

在我们的代码中

1
2
3
if (!real_fopen) {
real_fopen = (fopen_t)dlsym(RTLD_NEXT, "fopen");
}

这行的作用是说:

找下一个名为 fopen 的实现(也就是 libc 里的真正 fopen)

RTLD_NEXT 代表:

从当前库之后继续查找同名符号。

于是就能在 hook 函数里调用原版函数

1
return real_fopen(path, mode);

也就是说调试时会先找到我的fopen,调用我的这个函数,然后我去找真实的函数,并判断调用这个函数的path是否是/proc/self/status