forked from ropnop/go-clr
-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathio.go
230 lines (195 loc) · 7.67 KB
/
io.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//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"))
}
}
}