Advertisement
Guest User

Untitled

a guest
Aug 25th, 2019
183
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.90 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import argparse
  4. import functools
  5. import hmac
  6. import json
  7. import plistlib
  8. import subprocess
  9.  
  10.  
  11. memoize_forever = functools.lru_cache(maxsize=None)
  12.  
  13.  
  14. # Analogous to GetDeterministicMachineSpecificId from device_id_mac.cc,
  15. # except we shell out to `ioreg(1)` instead of using the C API
  16. @memoize_forever
  17. def get_machine_id():
  18. xml = subprocess.check_output('ioreg -c IOPlatformExpertDevice -d 1 -r -a'.split())
  19. data = plistlib.loads(xml)
  20. return data[0]['IOPlatformUUID']
  21.  
  22.  
  23. # Chromium uses the empty string for a seed. Chrome proper uses a blob stored
  24. # in the resource bundle with key IDR_PREF_HASH_SEED_BIN which is loaded from
  25. # the non-public file resources\settings_internal\pref_hash_seed.bin. This is
  26. # quite stupid, because the seed can trivially be extracted back out of the
  27. # resource bundle. Note that, as of 2019-08, there is no support for rolling
  28. # the seed, and this value appears to have been used consistently across
  29. # various versions and platforms.
  30. SEED = bytes.fromhex(
  31. 'e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26'
  32. 'b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8'
  33. )
  34.  
  35.  
  36. # port of GetDigestString from pref_hash_calculator.cc
  37. def get_digest_string(key: bytes, message: bytes) -> str:
  38. dgst = hmac.new(key=key, msg=message, digestmod='sha256')
  39. return dgst.hexdigest().upper()
  40.  
  41.  
  42. # loose port of CopyWithoutEmptyChildren and friends from values.cc
  43. def copy_without_empty_children(value):
  44. if isinstance(value, list):
  45. copy = []
  46. for child in value:
  47. child_copy = copy_without_empty_children(child)
  48. if child_copy is not None:
  49. copy.append(child_copy)
  50. return copy if copy else None
  51. elif isinstance(value, dict):
  52. copy = {}
  53. for k, v in value.items():
  54. v_copy = copy_without_empty_children(v)
  55. if v_copy is not None:
  56. copy[k] = v_copy
  57. return copy if copy else None
  58. elif isinstance(value, (type(None), bool, int, float, str, bytes)):
  59. return value
  60. else:
  61. raise TypeError("Can't copy_without_empty_children odd value of type %r" % (type(value),))
  62.  
  63.  
  64. # port of JSONWriter (yes, Chrome rolled their own).
  65. # Also JSONStringValueSerializer, which is a thin wrapper around it
  66. def chrome_json_ser(value) -> bytes:
  67. if value is None:
  68. return 'null'.encode()
  69. elif isinstance(value, bool):
  70. return (('true' if value else 'false').encode())
  71. elif isinstance(value, int):
  72. return str(value).encode()
  73. elif isinstance(value, float):
  74. real = str(value) # XXX assumes python is same as chrome's NumberToString
  75. if '.' not in real and 'e' not in real and 'E' not in real:
  76. real = real + '.0'
  77. elif real[0] == '.':
  78. real = '0' + real
  79. elif real[0] == '-' and real[0] == '.':
  80. real = '-0' + real[1:]
  81. return real.encode()
  82. elif isinstance(value, str):
  83. # XXX doesn't handle cases where str has illegal unicode
  84. out = b'"'
  85. for cp in value:
  86. special = {
  87. '\b': '\\b',
  88. '\f': '\\f',
  89. '\n': '\\n',
  90. '\r': '\\r',
  91. '\t': '\\t',
  92. '\\': '\\\\',
  93. '"': '\\"',
  94. '<': '\\u003C',
  95. '\u2028': '\\u2028',
  96. '\u2029': '\\u2029',
  97. }.get(cp, None)
  98. if special is not None:
  99. out += special.encode()
  100. elif ord(cp) < 32:
  101. out += ('\\u00%02x' % (ord(cp),)).encode()
  102. else:
  103. out += cp.encode()
  104. out += b'"'
  105. return out
  106. elif isinstance(value, list):
  107. out = b'['
  108. first = True
  109. for v in value:
  110. if not first:
  111. out += b','
  112. out += chrome_json_ser(v)
  113. first = False
  114. out += b']'
  115. return out
  116. elif isinstance(value, dict):
  117. out = b'{'
  118. first = True
  119. for k, v in value.items():
  120. if not first:
  121. out += b','
  122. out += chrome_json_ser(k)
  123. out += b':'
  124. out += chrome_json_ser(v)
  125. first = False
  126. out += b'}'
  127. return out
  128. elif isinstance(value, bytes):
  129. raise TypeError("Chrome's JSON library doesn't support bytes")
  130. else:
  131. raise TypeError("Can't chrome_json_ser odd value of type %r" % (type(value),))
  132.  
  133.  
  134. # port of ValueAsString pref_hash_calculator.cc
  135. def value_as_string(value) -> bytes:
  136. if isinstance(value, dict):
  137. value = copy_without_empty_children(value) or {}
  138. return chrome_json_ser(value)
  139.  
  140.  
  141. # port of GetMessage from pref_hash_calculator.cc
  142. def get_message(device_id: str, path: str, value_as_bytes: bytes) -> bytes:
  143. return device_id.encode() + path.encode() + value_as_bytes
  144.  
  145.  
  146. # port of PrefHashCalculator::Calculate from pref_hash_calculator.cc
  147. def calculate(path: str, value) -> str:
  148. return get_digest_string(
  149. key=SEED,
  150. message=get_message(
  151. device_id=get_machine_id(),
  152. path=path,
  153. value_as_bytes=value_as_string(value)))
  154.  
  155.  
  156. # Traverse the in-memory representation of a 'Secure Preferences' file and
  157. # replace all the MACs with new ones computed for the current computer.
  158. def recompute_protection(secure_prefs_data):
  159. # Chrome's prefs system appears to have coded-in handling for whether
  160. # certain keys are 'ATOMIC' or 'SPLIT'. Duplicating the list here seems
  161. # clowny, so I don't see a great way of generating the MACs based solely on
  162. # the corresponding values. However, traversing the already-existing MACs
  163. # and replacing them seems viable; if we assume that Chrome is the only
  164. # thing which added keys, then it should have written MACs out in the right
  165. # way when it added them. In terms of the implementation, I'm guessing a
  166. # bit, but this seems to handle most (if not all) cases and there's a limit
  167. # to how much time I want to spend reading Chrome's prefs code.
  168.  
  169. def recompute_mac_subtree(parent_keys, mac_subtree, prefs_subtree):
  170. for k in sorted(mac_subtree.keys()):
  171. if k not in prefs_subtree:
  172. # AFAICT Chrome just leaves macs lying around even when the
  173. # pref is gone. I think the right thing to do is skip them
  174. # here-- we have no value to recompute a new mac and deleting
  175. # them seems unnecessarily risky
  176. continue
  177. if isinstance(mac_subtree[k], dict):
  178. recompute_mac_subtree(parent_keys + [k], mac_subtree[k], prefs_subtree[k])
  179. elif isinstance(mac_subtree[k], str):
  180. mac_subtree[k] = calculate('.'.join(parent_keys + [k]), prefs_subtree[k])
  181. else:
  182. raise TypeError('Weird thing found in protection.macs dict')
  183.  
  184. if 'protection' in secure_prefs_data:
  185. protection = secure_prefs_data['protection']
  186. if 'macs' in protection:
  187. recompute_mac_subtree([], protection['macs'], secure_prefs_data)
  188. if 'super_mac' in protection:
  189. protection['super_mac'] = calculate('', protection['macs'])
  190.  
  191.  
  192. def resign(inpath, outpath):
  193. with open(inpath, 'r', encoding='utf8') as fp:
  194. data = json.load(fp)
  195. recompute_protection(data)
  196. with open(outpath, 'w', encoding='utf8') as fp:
  197. json.dump(data, fp)
  198.  
  199.  
  200. def main():
  201. parser = argparse.ArgumentParser(
  202. description="Resign Chrome's 'Secure Preferences' for the current machine",
  203. )
  204. parser.add_argument(
  205. '--in',
  206. metavar='FILE_PATH',
  207. help='path to old Secure Preferences file',
  208. required=True,
  209. )
  210. parser.add_argument(
  211. '--out',
  212. metavar='FILE_PATH',
  213. help='path to write out new Secure Preferences file',
  214. required=True
  215. )
  216. args = parser.parse_args()
  217. resign(vars(args)['in'], args.out)
  218.  
  219.  
  220. if __name__ == '__main__':
  221. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement