module Msf module Util module WindowsRegistry # # This utility class processes binary Windows registry key. It is usually # used when only offline processing is possible and [MS-RRP] BaseRegSaveKey() # is used to save a registry key to a file. # # It also includes helpers for specific registry keys (SAM, SECURITY) through # the `name` key word argument during instantiation. # class RegistryParser # Constants ROOT_KEY = 0x2c REG_NONE = 0x00 REG_SZ = 0x01 REG_EXPAND_SZ = 0x02 REG_BINARY = 0x03 REG_DWORD = 0x04 REG_MULTISZ = 0x07 REG_QWORD = 0x0b # Magic strings # REGF magic value: 'regf' REGF_MAGIC = 0x72656766 # NK magic value: 'nk' NK_MAGIC = 0x6E6B # VK magic value: 'vk' VK_MAGIC = 0x766B # LF magic value: 'lf' LF_MAGIC = 0x6C66 # LH magic value: 'lh' LH_MAGIC = 0x6C68 # RI magic value: 'ri' RI_MAGIC = 0x7269 # SK magic value: 'sk' SK_MAGIC = 0x7269 # HBIN magic value: 'hbin' HBIN_MAGIC = 0x6862696E # # [Windows NT Registry File (REGF) format specification](https://github.com/libyal/libregf/blob/main/documentation/Windows%20NT%20Registry%20File%20(REGF)%20format.asciidoc) # # Registry File Header class RegRegf < BinData::Record endian :little bit32 :magic, initial_value: REGF_MAGIC uint32 :sequence1 uint32 :sequence2 uint64 :last_change uint32 :major_version uint32 :minor_version uint32 :unknown1 uint32 :unknown2 uint32 :offset_first_record uint32 :data_size uint32 :unknown3 string :name, length: 48 string :remaining1, length: 411 uint32 :checksum, initial_value: 0xFFFFFFFF string :remaining2, length: 3585 end # Named key class RegNk < BinData::Record endian :little bit16 :magic, initial_value: NK_MAGIC uint16 :nk_type uint64 :last_change uint32 :unknown int32 :offset_parent uint32 :num_sub_keys uint32 :unknown2 int32 :offset_sub_key_lf uint32 :unknown3 uint32 :num_values int32 :offset_value_list int32 :offset_sk_rRecord int32 :offset_class_name string :unused, length: 20 uint16 :name_length, initial_value: -> { self.key_name.length } uint16 :class_name_length string :key_name, read_length: -> { self.name_length } end # Value key class RegVk < BinData::Record endian :little bit16 :magic, initial_value: VK_MAGIC uint16 :name_length, initial_value: -> { self.name.length } int32 :data_len uint32 :offset_data uint32 :value_type uint16 :flag uint16 :unused string :name, read_length: -> { self.name_length } end class RegHash < BinData::Record endian :little int32 :offset_nk string :key_name, length: 4 end class RegHash2 < BinData::Record endian :little int32 :offset_nk end # Sub keys list (LF) class RegLf < BinData::Record endian :little bit16 :magic, initial_value: LF_MAGIC uint16 :num_keys array :hash_records, type: :reg_hash, read_until: -> { index == (self.num_keys - 1) } end # Sub keys list (LH) class RegLh < BinData::Record endian :little bit16 :magic, initial_value: LH_MAGIC uint16 :num_keys array :hash_records, type: :reg_hash, read_until: -> { index == (self.num_keys - 1) } end # Sub keys list (RI) class RegRi < BinData::Record endian :little bit16 :magic, initial_value: RI_MAGIC uint16 :num_keys array :hash_records, type: :reg_hash2, read_until: -> { index == (self.num_keys - 1) } end # Security key class RegSk < BinData::Record endian :little bit16 :magic, initial_value: SK_MAGIC uint16 :unused int32 :offset_previous_sk int32 :offset_next_sk uint32 :usage_counter uint32 :size_sk, initial_length: -> { self.data.do_num_bytes } string :data, read_length: -> { self.size_sk } end # Hive bin cell class RegHbinBlock < BinData::Record attr_reader :record_type endian :little int32 :data_block_size#, byte_align: 4 choice :data, selection: -> { @obj.parent.record_type } do reg_nk 'nk' reg_vk 'vk' reg_lf 'lf' reg_lh 'lh' reg_ri 'ri' reg_sk 'sk' string :default, read_length: -> { self.data_block_size == 0 ? 0 : self.data_block_size.abs - 4 } end string :unknown, length: -> { self.data_block_size.abs - self.data.do_num_bytes - 4 } def do_read(io) io.with_readahead do io.seekbytes(4) @record_type = io.readbytes(2) end super(io) end end # Hive bin class RegHbin < BinData::Record endian :little bit32 :magic, initial_value: HBIN_MAGIC uint32 :offset_first_hbin uint32 :hbin_size string :unknown, length: 16 uint32 :offset_next_hbin # hbin_size array :reg_hbin_blocks, type: :reg_hbin_block, read_until: :eof end # @param hive_data [String] The binary registry data # @param name [Symbol] The key name to add specific helpers. Only `:sam` # and `:security` are supported at the moment. def initialize(hive_data, name: nil) @hive_data = hive_data.b @regf = RegRegf.read(@hive_data) @root_key = find_root_key case name when :sam require_relative 'sam' extend Sam when :security require_relative 'security' extend Security end end # Returns the ROOT key as a block # # @return [RegHbinBlock] The ROOT key block # @raise [StandardError] If an error occurs during parsing or if the ROOT # key is not found def find_root_key reg_hbin = nil # Split the data in 4096-bytes blocks @hive_data.unpack('a4096' * (@hive_data.size / 4096)).each do |data| next unless data[0,4] == 'hbin' reg_hbin = RegHbin.read(data) root_key = reg_hbin.reg_hbin_blocks.find do |block| block.data.respond_to?(:magic) && block.data.magic == NK_MAGIC && block.data.nk_type == ROOT_KEY end return root_key if root_key rescue IOError raise StandardError, 'Cannot parse the RegHbin structure' end raise StandardError, 'Cannot find the RootKey' unless reg_hbin end # Returns the type and the data of a given key/value pair # # @param reg_key [String] The registry key # @param reg_value [String] The value in the registry key # @return [Array] The type (Integer) and data (String) of the given # key/value as the first and second element of an array, respectively def get_value(reg_key, reg_value = nil) reg_key = find_key(reg_key) return nil unless reg_key if reg_key.data.num_values > 0 value_list = get_value_blocks(reg_key.data.offset_value_list, reg_key.data.num_values + 1) value_list.each do |value| if value.data.name == reg_value.to_s || reg_value.nil? && value.data.flag <= 0 return value.data.value_type.to_i, get_value_data(value.data) end end end nil end # Search for a given key from the ROOT key and returns it as a block # # @param key [String] The registry key to look for # @return [RegHbinBlock, nil] The key, if found, nil otherwise def find_key(key) # Let's strip '\' from the beginning, except for the case of # only asking for the root node key = key[1..-1] if key[0] == '\\' && key.size > 1 parent_key = @root_key if key.size > 0 && key[0] != '\\' key.split('\\').each do |sub_key| res = find_sub_key(parent_key, sub_key) return nil unless res parent_key = res end end parent_key end # Search for a sub key from a given base key # # @param parent_key [String] The base key # @param sub_key [String] The sub key to look for under parent_key # @return [RegHbinBlock, nil] The key, if found, nil otherwise # @raise [ArgumentError] If the parent key is not a NK record def find_sub_key(parent_key, sub_key) unless parent_key&.data&.magic == NK_MAGIC raise ArgumentError, "find_sub_key: parent key must be a NK record" end block = get_block(parent_key.data.offset_sub_key_lf) blocks = [] if block.data.magic == RI_MAGIC # ri points to lf/lh records, so we consolidate them in the main blocks array block.data.hash_records.each do |hash_record| blocks << get_block(hash_record.offset_nk) end else blocks << block end # Let's search the hash records for the name blocks.each do |block| block.data.hash_records.each do |hash_record| res = get_offset(block.data.magic, hash_record, sub_key) if res nk = get_block(res) return nk if nk.data.key_name == sub_key end end end nil end # Returns a registry block given its offset # # @param offset [String] The offset of the block # @return [RegHbinBlock] The registry block def get_block(offset) RegHbinBlock.read(@hive_data[4096+offset..-1]) end # Returns the offset of a given subkey in a hash record # # @param magic [Integer] The signtaure (MAGIC) # @param hash_rec [Integer] The hash record # @param key [Integer] The subkey to look for # @return [Integer] The offset of the subkey def get_offset(magic, hash_rec, key) case magic when LF_MAGIC if hash_rec.key_name.gsub(/(^\x00*)|(\x00*$)/, '') == key[0,4] return hash_rec.offset_nk end when LH_MAGIC if hash_rec.key_name.unpack('L<').first == get_lh_hash(key) return hash_rec.offset_nk end when RI_MAGIC # Special case here, don't know exactly why, an RI pointing to a NK offset = hash_rec.offset_nk nk = get_block(offset) return offset if nk.key_name == key else raise ArgumentError, "Unknown magic: #{magic}" end end # Returns the hash of a LH subkey # from http://www.sentinelchicken.com/data/TheWindowsNTRegistryFileFormat.pdf (Appendix C) # # @param key [Integer] The LH subkey # @return [Integer] The hash def get_lh_hash(key) res = 0 key.upcase.bytes do |byte| res *= 37 res += byte.ord end return res % 0x100000000 end # Returns a list of `count``value blocks from the offsets located at `offset` # # @param offset [Integer] The offset where the offsets of each value is located # @param count [Integer] The number of value blocks to retrieve # @return [Array] An array of registry blocks def get_value_blocks(offset, count) value_list = [] res = [] count.times do |i| value_list << @hive_data[4096+offset+i*4, 4].unpack('l<').first end value_list.each do |value_offset| if value_offset > 0 block = get_block(value_offset) res << block end end return res end # Returns the data of a VK record value # # @param record [String] The VK record # @return [String] The data # @raise [ArgumentError] If the parent key is not a VK record def get_value_data(record) unless record&.magic == VK_MAGIC raise ArgumentError, "get_value_data: record must be a VK record" end return '' if record.data_len == 0 # if DataLen < 5 the value itself is stored in the Offset field return record.offset_data.to_binary_s if record.data_len < 0 return self.get_data(record.offset_data, record.data_len + 4) end # Returns the data at a given offset from the end of the header in the raw # hive binary. # # @param offset [String] The offset from the end of the header # @param count [Integer] The size of the data. Since the 4 first bytes are # ignored, the data returned will be (count - 4) long. # @return [String] The resulting data def get_data(offset, count) @hive_data[4096+offset, count][4..-1] end # Enumerate the subkey names under `key` # # @param key [String] The parent key from which to enumerate # @return [Array] The key names # @raise [ArgumentError] If the parent key is not a NK record def enum_key(key) parent_key = find_key(key) return nil unless parent_key unless parent_key.data&.magic == NK_MAGIC raise ArgumentError, "enum_key: parent key must be a NK record" end block = get_block(parent_key.data.offset_sub_key_lf) records = [] if block.data.magic == RI_MAGIC # ri points to lf/lh records, so we consolidate the hash records in the main records array block.data.hash_records.each do |hash_record| record = get_block(hash_record.offset_nk) records.concat(record.data.hash_records) end else records.concat(block.data.hash_records) end records.map do |reg_hash| nk = get_block(reg_hash.offset_nk) nk.data.key_name.to_s.b end end # Enumerate the subkey values under `key` # # @param key [String] The parent key from which to enumerate # @return [Array] The key values # @raise [ArgumentError] If the parent key is not a NK record def enum_values(key) key_obj = find_key(key) return nil unless key_obj unless key_obj&.data&.magic == NK_MAGIC raise ArgumentError, "enum_values: key must be a NK record" end res = [] value_list = get_value_blocks(key_obj.data.offset_value_list, key_obj.data.num_values + 1) value_list.each do |value| res << (value.data.flag > 0 ? value.data.name : nil) end res end end end end end