Skip to content

Commit

Permalink
Land #679, Add at rest encryption to Meterpreter payloads
Browse files Browse the repository at this point in the history
  • Loading branch information
adfoster-r7 committed Oct 13, 2023
2 parents 294eaf6 + 1f84361 commit 5d29d8f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 7 deletions.
5 changes: 4 additions & 1 deletion gem/Rakefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "bundler/gem_tasks"
require 'openssl'
require 'metasploit-payloads/crypto'

c_source = "../c/meterpreter/"
java_source = "../java"
Expand Down Expand Up @@ -52,7 +53,9 @@ def copy_files(cnf, meterpreter_dest)
Dir.glob("#{f}/*.#{ext}").each do |bin|
target = File.join(meterpreter_dest, File.basename(bin))
print("Copying: #{bin} -> #{target}\n")
FileUtils.cp(bin, target)
contents = ::File.binread(::File.expand_path(bin))
encrypted_contents = ::MetasploitPayloads::Crypto.encrypt(plaintext: contents)
::File.binwrite(::File.expand_path(target), encrypted_contents)
end
end
end
Expand Down
24 changes: 18 additions & 6 deletions gem/lib/metasploit-payloads.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'openssl' unless defined? OpenSSL::Digest
require 'metasploit-payloads/version' unless defined? MetasploitPayloads::VERSION
require 'metasploit-payloads/error' unless defined? MetasploitPayloads::Error
require 'metasploit-payloads/crypto' unless defined? MetasploitPayloads::Crypto

#
# This module dispenses Metasploit payload binary files
Expand Down Expand Up @@ -43,8 +44,9 @@ def self.manifest_errors
manifest_contents.each_line do |line|
filename, hash_type, hash = line.chomp.split(':')
begin
filename = filename.sub('./data/', '')
# self.path prepends the gem data directory, which is already present in the manifest file.
out_path = self.path(filename.sub('./data/', ''))
out_path = self.path(filename)
# self.path can return a path to the gem data, or user's local data.
bundled_file = out_path.start_with?(data_directory)
if bundled_file
Expand Down Expand Up @@ -137,15 +139,25 @@ def self.path(*path_parts)

#
# Get the contents of any file packaged in this gem by local path and name.
# If the file is encrypted using ChaCha20, automatically decrypt it and return the file contents.
#
def self.read(*path_parts)
file_path = path(path_parts)
if file_path.nil?
full_path = ::File.join(path_parts)
raise ::MetasploitPayloads::NotFoundError, full_path, caller
file_path = self.path(path_parts)

begin
file_contents = ::File.binread(file_path)
rescue ::Errno::ENOENT => _e
raise ::MetasploitPayloads::NotFoundError, file_path, caller
rescue ::Errno::EACCES => _e
raise ::MetasploitPayloads::NotReadableError, file_path, caller
rescue ::StandardError => e
raise e
end

::File.binread(file_path)
encrypted_file = file_contents.start_with?(Crypto::ENCRYPTED_PAYLOAD_HEADER)
return file_contents unless encrypted_file

Crypto.decrypt(ciphertext: file_contents)
end

#
Expand Down
66 changes: 66 additions & 0 deletions gem/lib/metasploit-payloads/crypto.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'openssl'

module MetasploitPayloads
module Crypto
CIPHERS = {
chacha20: {
name: 'chacha20'.b,
version: 1,
iv: {
value: "\x52\x25\xd7\xab\x52\x8f\x3f\xf8\x94\x97\x08\x42\x33\xb9\xd3\xb6".b, # 16 bytes
version: 1
},
key: {
value: "\x28\x39\x97\x4c\x95\x11\x9d\x42\x6c\x8b\xff\x43\x3e\x5d\x3c\x33\x1b\x95\xd3\xea\xeb\xc9\xae\x71\x0a\x36\xe7\x98\x3d\x9d\x09\x52".b, # 32 bytes
version: 1
}
}
}.freeze
CURRENT_CIPHER = CIPHERS[:chacha20]
CIPHER_VERSION = CURRENT_CIPHER[:version]
KEY_VERSION = CURRENT_CIPHER[:key][:version]
IV_VERSION = CURRENT_CIPHER[:iv][:version]
# Binary String, unsigned char, unsigned char, unsigned char
ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC')

