Skip to content

Instantly share code, notes, and snippets.

@dnaroma
Last active April 19, 2024 13:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
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':
readObj = obj.read()
prodObj = readObj.m_Container[
"playersettings/android/production_android"].get_obj().read()
bs = BinaryStream(BytesIO(prodObj.raw_data))
while bs.base_stream.tell() < len(prodObj.raw_data) - 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
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.")
@erikchan002
Copy link

PACKAGE=com.sega.pjsekai
APK_PATH=$PACKAGE-$(adb shell dumpsys package $PACKAGE | grep versionName | cut -d= -f2)-base.apk
adb pull $(adb shell pm path $PACKAGE | grep base | cut -d: -f2) "$APK_PATH"
python3 extract.py "$APK_PATH"

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