Skip to content

Commit 54465f3

Browse files
authored
Land rapid7#19917, Add NIST SP 800 Crypto Primitives
Land rapid7#19917, Add NIST SP 800 Crypto Primitives
2 parents 59b862c + 2fd0511 commit 54465f3

File tree

8 files changed

+230
-9
lines changed

8 files changed

+230
-9
lines changed

lib/msf/core/exploit/remote/smb/client/kerberos_authentication.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,21 @@ def smb2_authenticate
122122

123123
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/7fd079ca-17e6-4f02-8449-46b606ea289c
124124
if @dialect == '0x0300' || @dialect == '0x0302'
125-
@application_key = RubySMB::Crypto::KDF.counter_mode(
125+
@application_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
126126
@session_key,
127-
"SMB2APP\x00",
128-
"SmbRpc\x00"
129-
)
127+
16,
128+
'SHA256',
129+
label: "SMB2APP\x00",
130+
context: "SmbRpc\x00"
131+
).first
130132
else
131-
@application_key = RubySMB::Crypto::KDF.counter_mode(
133+
@application_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
132134
@session_key,
133-
"SMBAppKey\x00",
134-
@preauth_integrity_hash_value
135-
)
135+
16,
136+
'SHA256',
137+
label: "SMBAppKey\x00",
138+
context: @preauth_integrity_hash_value
139+
).first
136140
end
137141
# otherwise, leave encryption to the default value that it was initialized to
138142
end

lib/msf_autoload.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ def custom_inflections
298298
'uds_errors' => 'UDSErrors',
299299
'smb_hash_capture' => 'SMBHashCapture',
300300
'rex_ntlm' => 'RexNTLM',
301-
'teamcity' => 'TeamCity'
301+
'teamcity' => 'TeamCity',
302+
'nist_sp_800_38f' => 'NIST_SP_800_38f',
303+
'nist_sp_800_108' => 'NIST_SP_800_108'
302304
}
303305
end
304306

lib/rex/crypto/key_derivation.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Rex::Crypto::KeyDerivation
2+
require 'rex/crypto/key_derivation/nist_sp_800_108'
3+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require 'openssl'
2+
3+
module Rex::Crypto::KeyDerivation::NIST_SP_800_108
4+
5+
# Generates key material using the NIST SP 800-108 R1 counter mode KDF.
6+
#
7+
# @param length [Integer] The desired output length of each key in bytes.
8+
# @param prf [Proc] The pseudorandom function used for key derivation.
9+
# @param keys [Integer] The number of derived keys to generate.
10+
# @param label [String] Optional label to distinguish different derivations.
11+
# @param context [String] Optional context to bind the key derivation to specific information.
12+
#
13+
# @return [Array<String>] An array of derived keys as binary strings, regardless of the number requested.
14+
def self.counter(length, prf, keys: 1, label: ''.b, context: ''.b)
15+
key_block = ''
16+
17+
counter = 0
18+
while key_block.length < (length * keys)
19+
counter += 1
20+
raise RangeError.new("counter overflow") if counter > 0xffffffff
21+
22+
info = [ counter ].pack('L>') + label + "\x00".b + context + [ length * keys * 8 ].pack('L>')
23+
key_block << prf.call(info)
24+
end
25+
26+
key_block.bytes.each_slice(length).to_a[...keys].map { |slice| slice.pack('C*') }
27+
end
28+
29+
# Generates key material using the NIST SP 800-108 R1 counter mode KDF with HMAC.
30+
#
31+
# @param secret [String] The secret key used as the HMAC key.
32+
# @param length [Integer] The desired output length of each key in bytes.
33+
# @param algorithm [String, Symbol] The HMAC hash algorithm (e.g., `SHA256`, `SHA512`).
34+
# @param keys [Integer] The number of derived keys to generate (default: 1).
35+
# @param label [String] Optional label to distinguish different derivations.
36+
# @param context [String] Optional context to bind the key derivation to specific information.
37+
#
38+
# @return [Array<String>] Returns an array of derived keys.
39+
#
40+
# @raise [ArgumentError] If the requested length is invalid or the algorithm is unsupported.
41+
def self.counter_hmac(secret, length, algorithm, keys: 1, label: ''.b, context: ''.b)
42+
prf = -> (data) { OpenSSL::HMAC.digest(algorithm, secret, data) }
43+
counter(length, prf, keys: keys, label: label, context: context)
44+
end
45+
end

lib/rex/crypto/key_wrap.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Rex::Crypto::KeyWrap
2+
require 'rex/crypto/key_wrap/nist_sp_800_38f'
3+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# see: [NIST SP 800-38F, Section 6.2](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf)
2+
module Rex; end
3+
module Rex::Crypto; end
4+
module Rex::Crypto::KeyWrap; end
5+
6+
module Rex::Crypto::KeyWrap::NIST_SP_800_38f
7+
8+
# Performs AES key unwrapping from NIST SP 800-38F.
9+
#
10+
# @param kek [String] The key-encryption key (KEK) used to unwrap the ciphertext.
11+
# @param key_data [String] The wrapped key data.
12+
# @param authenticate [Boolean] Whether to check the data integrity or not.
13+
# @return [String, nil] The unwrapped key on success, or nil if unwrapping fails.
14+
#
15+
# @see https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38F.pdf
16+
def self.aes_unwrap(kek, key_data, authenticate: true)
17+
# padded mode as described in Section 6.3 is not supported at this time
18+
raise Rex::ArgumentError.new('kek must be 16, 24 or 32-bytes long') unless [16, 24, 32].include?(kek.length)
19+
raise Rex::ArgumentError.new('key_data length must be a multiple of 8') unless key_data.length % 8 == 0
20+
icv1 = ("\xa6".b * 8)
21+
22+
r = key_data.bytes.each_slice(8).map { |c| c.pack('C*') }
23+
a = r.shift
24+
25+
ciph = -> (data) do
26+
# per-section 5.1, AES is the only suitable block cipher
27+
cipher = OpenSSL::Cipher::AES.new(kek.length * 8, :ECB).decrypt
28+
cipher.key = kek
29+
cipher.padding = 0
30+
cipher.update(data)
31+
end
32+
33+
n = r.length
34+
35+
5.downto(0) do |j|
36+
(n - 1).downto(0) do |i|
37+
atr = [a.unpack1('Q>') ^ ((n * j) + i + 1)].pack('Q>') + r[i]
38+
39+
b = ciph.call(atr)
40+
a = b[...8]
41+
r[i] = b[-8...]
42+
end
43+
end
44+
45+
# setting authenticate to true effectively switches the operation from Section 6.2 algorithm #2 to algorithm #4
46+
if authenticate && a != icv1
47+
raise Rex::RuntimeError.new('ICV1 integrity check failed in KW-AD(C)')
48+
end
49+
50+
r.join('')
51+
end
52+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require 'spec_helper'
2+
require 'rex/crypto/key_derivation/nist_sp_800_108'
3+
4+
RSpec.describe Rex::Crypto::KeyDerivation::NIST_SP_800_108 do
5+
describe '.counter' do
6+
let(:secret) { [ '000102030405060708090A0B0C0D0E0F' ].pack('H*') }
7+
let(:prf) { RSpec::Mocks::Double.new('prf') }
8+
let(:length) { 32 }
9+
let(:label) { "RSpec Test Label\0" }
10+
let(:context) { "RSpec Test Context\0" }
11+
12+
it 'builds the context block correctly for the prf' do
13+
info = [ 1 ].pack('L>') + label + "\x00".b + context + [ length * 8 ].pack('L>')
14+
expect(prf).to receive(:call).with(info).and_return(OpenSSL::HMAC.digest('SHA256', secret, info))
15+
described_class.counter(length, prf, label: label, context: context)
16+
end
17+
end
18+
19+
describe '.counter_hmac' do
20+
let(:secret) { [ '000102030405060708090A0B0C0D0E0F' ].pack('H*') }
21+
let(:length) { 32 }
22+
let(:label) { "RSpec Test Label\0" }
23+
let(:context) { "RSpec Test Context\0" }
24+
25+
context 'when the algorithm is invalid' do
26+
let(:algorithm) { 'InvalidAlgorithm' }
27+
28+
it 'raises an error' do
29+
expect { described_class.counter_hmac(secret, length, algorithm, label: label, context: context) }.to raise_error(RuntimeError, /digest algorithm/)
30+
end
31+
end
32+
33+
context 'when the algorithm is SHA256' do
34+
let(:algorithm) { 'SHA256' }
35+
before(:each) { expect(OpenSSL::HMAC).to receive(:digest).at_least(:once).with(algorithm, secret, anything).and_call_original }
36+
before(:each) { expect(described_class).to receive(:counter).with(length, anything, context: context, label: label, keys: instance_of(Integer)).and_call_original }
37+
38+
it 'uses SHA256 to calculate 1 key' do
39+
keys = described_class.counter_hmac(secret, length, algorithm, label: label, context: context)
40+
expect(keys.length).to eq 1
41+
expect(keys[0]).to eq ['5889a9fe18d9d51b5eb95272088acbe38bd2ea82517f1956b919dc549a945aa0'].pack('H*')
42+
end
43+
44+
it 'uses SHA256 to calculate 2 keys' do
45+
keys = described_class.counter_hmac(secret, length, algorithm, label: label, context: context, keys: 2)
46+
expect(keys.length).to eq 2
47+
expect(keys[0]).to eq ['2060ea190b9ac147ccfbe2c094c49be04dcac80db6d05b1c32c54529caf24d43'].pack('H*')
48+
expect(keys[1]).to eq ['f66a460fc1d03451c1ef669ee10953815460d368668be13301d6314878ed771d'].pack('H*')
49+
end
50+
end
51+
end
52+
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'spec_helper'
2+
require 'rex/crypto/key_wrap/nist_sp_800_38f'
3+
4+
RSpec.describe Rex::Crypto::KeyWrap::NIST_SP_800_38f do
5+
let(:expected_plaintext) { [ '00112233445566778899AABBCCDDEEFF' ].pack('H*') }
6+
7+
# Test vector from RFC 3394, Section 4.1 - 128-bit KEK
8+
let(:kek_128) { [ '000102030405060708090A0B0C0D0E0F' ].pack('H*') }
9+
let(:ciphertext_128) { [ '1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5' ].pack('H*') }
10+
11+
# Test vector from RFC 3394, Section 4.2 - 192-bit KEK
12+
let(:kek_192) { [ '000102030405060708090A0B0C0D0E0F1011121314151617' ].pack('H*') }
13+
let(:ciphertext_192) { [ '96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D' ].pack('H*') }
14+
15+
# Test vector from RFC 3394, Section 4.3 - 256-bit KEK
16+
let(:kek_256) { [ '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F' ].pack('H*') }
17+
let(:ciphertext_256) { [ '64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7' ].pack('H*') }
18+
19+
describe '.aes_unwrap' do
20+
it 'successfully unwraps a 128-bit key with a 128-bit KEK (RFC 3394, Section 4.1)' do
21+
unwrapped_key = described_class.aes_unwrap(kek_128, ciphertext_128)
22+
expect(unwrapped_key).to eq(expected_plaintext)
23+
end
24+
25+
it 'successfully unwraps a 128-bit key with a 192-bit KEK (RFC 3394, Section 4.2)' do
26+
unwrapped_key = described_class.aes_unwrap(kek_192, ciphertext_192)
27+
expect(unwrapped_key).to eq(expected_plaintext)
28+
end
29+
30+
it 'successfully unwraps a 128-bit key with a 256-bit KEK (RFC 3394, Section 4.3)' do
31+
unwrapped_key = described_class.aes_unwrap(kek_256, ciphertext_256)
32+
expect(unwrapped_key).to eq(expected_plaintext)
33+
end
34+
35+
context 'when the wrapped key is corrupted' do
36+
let(:corrupted_wrapped_key) { ['64E8C3F9CE0F5BA2A521427441A552DA'].pack('H*') }
37+
38+
context 'when authenticate is true' do
39+
it 'raises an exception' do
40+
expect { described_class.aes_unwrap(kek_128, corrupted_wrapped_key, authenticate: true) }.to raise_error(Rex::RuntimeError, /integrity check failed/)
41+
end
42+
end
43+
44+
context 'when authenticate is false' do
45+
it 'successfully unwraps the key' do
46+
unwrapped_key = described_class.aes_unwrap(kek_128, corrupted_wrapped_key, authenticate: false)
47+
expect(unwrapped_key).to eq ['3078ea9fbd99e7d7'].pack('H*')
48+
end
49+
end
50+
end
51+
52+
context 'when the wrapped key is invalid' do
53+
let(:invalid_wrapped_key) { ['64E8C3F9CE0F5B'].pack('H*') } # Not a multiple of 8
54+
55+
it 'rejects keys with invalid ciphertext length' do
56+
expect { described_class.aes_unwrap(kek_128, invalid_wrapped_key, authenticate: false) }.to raise_error(Rex::ArgumentError, /must be a multiple of 8/)
57+
end
58+
end
59+
end
60+
end

0 commit comments

Comments
 (0)