Files
2022-11-10 18:12:44 -05:00

231 lines
7.7 KiB
Go

//go:build windows
// +build windows
package clr
import (
"bufio"
"bytes"
"fmt"
"os"
"sync"
"syscall"
"time"
"golang.org/x/sys/windows"
)
// origSTDOUT is a Windows Handle to the program's original STDOUT
var origSTDOUT = windows.Stdout
// origSTDERR is a Windows Handle to the program's original STDERR
var origSTDERR = windows.Stderr
// rSTDOUT is an io.Reader for STDOUT
var rSTDOUT *os.File
// wSTDOUT is an io.Writer for STDOUT
var wSTDOUT *os.File
// rSTDERR is an io.Reader for STDERR
var rSTDERR *os.File
// wSTDERR is an io.Writer for STDERR
var wSTDERR *os.File
// Stdout is a buffer to collect anything written to STDOUT
// The CLR will return the COR_E_TARGETINVOCATION error on subsequent Invoke_3 calls if the
// redirected STDOUT writer is EVER closed while the parent process is running (e.g., a C2 Agent)
// The redirected STDOUT reader will never recieve EOF and therefore reads will block and that is
// why a buffer is used to stored anything that has been written to STDOUT while subsequent calls block
var Stdout bytes.Buffer
// Stderr is a buffer to collect anything written to STDERR
var Stderr bytes.Buffer
// errors is used to capture an errors from a goroutine
var errors = make(chan error)
// mutex ensures exclusive access to read/write on STDOUT/STDERR by one routine at a time
var mutex = &sync.Mutex{}
// RedirectStdoutStderr redirects the program's STDOUT/STDERR to an *os.File that can be read from this Go program
// The CLR executes assemblies outside of Go and therefore STDOUT/STDERR can't be captured using normal functions
// Intended to be used with a Command & Control framework so STDOUT/STDERR can be captured and returned
func RedirectStdoutStderr() (err error) {
// Create a new reader and writer for STDOUT
rSTDOUT, wSTDOUT, err = os.Pipe()
if err != nil {
err = fmt.Errorf("there was an error calling the os.Pipe() function to create a new STDOUT:\n%s", err)
return
}
// Create a new reader and writer for STDERR
rSTDERR, wSTDERR, err = os.Pipe()
if err != nil {
err = fmt.Errorf("there was an error calling the os.Pipe() function to create a new STDERR:\n%s", err)
return
}
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
getConsoleWindow := kernel32.NewProc("GetConsoleWindow")
// Ensure the process has a console because if it doesn't there will be no output to capture
hConsole, _, _ := getConsoleWindow.Call()
if hConsole == 0 {
// https://learn.microsoft.com/en-us/windows/console/allocconsole
allocConsole := kernel32.NewProc("AllocConsole")
// BOOL WINAPI AllocConsole(void);
ret, _, err := allocConsole.Call()
// A process can be associated with only one console, so the AllocConsole function fails if the calling process
// already has a console. So long as any console exists we are good to go and therefore don't care about errors
if ret == 0 {
return fmt.Errorf("there was an error calling kernel32!AllocConsole with return code %d: %s", ret, err)
}
// Get a handle to the newly created/allocated console
hConsole, _, _ = getConsoleWindow.Call()
user32 := windows.NewLazySystemDLL("user32.dll")
showWindow := user32.NewProc("ShowWindow")
// Hide the console window
ret, _, err = showWindow.Call(hConsole, windows.SW_HIDE)
if err != syscall.Errno(0) {
return fmt.Errorf("there was an error calling user32!ShowWindow with return %+v: %s", ret, err)
}
}
// Set STDOUT/STDERR to the new files from os.Pipe()
// https://docs.microsoft.com/en-us/windows/console/setstdhandle
if err = windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, windows.Handle(wSTDOUT.Fd())); err != nil {
err = fmt.Errorf("there was an error calling the windows.SetStdHandle function for STDOUT:\n%s", err)
return
}
if err = windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(wSTDERR.Fd())); err != nil {
err = fmt.Errorf("there was an error calling the windows.SetStdHandle function for STDERR:\n%s", err)
return
}
// Start STDOUT/STDERR buffer and collection
go BufferStdout()
go BufferStderr()
return
}
// RestoreStdoutStderr returns the program's original STDOUT/STDERR handles before they were redirected an *os.File
// Previously instantiated CLRs will continue to use the REDIRECTED STDOUT/STDERR handles and will not resume
// using the restored handles
func RestoreStdoutStderr() error {
if err := windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, origSTDOUT); err != nil {
return fmt.Errorf("there was an error calling the windows.SetStdHandle function to restore the original STDOUT handle:\n%s", err)
}
if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, origSTDERR); err != nil {
return fmt.Errorf("there was an error calling the windows.SetStdHandle function to restore the original STDERR handle:\n%s", err)
}
return nil
}
// ReadStdoutStderr reads from the REDIRECTED STDOUT/STDERR
// Only use when RedirectStdoutStderr was previously called
func ReadStdoutStderr() (stdout string, stderr string, err error) {
debugPrint("Entering into io.ReadStdoutStderr()...")
// Sleep for one Microsecond to wait for STDOUT/STDERR goroutines to finish reading
// Race condition between reading the buffers and reading STDOUT/STDERR to the buffers
// Can't close STDOUT/STDERR writers once the CLR invokes on assembly and EOF is not
// returned because parent program is perpetually running
time.Sleep(1 * time.Microsecond)
// Check the error channel to see if any of the goroutines generated an error
if len(errors) > 0 {
var totalErrors string
for e := range errors {
totalErrors += e.Error()
}
err = fmt.Errorf(totalErrors)
return
}
// Read STDOUT Buffer
if Stdout.Len() > 0 {
stdout = Stdout.String()
Stdout.Reset()
}
// Read STDERR Buffer
if Stderr.Len() > 0 {
stderr = Stderr.String()
Stderr.Reset()
}
return
}
// CloseStdoutStderr closes the Reader/Writer for the previously redirected STDOUT/STDERR
// that was changed to an *os.File
func CloseStdoutStderr() (err error) {
err = rSTDOUT.Close()
if err != nil {
err = fmt.Errorf("there was an error closing the STDOUT Reader:\n%s", err)
return
}
err = wSTDOUT.Close()
if err != nil {
err = fmt.Errorf("there was an error closing the STDOUT Writer:\n%s", err)
return
}
err = rSTDERR.Close()
if err != nil {
err = fmt.Errorf("there was an error closing the STDERR Reader:\n%s", err)
return
}
err = wSTDERR.Close()
if err != nil {
err = fmt.Errorf("there was an error closing the STDERR Writer:\n%s", err)
return
}
return nil
}
// BufferStdout is designed to be used as a go routine to monitor for data written to the REDIRECTED STDOUT
// and collect it into a buffer so that it can be collected and sent back to a server
func BufferStdout() {
debugPrint("Entering into io.BufferStdout()...")
stdoutReader := bufio.NewReader(rSTDOUT)
for {
// Standard STDOUT buffer size is 4k
buf := make([]byte, 4096)
line, err := stdoutReader.Read(buf)
if err != nil {
errors <- fmt.Errorf("there was an error reading from STDOUT in io.BufferStdout:\n%s", err)
}
if line > 0 {
// Remove null bytes and add contents to the buffer
Stdout.Write(bytes.TrimRight(buf, "\x00"))
}
}
}
// BufferStderr is designed to be used as a go routine to monitor for data written to the REDIRECTED STDERR
// and collect it into a buffer so that it can be collected and sent back to a server
func BufferStderr() {
debugPrint("Entering into io.BufferStderr()...")
stderrReader := bufio.NewReader(rSTDERR)
for {
// Standard STDOUT buffer size is 4k
buf := make([]byte, 4096)
line, err := stderrReader.Read(buf)
if err != nil {
errors <- fmt.Errorf("there was an error reading from STDOUT in io.BufferStdout:\n%s", err)
}
if line > 0 {
// Remove null bytes and add contents to the buffer
Stderr.Write(bytes.TrimRight(buf, "\x00"))
}
}
}