decode_url.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import binascii
  2. import re
  3. import struct
  4. from typing import Any
  5. HEX_PAYLOAD_PATTERN = r"csgo_econ_action_preview(?:%20| )([0-9A-Fa-f]+)$"
  6. MIN_DECODE_SCORE = 5
  7. STICKER_VARINT_FIELDS = {
  8. 1: "slot",
  9. 2: "sticker_id",
  10. 6: "tint_id",
  11. 10: "pattern",
  12. 11: "highlight_reel",
  13. 12: "wrapped_sticker",
  14. }
  15. STICKER_FIXED32_FIELDS = {
  16. 3: "wear",
  17. 4: "scale",
  18. 5: "rotation",
  19. 7: "offset_x",
  20. 8: "offset_y",
  21. 9: "offset_z",
  22. }
  23. ITEM_VARINT_FIELDS = {
  24. 1: "accountid",
  25. 2: "itemid",
  26. 3: "defindex",
  27. 4: "paintindex",
  28. 5: "rarity",
  29. 6: "quality",
  30. 8: "paintseed",
  31. 9: "killeaterscoretype",
  32. 10: "killeatervalue",
  33. 13: "inventory",
  34. 14: "origin",
  35. 15: "questid",
  36. 16: "dropreason",
  37. 17: "musicindex",
  38. 19: "petindex",
  39. 21: "style",
  40. 23: "upgrade_level",
  41. }
  42. ITEM_STICKER_LIST_FIELDS = {
  43. 12: "stickers",
  44. 20: "keychains",
  45. 22: "variations",
  46. }
  47. SCORED_FIELDS = (
  48. "itemid",
  49. "accountid",
  50. "paintindex",
  51. "paintseed",
  52. "quality",
  53. "rarity",
  54. "inventory",
  55. "origin",
  56. "paintwear",
  57. )
  58. def extract_hex_payload(value: str) -> str:
  59. match = re.search(HEX_PAYLOAD_PATTERN, value)
  60. if match:
  61. return match.group(1).upper()
  62. cleaned = value.strip()
  63. if re.fullmatch(r"[0-9A-Fa-f]+", cleaned):
  64. return cleaned.upper()
  65. raise ValueError("Input does not contain a masked inspect payload")
  66. def xor_mask(data: bytes, key: int) -> bytes:
  67. return bytes(byte ^ key for byte in data)
  68. def bytes_to_float_bits(value: int) -> float:
  69. return struct.unpack(">f", struct.pack(">I", value & 0xFFFFFFFF))[0]
  70. def zigzag_decode(value: int) -> int:
  71. return (value >> 1) ^ -(value & 1)
  72. def read_varint(buffer: bytes, offset: int) -> tuple[int, int]:
  73. result = 0
  74. shift = 0
  75. while True:
  76. if offset >= len(buffer):
  77. raise ValueError("Unexpected end of buffer while reading varint")
  78. byte = buffer[offset]
  79. offset += 1
  80. result |= (byte & 0x7F) << shift
  81. if not (byte & 0x80):
  82. return result, offset
  83. shift += 7
  84. if shift > 70:
  85. raise ValueError("Varint is too long")
  86. def read_fixed32(buffer: bytes, offset: int) -> tuple[int, int]:
  87. if offset + 4 > len(buffer):
  88. raise ValueError("Unexpected end of buffer while reading fixed32")
  89. return struct.unpack("<I", buffer[offset:offset + 4])[0], offset + 4
  90. def fixed32_to_float(value: int) -> float:
  91. return struct.unpack("<f", struct.pack("<I", value & 0xFFFFFFFF))[0]
  92. def read_length_delimited(buffer: bytes, offset: int) -> tuple[bytes, int]:
  93. length, offset = read_varint(buffer, offset)
  94. end = offset + length
  95. if end > len(buffer):
  96. raise ValueError("Length-delimited field exceeds buffer")
  97. return buffer[offset:end], end
  98. def skip_field(buffer: bytes, offset: int, wire_type: int) -> int:
  99. if wire_type == 0:
  100. _, offset = read_varint(buffer, offset)
  101. return offset
  102. if wire_type == 1:
  103. end = offset + 8
  104. if end > len(buffer):
  105. raise ValueError("Unexpected end of buffer while skipping fixed64")
  106. return end
  107. if wire_type == 2:
  108. _, offset = read_length_delimited(buffer, offset)
  109. return offset
  110. if wire_type == 5:
  111. end = offset + 4
  112. if end > len(buffer):
  113. raise ValueError("Unexpected end of buffer while skipping fixed32")
  114. return end
  115. raise ValueError(f"Unsupported wire type {wire_type}")
  116. def parse_sticker(buffer: bytes) -> dict[str, Any]:
  117. sticker: dict[str, Any] = {}
  118. offset = 0
  119. while offset < len(buffer):
  120. tag, offset = read_varint(buffer, offset)
  121. field_number = tag >> 3
  122. wire_type = tag & 0x7
  123. field_name = STICKER_VARINT_FIELDS.get(field_number)
  124. if field_name is not None:
  125. value, offset = read_varint(buffer, offset)
  126. sticker[field_name] = value
  127. continue
  128. field_name = STICKER_FIXED32_FIELDS.get(field_number)
  129. if field_name is not None:
  130. raw_value, offset = read_fixed32(buffer, offset)
  131. sticker[field_name] = fixed32_to_float(raw_value)
  132. continue
  133. offset = skip_field(buffer, offset, wire_type)
  134. return sticker
  135. def parse_econ_item(buffer: bytes) -> dict[str, Any]:
  136. item: dict[str, Any] = {
  137. "stickers": [],
  138. "keychains": [],
  139. "variations": [],
  140. }
  141. offset = 0
  142. while offset < len(buffer):
  143. tag, offset = read_varint(buffer, offset)
  144. field_number = tag >> 3
  145. wire_type = tag & 0x7
  146. field_name = ITEM_VARINT_FIELDS.get(field_number)
  147. if field_name is not None:
  148. value, offset = read_varint(buffer, offset)
  149. item[field_name] = value
  150. continue
  151. if field_number == 7:
  152. value, offset = read_varint(buffer, offset)
  153. item["paintwear"] = bytes_to_float_bits(value)
  154. continue
  155. if field_number == 11:
  156. raw, offset = read_length_delimited(buffer, offset)
  157. item["customname"] = raw.decode("utf-8")
  158. continue
  159. sticker_list_name = ITEM_STICKER_LIST_FIELDS.get(field_number)
  160. if sticker_list_name is not None:
  161. raw, offset = read_length_delimited(buffer, offset)
  162. item[sticker_list_name].append(parse_sticker(raw))
  163. continue
  164. if field_number == 18:
  165. value, offset = read_varint(buffer, offset)
  166. item["entindex"] = zigzag_decode(value)
  167. continue
  168. offset = skip_field(buffer, offset, wire_type)
  169. return item
  170. def unwrap_masked_payload(data: bytes) -> bytes:
  171. if len(data) >= 5 and data[0] == 0x00:
  172. return data[1:-4]
  173. return data
  174. def score_decoded_item(item: dict[str, Any]) -> int:
  175. score = 0
  176. if "defindex" in item:
  177. score += 4
  178. for field in SCORED_FIELDS:
  179. if field in item:
  180. score += 1
  181. score += len(item.get("stickers", []))
  182. score += len(item.get("keychains", []))
  183. score += len(item.get("variations", []))
  184. return score
  185. def decode_masked_payload(hex_payload: str) -> tuple[str, dict[str, Any], bytes]:
  186. raw = binascii.unhexlify(hex_payload)
  187. if not raw:
  188. raise ValueError("Masked payload is empty")
  189. transformed = xor_mask(raw, raw[0])
  190. parsed = parse_econ_item(unwrap_masked_payload(transformed))
  191. score = score_decoded_item(parsed)
  192. if score < MIN_DECODE_SCORE:
  193. raise ValueError("Failed to decode inspect payload")
  194. return "xor_first_byte", parsed, transformed
  195. def get_hax_url(url):
  196. hex_payload = extract_hex_payload(url)
  197. transform, decoded, transformed = decode_masked_payload(hex_payload)
  198. return decoded
  199. if __name__ == '__main__':
  200. u = "steam://run/730//+csgo_econ_action_preview%20524282E1CCC2E9534A587281577A5762566AC785B1A15112BF573AD1D2D2D25E225621030272"
  201. print(get_hax_url(u))