首页
社区
课程
招聘
[原创]LD_PRELOAD 劫持 fopen:优雅绕过 TracerPid 反调试
发表于: 5天前 609

[原创]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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回