Thank you to SpecterOps for supporting this research and to Lee and Sarah for proofreading and editing! Crossposted on GitHub.
感谢SpecterOps对这项研究的支持,感谢Lee和Sarah的校对和编辑!在 GitHub 上交叉发布。
TLDR: You may use fuse-loader or perfect-loader as examples for extending an OS’s native loader to support in-memory libraries.
TLDR:您可以使用 fuse loader 或 perfect-loader 作为扩展操作系统本机加载器以支持内存库的示例。
Some software applications require the ability to load dynamic libraries from the memory of the application’s own process. The majority of desktop OSes do not support this use case, so a number of developers have reimplemented the process of loading a library to overcome this limitation.
某些软件应用程序需要能够从应用程序自身进程的内存中加载动态库。大多数桌面操作系统不支持此用例,因此许多开发人员重新实现了加载库的过程以克服此限制。
The quality of these reimplementations may be judged by comparing the feature set of these custom loaders against what the OS’s native loader supports. As such, the native OS loader may be considered a “perfect loader,” but it should not be considered the only perfect loader.
可以通过将这些自定义加载程序的功能集与操作系统的本机加载程序支持的功能集进行比较来判断这些重新实现的质量。因此,本机操作系统加载程序可能被认为是“完美的加载程序”,但它不应被视为唯一完美的加载程序。
An OS’s loader can be modified or used with other native OS facilities to support in-memory libraries. Extending a native loader in such a manner will result in a new loader which supports both in-memory libraries and the entirety of the native loader’s original feature set (i.e., a new perfect loader). These approaches are explored in the following sections.
操作系统的加载程序可以修改或与其他本机操作系统工具一起使用,以支持内存库。以这种方式扩展本机加载器将产生一个新的加载器,该加载器既支持内存库,也支持本机加载器的整个原始功能集(即,一个新的完美加载器)。以下各节将探讨这些方法。
Native Loader Modifications
本机加载程序修改
Matt Miller and Jarkko Turkulainen authored the seminal work on modifying native loaders with their publication of “Remote Library Injection” in April, 2004. In the section titled “In-Memory,” they described placing hooks on relevant system routines an OS’s loader used (e.g., mmap and NtMapViewOfSection). Those hooks allowed them to use a native loader as intended while modifying the behavior of its underlying routines to have a library’s data be supplied from memory instead of the filesystem.
Matt Miller 和 Jarkko Turkulainen 于 2004 年 4 月出版了《远程库注入》,撰写了关于修改本机加载器的开创性著作。在标题为“内存中”的部分中,他们描述了在操作系统加载程序使用的相关系统例程上放置钩子(例如,mmap和NtMapViewOfSection)。这些钩子允许它们按预期使用本机加载器,同时修改其底层例程的行为,以便从内存而不是文件系统提供库的数据。
Although this technique was excellent, in 2011, Stephen Fewer’s ReflectiveDLLInjection project (which reimplemented LoadLibrary) overshadowed it. What Stephen developed was useful, but LoadLibrary reimplementations are incomplete by nature and their feature gaps will only grow with time.
尽管这种技术非常出色,但在2011年,Stephen Fewer的ReflectiveDLLInjection项目(重新实现了LoadLibrary)使它黯然失色。Stephen 开发的内容很有用,但 LoadLibrary 的重新实现本质上是不完整的,它们的功能差距只会随着时间的推移而扩大。
Matt and Jarkko’s approach for modifying the native Windows loader required manually parsing a library’s file format to map its sections into appropriately protected memory regions. Although this was required at the time, overwriting an open file in an uncommitted NTFS transaction and using it to create a section object can bypass this step. The native loader can then be redirected to use the section object with the updated file data instead of a section object with the original file data.
Matt 和 Jarkko 修改本机 Windows 加载程序的方法需要手动解析库的文件格式,以将其部分映射到适当保护的内存区域。尽管当时需要这样做,但在未提交的 NTFS 事务中覆盖打开的文件并使用它来创建节对象可以绕过此步骤。然后,可以将本机加载程序重定向为将节对象与更新的文件数据一起使用,而不是使用带有原始文件数据的节对象。
The original approach of using a section object created from an updated file in an uncommitted NTFS transaction was documented by Tal Liberman and Eugene Kogan in their work titled “Process Doppelgänging.” While their work only described using the section object to create a new process or thread, you can use it to extend LoadLibrary as described above. To my knowledge, this is a novel approach to using transactions and I personally refer to it as Module Doppelgänging to acknowledge Tal and Eugene’s prior work.
在未提交的NTFS事务中使用从更新文件创建的节对象的原始方法由Tal Liberman和Eugene Kogan在他们的题为“Process Doppelgänging”的工作中记录。虽然他们的工作仅描述使用 section 对象来创建新进程或线程,但您可以使用它来扩展 LoadLibrary,如上所述。据我所知,这是一种使用交易的新方法,我个人将其称为模块分身,以表彰Tal和Eugene之前的工作。
Combining Native Facilities
结合原生设施
A native loader may also be extended by combining it with other native facilities. Such an approach is arguably more stable because it does not require hooking the native loader’s internal implementation, which will change over time.
本地加载器也可以通过与其他本机设施组合来扩展。这种方法可以说更稳定,因为它不需要钩住本机加载器的内部实现,而内部实现会随着时间的推移而变化。
The most straightforward example of this is certainly the use of memfd_create in Linux 3.17 and newer to create a memory backed file descriptor whose full path may be provided to dlopen. Another simple approach used by developers supporting older versions of Linux and other POSIX platforms is to place libraries in tmpfs mounts (e.g., /dev/shm). While lesser known, POSIX developers have the additional option of hosting their libraries in a Filesystem in Userspace (FUSE) mount to use with dlopen as shown in fuse-loader.
最直接的例子当然是在 Linux 3.17 及更高版本中使用 memfd_create 来创建内存支持的文件描述符,其完整路径可以提供给 dlopen。支持旧版 Linux 和其他 POSIX 平台的开发人员使用的另一种简单方法是将库放置在 tmpfs 挂载中(例如 /dev/shm)。虽然鲜为人知,但 POSIX 开发人员还可以选择将其库托管在用户空间 (FUSE) 挂载中的文件系统中,以便与 dlopen 一起使用,如 fuse loader 所示。
Windows provides less approaches for combining a native loader with other native facilities to achieve in-memory loading, but there are solutions. The oldest available approach is to have your process host a WebDAV server, use LoadLibrary to load a path that resolves to your server, and have the server respond with the bytes of an in-memory library when that path is requested. Jonas Lyk created this approach and implemented it as a proof of concept (POC) for creating a new process from an in-memory executable, but WebDAV servers may also be used to load a library. Alexander Sotirov showed this use case in 2006 with his work titled “Tiny PE”, albeit it did not use a WebDAV server that the application’s own process hosted.
Windows 提供了较少的方法将本机加载程序与其他本机工具组合以实现内存中加载,但有一些解决方案。最早的可用方法是让进程托管 WebDAV 服务器,使用 LoadLibrary 加载解析为服务器的路径,并在请求该路径时让服务器使用内存中库的字节进行响应。Jonas喜欢创建这种方法并将其实现为概念证明(POC),用于从内存中的可执行文件创建新进程,但WebDAV服务器也可用于加载库。 Alexander Sotirov 在 2006 年用他名为“Tiny PE”的作品展示了这个用例,尽管它没有使用应用程序自己的进程托管的 WebDAV 服务器。
Newer versions of Windows with Windows Subsystem for Linux (WSL) come with a Plan9 multiple UNC provider (MUP) which allows users to access Linux files from their host using the \\wsl$ UNC prefix. Such an ability allows developers to now use some of the above described POSIX approaches on Windows.
较新版本的Windows与Windows子系统Linux(WSL)附带Plan9多UNC提供程序(MUP),允许用户使用\\wsl$ UNC前缀从其主机访问Linux文件。这种能力允许开发人员现在在Windows上使用上述一些POSIX方法。
Some readers who learn this may be tempted to try loading an in-memory library by writing it to a named pipe and passing its path to LoadLibrary. Unfortunately, the underlying driver for SMB does not support creating section objects from a pipe and LoadLibrary will encounter the error STATUS_INVALID_FILE_FOR_SECTION when it internally calls NtCreateSection.
一些了解这一点的读者可能会尝试通过将内存库写入命名管道并将其路径传递给 LoadLibrary 来加载内存库。遗憾的是,SMB 的基础驱动程序不支持从管道创建节对象,并且 LoadLibrary 在内部调用 NtCreateSection 时会遇到错误STATUS_INVALID_FILE_FOR_SECTION。
This summarizes the Windows approaches that I am aware of. Although few were listed, I am sure others will identify approaches I missed and newer approaches will become possible as Windows adds support for more technologies.
这总结了我所知道的Windows方法。虽然列出的方法很少,但我相信其他人会指出我错过的方法,随着 Windows 增加对更多技术的支持,更新的方法将成为可能。
Conclusion 结论
Although developers more commonly reimplement the process of loading a library to overcome the limitations of an OS’s native loader in regards to loading in-memory library data, such approaches are inherently incomplete. Further, reimplementing some native loader features can obligate developers to painful update cycles. An example of such an issue is with providing full exception handling support on Windows without using symbol data. Some developers achieve this by maintaining byte signatures of pertinent unexported NTDLL functions for every version of Windows.
尽管开发人员更常重新实现加载库的过程,以克服操作系统本机加载器在加载内存库数据方面的限制,但这种方法本质上是不完整的。此外,重新实现一些本机加载程序功能可能会使开发人员被迫进行痛苦的更新周期。此类问题的一个示例是在不使用符号数据的情况下在 Windows 上提供完整的异常处理支持。一些开发人员通过为每个版本的 Windows 维护相关未导出的 NTDLL 函数的字节签名来实现这一点。
Developers who use a perfect loader approach do not have these issues. Their implementations typically also require less code, less maintenance overhead, and will support more library loading features by design.
使用完美加载器方法的开发人员没有这些问题。它们的实现通常还需要更少的代码,更少的维护开销,并且将通过设计支持更多的库加载功能。
Two companion repositories were made for this blog to assist developers who are new to perfect loader approaches and interested in their use. The first is fuse-loader, which implements the FUSE mount approach for POSIX platforms. The second, perfect-loader, implements various approaches for modifying the native Windows loader. If either sound interesting to you, I encourage you to check them out and hope you find them useful!
为本博客制作了两个配套存储库,以帮助刚接触完美加载器方法并对其使用感兴趣的开发人员。第一种是保险丝装载机,它为 POSIX 平台实现了保险丝安装方法。第二个是完美加载器,它实现了修改本机Windows加载程序的各种方法。如果任何一个听起来对您来说很有趣,我鼓励您查看它们,并希望它们对您有用!
原文始发于Evan McBroom:Perfect Loader Implementations