Skip to content

Instantly share code, notes, and snippets.

@dnaroma
Last active April 10, 2025 07:15
Show Gist options
  • Save dnaroma/bf862a99126e6ef4312a57dd908fb37d to your computer and use it in GitHub Desktop.
Save dnaroma/bf862a99126e6ef4312a57dd908fb37d to your computer and use it in GitHub Desktop.
Extract AppHash from unity assets
from io import RawIOBase
from struct import *
from typing import Callable
def offset_decorate(func: Callable) -> Callable:
def func_wrapper(*args, **kwargs) -> Callable:
offset = kwargs.get('offset')
if offset is not None:
back = args[0].base_stream.tell()
args[0].base_stream.seek(offset)
d = func(*args)
args[0].base_stream.seek(back)
return d
return func(*args, **kwargs)
return func_wrapper
class BinaryStream:
def __init__(self, base_stream: RawIOBase, endian='little'):
self.base_stream = base_stream
self.endian = endian
def readByte(self) -> bytes:
return self.base_stream.read(1)
@offset_decorate
def readBytes(self, length: int) -> bytes:
return self.base_stream.read(length)
def readChar(self) -> int:
return self.unpack('b')
def readUChar(self) -> int:
return self.unpack('B')
def readBool(self) -> bool:
return self.unpack('?')
def readInt16(self) -> int:
if (self.endian == 'big'):
return self.unpack('>h', 2)
return self.unpack('h', 2)
def readUInt16(self) -> int:
if (self.endian == 'big'):
return self.unpack('>H', 2)
return self.unpack('H', 2)
def readInt32(self) -> int:
if (self.endian == 'big'):
return self.unpack('>i', 4)
return self.unpack('i', 4)
def readUInt32(self) -> int:
if (self.endian == 'big'):
return self.unpack('>I', 4)
return self.unpack('I', 4)
def readInt64(self) -> int:
if (self.endian == 'big'):
return self.unpack('>q', 8)
return self.unpack('q', 8)
def readUInt64(self) -> int:
if (self.endian == 'big'):
return self.unpack('>Q', 8)
return self.unpack('Q', 8)
def readFloat(self) -> float:
return self.unpack('f', 4)
def readDouble(self) -> float:
return self.unpack('d', 8)
def readString(self) -> bytes:
length = self.readUInt16()
return self.unpack(str(length) + 's', length)
@offset_decorate
def readStringLength(self, length: int) -> bytes:
return self.unpack(str(length) + 's', length)
@offset_decorate
def readStringToNull(self) -> bytes:
byte_str = b''
while 1:
b = self.readByte()
if (b == b'\x00'):
break
byte_str += b
return byte_str
def AlignStream(self, alignment=4):
pos = self.base_stream.tell()
# print('currPos is: ' + str(pos), pos % alignment)
if ((pos % alignment) != 0):
self.base_stream.seek(alignment - (pos % alignment), 1)
# print('aligned currPos is: ' + str(self.base_stream.tell()))
def writeBytes(self, value: bytes):
self.base_stream.write(value)
def writeChar(self, value: str):
self.pack('c', value)
def writeUChar(self, value: str):
self.pack('C', value)
def writeBool(self, value: bool):
self.pack('?', value)
def writeInt16(self, value: int):
self.pack('h', value)
def writeUInt16(self, value: int):
self.pack('H', value)
def writeInt32(self, value: int):
self.pack('i', value)
def writeUInt32(self, value: int):
self.pack('I', value)
def writeInt64(self, value: int):
self.pack('q', value)
def writeUInt64(self, value: int):
self.pack('Q', value)
def writeFloat(self, value: float):
self.pack('f', value)
def writeDouble(self, value: float):
self.pack('d', value)
def writeString(self, value: str):
length = len(value)
self.writeUInt16(length)
self.pack(str(length) + 's', value)
def pack(self, fmt: str, data) -> bytes:
return self.writeBytes(pack(fmt, data))
def unpack(self, fmt: str, length=1) -> tuple:
return unpack(fmt, self.readBytes(length))[0]
def unpack_raw(self, fmt: str) -> tuple:
length = Struct(fmt).size
return unpack(fmt, self.readBytes(length))
import os
import sys
from io import BytesIO
import UnityPy
import UnityPy.helpers
from binary import BinaryStream
DEST_DIR = os.path.join(os.getcwd(), 'extracted', 'images')
os.makedirs(DEST_DIR, exist_ok=True)
def readAppHash(path_to_apk: str):
env = UnityPy.load(os.path.join(os.getcwd(), path_to_apk))
strings = []
names = [
'memo', 'unknown', 'clientMajorVersion', 'clientMinorVersion',
'clientBuildVersion', 'snapshot', 'clientVersionSuffix',
'clientDataMajorVersion', 'clientDataMinorVersion',
'clientDataBuildVersion', 'clientDataRevision', 'companyName',
'productName', 'bundleIdentifier', 'bundleVersion', 'assetHash',
'clientAppHash', 'bundleVersionCode'
]
for obj in env.objects:
if obj.type.name == 'ResourceManager':
readObj = obj.read()
prodObj = next(
(x[1].deref() for x in readObj.m_Container
if x[0] == "playersettings/android/production_android"), None)
raw_data = prodObj.get_raw_data().tobytes()
bs = BinaryStream(BytesIO(raw_data))
bs.base_stream.seek(28)
while bs.base_stream.tell() < len(raw_data) - 4:
strLen = bs.readUInt32()
strings.append(bs.readStringLength(strLen).decode('utf-8'))
bs.AlignStream()
# if obj.type.name in ['Texture2D', 'Sprite']:
# readObj = obj.read()
# dest = os.path.join(DEST_DIR, readObj.name)
# dest, ext = os.path.splitext(dest)
# dest = f"{dest}.png"
# if os.path.exists(dest):
# dest = f"{dest[:-4]}_{obj.path_id}.png"
# try:
# image = readObj.image
# image.save(dest)
# except Exception as e:
# print(f"Failed to extract {readObj.name} as image: {e}")
# continue
# prodObj = readObj.m_Container[
# "playersettings/android/production_android"].get_obj().read()
app_hash_dict = dict(zip(names, strings))
return app_hash_dict
if __name__ == "__main__":
if len(sys.argv) > 1:
path_to_apk = sys.argv[1]
print(readAppHash(path_to_apk))
else:
print("Please provide the path to game apk as an argument.")
@stypr
Copy link

