文章比较基础,但是一步一步讲得很详细,适合第一次接触使用LD_PRELOAD来劫持函数的朋友~
原文地址:https://securityboulevard.com/2020/10/not-so-random-using-ld_preload-to-hijack-the-rand-function/
首发于ChaMD5安全团队公众号。
正文开始
今天,我想要继续LD_PRELOAD使用系列。在今天的文章中,我们将要在一个简单的随机数猜测游戏中使用LD_PRELOAD来劫持rand()函数,以控制随机数的生成,并通过让随机数具有极高的可预测性来有效地欺骗这个简单的游戏。该游戏的代码以及用于劫持该游戏的LD_PRELOAD共享库均已上传到ProfessionallyEvil GitHub页面。如果您想跟着文章一起动手实践,可以将例子在Kali虚拟机中构建。
克隆GitHub资源库
在Kail虚拟机中,打开一个终端然后运行下面的命令来下载示例代码:
|
|
这会将GitHub的仓库(资源库)下载到您当前所在的目录,然后进入创建的目录并列出其中内容。如果一切顺利,您将会看到类似下面的输出:
概览:下载的文件
该示例的存储库非常简单明了,但我仍想花一点时间来说明该资源库的文件结构,因此我们在这一页上。
- ./LICENSE:这是该资源库的一个许可证文件。它是一个MIT许可证。
- ./Makefile:这是一个Makefile。它用于向
make
命令说明如何构建项目的构建目标。这将在文章后面的部分中详细介绍。 - README.md:这是一个标准的GitHub资源库readme文件。
- ./src/:这个目录包含了游戏和共享库的源代码。
- ./src/guessing_game.c:随机数猜测游戏的源代码,它将是我们使用LD_PRELOAD针对的目标服务。
- ./src/rand_hijack.c:用于劫持的共享库的源代码,我们将使用它来劫持rand()函数。
审计猜测随机数游戏的代码
随机数猜测游戏的代码在 ./src/
源代码目录中一个叫做 guessing_game.c 的文件中。通常,建议您在使用之前检查一下下载的代码。您可以使用任何您喜欢的编辑器来打开和审计它。代码已经注释好了。游戏逻辑的简要概述如下:
- 初始化变量
- 打印banner信息
- 获取用户猜测的在0到31337范围内的随机数
- 使用 printf() 来打印提示符
- 使用 scanf() 从标准输入读取一个无符号整数
- 尝试确认输入正确,否则终止游戏
- 使用当前时间作为srand()的种子播种随机数生成器
- 获得一个在0到31337范围内的随机数
- 可选步骤:如果游戏是在debug模式下构建的,打印用户猜测的数字和随机数的变量值。
- 将生成的随机数与用户的猜测数字进行比较
- 如果它们一致,告诉用户他赢了
- 如果不一致,让用户知道他输了
在程序出错或是猜测错误的情况下,游戏将会以返回值为1退出。如果用户猜测出正确的数字,游戏将返回0。
在附带的Makefile中使用Make
返回到资源库的父目录中,有一个Makefile。这个文件会在每次调用make
命令的时候被使用。不使用任何参数调用make
命令时,将构建默认的构建目标,即构建在资源库根目录下的游戏和用于劫持的共享库二进制文件。也可以使用help
来显示所有可以在make
后面使用的构建目标。可以将游戏构建为调试版本,不提供颜色支持,或者将两者构建为可选版本。 下面是make help
命令输出的屏幕截图:
构建并运行这个游戏
首先,让我们使用下面的make命令构建随机数猜谜游戏的默认版本:
|
|
这条命令应在资源库的根目录中创建以./guessing_game
为名的二进制文件。 该make命令的输出和后续的ls -l
应该显示以下输出:
现在我们已经有了游戏的二进制文件,让我们玩几次以了解它。 要启动游戏,请使用以下命令:
|
|
以下是我进行游戏时的情况,在默认构建模式下,我玩了三次。
现在,让我们尝试构建游戏的调试版本,来查看随机数的值是什么。
构建并使用debug模式运行这个游戏
既然我们已经玩了游戏并对它有所了解,我们将使用make
来构建游戏的调试版本。游戏的调试版本将不剥离ELF的调试符号,并添加一些printf()
语句,这些语句会将用户的猜测的值和生成的随机数值输出到控制台。这将使我们看到随机数的值实际上是什么。要构建调试版本,请使用以下make命令:
|
|
这会将原始的 ./guessing_game 二进制文件替换为新的调试版本。下面的屏幕截图显示了该命令的输出,高亮了gcc命令中的-DDEBUG标志,并显示了后续的ls -l
命令,可以看到由于符号和额外的代码,二进制文件现在大了2-3kb。
现在,如果我们尝试像上次那样玩几次游戏,我们可能仍会猜错,但至少它可以向我们展示随机数,如下面的屏幕快照所示:
审计用于劫持的共享库的源代码
现在我们可以看到随机数的范围很大,我们将通过部署LD_PRELOAD来控制随机数的生成,在该游戏中作弊。但首先,我建议使用以下命令查看ld.so的手册页(man page):
|
|
一旦出现手册页,请查看有关LD_PRELOAD的部分。该文档将介绍我们在本节的第二句中将要使用的内容。以下是手册页中LD_PRELOAD部分的第一段的屏幕截图:
在审计中,我们感兴趣的用于我们攻击中劫持共享库代码在 ./src/rand_hijack.c 文件。在劫持程序中任何一个导入函数,所有您需要做的事情基本上是建立一个具有相同名称和签名的的共享库。由于rand()是一个libc函数,因此您可以在手册页或头文件中查找它。如果一个具有相同函数名的共享库被加载到二进制程序中,它将代替真实的函数。劫持rand()的文件的代码非常简单;实际上,我们只需要3行就可以完成这项工作。
此版本的rand()简单地将静态值42作为“随机数”返回。这意味着一旦我们使用LD_PRELOAD而不是libc库中预期的共享库部署了共享库,游戏将使用rand()的劫持版本。这将使游戏始终生成静态的“随机”值42。这将使我们的游戏更容易获胜!
构建用于劫持的共享库
要构建用于劫持的共享库二进制文件,我们将使用以下命令从资源库的根目录再次使用make命令:
|
|
该命令和后续ls -l
命令的输出如下所示:
如果您查看make命令运行的gcc命令,有两个非常重要的选项被使用。我构建的Makefile使其易于构建,但如果您想要知道自行构建一个Makefile时哪些内容是必须的。第一个选项是-FPIC
选项。此选项将二进制文件编译为“位置无关代码”(Position Independent Code),这通常是您如何构建共享库的方式。这会使代码被加载到随机地址空间中,并阻止其尝试对跳转或调用固定地址。第二个选项是-shared
选项,它告诉gcc将其构建为共享库ELF。如果在其上运行file命令,则可以看到它是一个共享库,如下所示:
使用 LD_PRELOAD 劫持来让猜测随机数游戏不再随机
现在我们已经建立了共享库二进制文件,我们可以使用它来使我们的游戏不再随机并且容易获胜。要进行攻击,我们要做的全部事情就是设置一个名为LD_PRELOAD的环境变量,并将其值设置为共享库的相对路径或共享库的绝对路径。无论哪种方式,都必须是这些路径之一,否则它将搜索常规库路径而找不到我们的共享库。
对于设置环境变量,我建议与命令一起内联执行而不是使用export
导入它。原因是内联只会将该环境变量应用于正在执行的命令。使用export
命令进行的设置会保留在环境中,并且从该终端启动的任何新进程也会尝试加载rand()劫持共享库,这并不理想。
要使用LD_PRELOAD环境变量劫持内联启动游戏,将使用以下命令:
|
|
现在,当我们继续玩几次我们之前构建的游戏的调试版本,并猜测随机数值为42,我们将每次都赢得胜利!下面是行动中劫持的屏幕截图!
只是为了好玩,为什么不尝试猜测41输掉劫持呢?我们仍然可以在调试输出中看到,rand()函数仍在生成42,如下所示:
使游戏恢复正常
劫持虽然很有趣,但最大的好处是我们没有像修改补丁那样触碰或修改原始二进制文件。这意味着,如果我们想要回到没有经过修改的行为下,我们要做的所有事情就是删除LD_PRELOAD环境变量,让该变量恢复正常状态,就像什么也没有发生。然后,如果我们想再次作弊,可以将其重新打开。下面的屏幕快照是从被黑掉到两个没有使用LD_PRELOAD的回合,然后重新打开LD_PRELOAD并再次获胜的屏幕截图。
总结
如示例所示,LD_PRELOAD可以在外部修改应用程序的行为,而不必修补实际的二进制文件本身。这也比对程序进行patch更灵活一些,因为它可以很容易地交叉编译,并且不像二进制补丁程序那样对CPU体系结构敏感。这使它成为修改应用程序行为的灵活方法。在以后的教程中,我们还将看到它可以用于对导入的函数进行更精细的控制,或者无需使用调试器即可进行调试!我希望您喜欢这个示例,它有助于使功能劫持用例更加清楚。此博客文章的视频演示也已创建,并发布在下面:
https://www.youtube.com/watch?v=pZCMxm1X7QU
如果您对基础安全感兴趣,我们有一个专业邪恶基础(PEF)频道,涵盖各种技术主题。我们还会在我们的知识中心内回答一些的基本问题。最后,如果您正在寻找渗透测试,为您的组织进行专业培训或仅遇到一般性安全问题,请与我们联系。