Skip to content

Commit 3768efb

Browse files
committed
Improve payload size generation script
1 parent fed897a commit 3768efb

File tree

58 files changed

+489
-687
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+489
-687
lines changed

lib/msf/core/payload.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,25 @@ def staged?
159159
# This method returns an optional cached size value
160160
#
161161
def self.cached_size
162-
csize = (const_defined?('CachedSize')) ? const_get('CachedSize') : nil
162+
csize = const_defined?('CachedSize') ? const_get('CachedSize') : nil
163+
if ancestors.include?(Msf::Payload::Stager)
164+
csize_overrides = const_defined?('CachedSizeOverrides') ? const_get('CachedSizeOverrides') : {}
165+
csize = csize_overrides.fetch(self.refname, csize)
166+
end
167+
163168
csize == :dynamic ? nil : csize
164169
end
165170

166171
#
167172
# This method returns whether the payload generates variable-sized output
168173
#
169174
def self.dynamic_size?
170-
csize = (const_defined?('CachedSize')) ? const_get('CachedSize') : nil
175+
csize = const_defined?('CachedSize') ? const_get('CachedSize') : nil
176+
if ancestors.include?(Msf::Payload::Stager)
177+
csize_overrides = const_defined?('CachedSizeOverrides') ? const_get('CachedSizeOverrides') : {}
178+
csize = csize_overrides.fetch(self.refname, csize)
179+
end
180+
171181
csize == :dynamic
172182
end
173183

lib/msf/core/payload/java.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
module Msf::Payload::Java
44

5+
# Mark the payload as dynamic as the generated JAR/zip files can differ in size depending on the host machine's zlib version
6+
ForceDynamicCachedSize = true
7+
58
#
69
# Used by stages; all java stages need to define +stage_class_files+ as an
710
# array of .class files located in data/java/

lib/msf/core/payload/java/reverse_http.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ module Payload::Java::ReverseHttp
1515
include Msf::Payload::UUID::Options
1616
include Msf::Payload::Java::PayloadOptions
1717

18+
# Mark the payload as dynamic as random length URIs are generated, and zip compression with a single char change can lead to size changes even if the original payload is the same length
19+
ForceDynamicCachedSize = true
20+
1821
#
1922
# Register Java reverse_http specific options
2023
#

lib/msf/core/payload/python.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# -*- coding: binary -*-
22

33
module Msf::Payload::Python
4+
# Mark the payload as dynamic, as the zlib compression with a single char change can lead to size changes even if the original payload is the same length
5+
ForceDynamicCachedSize = true
46

57
#
68
# Encode the given python command in base64 and wrap it with a stub

lib/msf/core/payload/python/meterpreter_loader.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module Msf
1010
###
1111

1212
module Payload::Python::MeterpreterLoader
13+
# Mark the payload as dynamic, as random uuid values lead to differing zlib compressed payloads
14+
ForceDynamicCachedSize = true
1315

1416
include Msf::Payload::Python
1517
include Msf::Payload::UUID::Options

lib/msf/core/payload/windows/powershell.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module Msf
99
###
1010

1111
module Payload::Windows::Powershell
12+
# Mark the payload as dynamic as powershell scripts are randomized
13+
ForceDynamicCachedSize = true
1214

1315
def initialize(info = {})
1416
ret = super(info)

