rex 2.0.9 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/rex/exploitation/cmdstager/bourne.rb +14 -8
- data/lib/rex/exploitation/cmdstager/echo.rb +3 -3
- data/lib/rex/exploitation/js/memory.rb +1 -1
- data/lib/rex/java/serialization/model/contents.rb +1 -1
- data/lib/rex/mime/message.rb +1 -1
- data/lib/rex/parser/acunetix_nokogiri.rb +2 -0
- data/lib/rex/parser/appscan_nokogiri.rb +1 -1
- data/lib/rex/parser/burp_issue_nokogiri.rb +139 -0
- data/lib/rex/parser/burp_session_nokogiri.rb +1 -1
- data/lib/rex/parser/fs/bitlocker.rb +233 -0
- data/lib/rex/parser/fusionvm_nokogiri.rb +2 -2
- data/lib/rex/parser/ini.rb +1 -8
- data/lib/rex/parser/nokogiri_doc_mixin.rb +5 -0
- data/lib/rex/payloads/meterpreter/config.rb +23 -4
- data/lib/rex/post/meterpreter/channel.rb +8 -3
- data/lib/rex/post/meterpreter/client.rb +1 -0
- data/lib/rex/post/meterpreter/client_core.rb +2 -2
- data/lib/rex/post/meterpreter/extensions/android/android.rb +86 -1
- data/lib/rex/post/meterpreter/extensions/android/tlv.rb +29 -0
- data/lib/rex/post/meterpreter/extensions/extapi/wmi/wmi.rb +1 -1
- data/lib/rex/post/meterpreter/extensions/stdapi/net/socket_subsystem/tcp_server_channel.rb +75 -89
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/event_log.rb +8 -2
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +10 -5
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/registry_subsystem/registry_key.rb +7 -2
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/registry_subsystem/remote_registry_key.rb +10 -5
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/thread.rb +8 -2
- data/lib/rex/post/meterpreter/extensions/stdapi/webcam/webcam.rb +1 -1
- data/lib/rex/post/meterpreter/packet.rb +38 -0
- data/lib/rex/post/meterpreter/packet_dispatcher.rb +101 -108
- data/lib/rex/post/meterpreter/packet_parser.rb +14 -6
- data/lib/rex/post/meterpreter/packet_response_waiter.rb +42 -21
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/android.rb +54 -4
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb +39 -13
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb +8 -0
- data/lib/rex/proto/adb.rb +7 -0
- data/lib/rex/proto/adb/client.rb +39 -0
- data/lib/rex/proto/adb/message.rb +164 -0
- data/lib/rex/proto/dcerpc/svcctl/packet.rb +9 -9
- data/lib/rex/proto/http/client_request.rb +2 -1
- data/lib/rex/proto/http/response.rb +1 -1
- data/lib/rex/proto/kademlia/bootstrap_response.rb +2 -2
- data/lib/rex/proto/ntp/modes.rb +17 -0
- data/lib/rex/text.rb +12 -0
- data/lib/rex/zip/blocks.rb +1 -1
- data/lib/rex/zip/entry.rb +1 -1
- data/rex.gemspec +28 -1
- metadata +106 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 905f84863b058a5c12986cb77bd82bfeb9e5948f
|
4
|
+
data.tar.gz: c13bf88f1ffda5f7b730a29d6131c80e6a3a106c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e2b6a00d84ebf9e551c906d131695a01842c0249c1bb7fb2c7882f41bfd1e5d8da535e092d03a2862c44a1a82355fa0d0efc55d70c680aadd8e38e0c8243561
|
7
|
+
data.tar.gz: 6702bf1c601ca43627ecf06303ebe80b507b7b178081b872f353956806949961eda4c3c21ed01f20400db12c7ac791ad13cab486aea80c03be3dd93113423bc3
|
@@ -12,14 +12,20 @@ class CmdStagerBourne < CmdStagerBase
|
|
12
12
|
def initialize(exe)
|
13
13
|
super
|
14
14
|
|
15
|
-
@var_encoded = Rex::Text.rand_text_alpha(5)
|
15
|
+
@var_encoded = Rex::Text.rand_text_alpha(5) + '.b64'
|
16
16
|
@var_decoded = Rex::Text.rand_text_alpha(5)
|
17
17
|
end
|
18
18
|
|
19
19
|
def generate(opts = {})
|
20
20
|
opts[:temp] = opts[:temp] || '/tmp/'
|
21
|
+
opts[:temp] = opts[:temp].empty?? opts[:temp] : opts[:temp] + '/'
|
22
|
+
opts[:temp] = opts[:temp].gsub(/\/{2,}/, '/')
|
21
23
|
opts[:temp] = opts[:temp].gsub(/'/, "\\\\'")
|
22
24
|
opts[:temp] = opts[:temp].gsub(/ /, "\\ ")
|
25
|
+
if (opts[:file])
|
26
|
+
@var_encoded = opts[:file] + '.b64'
|
27
|
+
@var_decoded = opts[:file]
|
28
|
+
end
|
23
29
|
super
|
24
30
|
end
|
25
31
|
|
@@ -29,7 +35,7 @@ class CmdStagerBourne < CmdStagerBase
|
|
29
35
|
def generate_cmds(opts)
|
30
36
|
# Set the start/end of the commands here (vs initialize) so we have @tempdir
|
31
37
|
@cmd_start = "echo -n "
|
32
|
-
@cmd_end = "
|
38
|
+
@cmd_end = ">>'#{@tempdir}#{@var_encoded}'"
|
33
39
|
xtra_len = @cmd_start.length + @cmd_end.length + 1
|
34
40
|
opts.merge!({ :extra => xtra_len })
|
35
41
|
super
|
@@ -78,27 +84,27 @@ class CmdStagerBourne < CmdStagerBase
|
|
78
84
|
decoder_cmd << "(which #{binary} >&2 && #{cmd})"
|
79
85
|
end
|
80
86
|
decoder_cmd = decoder_cmd.join(" || ")
|
81
|
-
decoder_cmd = "(" << decoder_cmd << ") 2> /dev/null > #{@tempdir}#{@var_decoded}
|
87
|
+
decoder_cmd = "(" << decoder_cmd << ") 2> /dev/null > '#{@tempdir}#{@var_decoded}' < '#{@tempdir}#{@var_encoded}'"
|
82
88
|
[ decoder_cmd ]
|
83
89
|
end
|
84
90
|
|
85
91
|
def compress_commands(cmds, opts)
|
86
92
|
# Make it all happen
|
87
|
-
cmds << "chmod +x #{@tempdir}#{@var_decoded}
|
93
|
+
cmds << "chmod +x '#{@tempdir}#{@var_decoded}'"
|
88
94
|
# Background the process, allowing the cleanup code to continue and delete the data
|
89
95
|
# while allowing the original shell to continue to function since it isn't waiting
|
90
96
|
# on the payload to exit. The 'sleep' is required as '&' is a command terminator
|
91
97
|
# and having & and the cmds delimiter ';' next to each other is invalid.
|
92
98
|
if opts[:background]
|
93
|
-
cmds << "#{@tempdir}#{@var_decoded}
|
99
|
+
cmds << "'#{@tempdir}#{@var_decoded}' & sleep 2"
|
94
100
|
else
|
95
|
-
cmds << "#{@tempdir}#{@var_decoded}
|
101
|
+
cmds << "'#{@tempdir}#{@var_decoded}'"
|
96
102
|
end
|
97
103
|
|
98
104
|
# Clean up after unless requested not to..
|
99
105
|
if (not opts[:nodelete])
|
100
|
-
cmds << "rm -f #{@tempdir}#{@var_decoded}
|
101
|
-
cmds << "rm -f #{@tempdir}#{@var_encoded}
|
106
|
+
cmds << "rm -f '#{@tempdir}#{@var_decoded}'"
|
107
|
+
cmds << "rm -f '#{@tempdir}#{@var_encoded}'"
|
102
108
|
end
|
103
109
|
|
104
110
|
super
|
@@ -35,7 +35,7 @@ class CmdStagerEcho < CmdStagerBase
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# by default use the 'hex' encoding
|
38
|
-
opts[:enc_format] = opts[:enc_format]
|
38
|
+
opts[:enc_format] = opts[:enc_format].nil? ? 'hex' : opts[:enc_format].to_s
|
39
39
|
|
40
40
|
unless ENCODINGS.keys.include?(opts[:enc_format])
|
41
41
|
raise RuntimeError, "CmdStagerEcho - Invalid Encoding Option: #{opts[:enc_format]}"
|
@@ -58,7 +58,7 @@ class CmdStagerEcho < CmdStagerBase
|
|
58
58
|
xtra_len = @cmd_start.length + @cmd_end.length
|
59
59
|
opts.merge!({ :extra => xtra_len })
|
60
60
|
|
61
|
-
@prefix = ENCODINGS[opts[:enc_format]]
|
61
|
+
@prefix = opts[:prefix] || ENCODINGS[opts[:enc_format]]
|
62
62
|
min_part_size = 5 # for both encodings
|
63
63
|
|
64
64
|
if (opts[:linemax] - opts[:extra]) < min_part_size
|
@@ -108,7 +108,7 @@ class CmdStagerEcho < CmdStagerBase
|
|
108
108
|
# Make it all happen
|
109
109
|
cmds << "chmod 777 #{@tempdir}#{@var_elf}"
|
110
110
|
#cmds << "chmod +x #{@tempdir}#{@var_elf}"
|
111
|
-
cmds << "#{@tempdir}#{@var_elf}"
|
111
|
+
cmds << "#{@tempdir}#{@var_elf}#{' & echo' if opts[:background]}"
|
112
112
|
|
113
113
|
# Clean up after unless requested not to..
|
114
114
|
unless opts[:nodelete]
|
data/lib/rex/mime/message.rb
CHANGED
@@ -126,7 +126,7 @@ class Message
|
|
126
126
|
header_string = self.header.to_s
|
127
127
|
|
128
128
|
msg = header_string.empty? ? '' : force_crlf(self.header.to_s + "\r\n")
|
129
|
-
msg << force_crlf(self.content + "\r\n") unless self.content.
|
129
|
+
msg << force_crlf(self.content + "\r\n") unless self.content.to_s.empty?
|
130
130
|
|
131
131
|
self.parts.each do |part|
|
132
132
|
msg << force_crlf("--" + self.bound + "\r\n")
|
@@ -59,6 +59,8 @@ module Rex
|
|
59
59
|
@text = nil
|
60
60
|
when "StartURL" # Populates @state[:starturl_uri], we use this a lot
|
61
61
|
@state[:has_text] = false
|
62
|
+
# StartURL does not always include the scheme
|
63
|
+
@text.prepend("http://") unless URI.parse(@text).scheme
|
62
64
|
collect_host
|
63
65
|
collect_service
|
64
66
|
@text = nil
|
@@ -195,7 +195,7 @@ module Rex
|
|
195
195
|
res_header = Rex::Proto::Http::Packet::Header.new
|
196
196
|
req_header.from_s request_headers.lstrip
|
197
197
|
res_header.from_s response_headers.lstrip
|
198
|
-
if response_body.
|
198
|
+
if response_body.to_s.empty?
|
199
199
|
response_body = ''
|
200
200
|
end
|
201
201
|
@state[:request_headers] = req_header
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
require "rex/parser/nokogiri_doc_mixin"
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Rex
|
6
|
+
module Parser
|
7
|
+
|
8
|
+
# If Nokogiri is available, define Burp Issue document class.
|
9
|
+
load_nokogiri && class BurpIssueDocument < Nokogiri::XML::SAX::Document
|
10
|
+
|
11
|
+
include NokogiriDocMixin
|
12
|
+
|
13
|
+
def start_element(name=nil,attrs=[])
|
14
|
+
attrs = normalize_attrs(attrs)
|
15
|
+
block = @block
|
16
|
+
@state[:current_tag][name] = true
|
17
|
+
case name
|
18
|
+
when "host", "name", "info", "issueDetail", "references"
|
19
|
+
@state[:has_text] = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def end_element(name=nil)
|
24
|
+
block = @block
|
25
|
+
case name
|
26
|
+
when "issue"
|
27
|
+
report_web_host_info
|
28
|
+
report_web_service_info
|
29
|
+
report_vuln
|
30
|
+
# Reset the state once we close a host
|
31
|
+
@state = @state.select {|k| [:current_tag].include? k}
|
32
|
+
when "host"
|
33
|
+
@state[:has_text] = false
|
34
|
+
collect_host_info
|
35
|
+
@text = nil
|
36
|
+
when "name"
|
37
|
+
@state[:has_text] = false
|
38
|
+
collect_name
|
39
|
+
@text = nil
|
40
|
+
when "issueDetail"
|
41
|
+
@state[:has_text] = false
|
42
|
+
collect_issue_detail
|
43
|
+
@text = nil
|
44
|
+
when "references"
|
45
|
+
@state[:has_text] = false
|
46
|
+
collect_references
|
47
|
+
@text = nil
|
48
|
+
end
|
49
|
+
@state[:current_tag].delete name
|
50
|
+
end
|
51
|
+
|
52
|
+
def collect_host_info
|
53
|
+
return unless in_issue
|
54
|
+
return unless has_text
|
55
|
+
uri = URI(@text)
|
56
|
+
|
57
|
+
@state[:host] = uri.host
|
58
|
+
@state[:service_name] = uri.scheme
|
59
|
+
@state[:proto] = "tcp"
|
60
|
+
|
61
|
+
case @state[:service_name]
|
62
|
+
when "http"
|
63
|
+
@state[:port] = 80
|
64
|
+
when "https"
|
65
|
+
@state[:port] = 443
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def collect_name
|
70
|
+
return unless in_issue
|
71
|
+
return unless has_text
|
72
|
+
@state[:vuln_name] = @text
|
73
|
+
end
|
74
|
+
|
75
|
+
def collect_issue_detail
|
76
|
+
return unless in_issue
|
77
|
+
return unless has_text
|
78
|
+
@state[:issue_detail] = @text
|
79
|
+
end
|
80
|
+
|
81
|
+
def collect_references
|
82
|
+
return unless in_issue
|
83
|
+
return unless has_text
|
84
|
+
uri = @text.match('href=[\'"]?([^\'" >]+)')[1]
|
85
|
+
@state[:refs] = ["URI-#{uri}"]
|
86
|
+
end
|
87
|
+
|
88
|
+
def report_web_host_info
|
89
|
+
return unless @state[:host]
|
90
|
+
address = Rex::Socket.resolv_to_dotted(@state[:host]) rescue nil
|
91
|
+
host_info = {:workspace => @args[:wspace]}
|
92
|
+
host_info[:address] = address
|
93
|
+
host_info[:name] = @state[:host]
|
94
|
+
db_report(:host, host_info)
|
95
|
+
end
|
96
|
+
|
97
|
+
def report_web_service_info
|
98
|
+
return unless @state[:host]
|
99
|
+
return unless @state[:port]
|
100
|
+
return unless @state[:proto]
|
101
|
+
return unless @state[:service_name]
|
102
|
+
service_info = {}
|
103
|
+
service_info[:host] = @state[:host]
|
104
|
+
service_info[:port] = @state[:port]
|
105
|
+
service_info[:proto] = @state[:proto]
|
106
|
+
service_info[:name] = @state[:service_name]
|
107
|
+
@state[:service_object] = db_report(:service, service_info)
|
108
|
+
end
|
109
|
+
|
110
|
+
def report_vuln
|
111
|
+
return unless @state[:service_object]
|
112
|
+
return unless @state[:vuln_name]
|
113
|
+
return unless @state[:issue_detail]
|
114
|
+
return unless @state[:refs]
|
115
|
+
vuln_info = {}
|
116
|
+
vuln_info[:service_id] = @state[:service_object].id
|
117
|
+
vuln_info[:host] = @state[:host]
|
118
|
+
vuln_info[:name] = @state[:vuln_name]
|
119
|
+
vuln_info[:info] = @state[:issue_detail]
|
120
|
+
vuln_info[:refs] = @state[:refs]
|
121
|
+
@state[:vuln_object] = db_report(:vuln, vuln_info)
|
122
|
+
end
|
123
|
+
|
124
|
+
def in_issue
|
125
|
+
return false unless in_tag("issue")
|
126
|
+
return false unless in_tag("issues")
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
|
130
|
+
def has_text
|
131
|
+
return false unless @text
|
132
|
+
return false if @text.strip.empty?
|
133
|
+
@text = @text.strip
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -157,7 +157,7 @@ module Rex
|
|
157
157
|
host_info = {:workspace => @args[:wspace]}
|
158
158
|
host_info[:address] = @state[:web_site].service.host.address
|
159
159
|
host_info[:name] = @state[:uri].host
|
160
|
-
|
160
|
+
db_report(:host, host_info)
|
161
161
|
end
|
162
162
|
|
163
163
|
def report_web_service_info
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
##
|
3
|
+
# This module requires Metasploit: http://metasploit.com/download
|
4
|
+
# Current source: https://github.com/rapid7/metasploit-framework
|
5
|
+
##
|
6
|
+
require 'openssl/ccm'
|
7
|
+
require 'metasm'
|
8
|
+
module Rex
|
9
|
+
module Parser
|
10
|
+
###
|
11
|
+
#
|
12
|
+
# This class parses the content of a Bitlocker partition file.
|
13
|
+
# Author : Danil Bazin <danil.bazin[at]hsc.fr> @danilbaz
|
14
|
+
#
|
15
|
+
###
|
16
|
+
class BITLOCKER
|
17
|
+
BLOCK_HEADER_SIZE = 64
|
18
|
+
METADATA_HEADER_SIZE = 48
|
19
|
+
|
20
|
+
ENTRY_TYPE_NONE = 0x0000
|
21
|
+
ENTRY_TYPE_VMK = 0x0002
|
22
|
+
ENTRY_TYPE_FVEK = 0x0003
|
23
|
+
ENTRY_TYPE_STARTUP_KEY = 0x0006
|
24
|
+
ENTRY_TYPE_DESC = 0x0007
|
25
|
+
ENTRY_TYPE_HEADER = 0x000f
|
26
|
+
|
27
|
+
VALUE_TYPE_ERASED = 0x0000
|
28
|
+
VALUE_TYPE_KEY = 0x0001
|
29
|
+
VALUE_TYPE_STRING = 0x0002
|
30
|
+
VALUE_TYPE_STRETCH_KEY = 0x0003
|
31
|
+
VALUE_TYPE_ENCRYPTED_KEY = 0x0005
|
32
|
+
VALUE_TYPE_TPM = 0x0006
|
33
|
+
VALUE_TYPE_VALIDATION = 0x0007
|
34
|
+
VALUE_TYPE_VMK = 0x0008
|
35
|
+
VALUE_TYPE_EXTERNAL_KEY = 0x0009
|
36
|
+
VALUE_TYPE_UPDATE = 0x000a
|
37
|
+
VALUE_TYPE_ERROR = 0x000b
|
38
|
+
|
39
|
+
PROTECTION_TPM = 0x0100
|
40
|
+
PROTECTION_CLEAR_KEY = 0x0000
|
41
|
+
PROTECTION_STARTUP_KEY = 0x0200
|
42
|
+
PROTECTION_RECOVERY_PASSWORD = 0x0800
|
43
|
+
PROTECTION_PASSWORD = 0x2000
|
44
|
+
|
45
|
+
def initialize(file_handler)
|
46
|
+
@file_handler = file_handler
|
47
|
+
volume_header = @file_handler.read(512)
|
48
|
+
@fs_sign = volume_header[3, 8]
|
49
|
+
unless @fs_sign == '-FVE-FS-'
|
50
|
+
fail ArgumentError, 'File system signature does not match Bitlocker :
|
51
|
+
#@fs_sign}, bitlocker not used', caller
|
52
|
+
end
|
53
|
+
@fve_offset = volume_header[176, 8].unpack('Q')[0]
|
54
|
+
|
55
|
+
@file_handler.seek(@fve_offset)
|
56
|
+
@fve_raw = @file_handler.read(4096)
|
57
|
+
@encryption_methods = @fve_raw[BLOCK_HEADER_SIZE + 36, 4].unpack('V')[0]
|
58
|
+
size = @fve_raw[BLOCK_HEADER_SIZE, 4].unpack('V')[0] -
|
59
|
+
METADATA_HEADER_SIZE
|
60
|
+
@metadata_entries = @fve_raw[BLOCK_HEADER_SIZE + METADATA_HEADER_SIZE,
|
61
|
+
size]
|
62
|
+
@version = @fve_raw[BLOCK_HEADER_SIZE + 4]
|
63
|
+
@fve_metadata_entries = fve_entries(@metadata_entries)
|
64
|
+
@vmk_entries_hash = vmk_entries
|
65
|
+
end
|
66
|
+
|
67
|
+
# Extract FVEK and prefix it with the encryption methods integer on
|
68
|
+
# 2 bytes
|
69
|
+
def fvek_from_recovery_password_dislocker(recoverykey)
|
70
|
+
[@encryption_methods].pack('v') +
|
71
|
+
fvek_from_recovery_password(recoverykey)
|
72
|
+
end
|
73
|
+
|
74
|
+
# stretch recovery key with all stretch key and try to decrypt all VMK
|
75
|
+
# encrypted with a recovery key
|
76
|
+
def vmk_from_recovery_password(recoverykey)
|
77
|
+
recovery_keys_stretched = recovery_key_transformation(recoverykey)
|
78
|
+
vmk_encrypted_in_recovery_password_list = @vmk_entries_hash[
|
79
|
+
PROTECTION_RECOVERY_PASSWORD]
|
80
|
+
vmk_recovery_password = ''
|
81
|
+
vmk_encrypted_in_recovery_password_list.each do |vmk|
|
82
|
+
vmk_encrypted = vmk[ENTRY_TYPE_NONE][VALUE_TYPE_ENCRYPTED_KEY][0]
|
83
|
+
recovery_keys_stretched.each do |recovery_key|
|
84
|
+
vmk_recovery_password = decrypt_aes_ccm_key(
|
85
|
+
vmk_encrypted, recovery_key)
|
86
|
+
break if vmk_recovery_password != ''
|
87
|
+
end
|
88
|
+
break if vmk_recovery_password != ''
|
89
|
+
end
|
90
|
+
if vmk_recovery_password == ''
|
91
|
+
fail ArgumentError, 'Wrong decryption, bad recovery key?'
|
92
|
+
end
|
93
|
+
vmk_recovery_password
|
94
|
+
end
|
95
|
+
|
96
|
+
# Extract FVEK using the provided recovery key
|
97
|
+
def fvek_from_recovery_password(recoverykey)
|
98
|
+
vmk_recovery_password = vmk_from_recovery_password(recoverykey)
|
99
|
+
fvek_encrypted = fvek_entries
|
100
|
+
fvek = decrypt_aes_ccm_key(fvek_encrypted, vmk_recovery_password)
|
101
|
+
fvek
|
102
|
+
end
|
103
|
+
|
104
|
+
def decrypt_aes_ccm_key(fve_entry, key)
|
105
|
+
nonce = fve_entry[0, 12]
|
106
|
+
mac = fve_entry[12, 16]
|
107
|
+
encrypted_data = fve_entry[28..-1]
|
108
|
+
ccm = OpenSSL::CCM.new('AES', key, 16)
|
109
|
+
decrypted_data = ccm.decrypt(encrypted_data + mac, nonce)
|
110
|
+
decrypted_data[12..-1]
|
111
|
+
end
|
112
|
+
|
113
|
+
# Parse the metadata_entries and return a hashmap using the
|
114
|
+
# following format:
|
115
|
+
# {metadata_entry_type => {metadata_value_type => [fve_entry,...]}}
|
116
|
+
def fve_entries(metadata_entries)
|
117
|
+
offset_entry = 0
|
118
|
+
entry_size = metadata_entries[0, 2].unpack('v')[0]
|
119
|
+
result = Hash.new({})
|
120
|
+
while entry_size != 0
|
121
|
+
metadata_entry_type = metadata_entries[
|
122
|
+
offset_entry + 2, 2].unpack('v')[0]
|
123
|
+
metadata_value_type = metadata_entries[
|
124
|
+
offset_entry + 4, 2].unpack('v')[0]
|
125
|
+
metadata_entry = metadata_entries[offset_entry + 8, entry_size - 8]
|
126
|
+
if result[metadata_entry_type] == {}
|
127
|
+
result[metadata_entry_type] = { metadata_value_type => [
|
128
|
+
metadata_entry] }
|
129
|
+
else
|
130
|
+
if result[metadata_entry_type][metadata_value_type].nil?
|
131
|
+
result[metadata_entry_type][metadata_value_type] = [
|
132
|
+
metadata_entry]
|
133
|
+
else
|
134
|
+
result[metadata_entry_type][metadata_value_type] += [
|
135
|
+
metadata_entry]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
offset_entry += entry_size
|
139
|
+
if metadata_entries[offset_entry, 2] != ''
|
140
|
+
entry_size = metadata_entries[offset_entry, 2].unpack('v')[0]
|
141
|
+
else
|
142
|
+
entry_size = 0
|
143
|
+
end
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
# Dummy strcpy to use with metasm and string asignement
|
149
|
+
def strcpy(str_src, str_dst)
|
150
|
+
(0..(str_src.length - 1)).each do |cpt|
|
151
|
+
str_dst[cpt] = str_src[cpt].ord
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# stretch all the Recovery key and returns it
|
156
|
+
def recovery_key_transformation(recoverykey)
|
157
|
+
# recovery key stretching phase 1
|
158
|
+
recovery_intermediate = recoverykey.split('-').map(&:to_i)
|
159
|
+
recovery_intermediate.each do |n|
|
160
|
+
n % 11 != 0 && (fail ArgumentError, 'Invalid recovery key')
|
161
|
+
end
|
162
|
+
recovery_intermediate =
|
163
|
+
recovery_intermediate.map { |a| (a / 11) }.pack('v*')
|
164
|
+
|
165
|
+
# recovery key stretching phase 2
|
166
|
+
recovery_keys = []
|
167
|
+
cpu = Metasm.const_get('Ia32').new
|
168
|
+
exe = Metasm.const_get('Shellcode').new(cpu)
|
169
|
+
cp = Metasm::C::Parser.new(exe)
|
170
|
+
bitlocker_struct_src = <<-EOS
|
171
|
+
typedef struct {
|
172
|
+
unsigned char updated_hash[32];
|
173
|
+
unsigned char password_hash[32];
|
174
|
+
unsigned char salt[16];
|
175
|
+
unsigned long long int hash_count;
|
176
|
+
} bitlocker_chain_hash_t;
|
177
|
+
EOS
|
178
|
+
cp.parse bitlocker_struct_src
|
179
|
+
btl_struct = Metasm::C::AllocCStruct.new(cp, cp.find_c_struct(
|
180
|
+
'bitlocker_chain_hash_t'))
|
181
|
+
vmk_protected_by_recovery_key = @vmk_entries_hash[
|
182
|
+
PROTECTION_RECOVERY_PASSWORD]
|
183
|
+
if vmk_protected_by_recovery_key.nil?
|
184
|
+
fail ArgumentError, 'No recovery key on disk'
|
185
|
+
end
|
186
|
+
vmk_protected_by_recovery_key.each do |vmk_encrypted|
|
187
|
+
vmk_encrypted_raw = vmk_encrypted[ENTRY_TYPE_NONE][
|
188
|
+
VALUE_TYPE_STRETCH_KEY][0]
|
189
|
+
stretch_key_salt = vmk_encrypted_raw[4, 16]
|
190
|
+
strcpy(Digest::SHA256.digest(recovery_intermediate),
|
191
|
+
btl_struct.password_hash)
|
192
|
+
strcpy(stretch_key_salt, btl_struct.salt)
|
193
|
+
btl_struct.hash_count = 0
|
194
|
+
sha256 = Digest::SHA256.new
|
195
|
+
btl_struct_raw = btl_struct.str
|
196
|
+
btl_struct_hash_count_offset = btl_struct.struct.fldoffset[
|
197
|
+
'hash_count']
|
198
|
+
(1..0x100000).each do |c|
|
199
|
+
updated_hash = sha256.digest(btl_struct_raw)
|
200
|
+
btl_struct_raw = updated_hash + btl_struct_raw \
|
201
|
+
[btl_struct.updated_hash.sizeof..(
|
202
|
+
btl_struct_hash_count_offset - 1)] + [c].pack('Q')
|
203
|
+
sha256.reset
|
204
|
+
end
|
205
|
+
recovery_keys += [btl_struct_raw[btl_struct.updated_hash.stroff,
|
206
|
+
btl_struct.updated_hash.sizeof]]
|
207
|
+
end
|
208
|
+
recovery_keys
|
209
|
+
end
|
210
|
+
|
211
|
+
# Return FVEK entry, encrypted with the VMK
|
212
|
+
def fvek_entries
|
213
|
+
@fve_metadata_entries[ENTRY_TYPE_FVEK][
|
214
|
+
VALUE_TYPE_ENCRYPTED_KEY][ENTRY_TYPE_NONE]
|
215
|
+
end
|
216
|
+
|
217
|
+
# Produce a hash map using the following format:
|
218
|
+
# {PROTECTION_TYPE => [fve_entry, fve_entry...]}
|
219
|
+
def vmk_entries
|
220
|
+
res = {}
|
221
|
+
(@fve_metadata_entries[ENTRY_TYPE_VMK][VALUE_TYPE_VMK]).each do |vmk|
|
222
|
+
protection_type = vmk[26, 2].unpack('v')[0]
|
223
|
+
if res[protection_type].nil?
|
224
|
+
res[protection_type] = [fve_entries(vmk[28..-1])]
|
225
|
+
else
|
226
|
+
res[protection_type] += [fve_entries(vmk[28..-1])]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
res
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|