Skip to content

Commit

Permalink
Land #682, Swap ChaCha20 to AES-256-CBC for at-rest encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
adfoster-r7 authored Oct 24, 2023
2 parents bf5bc65 + cf1b82f commit 0d77538
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 38 deletions.
3 changes: 0 additions & 3 deletions gem/lib/metasploit-payloads.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@ def self.read(*path_parts)
raise e
end

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: 46 additions & 20 deletions gem/lib/metasploit-payloads/crypto.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,42 @@
module MetasploitPayloads
module Crypto
CIPHERS = {
chacha20: {
1 => {
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
version: { iv: 1, key: 1 },
ivs: {
1 => {
value: "\x52\x25\xd7\xab\x52\x8f\x3f\xf8\x94\x97\x08\x42\x33\xb9\xd3\xb6".b # 16 bytes
}
},
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
keys: {
1 => {
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
}
}
},
2 => {
name: 'aes-256-cbc'.b,
version: { iv: 1, key: 1 },
ivs: {
1 => {
value: "\x3c\x09\x85\x95\x19\x09\x10\xff\x76\xf0\x48\xf7\x21\x1a\x5c\x59".b, # 16 bytes
}
},
keys: {
1 => {
value: "\x01\x93\x90\xfb\x84\xcd\x70\x16\x90\x1d\xc6\xf4\xf2\xfd\xcf\x59\xc4\x9c\x26\x35\x29\x67\x8c\x2d\x17\xb9\x35\xcb\x7d\xb0\x88\x7a".b, # 32 bytes
}
}
}
}.freeze
CURRENT_CIPHER = CIPHERS[:chacha20]
CIPHER_VERSION = CURRENT_CIPHER[:version]
KEY_VERSION = CURRENT_CIPHER[:key][:version]
IV_VERSION = CURRENT_CIPHER[:iv][:version]
CIPHER_VERSION = 2
CURRENT_CIPHER = CIPHERS[CIPHER_VERSION]
KEY_VERSION = CURRENT_CIPHER[:version][:key]
IV_VERSION = CURRENT_CIPHER[:version][:iv]

# Binary String, unsigned char, unsigned char, unsigned char
ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC')
ENCRYPTED_PAYLOAD_HEADER = ['msf', CIPHER_VERSION, IV_VERSION, KEY_VERSION].pack('A*CCC').freeze

private_constant :CIPHERS
private_constant :CURRENT_CIPHER
Expand All @@ -35,8 +52,8 @@ def self.encrypt(plaintext: '')
cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name])

cipher.encrypt
cipher.iv = CURRENT_CIPHER[:iv][:value]
cipher.key = CURRENT_CIPHER[:key][:value]
cipher.iv = CURRENT_CIPHER[:ivs][IV_VERSION][:value]
cipher.key = CURRENT_CIPHER[:keys][KEY_VERSION][:value]

output = ENCRYPTED_PAYLOAD_HEADER.dup
output << cipher.update(plaintext)
Expand All @@ -48,14 +65,23 @@ def self.encrypt(plaintext: '')
def self.decrypt(ciphertext: '')
raise ::ArgumentError, 'Unable to decrypt ciphertext: ' << ciphertext, caller unless ciphertext.to_s

cipher = ::OpenSSL::Cipher.new(CURRENT_CIPHER[:name])
return ciphertext unless ciphertext.start_with?('msf'.b)

# Use the correct algorithm based on the version in the header
msf_header, cipher_version, iv_version, key_version = ciphertext.unpack('A3CCC')

current_cipher = CIPHERS[cipher_version]
cipher = ::OpenSSL::Cipher.new(current_cipher[:name])
iv = current_cipher[:ivs][iv_version][:value]
key = current_cipher[:keys][key_version][:value]

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

# Remove encrypted header if present
ciphertext = ciphertext.sub(ENCRYPTED_PAYLOAD_HEADER, '')
header = [msf_header, cipher_version, iv_version, key_version].pack('A*CCC').b
# Remove encrypted header
ciphertext = ciphertext.sub(header, '')

output = cipher.update(ciphertext)
output << cipher.final
Expand Down
46 changes: 36 additions & 10 deletions gem/spec/metasploit_payloads/crypto_spec.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
require 'spec_helper'
require 'metasploit-payloads'

RSpec.describe ::MetasploitPayloads::Crypto do
let(:plaintext) { "Hello World!".b }

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 }
let(:encrypted_header) { "msf\x02\x01\x01".b }
let(:ciphertext) { encrypted_header + "F=\xF9\xCB\xF6\xA1\xE4h\x89\x96DD\xC0+\x04\xF1".b }

it 'can encrypt plaintext' do
it 'encrypts using aes-256-cbc' do
expect(described_class.encrypt(plaintext: plaintext)).to eq ciphertext
end
end

it 'can decrypt ciphertext' do
expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext
end
describe '#decrypt' do
context 'when the ciphertext is' do
context 'encrypted with chacha20' do
let(:encrypted_header) { "msf\x01\x01\x01".b }
let(:ciphertext) { encrypted_header + "\x89:^r\xC1\xC9\xD9\xA1\xDC\xEB\xBFm".b }

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

context 'encrypted with aes-256-cbc' do
let(:encrypted_header) { "msf\x02\x01\x01".b }
let(:ciphertext) { encrypted_header + "F=\xF9\xCB\xF6\xA1\xE4h\x89\x96DD\xC0+\x04\xF1".b }

it 'is idempotent' do
expect(described_class.decrypt(ciphertext: described_class.encrypt(plaintext: plaintext))).to eq plaintext
it 'returns plaintext' do
expect(described_class.decrypt(ciphertext: ciphertext)).to eq plaintext
end
end

context 'not encrypted' do
let(:ciphertext) { plaintext }

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

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

describe '#read' do
let(:encrypted_header) { 'encrypted_payload_chacha20_v1' }
let(:encrypted_header) { "msf\x02\x01\x01" }
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" }
# AES-256-CBC encrypted contents
let(:encrypted_contents) { "\xEA\x00q\xEB\a\xCA\xD2\xD3\xE2',N\x86\x1C\f?\xBE\xC4\x8AJRks\xAD\xD6\xDF\xA3.\xCD\xA7\x84\xD2".b }
let(:encrypted_file) { { name: raw_file[:name], contents: encrypted_header + encrypted_contents } }

before :each do
Expand Down
2 changes: 0 additions & 2 deletions gem/spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require 'metasploit_payloads/metasploit_payloads_spec'

# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
Expand Down

0 comments on commit 0d77538

Please sign in to comment.