近期阅读了一款开源远控Havoc的源码,留下了一些笔记,干脆发出来一起学习一下,这个远控据说使用了很多高端免杀技术,比如Ekko
,Ziliean
,FOLIAGE
睡眠混淆,返回地址欺骗,Indirect SysCall
,Etw Patch
,堆加密等等。
前言
先简单说说它TeamServer
端是用golang
写的,Agent
端是C
写的,UI
是C++
基于QT
的,具体使用还是很多BUG
,但抱着学习的心态来看看。
目录结构
首先想着看看这个C2
协议这块咋样,于是读读TeamServer
端的代码,看看目录结构,命名还是很清晰明了的。
话不多说,直接进入主题,远控提供了HTTP(S)
、SMB
的Agent
,SMB
是内网中继直连用的,直接来看HTTP(S)
方面的代码。。
握手前校验
首先只有POST
请求会被处理,其他请求都是直接跳fake404
页面。
h.GinEngine.POST("/*endpoint", h.request)
h.GinEngine.GET("/*endpoint", h.fake404)
request
里首先是对请求的header
头进行判断,不符合直接跳fake404
。
具体的Header
头定义在havoc.yaotl
中。
然后是检查url
、ua
,同样是不符合跳fake404,
默认配置的url
是这样子的。
Uris = [
"/funny_cat.gif",
"/index.php",
"/test.txt",
"/helloworld.js"
]
数据包的处理
经过一连串的判断后,来到parseAgentRequest
函数,开始对Body
内容进行判断。
在ParseHeader
中,最终是返回Header
结构。
type Header struct {
Size int
MagicValue int
AgentID int
Data *parser.Parser
}
NewParser
时,试图将body
内容赋值给Parser
结构的buffer
中,至于bigEndian
默认是true
。
type Parser struct {
buffer []byte
bigEndian bool
}
这里似乎将数据包分为了三种情况:
p.Length()
小于4的情况下,直接丢弃该包,返回空的Response
;
p.Length()
等于4的情况下,直接将所有data复制到Header.Data
中;
p.Length()
大于4的情况下,将从末尾分别切出Size
、MagicValue
、AgentID
,各为4个字节;
所以一个正常数据包的结构大致应该如下所示。
然后如果切出的MagicValue
等于DEMON_MAGIC_VALUE
,也就是0xDEADBEEF
。
意味着是普通Deomon
,否则是第三方Agent
...等会,它是不是忘了什么?加解密呢?这不是白给么,建议做blueteam
的小伙伴加一下流量规则。
Agent注册
继续跟进到DemonAgent
,进来直接查AgentID
;
if Teamserver.AgentExist(Header.AgentID){
...
}else{
...
}
函数内容是迭代Teamserver
中所有的Agent
,true
的话就是已经存在,先看false
情况,也就是注册的功能。
再次切掉一个CommandID
,如果CommandID
等于agent.DEMON_INIT
也就是99
,就意味着是注册包,然后切掉RequestID
丢掉,进入注册流程。
Agent = agent.ParseDemonRegisterRequest(Header.AgentID, Header.Data, ExternalIP)
if Agent == nil {
return Response, false
}
go Agent.BackgroundUpdateLastCallbackUI(Teamserver)
接着从末尾切出AESKey
和AESIv
,并调用Parser.DecryptBuffer
对Parser.buffer
进行解密,解密完的结果放回buffer
里。
所以数据包具体应该是这样的。
至于解密出来的buffer
,据官方说法如下。
[ Agent ID ] 4 bytes <-- this is needed to check if we successfully decrypted the data
[ Host Name ] size + bytes
[ User Name ] size + bytes
[ Domain ] size + bytes
[ IP Address ] 16 bytes?
[ Process Name ] size + bytes
[ Process ID ] 4 bytes
[ Parent PID ] 4 bytes
[ Process Arch ] 4 bytes
[ Elevated ] 4 bytes
[ Base Address ] 8 bytes
[ OS Info ] ( 5 * 4 ) bytes
[ OS Arch ] 4 bytes
..... more
如果注册成功,将返回这个Agent
的AgentID
。
心跳包
回到AgentExist
,如果已经注册,进入心跳包流程。
/* get our agent instance based on the agent id */
Agent = Teamserver.AgentInstance(Header.AgentID)
Agent.UpdateLastCallback(Teamserver)
先根据ID
查出对象,更新心跳时间,同样切出Command
和RequestID
。
Command = uint32(Header.Data.ParseInt32())
RequestID = uint32(Header.Data.ParseInt32())
这里有点小混乱,划分了第一次post
的包和重连的包,如果是第一次提交,先进行解密(小声叨叨:那重连的包不用解密了?)
然后判断命令,是任务回显还是GET_JOB
。
如果是GET_JOB
,就从Agent.GetQueuedJobs()
拿任务,又分别对COMMAND_PIVOT
、COMMAND_SOCKET
、COMMAND_FS
、COMMAND_MEM_FILE
额外追加了一些参数,尤其是COMMADN_PIVOT
内,如果是DEMON_PIVOT_SMB_COMMAND
另外特殊处理。另外三个还没有开发完毕,是空着的。
没有细看,大意是对内网SMB Agent
进行Socks
代理时的特殊处理。
小结
算了,通讯协议这部分大概就看到这里了,总结一下,其通讯协议整体来说是不那么可靠的。
CobaltStrike
、Sliver
常用的基本RSA+AES
模式都没有实现到,甚至AES
密钥同加密包一同发送。这个水准属于是有点让人失望了,希望在Agent
端能够让人改观。