调试LD_PRELOAD注入的代码

    LD_PRELOAD提供了平民化的注入方式固然方便,同时也有不便:注入库出错后调试比较困难。我琢磨了几天找到了可行的调试方法,当然未必是最有效的办法。抛出陋文,希望引来美玉~

    首先,写一段代码作为普通的动态库,公开接口,供人调用,如下:

//true.c
int fake(const char* s1,const char* s2)
{
	return 0;
}

gcc -g3 -O0 -o libtrue.so true.c -fPIC -shared
echo "/root/Desktop">>/etc/ld.so.conf
ldconfig
这差不多是个空函数。

    下面是LD_PRELOAD将要注入的代码:

//fake.c
#include <string.h>
#include <stdio.h>

int fake(const char* s1,const char* s2)
{
	printf("s1:%s-s2:%s\n",s1,s2);
	
	while(1)
			sleep(1);
	return 0;
} 

Makefile
all:
    gcc -g3 -O0 -fPIC -shared -Wa,-adlhn -c fake.c -fno-builtin-strcmp > fake.cod
    gcc -g3 -O0 -fPIC -shared -o fake.so fake.o -Wl,-Map,Sym.map
fake.c除了生成调试信息以外,同时生成符号映射文件,why?

    先不解释为什么,先来看下Sym.map中有什么:

.init           0x0000000000000498       0x18
 *(.init)
 .init          0x0000000000000498        0x9 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
                0x0000000000000498                _init
 .init          0x00000000000004a1        0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
 .init          0x00000000000004a6        0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtendS.o
 .init          0x00000000000004ab        0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o

.plt            0x00000000000004b0       0x40
 *(.plt)
 .plt           0x00000000000004b0       0x40 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
 *(.iplt)

.text           0x00000000000004f0      0x148
 *(.text.unlikely .text.*_unlikely)
 *(.text .stub .text.* .gnu.linkonce.t.*)
 .text          0x00000000000004f0       0x17 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
 *fill*         0x0000000000000507        0x9 90909090
 .text          0x0000000000000510       0xaa /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
 *fill*         0x00000000000005ba        0x2 90909090
 .text          0x00000000000005bc       0x40 fake.o
                0x00000000000005bc                fake
 *fill*         0x00000000000005fc        0x4 90909090
 .text          0x0000000000000600       0x36 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtendS.o
 *fill*         0x0000000000000636        0x2 90909090
 .text          0x0000000000000638        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
 *(.gnu.warning)

.fini           0x0000000000000638        0xe
 *(.fini)
 .fini          0x0000000000000638        0x4 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o
                0x0000000000000638                _fini
 .fini          0x000000000000063c        0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbeginS.o
 .fini          0x0000000000000641        0x5 /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
                0x0000000000000646                PROVIDE (__etext, .)
                0x0000000000000646                PROVIDE (_etext, .)
                0x0000000000000646                PROVIDE (etext, .)
这里仅截取了Sym.map代码段。代码段中有.init节.text节.fini节。每节中又由诺干.o文件组成,如crt运行时库相关的crti.o以及fake.c编译后生成的fake.o。编译器将源码编译为.o中间文件后,还需要把所有的中间文件按相同的页面属性连接到一起,并分配链接地址(关于编译连接的详细介绍可以参看<程序员的自我修养>)。sym.map文件显示了链接时,各个.o文件在整个fake.so文件中的偏移:如fake.o在文件中的偏移是0x5bc

    现在说明一下需要sym.map的原因:因为fake.so是动态库,程序运行时,加载到内存中的位置不固定。因为他的不固定性,所以很难下断点或者反汇编。但是,程序运行起来后,可以通过cat /proc/pidnum/maps查看进程内存加载的情况,并获得fake.so加载的基址。有了基址,加上偏移,就可以确定fake.c提供的代码在进程空间中的具体地址:


    图中显示fake.so被加载到地址7ffff7deb000-7ffff7dee000,其中7ffff7deb000是基址。加上前面sym.map显示的偏移,现在可以预知fake提供的函数在地址7ffff7deb5bc处。

    继续往下,有了动态库,还要有测试文件调用动态库接口:

//test.c
#include <string.h>
#include <unistd.h>

extern int fake(const char* s1,const char* s2);

int main()
{
	if(fake("1","2") == 0)
	{
		printf("nothing\n");
	}
	
	return 0;
}

gcc -o test test.c
strip --strip-all test
为了比较真实的模拟由他人发布的程序的环境,对于test.c不仅不生成调试信息,同时还剥离符号表。
下面开始注入并启动调试。

export LD_PRELOAD=/root/Desktop/fake.so
gdb test
由于没有调试信息,gdb找不到main函数,因此无法在main函数下断点,直接导致start后程序跑飞了:

(gdb) start 
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

Starting program: /root/Desktop/test 
s1:1-s2:2
好吧,目前只能重新调试它,并尝试找到程序从哪开始的:

[root@localhost Desktop]# gdb test
(gdb) info files 
Symbols from "/root/Desktop/test".
Local exec file:
    `/root/Desktop/test', file type elf64-x86-64.
    Entry point: 0x400520

这样就得到了程序的入口,并于此下断点然后start运行,程序在0x400520处停下:

(gdb) b *0x400520
Breakpoint 1 at 0x400520
(gdb) start 
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

Starting program: /root/Desktop/test 

Breakpoint 1, 0x0000000000400520 in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6_5.3.x86_64
(gdb) 
这时,进程的依赖的各个动态库也业已完成加载,可以查看内存加载情况:

[root@localhost ~]# ps x|grep  test
 4352 pts/8    S+     0:00 gdb test
 4381 pts/8    T      0:00 /root/Desktop/test

[root@localhost ~]# cat /proc/4381/maps 
00400000-00401000 r-xp 00000000 fd:00 131451                             /root/Desktop/test
00600000-00601000 rw-p 00000000 fd:00 131451                             /root/Desktop/test
3326400000-3326420000 r-xp 00000000 fd:00 75830                          /lib64/ld-2.12.so
332661f000-3326620000 r--p 0001f000 fd:00 75830                          /lib64/ld-2.12.so
3326620000-3326621000 rw-p 00020000 fd:00 75830                          /lib64/ld-2.12.so
3326621000-3326622000 rw-p 00000000 00:00 0 
3326c00000-3326d8b000 r-xp 00000000 fd:00 75831                          /lib64/libc-2.12.so
3326d8b000-3326f8a000 ---p 0018b000 fd:00 75831                          /lib64/libc-2.12.so
3326f8a000-3326f8e000 r--p 0018a000 fd:00 75831                          /lib64/libc-2.12.so
3326f8e000-3326f8f000 rw-p 0018e000 fd:00 75831                          /lib64/libc-2.12.so
3326f8f000-3326f94000 rw-p 00000000 00:00 0 
7ffff7bea000-7ffff7bed000 rw-p 00000000 00:00 0 
7ffff7bed000-7ffff7bee000 r-xp 00000000 fd:00 136044                     /root/Desktop/libtrue.so
7ffff7bee000-7ffff7ded000 ---p 00001000 fd:00 136044                     /root/Desktop/libtrue.so
7ffff7ded000-7ffff7dee000 rw-p 00000000 fd:00 136044                     /root/Desktop/libtrue.so
7ffff7dfc000-7ffff7dfd000 r-xp 00000000 fd:00 136014                     /root/Desktop/fake.so
7ffff7dfd000-7ffff7ffc000 ---p 00001000 fd:00 136014                     /root/Desktop/fake.so
7ffff7ffc000-7ffff7ffd000 rw-p 00000000 fd:00 136014                     /root/Desktop/fake.so
7ffff7ffd000-7ffff7ffe000 rw-p 00000000 00:00 0 
7ffff7ffe000-7ffff7fff000 r-xp 00000000 00:00 0                          [vdso]
7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

嗯,fake.so在7ffff7dfc000,fake.o应该位于7ffff7dfc5bc处了。另外也可以看到,被LD_PRELOAD覆盖的libtrue.so被加载到7ffff7bed000处

可以直接在这个位置下断,当然不放心的话,可以用objdump查看test的反汇编:

[root@localhost Desktop]# objdump -d test

Disassembly of section .plt:

00000000004004d8 <fake@plt-0x10>:
  4004d8:    ff 35 d2 04 20 00        pushq  0x2004d2(%rip)        # 6009b0 <_fini+0x2002a8>
  4004de:    ff 25 d4 04 20 00        jmpq   *0x2004d4(%rip)        # 6009b8 <_fini+0x2002b0>
  4004e4:    0f 1f 40 00              nopl   0x0(%rax)

00000000004004e8 <fake@plt>:
  4004e8:    ff 25 d2 04 20 00        jmpq   *0x2004d2(%rip)        # 6009c0 <_fini+0x2002b8>
  4004ee:    68 00 00 00 00           pushq  $0x0
  4004f3:    e9 e0 ff ff ff           jmpq   4004d8 <_init+0x18>

...
0000000000400520 <.text>:
  400520:    31 ed                    xor    %ebp,%ebp
  400522:    49 89 d1                 mov    %rdx,%r9
  400525:    5e                       pop    %rsi
  400526:    48 89 e2                 mov    %rsp,%rdx
  400529:    48 83 e4 f0              and    $0xfffffffffffffff0,%rsp
  40052d:    50                       push   %rax
  40052e:    54                       push   %rsp
  40052f:    49 c7 c0 30 06 40 00     mov    $0x400630,%r8
  400536:    48 c7 c1 40 06 40 00     mov    $0x400640,%rcx
  40053d:    48 c7 c7 04 06 40 00     mov    $0x400604,%rdi
  400544:    e8 bf ff ff ff           callq  400508 <__libc_start_main@plt>

其中

00000000004004e8 <fake@plt>:
  4004e8:    ff 25 d2 04 20 00        jmpq   *0x2004d2(%rip)        # 6009c0 <_fini+0x2002b8>
  4004ee:    68 00 00 00 00           pushq  $0x0
  4004f3:    e9 e0 ff ff ff           jmpq   4004d8 <_init+0x18>
是test向fake.so中导出的函数跳转的地址,可以在此处也下个断点。

======================================================================

附注,测试这段代码时,我已经关闭了随机地址加载所以objdump -d输出的连接地址和test加载地址相同,都是0x400520。关闭随机地址加载的方法如下:

[root@localhost ~]# echo 0>/proc/sys/kernel/randomize_va_space 
======================================================================

(gdb) b *0x04004e8
Breakpoint 2 at 0x4004e8
(gdb) b *0x7ffff7dfc5bc
Breakpoint 3 at 0x7ffff7dfc5bc: file fake.c, line 5.
(gdb) 
继续运行,程序在0x4004e8出停下后反汇编看看,然后继续运行到fake.so中

(gdb) c
Continuing.

Breakpoint 2, 0x00000000004004e8 in fake@plt ()
(gdb) x /32i $pc
=> 0x4004e8 <fake@plt>:	jmpq   *0x2004d2(%rip)        # 0x6009c0 <fake@got.plt>
   0x4004ee <fake@plt+6>:	pushq  $0x0
   0x4004f3 <fake@plt+11>:	jmpq   0x4004d8

(gdb) c
Continuing.

Breakpoint 3, fake (s1=0x3326621188 "", 
    s2=0x332640e9f0 "UH\211\345AWAVAUATE1\344S1\333H\203\354HH\307E\250")
    at fake.c:5
5	{
(gdb) x /32i $pc
 (gdb) x /32i $pc
=> 0x7ffff7dfc5bc <fake>:    push   %rbp
   0x7ffff7dfc5bd <fake+1>:    mov    %rsp,%rbp
   0x7ffff7dfc5c0 <fake+4>:    sub    $0x10,%rsp
   0x7ffff7dfc5c4 <fake+8>:    mov    %rdi,-0x8(%rbp)
   0x7ffff7dfc5c8 <fake+12>:    mov    %rsi,-0x10(%rbp)
   0x7ffff7dfc5cc <fake+16>:    lea    0x73(%rip),%rax        # 0x7ffff7dfc646
   0x7ffff7dfc5d3 <fake+23>:    mov    -0x10(%rbp),%rdx
   0x7ffff7dfc5d7 <fake+27>:    mov    -0x8(%rbp),%rcx
   0x7ffff7dfc5db <fake+31>:    mov    %rcx,%rsi
   0x7ffff7dfc5de <fake+34>:    mov    %rax,%rdi
   0x7ffff7dfc5e1 <fake+37>:    mov    $0x0,%eax
   0x7ffff7dfc5e6 <fake+42>:    callq  0x7ffff7dfc4c0 <printf@plt>
   0x7ffff7dfc5eb <fake+47>:    mov    $0x1,%edi
   0x7ffff7dfc5f0 <fake+52>:    mov    $0x0,%eax
   0x7ffff7dfc5f5 <fake+57>:    callq  0x7ffff7dfc4e0 <sleep@plt>
   0x7ffff7dfc5fa <fake+62>:    jmp    0x7ffff7dfc5eb <fake+47>
在这段反汇编代码中,我们看到了2个函数:printf/sleep,有点像fake.c中的代码了。

由于,fake.so是源码编译的,可以在此看到源码,并下断点:

  0x7ffff7dfc604 <__do_global_ctors_aux+4>:	push   %rbx
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) list
1	#include <string.h>
2	#include <stdio.h>
3	
4	int fake(const char* s1,const char* s2)
5	{
6		printf("s1:%s-s2:%s\n",s1,s2);
7		
8		while(1)
9				sleep(1);
10		return 0;
(gdb) b 8
Breakpoint 4 at 0x7ffff7dfc5eb: file fake.c, line 8.
(gdb) 

当然,还支持查看变量:

$2 = 0x40072a "1"
(gdb) p s2
$3 = 0x400728 "2"
(gdb) 


至此,调试LD_PRELOAD注入的so文件结束,3q

qq:562703006@qq.com

<think>我们正在讨论如何禁用或关闭LD_PRELOAD环境变量。根据引用[1]和[2],我们知道LD_PRELOAD是一个环境变量,它允许在程序运行前优先加载指定的动态链接库(so文件)。这可以用于覆盖正常的库函数,但也可能被滥用,因此有时需要禁用它。 禁用LD_PRELOAD的方法: 1. **全局禁用(系统级)**:可以通过修改系统配置来全局禁用LD_PRELOAD,但这通常需要修改内核参数或使用安全模块(如SELinux)来限制。但请注意,Linux内核本身并没有提供直接禁用LD_PRELOAD的开关,因为它是动态链接器(ld-linux.so)的功能。 2. **通过安全模块限制**: - **SELinux**:可以配置SELinux策略来禁止程序使用LD_PRELOAD。例如,可以设置一个策略禁止进程执行`setenv`来设置LD_PRELOAD,或者禁止动态链接器加载非标准路径的库。但这需要较强的SELinux策略编写能力。 - **AppArmor**:类似地,使用AppArmor可以为特定程序配置一个配置文件,禁止加载不在允许列表中的库,从而间接禁用LD_PRELOAD的效果。 3. **通过控制环境变量**: - 在启动程序时,确保环境变量中不包含LD_PRELOAD。例如,在shell中启动程序前,可以清除环境变量: ```bash env -i PATH=$PATH command ``` 这样会清空所有环境变量,只保留指定的PATH,然后运行命令。也可以单独移除LD_PRELOAD: ```bash unset LD_PRELOAD command ``` 或者: ```bash LD_PRELOAD= command ``` 4. **通过sudo配置**:如果程序是通过sudo运行的,可以在sudoers文件中配置`env_reset`,并且使用`env_delete`来删除LD_PRELOAD。例如: ``` Defaults env_reset Defaults env_delete = "LD_PRELOAD" ``` 这样,每次通过sudo运行命令时,环境变量都会被重置,并且LD_PRELOAD会被删除。 5. **在程序内部禁用**:如果我们是程序的开发者,可以在程序的启动脚本中清除LD_PRELOAD,或者在程序启动时调用`unsetenv("LD_PRELOAD")`(C语言)。但注意,如果攻击者已经设置了LD_PRELOAD,那么程序启动时动态链接器已经加载了指定的库,所以清除环境变量的操作应该在程序启动前进行(如通过脚本)。 6. **使用静态链接**:将程序静态链接,这样在运行时就不会加载动态库,因此LD_PRELOAD不会生效。但是静态链接会增大程序体积,并且可能带来其他问题。 7. **使用chroot或容器隔离**:在chroot环境或容器(如Docker)中运行程序,限制可用的库文件,这样即使设置了LD_PRELOAD,也无法加载外部的恶意库。 8. **使用seccomp-bpf**:通过seccomp-bpf限制进程的系统调用,可以禁止加载动态库的系统调用(如`open`、`mmap`等),但这种方法比较复杂,且可能影响程序正常功能。 注意:禁用LD_PRELOAD可能会影响一些正常使用该机制的程序(如某些调试工具或性能分析工具)。因此,在实施禁用时应谨慎。 根据引用[3]和[4],LD_PRELOAD常被用于绕过函数禁用或进行SSL加密数据的获取,这也说明了为什么有时需要禁用它(安全原因)。 综上所述,禁用LD_PRELOAD有多种方法,具体选择取决于应用场景和安全需求。最常用且简单的方法是在启动程序前清除环境变量,或者通过sudo配置来清除。 生成相关问题: 1. 如何检查一个程序是否使用了LD_PRELOAD加载了额外的库? 2. 除了LD_PRELOAD,还有哪些环境变量可能影响程序的安全? 3. 在Docker容器中如何禁用LD_PRELOAD? 4. 为什么LD_PRELOAD会被用于攻击?请举例说明。</think>### 如何禁用或关闭 LD_PRELOAD #### 禁用方法 1. **临时清除环境变量** 在终端会话中直接取消设置: ```bash unset LD_PRELOAD # 当前会话生效 ``` 或启动程序时覆盖为空: ```bash LD_PRELOAD= your_command ``` 2. **永久全局禁用** 在系统配置文件中禁用(需 root 权限): ```bash # 编辑 /etc/environment 或 /etc/profile echo "unset LD_PRELOAD" | sudo tee -a /etc/profile source /etc/profile # 立即生效 ``` 3. **sudo 环境强制清除** 修改 sudoers 配置(`visudo`): ```bash Defaults env_reset Defaults env_delete = "LD_PRELOAD" # 删除该变量 ``` 4. **安全模块限制** 通过 AppArmor/SELinux 配置策略: ```bash # AppArmor 示例:禁止程序加载外部库 deny /etc/ld.so.preload r, deny /usr/local/lib/*.so mrwkl, ``` 5. **容器化隔离** 在 Docker 中运行时显式禁用: ```bash docker run --env LD_PRELOAD= your_image ``` #### 注意事项 - **权限要求**:永久性系统级修改需 root 权限[^2]。 - **副作用**:可能影响依赖动态库注入的正常程序(如调试工具)。 - **安全边界**:无法完全阻止恶意进程通过 `ptrace` 等系统调用绕过限制[^3]。 - **审计建议**:定期检查 `/etc/ld.so.preload` 文件内容,防止隐蔽注入。 --- ### 相关问题 1. **LD_PRELOAD 被禁用后,如何调试程序依赖的动态库加载问题?** 2. **在 Kubernetes 集群中如何批量禁用容器的 LD_PRELOAD?** 3. **LD_PRELOAD 与 `ld.so.preload` 文件的优先级区别是什么?** 4. **如何检测系统是否存在利用 LD_PRELOAD 的隐蔽后门?** [^1]: LD_PRELOADLinux 下的环境变量,用于优先加载动态链接库,可覆盖正常函数库。 [^2]: 通过 sudoers 配置可强制清除环境变量,但需 root 权限。 [^3]: 禁用 LD_PRELOAD 无法完全阻止通过 `ptrace` 等系统调用的代码注入攻击。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值