Skip to content

Commit 2e7a56b

Browse files
committed
Land rapid7#3001 - SUB Encoder
2 parents 4ca4d82 + b2d09ed commit 2e7a56b

File tree

1 file changed

+337
-0
lines changed

1 file changed

+337
-0
lines changed

modules/encoders/x86/opt_sub.rb

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

0 commit comments

Comments
 (0)