verify_ember_movie_native.py 5.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """Offline ABI verification against a mapped/decrypted image, never an on-disk encrypted EXE.
  2. Usage: python3 tests/verify_ember_movie_native.py path/to/game_image.bin [packages-directory]
  3. """
  4. import re
  5. import struct
  6. import sys
  7. from pathlib import Path
  8. repo = Path(__file__).resolve().parents[1]
  9. data = Path(sys.argv[1]).read_bytes()
  10. def signature(file, name):
  11. source = (repo / file).read_text()
  12. pattern = re.search(r'constexpr auto ' + name + r'\s*=\s*signature<signature_length\("([^"]+)"\)', source)[1]
  13. regex = b''.join(b'.' if token == '?' else re.escape(bytes([int(token, 16)])) for token in pattern.split())
  14. matches = [m.start() for m in re.finditer(regex, data, re.S)]
  15. assert len(matches) == 1, (name, matches)
  16. return matches[0]
  17. def target(base, offset, expected):
  18. assert data[base + offset] == 0xE8
  19. value = base + offset + 5 + struct.unpack_from('<i', data, base + offset + 1)[0]
  20. assert value == expected, (hex(base), hex(offset), hex(value), hex(expected))
  21. path = 'Sunrise/src/client/hooks/ember_movies/resources.cpp'
  22. load = signature(path, 'loadSig')
  23. end = signature(path, 'endSig')
  24. assert load == 0xB46E10 and end == 0xB44020
  25. for offset, expected in [(0x96, 0x4294D0), (0xD1, 0x423EF0), (0x14C, 0x4312D0), (0x157, 0x435AA0)]:
  26. target(load, offset, expected)
  27. for offset, expected in [(0x85, 0x42C650), (0x9F, 0x425310)]:
  28. target(end, offset, expected)
  29. assert data[end + 0x2E:end + 0x31] == bytes.fromhex('48 8B 05')
  30. assert end + 0x35 + struct.unpack_from('<i', data, end + 0x31)[0] == 0x2439C70
  31. # Native tag classifier, including the semantic distinction missed by the old test:
  32. # ordinary tag -> kind 1; shared type-16 tag (type_info & F000 == 2000) -> kind 2.
  33. assert data[0x42694F:0x42696F] == bytes.fromhex(
  34. '8b 45 04 8b cb 48 89 7c 24 30 25 00 f0 00 00 33 ff 3d 00 20 00 00 40 0f 94 c7 45 33 c0 8d 57 01')
  35. target(0x426920, 0x4F, 0x433050)
  36. # Kind 2 is routed to root+10; ordinary metadata belongs in root+20.
  37. assert data[0x4313CD:0x4313E2] == bytes.fromhex(
  38. '83 3f 02 b9 10 00 00 00 8b 57 04 41 b8 20 00 00 00 44 0f 44 c1')
  39. # For stream type_info & 30000 == 10000, the load job maps offset|patch and
  40. # size|C0000000 directly. It bypasses the ordinary allocation/read branch.
  41. assert data[0x3592C6:0x3592EB] == bytes.fromhex(
  42. '8b c3 c1 e8 10 83 e0 03 83 f8 01 75 2f 41 0f b7 4d 20 41 81 cf 00 00 00 c0 8b 55 50 45 8b c7 48 0b d1 8b 4d 48')
  43. target(0x3591B0, 0x13B, 0x351D00)
  44. target(0x41A160, 0x16, 0x3597C0) # native video I/O opens this mapped package/patch
  45. target(0x41A160, 0x2C, 0x357DA0) # then obtains offset and byte length
  46. movie = 'Sunrise/src/client/hooks/ember_movies/ember_movies.cpp'
  47. start, stop, busy = (signature(movie, name) for name in ('startSig', 'stopSig', 'busySig'))
  48. for offset, expected in [(0x72, 0x41B040), (0x7A, 0x41A3C0), (0x8E, 0x41CD20)]:
  49. target(start, offset, expected)
  50. for offset, expected in [(0x18, 0x41D0C0), (0x25, 0x41A980)]:
  51. target(stop, offset, expected)
  52. assert busy == 0x41B420
  53. target(busy, 0x48, 0x41AB70)
  54. if len(sys.argv) > 2:
  55. # Read only container metadata, without unpacking data or starting the game.
  56. latest = {}
  57. for path in Path(sys.argv[2]).glob('*.pkg'):
  58. with path.open('rb') as stream:
  59. header = stream.read(0x170)
  60. package = struct.unpack_from('<H', header, 4)[0]
  61. version = (struct.unpack_from('<Q', header, 0x10)[0],
  62. struct.unpack_from('<I', header, 0x1C)[0],
  63. struct.unpack_from('<H', header, 0x20)[0])
  64. if package not in latest or version > latest[package][0]:
  65. latest[package] = version, path, header
  66. for tag, expected in [(0x80BCA001, 0x80808495), (0x80BCA003, 0x80808495),
  67. (0x80BCA000, 0x80808499), (0x80BCA002, 0x80808499),
  68. (0x80B9EB33, 0x80809A88), (0x80B9EB34, 0x80809A88),
  69. (0x80BCA032, 0x80806B8F),
  70. (0x80BCA034, 0xFFFFFFFF), (0x80C7C000, 0xFFFFFFFF)]:
  71. # Tag package IDs include the bank: 80BCAxxx belongs to package 01E5.
  72. package = (tag >> 13) & 0x3FF
  73. _, path, header = latest[package]
  74. table = (struct.unpack_from('<I', header, 0x110)[0] + 0x60 if header[0x1A] == 1
  75. else struct.unpack_from('<I', header, 0xB8)[0])
  76. with path.open('rb') as stream:
  77. stream.seek(table + (tag & 0x1FFF) * 16)
  78. reference, type_info, _ = struct.unpack('<IIQ', stream.read(16))
  79. assert reference == expected, (hex(tag), hex(reference), path)
  80. assert type_info & 0xF000 != 0x2000, (hex(tag), hex(type_info))
  81. if expected == 0xFFFFFFFF:
  82. assert (type_info & 0x30000) == 0x10000 and (type_info >> 6) & 0x3F == 24
  83. print('Installed movie metadata, compact video streams and kind-1 package types verified.')
  84. attach = signature('Sunrise/src/client/hooks/bootflow/ember_sunburn.cpp', 'sig')
  85. assert attach == 0x9F2760
  86. # Native attach dereferences the runtime relative template, then passes it to the child factory.
  87. target(attach, 0x68, 0x32BBD0)
  88. target(attach, 0x78, 0x56DE00)
  89. assert data[0x4AE000:0x4AE002] == bytes.fromhex('8B 09') # factory reads resource at request+0
  90. print('Native resource kind selection, request lifecycle, movie playback and sunburn attachment ABI verified.')