lib/msf/util/payload_cached_size.rb

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ class PayloadCachedSize
1717
OPTS = {
1818
'Format' => 'raw',
1919
'Options' => {
20+
'VERBOSE' => false,
2021
'CPORT' => 4444,
2122
'LPORT' => 4444,
23+
'RPORT' => 4444,
2224
'CMD' => '/bin/sh',
2325
'URL' => 'http://a.com',
2426
'PATH' => '/',
@@ -46,88 +48,196 @@ class PayloadCachedSize
4648

4749
OPTS_IPV4 = {
4850
'LHOST' => '255.255.255.255',
51+
'RHOST' => '255.255.255.255',
4952
'KHOST' => '255.255.255.255',
5053
'AHOST' => '255.255.255.255'
5154
}.freeze
5255

5356
OPTS_IPV6 = {
5457
'LHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
58+
'RHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
5559
'KHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
5660
'AHOST' => 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'
5761
}.freeze
5862

59-
# Insert a new CachedSize value into the text of a payload module
63+
# Inserts or updates the CachedSize constant in the text of a payload module.
6064
#
6165
# @param data [String] The source code of a payload module
62-
# @param cached_size [String] The new value for cached_size, which
63-
# which should be either numeric or the string :dynamic
64-
# @return [String]
66+
# @param cached_size [String, Integer] The new value for CachedSize, which should be either an integer or the string ":dynamic"
67+
# @return [String] The updated source code with the new CachedSize value
6568
def self.update_cache_constant(data, cached_size)
6669
data.
6770
gsub(/^\s*CachedSize\s*=\s*(\d+|:dynamic).*/, '').
6871
gsub(/^(module MetasploitModule)\s*\n/) do |m|
69-
"#{m.strip}\n\n CachedSize = #{cached_size}\n\n"
72+
"#{m.strip}\n CachedSize = #{cached_size}\n\n"
7073
end
7174
end
7275

73-
# Insert a new CachedSize value into a payload module file
76+
# Inserts or updates the CachedSizeOverrides constant in the text of a payload module,
77+
# removing any previous CachedSizeStages, # Other stager sizes, or CachedSizeOverrides lines.
78+
#
79+
# @param data [String] The source code of a payload module
80+
# @param stages_with_sizes [Array<{:stage => Msf::Payload::Stager, :size => Integer}>] Array of hashes with :stage (an Msf::Payload::Stager instance) and :size (Integer)
81+
# @return [String] The updated source code with the new CachedSizeOverrides value
82+
def self.update_stage_sizes_constant(data, stages_with_sizes)
83+
sizes = stages_with_sizes.sort_by { |stage_with_size| stage_with_size[:stage].refname }.map do |stage_with_size|
84+
[stage_with_size[:stage].refname, stage_with_size[:size]]
85+
end
86+
data_without_other_stages = data.gsub(/^\s*CachedSizeOverrides\s*=.*\n/, '')
87+
return data_without_other_stages if sizes.empty?
88+
89+
data_without_other_stages.gsub(/^\s*(CachedSize\s*=\s*(\d+|:dynamic))\s*\n/) do |m|
90+
" #{m.strip}\n CachedSizeOverrides = {#{sizes.map { |(k, v)| %Q{"#{k}" => #{v}}}.join(', ')}}\n\n"
91+
end
92+
end
93+
94+
# Insert or update the CachedSize value into a payload module file
7495
#
7596
# @param mod [Msf::Payload] The class of the payload module to update
76-
# @param cached_size [String] The new value for cached_size, which
77-
# which should be either numeric or the string :dynamic
97+
# @param cached_size [String, Integer] The new value for cached_size, which
98+
# should be either an integer or the string ":dynamic"
7899
# @return [void]
79100
def self.update_cached_size(mod, cached_size)
80101
mod_data = ""
81102

82-
::File.open(mod.file_path, 'rb') do |fd|
103+
file_path = mod.file_path
104+
105+
::File.open(file_path, 'rb') do |fd|
83106
mod_data = fd.read(fd.stat.size)
84107
end
85108

86-
::File.open(mod.file_path, 'wb') do |fd|
109+
::File.open(file_path, 'wb') do |fd|
87110
fd.write update_cache_constant(mod_data, cached_size)
88111
end
89112
end
90113

91-
# Updates the payload module specified with the current CachedSize
114+
# Insert or update the CachedSize value into a payload module file
92115
#
93116
# @param mod [Msf::Payload] The class of the payload module to update
117+
# @param stages_with_sizes [Array<{:stage => Msf::Payload::Stager, :size => Integer}>] Array of hashes with :stage (an Msf::Payload::Stager instance) and :size (Integer)
94118
# @return [void]
95-
def self.update_module_cached_size(mod)
96-
update_cached_size(mod, compute_cached_size(mod))
119+
def self.update_stager_cached_sizes(mod, stages_with_sizes)
120+
mod_data = ""
121+
122+
file_path = mod.file_path
123+
124+
::File.open(file_path, 'rb') do |fd|
125+
mod_data = fd.read(fd.stat.size)
126+
end
127+
128+
::File.open(file_path, 'wb') do |fd|
129+
fd.write update_stage_sizes_constant( mod_data, stages_with_sizes)
130+
end
131+
end
132+
133+
# Updates the payload module specified with the current CachedSize
134+
#
135+
# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation
136+
# @param mod [Msf::Payload] The class of the payload module to update
137+
# @return [String, Integer] The updated CachedSize value
138+
def self.update_module_cached_size(framework, mod)
139+
cached_size = compute_cached_size(framework, mod)
140+
update_cached_size(mod, cached_size)
141+
cached_size
142+
end
143+
144+
# Updates the stager payload module with the most frequent CachedSize value and sets CachedSizeOverrides for other stages.
145+
#
146+
# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation
147+
# @param stages [Array<Msf::Payload>] Array of stager modules to update
148+
# @return [Integer, String] The new CachedSize value set for the stager
149+
def self.update_stager_module_cached_size(framework, stages)
150+
stages_with_sizes = stages.map do |stage|
151+
{ stage: stage, size: compute_cached_size(framework, stage) }
152+
end
153+
most_frequent_cached_size = stages_with_sizes.map { |stage_with_size| stage_with_size[:size] }
154+
.select { |size| size.is_a?(Numeric) }.tally.sort_by(&:last).to_h.keys.last
155+
156+
new_size = most_frequent_cached_size || stages_with_sizes.first[:size]
157+
other_sizes = stages_with_sizes.select { |stage_with_size| stage_with_size[:size] != new_size }
158+
159+
update_cached_size(stages.first, new_size)
160+
update_stager_cached_sizes(stages.first, other_sizes)
161+
162+
new_size
97163
end
98164

99165
# Calculates the CachedSize value for a payload module
100166
#
101167
# @param mod [Msf::Payload] The class of the payload module to update
102-
# @return [Integer]
103-
def self.compute_cached_size(mod)
104-
return ":dynamic" if is_dynamic?(mod)
168+
# @return [Integer, String]
169+
def self.compute_cached_size(framework, mod)
170+
return ":dynamic" if is_dynamic?(framework, mod)
105171

106-
mod.generate_simple(module_options(mod)).size
172+
mod.replicant.generate_simple(module_options(mod)).bytesize
107173
end
108174

109175
# Determines whether a payload generates a static sized output
110176
#
111177
# @param mod [Msf::Payload] The class of the payload module to update
112178
# @param generation_count [Integer] The number of iterations to use to
113179
# verify that the size is static.
114-
# @return [Integer]
115-
def self.is_dynamic?(mod, generation_count=5)
180+
# @return [Boolean]
181+
def self.is_dynamic?(framework, mod, generation_count=10)
182+
return true if mod.class.const_defined?('ForceDynamicCachedSize') && mod.class::ForceDynamicCachedSize
116183
opts = module_options(mod)
117-
[*(1..generation_count)].map do |x|
118-
mod.generate_simple(opts).size
119-
end.uniq.length != 1
184+
last_bytesize = nil
185+
generation_count.times do
186+
# Ensure a new module instance is created for each attempt, as some options are randomized on load - such as tmp file path names etc
187+
new_mod = framework.payloads.create(mod.refname)
188+
bytesize = new_mod.generate_simple(opts).bytesize
189+
last_bytesize ||= bytesize
190+
if last_bytesize != bytesize
191+
return true
192+
end
193+
end
194+
195+
false
120196
end
121197

122198
# Determines whether a payload's CachedSize is up to date
123199
#
124200
# @param mod [Msf::Payload] The class of the payload module to update
125201
# @return [Boolean]
126-
def self.is_cached_size_accurate?(mod)
127-
return true if mod.dynamic_size? && is_dynamic?(mod)
202+
def self.is_cached_size_accurate?(framework, mod)
203+
return true if mod.dynamic_size? && is_dynamic?(framework, mod)
128204
return false if mod.cached_size.nil?
129205

130-
mod.cached_size == mod.generate_simple(module_options(mod)).size
206+
mod.cached_size == mod.replicant.generate_simple(module_options(mod)).bytesize
207+
end
208+
209+
# Checks for errors or inconsistencies in the CachedSize value for a payload module.
210+
# Returns nil if the cache is correct, or a string describing the error if not.
211+
#
212+
# @param framework [Msf::Framework] The Metasploit framework instance used for payload generation
213+
# @param mod [Msf::Payload] The payload module to check
214+
# @return [String, nil] Error message if there is a problem, or nil if the cache is correct
215+
def self.cache_size_errors_for(framework, mod)
216+
is_payload_size_different_on_each_generation = is_dynamic?(framework,mod)
217+
module_marked_as_dynamic = mod.dynamic_size?
218+
payload_cached_static_size = mod.cached_size
219+
220+
# Validate dynamic scenario
221+
return if is_payload_size_different_on_each_generation && module_marked_as_dynamic
222+
223+
if is_payload_size_different_on_each_generation && !module_marked_as_dynamic
224+
return 'Module generated different sizes for each generation attempt. CacheSize must be set to :dynamic'
225+
end
226+
227+
if payload_cached_static_size.nil?
228+
return 'Module missing CachedSize and not marked as dynamic'
229+
end
230+
231+
payload_size_after_one_generation = mod.replicant.generate_simple(module_options(mod)).bytesize
232+
233+
# Validate static scenario
234+
return if payload_cached_static_size == payload_size_after_one_generation
235+
236+
if payload_cached_static_size != payload_size_after_one_generation
237+
return "Module marked as having size #{payload_cached_static_size} but after one generation was #{payload_size_after_one_generation}"
238+
end
239+
240+
raise "unhandled scenario"
131241
end
132242

133243
# Get a set of sane default options for the module so it can generate a

modules/payloads/singles/bsd/sparc/shell_bind_tcp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
##
55

66
module MetasploitModule
7-
CachedSize = 164
7+
CachedSize = 168
88

99
include Msf::Payload::Single
1010
include Msf::Payload::Bsd

modules/payloads/singles/bsd/sparc/shell_reverse_tcp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
##
55

66
module MetasploitModule
7-
CachedSize = 128
7+
CachedSize = 132
88

99
include Msf::Payload::Single
1010
include Msf::Payload::Bsd

modules/payloads/singles/cmd/mainframe/apf_privesc_jcl.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
module MetasploitModule
2222
CachedSize = 3156
23+
2324
include Msf::Payload::Single
2425
include Msf::Payload::Mainframe
2526

0 commit comments

Comments
 (0)