CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/encoders/x86/opt_sub.rb
Views: 11780
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Encoder
7
Rank = ManualRanking
8
9
ASM_SUBESP20 = "\x83\xEC\x20"
10
11
SET_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
12
SET_SYM = '!@#$%^&*()_+\\-=[]{};\'":<>,.?/|~'
13
SET_NUM = '0123456789'
14
SET_FILESYM = '()_+-=\\/.,[]{}@!$%^&='
15
16
CHAR_SET_ALPHA = SET_ALPHA + SET_SYM
17
CHAR_SET_ALPHANUM = SET_ALPHA + SET_NUM + SET_SYM
18
CHAR_SET_FILEPATH = SET_ALPHA + SET_NUM + SET_FILESYM
19
20
def initialize
21
super(
22
'Name' => 'Sub Encoder (optimised)',
23
'Description' => %q{
24
Encodes a payload using a series of SUB instructions and writing the
25
encoded value to ESP. This concept is based on the known SUB encoding
26
approach that is widely used to manually encode payloads with very
27
restricted allowed character sets. It will not reset EAX to zero unless
28
absolutely necessary, which helps reduce the payload by 10 bytes for
29
every 4-byte chunk. ADD support hasn't been included as the SUB
30
instruction is more likely to avoid bad characters anyway.
31
32
The payload requires a base register to work off which gives the start
33
location of the encoder payload in memory. If not specified, it defaults
34
to ESP. If the given register doesn't point exactly to the start of the
35
payload then an offset value is also required.
36
37
Note: Due to the fact that many payloads use the FSTENV approach to
38
get the current location in memory there is an option to protect the
39
start of the payload by setting the 'OverwriteProtect' flag to true.
40
This adds 3-bytes to the start of the payload to bump ESP by 32 bytes
41
so that it's clear of the top of the payload.
42
},
43
'Author' => 'OJ Reeves <oj[at]buffered.io>',
44
'Arch' => ARCH_X86,
45
'License' => MSF_LICENSE,
46
'Decoder' => { 'BlockSize' => 4 }
47
)
48
49
register_options(
50
[
51
OptString.new( 'ValidCharSet', [ false, "Specify a known set of valid chars (ALPHA, ALPHANUM, FILEPATH)" ]),
52
OptBool.new( 'OverwriteProtect', [ false, "Indicate if the encoded payload requires protection against being overwritten", false])
53
],
54
self.class)
55
end
56
57
#
58
# Convert the shellcode into a set of 4-byte chunks that can be
59
# encoding while making sure it is 4-byte aligned.
60
#
61
def prepare_shellcode(sc, protect_payload)
62
# first instructions need to be ESP offsetting if the payload
63
# needs to be protected
64
sc = ASM_SUBESP20 + sc if protect_payload == true
65
66
# first of all we need to 4-byte align the payload if it
67
# isn't already aligned, by prepending NOPs.
68
rem = sc.length % 4
69
sc = @asm['NOP'] * (4 - rem) + sc if rem != 0
70
71
# next we break it up into 4-byte chunks, convert to an unsigned
72
# int block so calculations are easy
73
chunks = []
74
sc = sc.bytes.to_a
75
while sc.length > 0
76
chunk = sc.shift + (sc.shift << 8) + (sc.shift << 16) + (sc.shift << 24)
77
chunks << chunk
78
end
79
80
# return the array in reverse as this is the order the instructions
81
# will be written to the stack.
82
chunks.reverse
83
end
84
85
#
86
# From the list of characters given, find two bytes that when
87
# ANDed together result in 0. Returns nil if not found.
88
#
89
def find_opposite_bytes(list)
90
list.each_char do |b1|
91
list.each_char do |b2|
92
if b1.ord & b2.ord == 0
93
return (b1 * 4), (b2 * 4)
94
end
95
end
96
end
97
return nil, nil
98
end
99
100
#
101
# Entry point to the decoder.
102
#
103
def decoder_stub(state)
104
return state.decoder_stub if state.decoder_stub
105
106
# configure our instruction dictionary
107
@asm = {
108
'NOP' => "\x90",
109
'AND' => { 'EAX' => "\x25" },
110
'SUB' => { 'EAX' => "\x2D" },
111
'PUSH' => {
112
'EBP' => "\x55", 'ESP' => "\x54",
113
'EAX' => "\x50", 'EBX' => "\x53",
114
'ECX' => "\x51", 'EDX' => "\x52",
115
'EDI' => "\x57", 'ESI' => "\x56"
116
},
117
'POP' => { 'ESP' => "\x5C", 'EAX' => "\x58", }
118
}
119
120
# set up our base register, defaulting to ESP if not specified
121
@base_reg = (datastore['BufferRegister'] || 'ESP').upcase
122
123
# determine the required bytes
124
@required_bytes =
125
@asm['AND']['EAX'] +
126
@asm['SUB']['EAX'] +
127
@asm['PUSH']['EAX'] +
128
@asm['POP']['ESP'] +
129
@asm['POP']['EAX'] +
130
@asm['PUSH'][@base_reg]
131
132
# generate a sorted list of valid characters
133
char_set = ""
134
case (datastore['ValidCharSet'] || "").upcase
135
when 'ALPHA'
136
char_set = CHAR_SET_ALPHA
137
when 'ALPHANUM'
138
char_set = CHAR_SET_ALPHANUM
139
when 'FILEPATH'
140
char_set = CHAR_SET_FILEPATH
141
else
142
for i in 0 .. 255
143
char_set += i.chr.to_s
144
end
145
end
146
147
# remove any bad chars and populate our valid chars array.
148
@valid_chars = ""
149
char_set.each_char do |c|
150
@valid_chars << c.to_s unless state.badchars.include?(c.to_s)
151
end
152
153
# we need the valid chars sorted because of the algorithm we use
154
@valid_chars = @valid_chars.chars.sort.join
155
@valid_bytes = @valid_chars.bytes.to_a
156
157
all_bytes_valid = @required_bytes.bytes.reduce(true) { |a, byte| a && @valid_bytes.include?(byte) }
158
159
# determine if we have any invalid characters that we rely on.
160
unless all_bytes_valid
161
raise EncodingError, "Bad character set contains characters that are required for this encoder to function."
162
end
163
164
unless @asm['PUSH'][@base_reg]
165
raise EncodingError, "Invalid base register"
166
end
167
168
# get the offset from the specified base register, or default to zero if not specified
169
reg_offset = (datastore['BufferOffset'] || 0).to_i
170
171
# calculate two opposing values which we can use for zeroing out EAX
172
@clear1, @clear2 = find_opposite_bytes(@valid_chars)
173
174
# if we can't then we bomb, because we know we need to clear out EAX at least once
175
unless @clear1
176
raise EncodingError, "Unable to find AND-able chars resulting 0 in the valid character set."
177
end
178
179
# with everything set up, we can now call the encoding routine
180
state.decoder_stub = encode_payload(state.buf, reg_offset, datastore['OverwriteProtect'])
181
182
state.buf = ""
183
state.decoder_stub
184
end
185
186
#
187
# Determine the bytes, if any, that will result in the given chunk
188
# being decoded using SUB instructions from the previous EAX value
189
#
190
def sub_3(chunk, previous)
191
carry = 0
192
shift = 0
193
target = previous - chunk
194
sum = [0, 0, 0]
195
196
4.times do |idx|
197
b = (target >> shift) & 0xFF
198
lo = md = hi = 0
199
200
# keep going through the character list under the "lowest" valid
201
# becomes too high (ie. we run out)
202
while lo < @valid_bytes.length
203
# get the total of the three current bytes, including the carry from
204
# the previous calculation
205
total = @valid_bytes[lo] + @valid_bytes[md] + @valid_bytes[hi] + carry
206
207
# if we matched a byte...
208
if (total & 0xFF) == b
209
# store the carry for the next calculation
210
carry = (total >> 8) & 0xFF
211
212
# store the values in the respective locations
213
sum[2] |= @valid_bytes[lo] << shift
214
sum[1] |= @valid_bytes[md] << shift
215
sum[0] |= @valid_bytes[hi] << shift
216
break
217
end
218
219
hi += 1
220
if hi >= @valid_bytes.length
221
md += 1
222
hi = md
223
end
224
225
if md >= @valid_bytes.length
226
lo += 1
227
hi = md = lo
228
end
229
end
230
231
# we ran out of chars to try
232
if lo >= @valid_bytes.length
233
return nil, nil
234
end
235
236
shift += 8
237
end
238
239
return sum, chunk
240
end
241
242
#
243
# Helper that writes instructions to zero out EAX using two AND instructions.
244
#
245
def zero_eax
246
data = ""
247
data << @asm['AND']['EAX']
248
data << @clear1
249
data << @asm['AND']['EAX']
250
data << @clear2
251
data
252
end
253
254
#
255
# Write instructions that perform the subtraction using the given encoded numbers.
256
#
257
def create_sub(encoded)
258
data = ""
259
encoded.each do |e|
260
data << @asm['SUB']['EAX']
261
data << [e].pack("L")
262
end
263
data << @asm['PUSH']['EAX']
264
data
265
end
266
267
#
268
# Encoding the specified payload buffer.
269
#
270
def encode_payload(buf, reg_offset, protect_payload)
271
data = ""
272
273
# prepare the shellcode for munging
274
chunks = prepare_shellcode(buf, protect_payload)
275
276
# start by reading the value from the base register and dropping it into EAX for munging
277
data << @asm['PUSH'][@base_reg]
278
data << @asm['POP']['EAX']
279
280
# store the offset of the stubbed placeholder
281
base_reg_offset = data.length
282
283
# Write out a stubbed placeholder for the offset instruction based on
284
# the base register, we'll update this later on when we know how big our payload is.
285
encoded, _ = sub_3(0, 0)
286
raise EncodingError, "Couldn't offset base register." if encoded.nil?
287
data << create_sub(encoded)
288
289
# finally push the value of EAX back into ESP
290
data << @asm['PUSH']['EAX']
291
data << @asm['POP']['ESP']
292
293
# start instruction encoding from a clean slate
294
data << zero_eax
295
296
# keep track of the previous instruction, because we use that as the starting point
297
# for the next instruction, which saves us 10 bytes per 4 byte block. If we can't
298
# offset correctly, we zero EAX and try again.
299
previous = 0
300
chunks.each do |chunk|
301
encoded, previous = sub_3(chunk, previous)
302
303
if encoded.nil?
304
# try again with EAX zero'd out
305
data << zero_eax
306
encoded, previous = sub_3(chunk, 0)
307
end
308
309
# if we're still nil here, then we have an issue
310
raise EncodingError, "Couldn't encode payload" if encoded.nil?
311
312
data << create_sub(encoded)
313
end
314
315
# Now that the entire payload has been generated, we figure out offsets
316
# based on sizes so that the payload overlaps perfectly with the end of
317
# our decoder
318
total_offset = reg_offset + data.length + (chunks.length * 4) - 1
319
encoded, _ = sub_3(total_offset, 0)
320
321
# if we're still nil here, then we have an issue
322
raise EncodingError, "Couldn't encode protection" if encoded.nil?
323
patch = create_sub(encoded)
324
325
# patch in the correct offset back at the start of our payload
326
data[base_reg_offset .. base_reg_offset + patch.length] = patch
327
328
# and we're done finally!
329
data
330
end
331
end
332
333
334