介绍 Introduction

As a blend between offensive security engineer and developer, I find myself frustrated in attempting to adhere to the software development lifecycle (SDLC). The modern day security consultant requires so many disparate tools across a variety of maintainers to be successful in operations, and integrating them into a workflow is awkward at best. Worse, the methods in which these tools are deployed into an environment are often immutable to the user, leaving them with little to no alternatives. Many agents are compiled with a set of static commands, with usually some set of functionality that allows the end user to extend it in a limited capacity. Besides these commands being immutable once compiled and loaded, if an agent were to be compromised by a clever analyst, they’d be able to create detections around it and its entire feature set.

作为进攻性安全工程师和开发人员的混合体,我发现自己对遵守软件开发生命周期(SDLC)感到沮丧。 当今的安全顾问需要跨多个维护人员的众多不同工具才能成功运行,并且将它们集成到工作流中充其量是很尴尬的。 更糟糕的是,将这些工具部署到环境中的方法通常对用户是不可变的,从而几乎没有替代品。 许多代理程序是用一组静态命令编译的,通常具有一些功能,这些功能允许最终用户以有限的能力扩展它。 除了这些命令一旦编译和加载后是不变的,如果一个代理要被一个聪明的分析家破坏,他们将能够围绕它及其整个功能集创建检测。

Instead, what if we could load agent functionality as it was needed? A minimal agent core is delivered to the target whose primary function is to load commands from the control server. Once loaded, the commands could then be executed by the agent, dispatching requisite data to the modules and communicating the results. This is exciting because it solves many of the problems outlined above:

相反,如果我们可以按需加载代理功能怎么办? 最小的代理程序核心将传递给目标,目标核心的主要功能是从控制服务器加载命令。 加载后,命令便可以由代理执行,将必需的数据分发到模块并传达结果。 这很令人兴奋,因为它解决了上面概述的许多问题:

  1. If any agent in a mesh were compromised, the capabilities exposed would only be limited to that which was loaded in memory at that time. If the agent isn’t carved from memory, a defender only sees the bare-bones loading functionality. If one were to clean up their memory after executing a module, the module functionality also remains safe unless a defender dumps the memory of the machine while that module is executing. 如果网格中的任何代理程序遭到破坏,则公开的功能将仅限于当时在内存中加载的功能。 如果代理不是从内存中分离出来的,则防御者只会看到准系统加载功能。 如果执行模块后要清理其内存,则除非防御者在执行该模块时转储了机器的内存,否则该模块的功能也将保持安全。
  2. Modules can live as their own separate code repositories, allowing for easier maintainability and QA testing. 模块可以作为自己的独立代码存储库使用,从而使维护和质量检查变得更加容易。
  3. Modules can be written in any language so long as they compile to shared libraries. 只要模块可以编译到共享库中,就可以用任何语言编写。
  4. Modules can have versioning associated with them, which is a model Cody Thomas’s (https://twitter.com/its_a_feature_) Mythic C2 (https://github.com/its-a-feature/Mythic/) framework supports.

    模块可以具有与之关联的版本控制,这是Cody Thomas( https://twitter.com/its_a_feature_ )Mythic C2( https://github.com/its-a-feature/Mythic/ )框架支持的模型。

  5. In shops where a dedicated development team does not exist, and new innovations are driven by individuals, it becomes much easier to integrate new functionality any one person develops either on assessment or otherwise. 在不存在专门的开发团队且个人推动新创新的商店中,集成任何人通过评估或其他方式开发的新功能变得容易得多。

In this article, I’ll outline a proof-of-concept (POC) written in Go that is capable of loading shared libraries during its run time, and demonstrate how to accomplish two-way communication between an arbitrary shared library and the application core. This POC is written for Linux for the sake of simplicity; however, the same concept can be applied to Windows using Stephen Fewer’s excellent Reflective DLL Injection project (https://github.com/stephenfewer/reflectivedllinjection).

在本文中,我将概述用Go编写的概念证明(POC),它可以在运行时加载共享库,并演示如何完成任意共享库和应用程序核心之间的双向通信。 。 为了简单起见,此POC是为Linux编写的。 但是,可以使用Stephen Fewer出色的Reflective DLL注入项目( https://github.com/stephenfewer/reflectivedllinjection )将相同的概念应用于Windows。

为什么去? Why Go?

Go was appealing as an application core for three reasons. First, Go is capable of cross-platform compilation. It’d provide a stable code warehouse for every operating system (OS), and building is (for the most part) straight forward for whatever OS you’d want to deploy on. Second, the ability for Go to interact with C code allows a developer to more easily manage C code without having to code in pure C. Moreover, C gives us direct access to native APIs so that we don’t have to perform reflection to access them. Lastly, I wanted to learn a new language and enjoy Go quite a bit. It’s a minor thing, but hey, it’s important to enjoy what you do.

Go之所以吸引其作为应用程序核心,原因有三点。 首先,Go能够进行跨平台编译。 它会为每个操作系统(OS)提供一个稳定的代码仓库,并且(对于大多数情况而言)对于要在其上部署的任何OS而言,构建都是很简单的。 其次,Go与C代码进行交互的能力使开发人员可以更轻松地管理C代码,而不必使用纯C编写代码。此外,C使我们可以直接访问本机API,因此我们不必执行反射即可访问他们。 最后,我想学习一种新的语言,并且相当喜欢Go。 这是一件小事,但是,享受您的工作很重要。

应用程序设计注意事项 Application Design Considerations

Before we write any code, we should define what functionality it is we’re trying to build. Our aim is to create a shared library loader that can load libraries in memory, invoke function exports from said library, and return the results of that function. The results will be wildly different from function to function, so results should be stuffed into a datagram structure that both the application and library agree upon. These datagrams should be flexible enough such that when received, the application or library can perform more complex logic with the data within. A module loaded this way may be long running and need to stream output back to the application core. As such, the loading application needs to expose callback functionality a library can call. Conversely, a loaded library may require more data from the application core (such as in the case of chunked file downloads), and thus the library must also have a callback function the application core can feed data into. Both the application and library callback functions must exist for two way communication to occur and is critical for more complex functionality.

在编写任何代码之前,我们应该定义我们要构建的功能。 我们的目标是创建一个共享库加载器,该加载器可以将库加载到内存中,从该库调用函数导出,并返回该函数的结果。 结果因函数而异,因此结果应填充到应用程序和库都同意的数据报结构中。 这些数据报应足够灵活,以使应用程序或库在接收到数据报时可以对其中的数据执行更复杂的逻辑。 以这种方式加载的模块可能会长时间运行,并且需要将输出流回到应用程序核心。 这样,正在加载的应用程序需要公开库可以调用的回调功能。 相反,已加载的库可能需要来自应用程序核心的更多数据(例如在分块下载文件的情况下),因此该库还必须具有应用程序核心可以将数据馈入的回调函数。 应用程序和库回调函数必须同时存在,才能进行双向通信,这对于更复杂的功能至关重要。

These requirements are defined more succinctly as follows:

这些要求的定义如下:

  1. The application must be capable of loading libraries in memory* (Linux is a special snowflake). 该应用程序必须能够在内存中加载库*(Linux是一种特殊的雪花)。
  2. The application must be capable of parsing function exports from these in-memory libraries. 该应用程序必须能够解析这些内存库中的函数导出。
  3. The application must expose an interface for the library so that the newly loaded library can stream data to the application core. 应用程序必须公开库的接口,以便新加载的库可以将数据流传输到应用程序核心。
  4. The loaded library must expose an interface for the application core to invoke so that it may receive more data (if required). 加载的库必须公开一个接口,供应用程序核心调用,以便它可以接收更多数据(如果需要)。
  5. Communications must adhere to a datagram specification such that both the application and library can parse received data correctly. 通信必须遵守数据报规范,以便应用程序和库都可以正确解析收到的数据。
CGo简介 A Brief Foreword on CGo

Before proceeding into the implementation, I wanted to say a few words on how CGo (e.g., how Go can call C and vice-versa) works. This is by no means a complete primer or replacement for the stellar package documentation (found here: https://golang.org/cmd/cgo/). Instead, I intend to give a high level overview of the critical concepts required to implement this in Go.

在继续实施之前,我想谈谈CGo的工作方式(例如,Go如何调用C,反之亦然)。 这绝不是完整的入门资料或替代恒星软件包文档(可在此处找到: https : //golang.org/cmd/cgo/ )。 相反,我打算对在Go中实现此功能所需的关键概念进行高层概述。

First, C can call Go functions so long as those functions are exported in your package. This is done by the //export flag preceding your Go function definition. You then declare in your C code that there exists some function declared outside the scope of your C file that you can invoke using the extern flag. The documentation is more succinct (see: https://golang.org/cmd/cgo/#hdr-C_references_to_Go), but can be summarized by this excerpt here:

首先,C可以调用Go函数,只要这些函数在包中导出即可。 这是通过Go函数定义之前的// export标志完成的。 然后,您在C代码中声明存在一些在C文件范围之外声明的函数,可以使用extern标志调用该函数。 该文档更加简洁(请参阅: https : //golang.org/cmd/cgo/#hdr-C_references_to_Go ),但是可以通过以下摘录进行总结:

Conversely, Go can invoke C code directly by using the “C” package so long as the C function is defined in the header file and included in the calling Go file. Again, the documentation can demonstrate this clearly here:

相反,只要C函数在头文件中定义并包含在调用的Go文件中,Go便可以使用“ C”包直接调用C代码。 同样,文档可以在这里清楚地证明这一点:

Documentation excerpt calling C code from Go. 从Go调用C代码的文档摘录。

The last key concept critical to this POC is understanding how pointers in applications work. A pointer is an address in memory that points to something, be it an object like a datagram or the address of a function. In this POC we’ll be passing pointers of both datagrams and functions betwixt the libraries and application; however, we cannot pass Go function pointers directly. Moreover, the documentation states that “Calling C function pointers is currently not supported, however you can declare Go variables which hold C function pointers and pass them back and forth between Go and C. C code may call function pointers received from Go.”

此POC的最后一个关键概念是了解应用程序中的指针如何工作。 指针是内存中指向某物的地址它可以是数据报之类的对象,也可以是函数的地址。 在这个POC中,我们将在库和应用程序之间传递数据报和函数的指针。 但是,我们不能直接传递Go函数指针。 此外,文档指出“当前不支持调用C函数指针,但是您可以声明Go变量,该变量包含C函数指针,并将它们在Go和C之间来回传递。C代码可能调用从Go接收到的函数指针。”

This gives us everything we need for two way communications. Our Go code can expose a function that C can obtain the address to. C can define a function to invoke an arbitrary function pointer, and Go can invoke this newly defined C function. The address of this C function can be passed between the application core and a newly loaded library, which would complete our requirement of two-way communication.

这为我们提供了双向通信所需的一切。 我们的Go代码可以公开C可以获取地址的功能。 C可以定义一个函数来调用任意函数指针,而Go可以调用这个新定义的C函数。 可以在应用程序内核和新加载的库之间传递此C函数的地址,这将满足我们双向通信的要求。