跳过内容

MASTG-TECH-0041: 库注入

在前一节中,我们学习了如何修补应用程序代码以辅助我们的分析,但这种方法存在一些局限性。例如,您可能希望记录通过网络发送的所有内容,而无需执行中间人攻击。为此,您必须修补所有可能的网络 API 调用,这在处理大型应用程序时很快就会变得不切实际。此外,由于修补程序对于每个应用程序都是唯一的,因此也可以认为这是一个缺点,因为此代码不容易重用。

使用库注入,您可以开发可重用的库并将它们注入到不同的应用程序中,从而有效地使它们以不同的方式运行,而无需修改其原始源代码。这在 Windows 上被称为 DLL 注入(广泛用于修改和绕过游戏中的反作弊机制),在 Linux 上称为 LD_PRELOAD,在 macOS 上称为 DYLD_INSERT_LIBRARIES。在 Android 和 iOS 上,一个常见的例子是使用 Frida Gadget,只要 Frida 所谓的注入模式不适用(即您无法在目标设备上运行 Frida 服务器)。在这种情况下,您可以通过使用与本节中将要学习的相同方法注入 Gadget库。

在许多情况下,库注入是可取的,例如

  • 执行进程内省(例如,列出类、跟踪方法调用、监视访问的文件、监视网络访问、获取直接内存访问)。
  • 使用您自己的实现来支持或替换现有代码(例如,替换应该给出随机数的函数)。
  • 向现有应用程序引入新功能。
  • 调试和修复您没有原始源代码的代码上难以捉摸的运行时错误。
  • 在未 Root 的设备上启用动态测试(例如,使用 Frida)。

在本节中,我们将学习在 Android 上执行库注入的技术,主要包括修补应用程序代码(smali 或 native),或者使用操作系统加载器本身提供的 LD_PRELOAD 功能。

修补应用程序的 Smali 代码

可以修补 Android 应用程序的反编译 smali 代码,以引入对 System.loadLibrary 的调用。以下 smali 修补程序注入一个名为 libinject.so 的库

const-string v0, "inject"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

理想情况下,您应该在应用程序生命周期的早期插入上述代码,例如在 onCreate 方法中。重要的是要记住在 APK 中 lib 文件夹的相应体系结构文件夹(armeabi-v7a、arm64-v8a、x86)中添加库 libinject.so。最后,您需要在使用应用程序之前重新签名。

这种技术的一个众所周知的用例是将 Frida gadget 加载到应用程序中,尤其是在使用未 Root 的设备时(这基本上就是 objection patchapk 所做的)。

修补应用程序的 Native Library

许多 Android 应用程序除了 Java 代码外,还使用 native 代码,这是出于各种性能和安全原因。native 代码以 ELF 共享库的形式存在。ELF 可执行文件包括链接到可执行文件的共享库(依赖项)列表,以使其功能最佳。可以修改此列表以插入要注入到进程中的附加库。

手动修改 ELF 文件结构以注入库可能很麻烦且容易出错。但是,使用 LIEF (用于检测可执行格式的库) 可以相对容易地执行此任务。使用它只需要几行 Python 代码,如下所示

import lief

libnative = lief.parse("libnative.so")
libnative.add_library("libinject.so") # Injection!
libnative.write("libnative.so")

在上面的示例中,libinject.so 库作为 native 库 (libnative.so) 的依赖项注入,应用程序默认加载该 native 库。如 LIEF 的文档中所述,可以使用此方法将 Frida gadget 注入到应用程序中。与上一节一样,重要的是要记住将库添加到 APK 中相应的体系结构 lib 文件夹中,最后重新签名应用程序。

预加载符号

上面我们研究了需要某种修改应用程序代码的技术。还可以使用操作系统加载器提供的功能将库注入到进程中。在基于 Linux 操作系统的 Android 上,您可以通过设置 LD_PRELOAD 环境变量来加载其他库。

ld.so 手册页所述,从使用 LD_PRELOAD 传递的库加载的符号始终具有优先权,即加载程序在解析符号时首先搜索它们,从而有效地覆盖原始符号。此功能通常用于检查一些常用 libc 函数(例如 fopenreadwritestrcmp 等)的输入参数,尤其是在混淆的程序中,其中了解其行为可能具有挑战性。因此,深入了解正在打开哪些文件或正在比较哪些字符串可能非常有价值。这里的关键思想是“函数包装”,这意味着您无法修补 libc 的 fopen 等系统调用,但您可以覆盖(包装)它,包括自定义代码,例如,该代码将为您打印输入参数,并且仍然调用原始 fopen,对调用者保持透明。

在 Android 上,设置 LD_PRELOAD 与其他 Linux 发行版略有不同。如果您从“平台概述”部分回忆一下,Android 中的每个应用程序都是从 Zygote 分叉出来的,Zygote 在 Android 启动期间很早就启动了。因此,无法在 Zygote 上设置 LD_PRELOAD。作为此问题的解决方法,Android 支持 setprop(设置属性)功能。在下面,您可以看到包名称为 com.foo.bar 的应用程序的示例(请注意附加的 wrap. 前缀)

setprop wrap.com.foo.bar LD_PRELOAD=/data/local/tmp/libpreload.so

请注意,如果要预加载的库未分配 SELinux 上下文,从 Android 5.0(API 级别 21)开始,您需要禁用 SELinux 才能使 LD_PRELOAD 工作,这可能需要 root。