-
-
[原创]LD_PRELOAD 劫持 fopen:优雅绕过 TracerPid 反调试
-
发表于: 5天前 609
-
题目名称:DebugMe
平台:crackmes.one
分类:Reverse
难度:2.0
架构:x86-64
题目限制:
A valid solution does not patch the program nor change register values with a debugger. Debugging is encouraged for analysis, but the solution should not rely on it.
基础检查
checksec ./DebugMe
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
objdump -T ./DebugMe
文件格式 elf64-x86-64
DYNAMIC SYMBOL TABLE: strncmp,puts,fclose,printf,fgets,signal,fflush,fopen,atoi,exit,stdout
因为函数未生成符号表,所以根据 __libc_start_main函数的第一个参数来找到main函数的地址
break __libc_start_main
Breakpoint 1, __libc_start_main_impl (main=0x55acbaeab0e0, argc=1, argv=0x7ffe91fad058, init=0x0, fini=0x0, rtld_fini=0x7f9864fc8040 <_dl_fini>, stack_end=0x7ffe91fad048)
全流程分析:
main 入口点: 10e0
10e0: 55 push %rbp
10e1: 89 fd mov %edi,%ebp ; ebp = argc
10e3: bf 08 00 00 00 mov $0x8,%edi
10e8: 53 push %rbx
10e9: 48 89 f3 mov %rsi,%rbx ; rbx = argv
10ec: 48 8d 35 26 02 00 00 lea 0x226(%rip),%rsi
10f3: 51 push %rcx
10f4: e8 97 ff ff ff callq 1090 <signal@plt> ; signal(8, handler_address);
10f9: 48 8d 3d 6e 0f 00 00 lea 0xf6e(%rip),%rdi
1100: 31 c0 xor %eax,%eax
1102: e8 69 ff ff ff callq 1070 <printf@plt> ; printf("DebugMe stage 1:");
1107: 48 8b 3d 5a 2f 00 00 mov 0x2f5a(%rip),%rdi
110e: e8 8d ff ff ff callq 10a0 <fflush@plt>
1113: b8 ff ff ff ff mov $0xffffffff,%eax ; eax = -1;
1118: 83 fd 02 cmp $0x2,%ebp ; if (argv[0] == 2) ?
111b: 75 09 jne 1126 <exit@plt+0x56>
111d: 48 8b 7b 08 mov 0x8(%rbx),%rdi
1121: e8 9a ff ff ff callq 10c0 <atoi@plt> ; eax=atoi(argv[1]);
1126: 89 05 48 2f 00 00 mov %eax,0x2f48(%rip) ; 全局变量:eax=4074 默认为-1
112c: e8 e3 01 00 00 callq 1314 <exit@plt+0x244> ; eax=checkTracerPid()
1131: 89 c1 mov %eax,%ecx
1133: b8 43 00 00 00 mov $0x43,%eax ; eax = 67
1138: 99 cltd
1139: f7 f9 idiv %ecx ; 67 / TracerPid
113b: 83 f8 43 cmp $0x43,%eax ; if (67 / TracerPid的商 == 67)
113e: 74 0c je 114c <exit@plt+0x7c> ; exit
1140: 48 8d 3d 22 0f 00 00 lea 0xf22(%rip),%rdi
1147: e8 f4 fe ff ff callq 1040 <puts@plt> ; put("Fail");
114c: 5a pop %rdx
114d: 31 c0 xor %eax,%eax
114f: 5b pop %rbx
1150: 5d pop %rbp
1151: c3 retq观察得出:
1. 浮点异常handler在1319; ps: Linux 下 SIGFPE 覆盖了整数除零异常
2. 通过判断checkTracerPid的返回值除以67后商是否为67,,如果不等于则输出Fail,等于则退出。
即checkTracerPid返回值=0 触发handler ,其他情况都为失败
查看checkTracerPid: 1259
1259: 55 push %rbp
125a: 48 8d 3d a5 0d 00 00 lea 0xda5(%rip),%rdi ; rdi = "/proc/self/status"
1261: 53 push %rbx
1262: 48 81 ec 28 01 00 00 sub $0x128,%rsp
1269: 64 48 8b 34 25 28 00 mov %fs:0x28,%rsi ; stack canary
1270: 00 00
1272: 48 89 b4 24 18 01 00 mov %rsi,0x118(%rsp)
1279: 00
127a: 48 8d 35 83 0d 00 00 lea 0xd83(%rip),%rsi ; rsi = "r"
1281: e8 2a fe ff ff callq 10b0 <fopen@plt> ; fopen("/proc/self/status", "r")
1286: 48 85 c0 test %rax,%rax
1289: 74 60 je 12eb <exit@plt+0x21b> ; if (f==NULL) return 1;
128b: 48 89 c5 mov %rax,%rbp ; rbp = FILE*
128e: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi ; rsp+0xf = buffer;
1293: b9 09 01 00 00 mov $0x109,%ecx
1298: 31 c0 xor %eax,%eax
129a: f3 aa rep stos %al,%es:(%rdi) ; memset(buffer, 0, 256);
129c: 48 89 ea mov %rbp,%rdx ; rdx = FILE*
129f: be 09 01 00 00 mov $0x109,%esi ; esi = 256
12a4: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi
12a9: e8 d2 fd ff ff callq 1080 <fgets@plt> ; fgets(buffer, 256, FILE*)
12ae: 48 85 c0 test %rax,%rax
12b1: 74 30 je 12e3 <exit@plt+0x213> ; if ( fgets(buffer, 256, FILE*) == NULL)
12b3: ba 0a 00 00 00 mov $0xa,%edx ; edx=10;
12b8: 48 8d 35 59 0d 00 00 lea 0xd59(%rip),%rsi ; rsi = TracerPid:
12bf: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi
12c4: e8 67 fd ff ff callq 1030 <strncmp@plt> ; strncmp(buffer, "TracerPid", 10);
12c9: 85 c0 test %eax,%eax ; if (strncmp(buffer, "TracerPid", 10) != 0)
12cb: 75 cf jne 129c <exit@plt+0x1cc> ; while
12cd: 48 8d 7c 24 19 lea 0x19(%rsp),%rdi
12d2: e8 e9 fd ff ff callq 10c0 <atoi@plt> ; rax = atoi(buffer+0xa)
12d7: 48 89 ef mov %rbp,%rdi
12da: 89 c3 mov %eax,%ebx
12dc: e8 6f fd ff ff callq 1050 <fclose@plt> ; fclose
12e1: eb 0d jmp 12f0 <exit@plt+0x220>
12e3: 48 89 ef mov %rbp,%rdi
12e6: e8 65 fd ff ff callq 1050 <fclose@plt>
12eb: bb 01 00 00 00 mov $0x1,%ebx ; 默认返回1
12f0: 48 8b 84 24 18 01 00 mov 0x118(%rsp),%rax
12f7: 00
12f8: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax
12ff: 00 00
1301: 74 05 je 1308 <exit@plt+0x238>
1303: e8 58 fd ff ff callq 1060 <__stack_chk_fail@plt>
1308: 48 81 c4 28 01 00 00 add $0x128,%rsp
130f: 89 d8 mov %ebx,%eax ; return atoi(buffer+0xa)
1311: 5b pop %rbx
1312: 5d pop %rbp
1313: c3 retq观察得出:
通过fgets迭代行读取/proc/self/status内的TracerPid来判断程序是否进行调试(TracerPid=0则没有调试,其他值则在调试)
查看handler: 1259
1259: 55 push %rbp
125a: 48 8d 3d a5 0d 00 00 lea 0xda5(%rip),%rdi ; rdi = "/proc/self/status"
1261: 53 push %rbx
1262: 48 81 ec 28 01 00 00 sub $0x128,%rsp
1269: 64 48 8b 34 25 28 00 mov %fs:0x28,%rsi ; stack canary
1270: 00 00
1272: 48 89 b4 24 18 01 00 mov %rsi,0x118(%rsp)
1279: 00
127a: 48 8d 35 83 0d 00 00 lea 0xd83(%rip),%rsi ; rsi = "r"
1281: e8 2a fe ff ff callq 10b0 <fopen@plt> ; fopen("/proc/self/status", "r")
1286: 48 85 c0 test %rax,%rax
1289: 74 60 je 12eb <exit@plt+0x21b> ; if (f==NULL) return 1;
128b: 48 89 c5 mov %rax,%rbp ; rbp = FILE*
128e: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi ; rsp+0xf = buffer;
1293: b9 09 01 00 00 mov $0x109,%ecx
1298: 31 c0 xor %eax,%eax
129a: f3 aa rep stos %al,%es:(%rdi) ; memset(buffer, 0, 256);
129c: 48 89 ea mov %rbp,%rdx ; rdx = FILE*
129f: be 09 01 00 00 mov $0x109,%esi ; esi = 256
12a4: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi
12a9: e8 d2 fd ff ff callq 1080 <fgets@plt> ; fgets(buffer, 256, FILE*)
12ae: 48 85 c0 test %rax,%rax
12b1: 74 30 je 12e3 <exit@plt+0x213> ; if ( fgets(buffer, 256, FILE*) == NULL)
12b3: ba 0a 00 00 00 mov $0xa,%edx ; edx=10;
12b8: 48 8d 35 59 0d 00 00 lea 0xd59(%rip),%rsi ; rsi = TracerPid:
12bf: 48 8d 7c 24 0f lea 0xf(%rsp),%rdi
12c4: e8 67 fd ff ff callq 1030 <strncmp@plt> ; strncmp(buffer, "TracerPid", 10);
12c9: 85 c0 test %eax,%eax ; if (strncmp(buffer, "TracerPid", 10) != 0)
12cb: 75 cf jne 129c <exit@plt+0x1cc> ; while
12cd: 48 8d 7c 24 19 lea 0x19(%rsp),%rdi
12d2: e8 e9 fd ff ff callq 10c0 <atoi@plt> ; rax = atoi(buffer+0xa)
12d7: 48 89 ef mov %rbp,%rdi
12da: 89 c3 mov %eax,%ebx
12dc: e8 6f fd ff ff callq 1050 <fclose@plt> ; fclose
12e1: eb 0d jmp 12f0 <exit@plt+0x220>
12e3: 48 89 ef mov %rbp,%rdi
12e6: e8 65 fd ff ff callq 1050 <fclose@plt>
12eb: bb 01 00 00 00 mov $0x1,%ebx ; 默认返回1
12f0: 48 8b 84 24 18 01 00 mov 0x118(%rsp),%rax
12f7: 00
12f8: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax
12ff: 00 00
1301: 74 05 je 1308 <exit@plt+0x238>
1303: e8 58 fd ff ff callq 1060 <__stack_chk_fail@plt>
1308: 48 81 c4 28 01 00 00 add $0x128,%rsp
130f: 89 d8 mov %ebx,%eax ; return atoi(buffer+0xa)
1311: 5b pop %rbx
1312: 5d pop %rbp
1313: c3 retq观察得出:
1. 想要进入该函数,则必须触发浮点异常
2. 如果想要输出DebugMe stage 2: Pass, 则需要ebx = call checkTracerPid / ecx = call checkTracerPid 不为0
3. 如果想要进入第三关即输出Well done :),则需要argv[1] == call checkTracerPid
总结:
1. 想要触发第一关的Pass,需要程序不被调试,checkTracerPid的结果为0。
2. 想要触发第二关的Pass,需要进程被调试, a/b !=0。
3. 想要触发第三关的Well done :), 需要程序传入的参数等于TracerPid。
这互相冲突...
解决办法:LD_PRELOAD:
利用LD_PRELOAD共享库来覆盖fopen,如果读取的是/proc/self/status文件,则我们返回给他一个假的内存文件
第一次触发checkTracerPid时,返回0,让程序进入到handler中执行,后面每次触发则让他返回1,则 a/b != 0
成立,因为每次checkTracerPid的返回值都为1,所以判断 a == argv[1] 时 argv[1]传入1就可以了
// POC:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
static FILE *(*real_fopen)(const char *, const char *) = NULL;
static int call_count = 0;
FILE *fopen(const char *path, const char *mode) {
if (real_fopen == NULL) {
real_fopen = dlsym(RTLD_NEXT, "fopen");
}
if (strcmp(path, "/proc/self/status") == 0) {
call_count++;
if (call_count == 1) {
/* 首次调用 (from main): TracerPid 0 -> 67/0 -> SIGFPE */
return fmemopen("TracerPid:\t0\n", 14, "r");
} else {
/* 非首次调用 (from handler): TracerPid 1 -> 1/1=1 != 0 */
return fmemopen("TracerPid:\t1\n", 14, "r");
}
}
return real_fopen(path, mode);
}编译为共享库:
gcc -shared -fPIC -o override.so override_fopen.c -ldl
执行:
LD_PRELOAD=./override.so ./DebugMe 1
输出:
DebugMe stage 1: Pass
DebugMe stage 2: Pass
DebugMe stage 3: Pass
Well done :)
总结:
程序在执行时,会加载所需要的共享库列表,并映射到内存空间中,然后进行符号重定位:把程序中对 fopen、printf等外部函数的调用,绑定到共享库中对应函数的实际地址
而符号在查找的过程中,会先根据LD_PRELOAD环境变量指定的共享库进行加载,这样我们覆盖了fopen后,就会先去执行我们已经覆盖的
fopen函数了,如果fopen的文件不为/proc/self/status则会通过real_fopen = dlsym(RTLD_NEXT, "fopen");来找到真正的fopen的
地址,进行fopen操作,以防止读取其他文件时也读取到我们伪造的“文件”。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。