1package Parse::Win32Registry::WinNT::Key;
2
3use strict;
4use warnings;
5
6use base qw(Parse::Win32Registry::Key);
7
8use Carp;
9use Encode;
10use Parse::Win32Registry::Base qw(:all);
11use Parse::Win32Registry::WinNT::Value;
12use Parse::Win32Registry::WinNT::Security;
13
14use constant NK_HEADER_LENGTH => 0x50;
15use constant OFFSET_TO_FIRST_HBIN => 0x1000;
16
17sub new {
18    my $class = shift;
19    my $regfile = shift;
20    my $offset = shift; # offset to nk record relative to start of file
21    my $parent_key_path = shift; # parent key path (optional)
22
23    croak 'Missing registry file' if !defined $regfile;
24    croak 'Missing offset' if !defined $offset;
25
26    my $fh = $regfile->get_filehandle;
27
28    # 0x00 dword = key length (negative = allocated)
29    # 0x04 word  = 'nk' signature
30    # 0x06 word  = flags
31    # 0x08 qword = timestamp
32    # 0x10
33    # 0x14 dword = offset to parent
34    # 0x18 dword = number of subkeys
35    # 0x1c
36    # 0x20 dword = offset to subkey list (lf, lh, ri, li)
37    # 0x24
38    # 0x28 dword = number of values
39    # 0x2c dword = offset to value list
40    # 0x30 dword = offset to security
41    # 0x34 dword = offset to class name
42    # 0x38 dword = max subkey name length
43    # 0x3c dword = max class name length
44    # 0x40 dword = max value name length
45    # 0x44 dword = max value data length
46    # 0x48
47    # 0x4c word  = key name length
48    # 0x4e word  = class name length
49    # 0x50       = key name [for key name length bytes]
50
51    # Extracted offsets are always relative to first hbin
52
53    sysseek($fh, $offset, 0);
54    my $bytes_read = sysread($fh, my $nk_header, NK_HEADER_LENGTH);
55    if ($bytes_read != NK_HEADER_LENGTH) {
56        warnf('Could not read key at 0x%x', $offset);
57        return;
58    }
59
60    my ($length,
61        $sig,
62        $flags,
63        $timestamp,
64        $offset_to_parent,
65        $num_subkeys,
66        $offset_to_subkey_list,
67        $num_values,
68        $offset_to_value_list,
69        $offset_to_security,
70        $offset_to_class_name,
71        $name_length,
72        $class_name_length,
73        ) = unpack('Va2va8x4VVx4Vx4VVVVx20vv', $nk_header);
74
75    $offset_to_parent += OFFSET_TO_FIRST_HBIN
76        if $offset_to_parent != 0xffffffff;
77    $offset_to_subkey_list += OFFSET_TO_FIRST_HBIN
78        if $offset_to_subkey_list != 0xffffffff;
79    $offset_to_value_list += OFFSET_TO_FIRST_HBIN
80        if $offset_to_value_list != 0xffffffff;
81    $offset_to_security += OFFSET_TO_FIRST_HBIN
82        if $offset_to_security != 0xffffffff;
83    $offset_to_class_name += OFFSET_TO_FIRST_HBIN
84        if $offset_to_class_name != 0xffffffff;
85
86    my $allocated = 0;
87    if ($length > 0x7fffffff) {
88        $allocated = 1;
89        $length = (0xffffffff - $length) + 1;
90    }
91    # allocated should be true
92
93    if ($length < NK_HEADER_LENGTH) {
94        warnf('Invalid value entry length at 0x%x', $offset);
95        return;
96    }
97
98    if ($sig ne 'nk') {
99        warnf('Invalid signature for key at 0x%x', $offset);
100        return;
101    }
102
103    $bytes_read = sysread($fh, my $name, $name_length);
104    if ($bytes_read != $name_length) {
105        warnf('Could not read name for key at 0x%x', $offset);
106        return;
107    }
108
109    if ($flags & 0x20) {
110        $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name);
111    }
112    else {
113        $name = decode('UCS-2LE', $name);
114    }
115
116    my $key_path = (defined $parent_key_path)
117                 ? "$parent_key_path\\$name"
118                 : "$name";
119
120    my $class_name;
121    if ($offset_to_class_name != 0xffffffff) {
122        sysseek($fh, $offset_to_class_name + 4, 0);
123        $bytes_read = sysread($fh, $class_name, $class_name_length);
124        if ($bytes_read != $class_name_length) {
125            warnf('Could not read class name at 0x%x', $offset_to_class_name);
126            $class_name = undef;
127        }
128        else {
129            $class_name = decode('UCS-2LE', $class_name);
130        }
131    }
132
133    my $self = {};
134    $self->{_regfile} = $regfile;
135    $self->{_offset} = $offset;
136    $self->{_length} = $length;
137    $self->{_allocated} = $allocated;
138    $self->{_tag} = $sig;
139    $self->{_name} = $name;
140    $self->{_name_length} = $name_length;
141    $self->{_key_path} = $key_path;
142    $self->{_flags} = $flags;
143    $self->{_offset_to_parent} = $offset_to_parent;
144    $self->{_num_subkeys} = $num_subkeys;
145    $self->{_offset_to_subkey_list} = $offset_to_subkey_list;
146    $self->{_num_values} = $num_values;
147    $self->{_offset_to_value_list} = $offset_to_value_list;
148    $self->{_timestamp} = unpack_windows_time($timestamp);
149    $self->{_offset_to_security} = $offset_to_security;
150    $self->{_offset_to_class_name} = $offset_to_class_name;
151    $self->{_class_name_length} = $class_name_length;
152    $self->{_class_name} = $class_name;
153    bless $self, $class;
154
155    return $self;
156}
157
158sub get_timestamp {
159    my $self = shift;
160
161    return $self->{_timestamp};
162}
163
164sub get_timestamp_as_string {
165    my $self = shift;
166
167    return iso8601($self->get_timestamp);
168}
169
170sub get_class_name {
171    my $self = shift;
172
173    return $self->{_class_name};
174}
175
176sub is_root {
177    my $self = shift;
178
179    my $flags = $self->{_flags};
180    return $flags & 4 || $flags & 8;
181}
182
183sub get_parent {
184    my $self = shift;
185
186    my $regfile = $self->{_regfile};
187    my $offset_to_parent = $self->{_offset_to_parent};
188    my $key_path = $self->{_key_path};
189
190    return if $self->is_root;
191
192    my $grandparent_key_path;
193    my @keys = split /\\/, $key_path, -1;
194    if (@keys > 2) {
195        $grandparent_key_path = join('\\', @keys[0..$#keys-2]);
196    }
197
198    return Parse::Win32Registry::WinNT::Key->new($regfile,
199                                                 $offset_to_parent,
200                                                 $grandparent_key_path);
201}
202
203sub get_security {
204    my $self = shift;
205
206    my $regfile = $self->{_regfile};
207    my $offset_to_security = $self->{_offset_to_security};
208    my $key_path = $self->{_key_path};
209
210    if ($offset_to_security == 0xffffffff) {
211        return;
212    }
213
214    return Parse::Win32Registry::WinNT::Security->new($regfile,
215                                                      $offset_to_security,
216                                                      $key_path);
217}
218
219sub as_string {
220    my $self = shift;
221
222    my $string = $self->get_path . ' [' . $self->get_timestamp_as_string . ']';
223    return $string;
224}
225
226sub parse_info {
227    my $self = shift;
228
229    my $info = sprintf '0x%x nk len=0x%x alloc=%d "%s" par=0x%x keys=%d,0x%x vals=%d,0x%x sec=0x%x class=0x%x',
230        $self->{_offset},
231        $self->{_length},
232        $self->{_allocated},
233        $self->{_name},
234        $self->{_offset_to_parent},
235        $self->{_num_subkeys}, $self->{_offset_to_subkey_list},
236        $self->{_num_values}, $self->{_offset_to_value_list},
237        $self->{_offset_to_security},
238        $self->{_offset_to_class_name};
239    if (defined $self->{_class_name}) {
240        $info .= sprintf ',len=0x%x', $self->{_class_name_length};
241    }
242    return $info;
243}
244
245sub _get_offsets_to_subkeys {
246    my $self = shift;
247
248    # Offset is passed as a parameter for recursive lists such as 'ri'
249    my $offset_to_subkey_list = shift || $self->{_offset_to_subkey_list};
250
251    my $regfile = $self->{_regfile};
252    my $fh = $regfile->get_filehandle;
253
254    return if $offset_to_subkey_list == 0xffffffff
255           || $self->{_num_subkeys} == 0;
256
257    sysseek($fh, $offset_to_subkey_list, 0);
258    my $bytes_read = sysread($fh, my $subkey_list_header, 8);
259    if ($bytes_read != 8) {
260        warnf('Could not read subkey list header at 0x%x',
261            $offset_to_subkey_list);
262        return;
263    }
264
265    # 0x00 dword = subkey list length (negative = allocated)
266    # 0x04 word  = 'lf' signature
267    # 0x06 word  = number of entries
268    # 0x08 dword = offset to 1st subkey
269    # 0x0c dword = first four characters of the key name
270    # 0x10 dword = offset to 2nd subkey
271    # 0x14 dword = first four characters of the key name
272    # ...
273
274    # 0x00 dword = subkey list length (negative = allocated)
275    # 0x04 word  = 'lh' signature
276    # 0x06 word  = number of entries
277    # 0x08 dword = offset to 1st subkey
278    # 0x0c dword = hash of the key name
279    # 0x10 dword = offset to 2nd subkey
280    # 0x14 dword = hash of the key name
281    # ...
282
283    # 0x00 dword = subkey list length (negative = allocated)
284    # 0x04 word  = 'ri' signature
285    # 0x06 word  = number of entries in ri list
286    # 0x08 dword = offset to 1st lf/lh/li list
287    # 0x0c dword = offset to 2nd lf/lh/li list
288    # 0x10 dword = offset to 3rd lf/lh/li list
289    # ...
290
291    # 0x00 dword = subkey list length (negative = allocated)
292    # 0x04 word  = 'li' signature
293    # 0x06 word  = number of entries in li list
294    # 0x08 dword = offset to 1st subkey
295    # 0x0c dword = offset to 2nd subkey
296    # ...
297
298    # Extracted offsets are always relative to first hbin
299
300    my @offsets_to_subkeys = ();
301
302    my ($length,
303        $sig,
304        $num_entries,
305        ) = unpack('Va2v', $subkey_list_header);
306
307    my $subkey_list_length;
308    if ($sig eq 'lf' || $sig eq 'lh') {
309        $subkey_list_length = 2 * 4 * $num_entries;
310    }
311    elsif ($sig eq 'ri' || $sig eq 'li') {
312        $subkey_list_length = 4 * $num_entries;
313    }
314    else {
315        warnf('Invalid signature for subkey list at 0x%x',
316            $offset_to_subkey_list);
317        return;
318    }
319
320    $bytes_read = sysread($fh, my $subkey_list, $subkey_list_length);
321    if ($bytes_read != $subkey_list_length) {
322        warnf('Could not read subkey list at 0x%x',
323            $offset_to_subkey_list);
324        return;
325    }
326
327    if ($sig eq 'lf') {
328        foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) {
329            push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset;
330        }
331    }
332    elsif ($sig eq 'lh') {
333        foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) {
334            push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset;
335        }
336    }
337    elsif ($sig eq 'ri') {
338        foreach my $offset (unpack("V$num_entries", $subkey_list)) {
339            my $offsets_ref =
340                $self->_get_offsets_to_subkeys(OFFSET_TO_FIRST_HBIN + $offset);
341            if (defined $offsets_ref && ref $offsets_ref eq 'ARRAY') {
342                push @offsets_to_subkeys, @{ $offsets_ref };
343            }
344        }
345    }
346    elsif ($sig eq 'li') {
347        foreach my $offset (unpack("V$num_entries", $subkey_list)) {
348            push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset;
349        }
350    }
351
352    return \@offsets_to_subkeys;
353}
354
355sub get_subkey_iterator {
356    my $self = shift;
357
358    my $regfile = $self->{_regfile};
359    my $key_path = $self->{_key_path};
360
361    my @offsets_to_subkeys = ();
362    if ($self->{_num_subkeys} > 0) {
363        my $offsets_to_subkeys_ref = $self->_get_offsets_to_subkeys;
364        if (defined $offsets_to_subkeys_ref) {
365            @offsets_to_subkeys = @{$self->_get_offsets_to_subkeys};
366        }
367    }
368
369    return Parse::Win32Registry::Iterator->new(sub {
370        while (defined(my $offset_to_subkey = shift @offsets_to_subkeys)) {
371            my $subkey = Parse::Win32Registry::WinNT::Key->new($regfile,
372                                            $offset_to_subkey, $key_path);
373            if (defined $subkey) {
374                return $subkey;
375            }
376        }
377        return; # no more offsets to subkeys
378    });
379}
380
381sub _get_offsets_to_values {
382    my $self = shift;
383
384    my $regfile = $self->{_regfile};
385    my $fh = $regfile->get_filehandle;
386    my $offset_to_value_list = $self->{_offset_to_value_list};
387
388    my $num_values = $self->{_num_values};
389    return if $num_values == 0;
390    # Actually, this could probably just fall through
391    # as unpack("x4V0", ...) would return an empty array.
392
393    my @offsets_to_values = ();
394
395    # 0x00 dword = value list length (negative = allocated)
396    # 0x04 dword = 1st offset
397    # 0x08 dword = 2nd offset
398    # ...
399
400    # Extracted offsets are always relative to first hbin
401
402    sysseek($fh, $offset_to_value_list, 0);
403    my $value_list_length = 0x4 + $num_values * 4;
404    my $bytes_read = sysread($fh, my $value_list, $value_list_length);
405    if ($bytes_read != $value_list_length) {
406        warnf("Could not read value list at 0x%x",
407            $offset_to_value_list);
408        return;
409    }
410
411    foreach my $offset (unpack("x4V$num_values", $value_list)) {
412        push @offsets_to_values, OFFSET_TO_FIRST_HBIN + $offset;
413    }
414
415    return \@offsets_to_values;
416}
417
418sub get_value_iterator {
419    my $self = shift;
420
421    my $regfile = $self->{_regfile};
422    my $key_path = $self->{_key_path};
423
424    my @offsets_to_values = ();
425    if ($self->{_num_values} > 0) {
426        my $offsets_to_values_ref = $self->_get_offsets_to_values;
427        if (defined $offsets_to_values_ref) {
428            @offsets_to_values = @{$self->_get_offsets_to_values};
429        }
430    }
431
432    return Parse::Win32Registry::Iterator->new(sub {
433        while (defined(my $offset_to_value = shift @offsets_to_values)) {
434            my $value = Parse::Win32Registry::WinNT::Value->new($regfile,
435                                                        $offset_to_value);
436            if (defined $value) {
437                return $value;
438            }
439        }
440        return; # no more offsets to values
441    });
442}
443
4441;
445