Havoc遠(yuǎn)控源碼剖析(協(xié)議篇)
近期閱讀了一款開源遠(yuǎn)控Havoc的源碼,留下了一些筆記,干脆發(fā)出來一起學(xué)習(xí)一下,這個遠(yuǎn)控?fù)?jù)說使用了很多高端免殺技術(shù),比如Ekko,Ziliean,FOLIAGE睡眠混淆,返回地址欺騙,Indirect SysCall,Etw Patch,堆加密等等。
前言
FullSessionGraph.jpeg
先簡單說說它TeamServer端是用golang寫的,Agent端是C寫的,UI是C++基于QT的,具體使用還是很多BUG,但抱著學(xué)習(xí)的心態(tài)來看看。
目錄結(jié)構(gòu)
首先想著看看這個C2協(xié)議這塊咋樣,于是讀讀TeamServer端的代碼,看看目錄結(jié)構(gòu),命名還是很清晰明了的。
Pasted image 20230906161038.png
話不多說,直接進(jìn)入主題,遠(yuǎn)控提供了HTTP(S)、SMB的Agent,SMB是內(nèi)網(wǎng)中繼直連用的,直接來看HTTP(S)方面的代碼。。
握手前校驗
首先只有POST請求會被處理,其他請求都是直接跳fake404頁面。
h.GinEngine.POST("/*endpoint", h.request)
h.GinEngine.GET("/*endpoint", h.fake404)
request里首先是對請求的header頭進(jìn)行判斷,不符合直接跳fake404。
Pasted image 20230906160945.png
具體的Header頭定義在havoc.yaotl中。
Pasted image 20230906160453.png
然后是檢查url、ua,同樣是不符合跳fake404,默認(rèn)配置的url是這樣子的。
Uris = [
"/funny_cat.gif",
"/index.php",
"/test.txt",
"/helloworld.js"
]
Pasted image 20230906161244.png
數(shù)據(jù)包的處理
經(jīng)過一連串的判斷后,來到parseAgentRequest函數(shù),開始對Body內(nèi)容進(jìn)行判斷。
Pasted image 20230906161529.png
在ParseHeader中,最終是返回Header結(jié)構(gòu)。
type Header struct {
Size int
MagicValue int
AgentID int
Data *parser.Parser
}
Pasted image 20230906162130.png
NewParser時,試圖將body內(nèi)容賦值給Parser結(jié)構(gòu)的buffer中,至于bigEndian默認(rèn)是true。
type Parser struct {
buffer []byte
bigEndian bool
}
這里似乎將數(shù)據(jù)包分為了三種情況:
p.Length()小于4的情況下,直接丟棄該包,返回空的Response;
p.Length()等于4的情況下,直接將所有data復(fù)制到Header.Data中;
p.Length()大于4的情況下,將從末尾分別切出Size、MagicValue、AgentID,各為4個字節(jié);
所以一個正常數(shù)據(jù)包的結(jié)構(gòu)大致應(yīng)該如下所示。
Pasted image 20230906172119.png
然后如果切出的MagicValue等于DEMON_MAGIC_VALUE,也就是0xDEADBEEF。
Pasted image 20230906170312.png
意味著是普通Deomon,否則是第三方Agent...等會,它是不是忘了什么?加解密呢?這不是白給么,建議做blueteam的小伙伴加一下流量規(guī)則。
Agent注冊
繼續(xù)跟進(jìn)到DemonAgent,進(jìn)來直接查AgentID;
if Teamserver.AgentExist(Header.AgentID){
...
}else{
...
}
函數(shù)內(nèi)容是迭代Teamserver中所有的Agent,true的話就是已經(jīng)存在,先看false情況,也就是注冊的功能。
Pasted image 20230906171621.png
再次切掉一個CommandID,如果CommandID等于agent.DEMON_INIT也就是99,就意味著是注冊包,然后切掉RequestID丟掉,進(jìn)入注冊流程。
Agent = agent.ParseDemonRegisterRequest(Header.AgentID, Header.Data, ExternalIP)
if Agent == nil {
return Response, false
}
go Agent.BackgroundUpdateLastCallbackUI(Teamserver)
接著從末尾切出AESKey和AESIv,并調(diào)用Parser.DecryptBuffer對Parser.buffer進(jìn)行解密,解密完的結(jié)果放回buffer里。
Pasted image 20230906204004.png
所以數(shù)據(jù)包具體應(yīng)該是這樣的。
Pasted image 20230906204313.png
至于解密出來的buffer,據(jù)官方說法如下。
[ 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。
Pasted image 20230906205319.png
心跳包
回到AgentExist,如果已經(jīng)注冊,進(jìn)入心跳包流程。
/* get our agent instance based on the agent id */
Agent = Teamserver.AgentInstance(Header.AgentID)
Agent.UpdateLastCallback(Teamserver)
先根據(jù)ID查出對象,更新心跳時間,同樣切出Command和RequestID。
Command = uint32(Header.Data.ParseInt32())
RequestID = uint32(Header.Data.ParseInt32())
這里有點小混亂,劃分了第一次post的包和重連的包,如果是第一次提交,先進(jìn)行解密(小聲叨叨:那重連的包不用解密了?)
Pasted image 20230906210103.png
然后判斷命令,是任務(wù)回顯還是GET_JOB。
Pasted image 20230906210153.png
如果是GET_JOB,就從Agent.GetQueuedJobs()拿任務(wù),又分別對COMMAND_PIVOT、COMMAND_SOCKET、COMMAND_FS、COMMAND_MEM_FILE額外追加了一些參數(shù),尤其是COMMADN_PIVOT內(nèi),如果是DEMON_PIVOT_SMB_COMMAND另外特殊處理。另外三個還沒有開發(fā)完畢,是空著的。
Pasted image 20230906211059.png
沒有細(xì)看,大意是對內(nèi)網(wǎng)SMB Agent進(jìn)行Socks代理時的特殊處理。
小結(jié)
算了,通訊協(xié)議這部分大概就看到這里了,總結(jié)一下,其通訊協(xié)議整體來說是不那么可靠的。
CobaltStrike、Sliver常用的基本RSA+AES模式都沒有實現(xiàn)到,甚至AES密鑰同加密包一同發(fā)送。這個水準(zhǔn)屬于是有點讓人失望了,希望在Agent端能夠讓人改觀。
本文作者:t43, 轉(zhuǎn)載請注明來自FreeBuf.COM