stypr commented Mar 20, 2025

On latest UnityPy (v 1.21.2), the code needs to be changed due to breaking changes of UnityPy.

import os
import sys
from io import BytesIO

import UnityPy

from binary import BinaryStream

def readAppHash(path_to_apk: str):
    env = UnityPy.load(os.path.join(os.getcwd(), path_to_apk))
    strings = []
    names = [
        'memo', 'clientMajorVersion', 'clientMinorVersion',
        'clientBuildVersion', 'snapshot', 'clientVersionSuffix',
        'clientDataMajorVersion', 'clientDataMinorVersion',
        'clientDataBuildVersion', 'clientDataRevision', 'companyName',
        'productName', 'bundleIdentifier', 'bundleVersion', 'assetHash',
        'clientAppHash', 'bundleVersionCode'
    ]
    for obj in env.objects:
        if obj.type.name == 'ResourceManager':
            target_object = None
            readObj = obj.read()
            for container in readObj.m_Container:
                if container[0] == "playersettings/android/production_android":
                    target_object = container[1]
                    break

            if not target_object:
                continue

            target_object_start = int(target_object.deref().byte_start / 4)
            target_object_raw = target_object.deref().get_raw_data().tobytes()
            target_object_raw_real = target_object_raw[target_object_start:]

            bs = BinaryStream(BytesIO(target_object_raw_real))
            while bs.base_stream.tell() < len(target_object_raw_real) - 4:
                strLen = bs.readUInt32()
                strings.append(bs.readStringLength(strLen).decode('utf-8'))
                bs.AlignStream()

    app_hash_dict = dict(zip(names, strings))
    return app_hash_dict

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment