/
memory.go
278 lines (243 loc) · 10.5 KB
/
memory.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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see <http://www.gnu.org/licenses/>.
*/
package memory
import (
// Standard
"encoding/hex"
"fmt"
"log/slog"
"strconv"
"strings"
// 3rd Party
"github.com/chzyer/readline"
"github.com/google/uuid"
// Internal
"github.com/Ne0nd0g/merlin-cli/commands"
"github.com/Ne0nd0g/merlin-cli/entity/help"
"github.com/Ne0nd0g/merlin-cli/entity/menu"
"github.com/Ne0nd0g/merlin-cli/entity/os"
"github.com/Ne0nd0g/merlin-cli/message"
"github.com/Ne0nd0g/merlin-cli/services/rpc"
)
// Command is an aggregate structure for a command executed on the command line interface
type Command struct {
name string // name is the name of the command
help help.Help // help is the Help structure for the command
menus []menu.Menu // menu is the Menu the command can be used in
native bool // native is true if the command is executed by an Agent using only Golang native code
os os.OS // os is the supported operating system the Agent command can be executed on
}
// NewCommand is a factory that builds and returns a Command structure that implements the Command interface
func NewCommand() *Command {
var cmd Command
cmd.name = "memory"
cmd.menus = []menu.Menu{menu.AGENT}
cmd.os = os.WINDOWS
description := "Read, write, or patch the Agent process' virtual memory"
// Style guide for usage https://developers.google.com/style/code-syntax
usage := "memory {read|write|patch} module procedure {readLength | hexData}"
example := ""
notes := "Uses direct syscalls for NtReadVirtualMemory, NtProtectVirtualMemory, & ZwWriteVirtualMemory " +
"implemented using BananaPhone at https://github.com/C-Sto/BananaPhone"
cmd.help = help.NewHelp(description, example, notes, usage)
return &cmd
}
// Completer returns the data that is displayed in the CLI for tab completion depending on the menu the command is for
// Errors are not returned to ensure the CLI is not interrupted.
// Errors are logged and can be viewed by enabling debug output in the CLI
func (c *Command) Completer(menu.Menu, uuid.UUID) readline.PrefixCompleterInterface {
comp := readline.PcItem(c.name,
readline.PcItem("read"),
readline.PcItem("patch"),
readline.PcItem("write"),
)
return comp
}
// Do executes the command and returns a Response to the caller to facilitate changes in the CLI service
// m, an optional parameter, is the Menu the command was executed from
// id, an optional parameter, used to identify a specific Agent or Listener
// arguments, and optional, parameter, is the full unparsed string entered on the command line to include the
// command itself passed into command for processing
func (c *Command) Do(m menu.Menu, id uuid.UUID, arguments string) (response commands.Response) {
slog.Debug("entering into function", "menu", m, "id", id, "arguments", arguments)
// Parse the arguments
args := strings.Split(arguments, " ")
// Validate at least one argument, in addition to the command, was provided
if len(args) < 2 {
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s' command requires at least one argument\n%s", c, c.help.Usage()))
return
}
switch strings.ToLower(args[1]) {
case "help", "-h", "--help", "?", "/?":
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s' command help\n\nDescription:\n\t%s\nUsage:\n\t%s\nExample:\n\t%s\nNotes:\n\t%s", c, c.help.Description(), c.help.Usage(), c.help.Example(), c.help.Notes()))
return
case "patch":
return c.Patch(id, arguments)
case "read":
return c.Read(id, arguments)
case "write":
return c.Write(id, arguments)
default:
response.Message = message.NewUserMessage(message.Warn, fmt.Sprintf("'%s' is not a valid argument for the '%s' command\n%s", args[1], c, c.help.Usage()))
return
}
}
// Patch overwrites the starting bytes at the specified function with the provided data
func (c *Command) Patch(id uuid.UUID, arguments string) (response commands.Response) {
sub := "patch"
description := "Overwrites the starting bytes at the specified function with the provided data"
example := "Merlin[agent][c1090dbc-f2f7-4d90-a241-86e0c0217786]» memory patch ntdll.dll EtwEventWrite 9090C3\n" +
"\t[-] Created job quRORyMMxS for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[-] Results job quRORyMMxS for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[+]\n" +
"\tRead 3 bytes from ntdll.dll!EtwEventWrite: 4C8BDC\n" +
"\tWrote 3 bytes to ntdll.dll!EtwEventWrite: 9090C3\n" +
"\tRead 3 bytes from ntdll.dll!EtwEventWrite: 9090C3"
usage := "memory patch module function bytes"
notes := "This command locates the address of the provided procedure/function, reads the existing bytes, " +
"and the overwrites them with the provided bytes. A second read is performed to validate the write event. " +
"The command would be the same as calling the read and write commands individually. The bytes should be provided in hex format."
h := help.NewHelp(description, example, notes, usage)
// Parse the arguments
args := strings.Split(arguments, " ")
// Check for help first
// 0. memory, 1. patch, 2. help
if len(args) > 2 {
switch strings.ToLower(args[2]) {
case "help", "-h", "--help", "?", "/?":
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command help\n\n"+
"Description:\n\t%s\n"+
"Usage:\n\t%s\n"+
"Example:\n\t%s\n"+
"Notes:\n\t%s",
c, sub, h.Description(), h.Usage(), h.Example(), h.Notes()))
return
}
}
// 0. memory, 1. patch, 2. module, 3. function, 4. bytes
if len(args) < 5 {
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command requires four arguments\n%s", c, sub, h.Usage()))
return
}
// Validate the bytes are in hex format
if _, err := hex.DecodeString(args[4]); err != nil {
response.Message = message.NewErrorMessage(fmt.Errorf("'%s' is not a valid hex string\n%s", args[4], h.Usage()))
return
}
response.Message = rpc.Memory(id, args[1:])
return
}
// Read returns the starting bytes at the specified function
func (c *Command) Read(id uuid.UUID, arguments string) (response commands.Response) {
sub := "read"
description := "Reads the starting bytes at the specified function"
example := "Merlin[agent][c1090dbc-f2f7-4d90-a241-86e0c0217786]» memory read ntdll.dll EtwEventWrite 3\n" +
"\t[-] Created job YlqClnqRdK for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[-] Results job YlqClnqRdK for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[+] Read 3 bytes from ntdll.dll!EtwEventWrite: 4C8BDC"
usage := "memory read module function length"
notes := ""
h := help.NewHelp(description, example, notes, usage)
// Parse the arguments
args := strings.Split(arguments, " ")
// Check for help first
if len(args) > 2 {
switch strings.ToLower(args[2]) {
case "help", "-h", "--help", "?", "/?":
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command help\n\n"+
"Description:\n\t%s\n"+
"Usage:\n\t%s\n"+
"Example:\n\t%s\n"+
"Notes:\n\t%s",
c, sub, h.Description(), h.Usage(), h.Example(), h.Notes()))
return
}
}
// 0. memory, 1. read, 2. module, 3. function, 4. length
if len(args) < 5 {
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command requires four arguments\n%s", c, sub, h.Usage()))
return
}
// Validate the length is an integer
if _, err := strconv.Atoi(args[4]); err != nil {
response.Message = message.NewErrorMessage(fmt.Errorf("there was an error converting %s to an integer\n%s", args[4], c.help.Usage()))
return
}
response.Message = rpc.Memory(id, args[1:])
return
}
// Write overwrites the starting bytes at the specified function with the provided data
func (c *Command) Write(id uuid.UUID, arguments string) (response commands.Response) {
sub := "write"
description := "Writes the provided bytes to the start of the specified function"
example := "Merlin[agent][c1090dbc-f2f7-4d90-a241-86e0c0217786]» memory write ntdll.dll EtwEventWrite 9090C3\n" +
"\t[-] Created job XTXJBLoZuO for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[-] Results job XTXJBLoZuO for agent c1090dbc-f2f7-4d90-a241-86e0c0217786\n" +
"\t[+]\n" +
"\tWrote 3 bytes to ntdll.dll!EtwEventWrite: 9090C3"
usage := "memory write module function bytes"
notes := "The bytes should be provided in hex format."
h := help.NewHelp(description, example, notes, usage)
// Parse the arguments
args := strings.Split(arguments, " ")
// Check for help first
// 0. memory, 1. write, 2. help
if len(args) > 2 {
switch strings.ToLower(args[2]) {
case "help", "-h", "--help", "?", "/?":
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command help\n\n"+
"Description:\n\t%s\n"+
"Usage:\n\t%s\n"+
"Example:\n\t%s\n"+
"Notes:\n\t%s",
c, sub, h.Description(), h.Usage(), h.Example(), h.Notes()))
return
}
}
// 0. memory, 1. write, 2. module, 3. function, 4. bytes
if len(args) < 5 {
response.Message = message.NewUserMessage(message.Info, fmt.Sprintf("'%s %s' command requires four arguments\n%s", c, sub, h.Usage()))
return
}
// Validate the bytes are in hex format
if _, err := hex.DecodeString(args[4]); err != nil {
response.Message = message.NewErrorMessage(fmt.Errorf("there was an error decoding the bytes '%s':%s\n%s", args[4], err, h.Usage()))
return
}
response.Message = rpc.Memory(id, args[1:])
return
}
// Help returns a help.Help structure that can be used to view a command's Description, Notes, Usage, and an example
func (c *Command) Help(menu.Menu) help.Help {
return c.help
}
// Menu checks to see if the command is supported for the provided menu
func (c *Command) Menu(m menu.Menu) bool {
for _, v := range c.menus {
if v == m || v == menu.ALLMENUS {
return true
}
}
return false
}
// OS returns the supported operating system the Agent command can be executed on
func (c *Command) OS() os.OS {
return c.os
}
// String returns the unique name of the command as a string
func (c *Command) String() string {
return c.name
}