Meterpreter通讯分析
前几天玩鹤城杯遇到一个 Meterpreter 流量分析,花了一下午时间尝试恢复明文通讯无果,于是打算直接从源码的层面上剖析其通讯流程。
获取源码
我们可以直接在 Metasploit 的 Github 上获取 meterpreter 的源码,在 rapid7/metasploit-payloads。
工作流程
寻找入口
由于我并没有足够的二进制经验,也只能依靠自己仅存的开发经验来找。猜测是 DLL 注入,定位入口 c/meterpreter/source/metsrv.c
中的 DllMain
,并通过 LPVOID lpReserved
,将参数 MetsrvConfig config
传入,其中参数包含以下内容:
1 | // source/common/common_config.h |
看起来这里定义了会话以及通讯相关的参数,包括:会话 GUID、UUID、通讯地址、重试参数、退出方式等。
初始化设定
接下来调用了 Init()
,Init()
调用了 server_setup()
,开始了整个通讯过程。这里我们不去关心他实现的细节,直奔协议处理部分。
调用了 remote_allocate()
分配了一个远程会话,其结构体定义如下:
1 | typedef struct _Remote |
这里包含了基本所有远程通讯相关的东西了,同时包含加密相关的 context。
我们看一下加密相关的 context 所对应的数据结构定义:
1 | typedef struct _Aes256Key |
很明显 AES-256 具体的模式未知,我们继续往下看,下面设置了 expiry times,并建立了对应的 transport,每种 transport 对应的 handler 可以在 source/metsrv/server_transport_*.c
中找到,最终将充满函数指针的函数体赋值给 remote->transport
。后面就是获取一些列基础信息和进行一系类初始化了,直到后面开始调用 remote->transport->transport_init()
以及进行对应的异常处理。所以我们可以直接去看对应的 server_transport_*.c
。
从 TCP 的 transport handler 开始 dive deeper
首先上游是调用了 transport_init()
,其对应了 tcp 中的 configure_tcp_connection()
,最终发现是 server_dispatch_tcp()
来负责处理数据包的接受,使用 packet_receive()
接受数据包 同时调用 command_handle()
来处理接收到的指令。
先看下 packet_receive()
:
packet_receive()
首先我们列一下用到的数据结构
1 | // guiddef.h |
其具体流程如下:
- 接受一个 PacketHeader 的大小 (经计算为 32 字节)
- 对 packet header 的 xor key 进行检查 (xor key没有零字节)
- 调用
xor_bytes()
对剩余的 packet header 进行解密 - 取出 payload length (header.length - sizeof(TlvHeader))
- 根据 payload length 循环接收数据,直到获取到完整的 payload
- 检查 payload 中的 GUID 是否为空或者与设置中的一致,符合条件则调用
decrypt_packet()
解密,不符合则继续找对应的 pivot - 对 payload 部分做 xor 处理,根据 enc_flags 以及对应的 context 的完整性决定是否进行解密 其中 enc_flags 为 0x0 则不解密 0x1 使用 AES-256 (
source/metsrv/packet_encryption.h
) 同时根据下文得出,meterpreter 使用了 AES-256-CBC - 如果上述流程均没有问题,则进入下一步
command_handle()
老样子,列一下我们需要的数据结构:
1 | typedef struct |
我们直接看 command_handle()
的源码,其流程如下:
- 首先调用了
packet_get_tlv_value_uint()
,获取对应的COMMAND_ID
,后面经过一系列调用,最终来到packet_find_tlv_buf()
,这个函数会遍历除去 Packet Header 外所有的部分来找对应的类型。使用到了上面提到的Tlv
数据结构。在处理 Header 的时候也会检查 type 是否包含TLV_META_TYPE_COMPRESSED
这一个 flag,是的话则对 payload 部分进行 zlib 解压,同时去掉 flag 中的TLV_META_TYPE_COMPRESSED
,为上层提供透明访问。 - 找到
COMMAND_ID
后,根据这个 ID 去寻找对应的拓展,找到拓展后判断是否为 inline,是 inline 则调用command_process_inline()
, 不是 inline 的话就另开线程运行 - 这里直接看
command_process_inline()
,后面是直接调用了 由第三步command_locate_extension()
获取到的 handler,最后调用packet_call_completion_handlers()
这里我们只关心协议,重点关心 packet_find_tlv_buf()
即可,可以看到,这个函数通过逐一读取 TLV 部分的 header,获取长度对整个 buffer 进行遍历。
对应 type 和 meta 的定义我们可以在 source/common/common_core.h
中找到。
先简单写个程序验证下(以刚开始上传 public key 为例):
对称密钥协商与解密
只找到协议大致的解析方式还是远远不够的,我们还需要后面对称密码协商的过程,因此继续往下看:
source/metsrv/packet_encryption.c
中的 request_negotiate_aes_key()
: 先生成对应的 AES 密钥,再做使用之前获取的 RSA 公钥进行加密,我们不妨看下 public_key_encrypt()
函数,标准的 RSA 加密,直接使用 pycryptodome 就能解出来。
如果有 enc_flags 为 1,则使用 AES-256-CBC 进行解密,Packet Header 后面的 16 字节做 IV,IV 后面即加密的数据,该程序是解密后将解密后的数据放到了 Packet Header 后,为上层提供透明服务,这里同样,使用 pycryptodome 解出来即可。
模型整理
根据上述的分析,整理得如下模型:
对于所有 traffic,均有:
1 | [4-bytes xor key][xor-ed payloads] |
其中,这四字节的 xor key 是属于 Packet Header 部分的,后面的数据包括剩余的 Packet Header 以及后续的数据,因此,在处理流量时应当使用 xor key 对后续数据进行异或操作。
在进行异或操作后,整个 packet 大致可分为以下两种情况:
对于没有加密的情况(enc_flags = 0):
1 | [32-bytes packet header][variable-length payload] |
对于启用加密的情况(enc_flags = 1):
1 | [32-bytes packet header][16-bytes IV][variable-length payload] |
根据源码可知,目前只有一种加密,即 AES-256-CBC
其中 Packet Header:
1 | [4-bytes xor key][16-bytes session GUID][4-bytes enc-flags][4-bytes payload-length(including 8-bytes tlv header length)][4-bytes packet-type] |
Payload 部分:
Payload 部分可以由多个 TLV 单元组成,解析过程中,会根据 packet header 里的 length 部分,对整个 payload 进行逐一解析,直到找到其需要的数据。其中,每一个 TLV 单元组成如下:
1 | [4-bytes length][4-bytes type][variable-length payload] |
TLV_TYPE_SYM_KEY 的 TLV 单元:
1 | [4-bytes length][4-bytes type][variable-length encrypted payload] |
在协商对称加密密钥过程中,payload 使用了预设的 RSA 公钥进行了加密。
一次通讯过程
在 meterpreter 完成加载后,会发生以下过程:
- LHOST 生成 RSA 密钥对,将
core_negotiate_tlv_encryption
与对应的 RSA 公钥一起发送到 RHOST - RHOST 生成 AES 密钥,使用接收到的 RSA 公钥进行加密(即
TLV_TYPE_SYM_KEY
),发送回 LHOST - 两端进行测试通信,无差错的话则以后都使用 AES-256-CBC 进行通讯,即
enc_flags
标志位置 1,在 Packet Header 后面插入 16 字节 IV,同时加密 payload 部分 - 直到断开连接
代码
这里我简单写了一个程序来对通讯过程进行解密,效果如下:
当前已实现功能:
- 正确解析 Meterpreter 的数据包
- 对 Meterpreter 数据包类型进行识别
- 对 Meterpreter 数据包进行解密
- 使用私钥提取 Meterpreter 的对称加密密钥
- 识别 TLV Unit 的 Meta 与类型
代码可以从以下渠道获得: