Blog

GoMacro: a small utility to create Word macros with Go

A small utility and library written in Go to create Word Documents with malicious macros.

When I was browsing Github, I found a really nice project by @EmericNasi called macro_pack. The project uses the win32com python library to manipulate Office documents and inject macros.
When I found out about this project, I was learning Go while also reading Black Hat Go by Tom Steele, Chris Patten, and Dan Kottmann. So, naturally, I wanted to reproduce some capabilities of macro_pack, but in pure Go. Because… why not?

The Python library win32com, uses a subset of Component Object Model (COM) called Object Linking and Embedding (OLE) to communicate with Office (Word, Excel, etc). I won’t go into details because my understanding stops here, so in short, you need a client that talks OLE to be able to send commands to Office. This is exactly what go-ole does. Except, the library is a bit too low level to be easily usable, so I started writing gomacro.

GoMacro

The repository can be found on github -> https://github.com/oXis/gomacro

It is organised in multiple parts

GoMacro internals

In this section we are going to talk about the gomacro package. This package is a wrapper to go-ole that facilitates Word documents manipulation.

I wanted the syntax to be close to what win32com proposes.

The code below shows how to create a new document and access its VBComponent field.

// Initialise the lib
gomacro.Init()
defer gomacro.Uninitialize()

// Open Word and get a hendle to documents
documents := gomacro.NewDocuments(false)
defer documents.Close()

fmt.Printf("Word version is %s\n", documents.Application.Version)

// Add a new document
document := documents.AddDocument()

// Set the name of the new doc
document.VBProject.SetName(obf.RandWord())

// Get a handle "ThisDocument" VBA project
thisDoc, err := document.VBProject.VBComponents.GetVBComponent("ThisDocument")
if err != nil {
    fmt.Printf("%s", err)
    document.Save()
    documents.Close()
}

go-ole needs to be initialised with ole.CoInitialize(0), gomacro.Init() is just a wrapper to that call.

gomacro.NewDocuments(false) creates a new document. The implementation is as follow.

// NewDocument Create a new Word document
func (d *Documents) NewDocument(v bool) *Documents {

    // Create a Word.Application object 
    unknown, err := oleutil.CreateObject("Word.Application")
	if err != nil {
        panic("Cannot create Word.Application")
	}

    // Get a handle to Word.Application
	word, err := unknown.QueryInterface(ole.IID_IDispatch)
	if err != nil {
		panic("Cannot QueryInterface of Word")
	}

    // Population the Application field of struct Documents
	d.Application = &Application{_Application: word,
		Options: &Options{_Options: oleutil.MustGetProperty(word, "Options").ToIDispatch()}}

    // Set visibility to v bool
	oleutil.PutProperty(d.Application._Application, "Visible", v)

    // Get a handle to the Documents from the application
	d._Documents = oleutil.MustGetProperty(d.Application._Application, "Documents").ToIDispatch()
	d.Application.Version = oleutil.MustGetProperty(d.Application._Application, "Version").ToString()

    // Permit OLE to manage VBA code inside documents
	setupRegistry(d.Application.Version, 1)

	return d
}

Structure that represents an Application

//Application Holds the OLE app
type Application struct {
	_Application *ole.IDispatch // Handle to the application
	Options      *Options       // Handle to the application options
	Version      string         // Version
}

Documents and Document structures. Documents contains a Document and also contains an Application. A Document contains a VBProject.

// Documents represents Word documents
type Documents struct {
	_Documents  *ole.IDispatch
	Application *Application

	Document *Document
}

// Document represents a Word document
type Document struct {
	_Document   *ole.IDispatch
	Application *Application
	VBProject   *VBProject
}

A VBProject contains a VBComponents.

// VBProject Holds VB projects
type VBProject struct {
	_VBProject   *ole.IDispatch
	Name         string
	VBComponents VBComponents
}

A VBComponents object contains many Components and Forms. A Components onject contains a CodeModule.

// VBComponents Holds VB conponents
type VBComponents struct {
	_VBComponents *ole.IDispatch
	Components    map[string]*VBComponent
	Forms         map[string]*Form
}