private_constant :CIPHERS
private_constant :CURRENT_CIPHER
private_constant :CIPHER_VERSION
private_constant :KEY_VERSION
private_constant :IV_VERSION

def self.encrypt(plaintext: '')
raise ::ArgumentError, 'Unable to encrypt plaintext: ' << plaintext, caller unless plaintext.to_s

cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name])

cipher.encrypt
cipher.iv = CURRENT_CIPHER[:iv][:value]
cipher.key = CURRENT_CIPHER[:key][:value]

output = ENCRYPTED_PAYLOAD_HEADER.dup
output << cipher.update(plaintext)
output << cipher.final

output
end

def self.decrypt(ciphertext: '')
raise ::ArgumentError, 'Unable to decrypt ciphertext: ' << ciphertext, caller unless ciphertext.to_s

cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name])

cipher.decrypt
cipher.iv = CURRENT_CIPHER[:iv][:value]
cipher.key = CURRENT_CIPHER[:key][:value]

# Remove encrypted header if present
ciphertext = ciphertext.sub(ENCRYPTED_PAYLOAD_HEADER, '')

output = cipher.update(ciphertext)
output << cipher.final

output
end
end
end
22 changes: 22 additions & 0 deletions gem/spec/metasploit_payloads/crypto_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'
require 'metasploit-payloads'

RSpec.describe ::MetasploitPayloads::Crypto do
describe '#encrypt' do
let(:encrypted_header) { ::MetasploitPayloads::Crypto::ENCRYPTED_PAYLOAD_HEADER }
let(:plaintext) { "Hello World!".b }
let(:ciphertext) { encrypted_header + "\x89:^r\xC1\xC9\xD9\xA1\xDC\xEB\xBFm".b }

it 'can encrypt plaintext' do
expect(described_class.encrypt(plaintext: plaintext)).to eq ciphertext
end

it 'can decrypt ciphertext' do
expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext
end

it 'is idempotent' do
expect(described_class.decrypt(ciphertext: described_class.encrypt(plaintext: plaintext))).to eq plaintext
end
end
end
30 changes: 30 additions & 0 deletions gem/spec/metasploit_payloads/metasploit_payloads_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,34 @@
end
end
end

describe '#read' do
let(:encrypted_header) { 'encrypted_payload_chacha20_v1' }
let(:raw_file) { { name: 'meterpreter.py', contents: 'sample_file_contents' } }
# ChaCha20 encrypted contents
let(:encrypted_contents) { "gg\xB7R\x96\xA00\x84\xC4\xBF5\x1D\xDBG6J\n\x86\x06\xF1" }
let(:encrypted_file) { { name: raw_file[:name], contents: encrypted_header + encrypted_contents } }

before :each do
allow(::MetasploitPayloads).to receive(:path).and_call_original
allow(::MetasploitPayloads).to receive(:path).with([encrypted_file[:name]]).and_return(encrypted_file[:name])
allow(::MetasploitPayloads).to receive(:path).with([raw_file[:name]]).and_return(raw_file[:name])

allow(::File).to receive(:binread).and_call_original
allow(::File).to receive(:binread).with(encrypted_file[:name]).and_return(encrypted_file[:contents])
allow(::File).to receive(:binread).with(raw_file[:name]).and_return(raw_file[:contents])
end

context 'an encrypted file' do
it 'returns plain-text file contents' do
expect(subject.read(encrypted_file[:name])).to eq(raw_file[:contents])
end
end

context 'a plain-text file' do
it 'returns plain-text file contents' do
expect(subject.read(raw_file[:name])).to eq(raw_file[:contents])
end
end
end
end

0 comments on commit 5d29d8f

Please sign in to comment.