// VBComponent Holds VB conponent
type VBComponent struct {
	_VBComponent *ole.IDispatch
	CodeModule   *CodeModule
}

I know it’s confusing, but this structure follows Microsoft’s definition (I think…). From there, it’s simply a matter of implementing all functions. For example: AddVBComponent is used to add a new component (module) to a VB project.

// AddVBComponent Add a new Module
func (v *VBComponents) AddVBComponent(name string, cType int) *VBComponent {
    // Call the Add function on the handle to _VBComponents OLE object, inside VBComponents. 
	comp := oleutil.MustCallMethod(v._VBComponents, "Add", cType).ToIDispatch()

	// Set its name
    comp.PutProperty("Name", name)

    // Add the new module to the list of modules inside Components and retrieve the codeModule.
	v.Components[name] = &VBComponent{_VBComponent: comp,
		CodeModule: &CodeModule{_CodeModule: oleutil.MustGetProperty(comp, "codeModule").ToIDispatch()}}

	return v.Components[name]
}

VB code can be added to a CodeModule by calling AddFromString on the handle to the OLE object.

//AddFromString Add content to the code module
func (c *CodeModule) AddFromString(content string) {
	oleutil.MustCallMethod(c._CodeModule, "AddFromString", content).ToIDispatch()
}

The gomacro library is not complete, but adding new functions should be very easy, the hard part is working out whether to use MustCallMethod or MustGetProperty to set options, and finding the correct parameters, and function name.

VB Obfuscation

The obfuscation is handled by this function. The function gets all functions, function parameters, variables and strings from a VB code. Then, a map is created for each item that links the original plaintext name to the new random obfuscated name.

// ObfuscateVBCode ...
func ObfuscateVBCode(code string, objFunc, objParam, objVar, objString bool) (string, map[string]string, map[string]string, map[string]string, map[string]string) {

	funcMap := make(map[string]string)
	if objFunc {
		functions := removeDuplicatesFromSlice(getFunctions(code))
		for _, s := range functions {
			funcMap[s] = RandWord()
		}
	}

	paramMap := make(map[string]string)
	if objParam {
		parameters := removeDuplicatesFromSlice(getFunctionsParameters(code))
		for _, p := range parameters {
			paramMap[p] = RandWord()
		}
	}

	varMap := make(map[string]string)
	if objVar {
		variables := removeDuplicatesFromSlice(getVariables(code))
		for _, p := range variables {
			varMap[p] = RandWord()
		}
	}

	stringMap := make(map[string]string)
	if objString {
		str := removeDuplicatesFromSlice(getStrings(code))
		for _, p := range str {
			formatString, formatStringList := shuffleString(p)
			stringMap[p] = fmt.Sprintf(`Format("%v", %v)`, formatString, formatStringList)
		}
	}

	return code, funcMap, paramMap, varMap, stringMap
}

The next step is to perform the actual renaming of the functions, function parameters, variables and strings using the return values generated by ObfuscateVBCode:

func ReplaceAllInCode(code string, funcMap, paramMap, varMap, stringMap map[string]string) string

Main

The main function is heavily commented and is rather easy to follow.

Important note:
When calling -EncodedCommand, Powershell requires a UTF16-LE base64 encoded string. The function bellow, takes any Go string, encode the string to UTF16, add NULL bytes between each characters to confuse AVs, and finally base64 encode the resulting string. The result is compatible with -EncodedCommand option.

// newEncodedPSScript returns a UTF16-LE, base64 encoded script.
// The -EncodedCommand parameter expects this encoding for any base64 script we send over.
func newEncodedPSScript(script string) (string, error) {
	uni := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
	encoded, err := uni.NewEncoder().String(script)
	if err != nil {
		return "", err
	}

	var encodedNull []byte = make([]byte, len(encoded)*2)
	for _, c := range encoded {
		encodedNull = append(encodedNull, byte(c), 0x00)
	}

	return base64.StdEncoding.EncodeToString([]byte(encoded)), nil
}

After calling the compiled Go binary, a new doc is generated and placed into the current directory. Windows Defender forbids executing powershell from a Word Macro, so real-time protection should be disabled. This can be bypassed but I let you find a way. This post is about making Word macros with Go, not weaponising those macros.