1
0
mirror of https://github.com/0O0o0oOoO00/Alas.git synced 2026-05-14 11:39:25 +08:00
Files
Alas/module/luahook/crack.py
2026-02-07 11:56:30 +08:00

386 lines
13 KiB
Python

import hashlib
import json
import os
import time
from pathlib import Path
from typing import Union, List, Type
import requests
from pydantic import BaseModel
from module.config.config import AzurLaneConfig
from module.config.deep import deep_get
from module.device.device import Device
from module.luahook.exception import CrackerError
from module.logger import logger
from module.luahook.api import CrackApi
from module.luahook.op import CrackOp
REMOTE_PORT = 23897
class Cracker(CrackApi):
def __init__(self, config: AzurLaneConfig, device: Device):
self.config = config
self.device = device
super().__init__(f"http://127.0.0.1:{device.adb.forward_port(REMOTE_PORT)}")
def do_crack_op_on_func(
before_call: Union[Type[CrackOp.Op], List[Type[CrackOp.Op]]] = None,
after_call: Union[Type[CrackOp.Op], List[Type[CrackOp.Op]]] = None
):
def wrapper(func):
def inner(*args, **kwargs):
obj = args[0]
if before_call is not None:
do_crack_op(obj.config, obj.device, before_call)
try:
ret = func(*args, **kwargs)
except Exception as e:
if after_call is not None:
do_crack_op(obj.config, obj.device, after_call)
raise e
if after_call is not None:
do_crack_op(obj.config, obj.device, after_call)
return ret
return inner
return wrapper
g_cracker_has_inited = False
def do_crack_op(config: AzurLaneConfig, device: Device, ops: Union[Type[CrackOp.Op], List[Type[CrackOp.Op]]]):
if not config.full_config.Hook_HookGeneral_Enable:
return
if not device.app_is_running():
logger.info("Game not running, do not crack")
return
if isinstance(ops, list):
l = ops
else:
if ops == CrackOp.EnableAll:
l = CrackOp.ALL_ENABLE_OPS
else:
l = [ops]
full_config = config.full_config
base_url = f"http://127.0.0.1:{device.adb.forward_port(REMOTE_PORT)}"
timeout = full_config.Hook_HookGeneral_RequestTimeLimit
api = CrackApi(base_url, timeout=timeout)
without_pause = full_config.Hook_HookGeneral_OperateWithoutPause
global g_cracker_has_inited
if not g_cracker_has_inited:
if without_pause:
api.init_without_pause()
else:
api.init()
g_cracker_has_inited = True
for op in l:
if issubclass(op, CrackOp.Op):
obj = op(full_config, api)
if without_pause:
obj.execute_without_pause()
else:
obj.execute_with_pause()
else:
logger.error(f"Unsupported op: {op}")
crack_op = do_crack_op_on_func
def disable_all_crack(f):
def wrapper(*args, **kwargs):
obj = args[0]
logger.info("Disabe all luahook cracks")
do_crack_op(obj.config, obj.device, CrackOp.DisableAll)
return f(*args, **kwargs)
return wrapper
def enable_all_crack(f):
def wrapper(*args, **kwargs):
obj = args[0]
do_crack_op(obj.config, obj.device, CrackOp.EnableAll)
return f(*args, **kwargs)
return wrapper
def luahook_crack_all(config: AzurLaneConfig, device: Device):
logger.info("Crack all with luahook")
do_crack_op(config, device, CrackOp.EnableAll)
def luahook_disable_all(config: AzurLaneConfig, device: Device):
logger.info("Disable all luahook")
do_crack_op(config, device, CrackOp.DisableAll)
def chapter_task_crack(f):
def wrapper(*args, **kwargs):
obj = args[0]
do_crack_op(obj.config, obj.device, CrackOp.CHAPTER_CRACK_OPS)
return f(*args, **kwargs)
return wrapper
def opsi_task_crack(f):
def wrapper(*args, **kwargs):
obj = args[0]
do_crack_op(obj.config, obj.device, CrackOp.OPSI_CRACK_OPS)
return f(*args, **kwargs)
return wrapper
class UpdateServerApi:
class ResourceFile(BaseModel):
arch: str
file: str
def __init__(self, api_url: str = ""):
self.api_url = api_url
def post(self, path: str, data=None):
logger.info(f"UpdateServerApi post: {path}")
url = f'{self.api_url}/{path}'
try:
response = requests.post(url, data=data) # TODO: add timeout
except requests.exceptions.Timeout:
raise CrackerError('UpdateServerApi request timeout')
except Exception as e:
raise CrackerError(f'UpdateServerApi request error: {e}')
if response.status_code != 200:
raise CrackerError(f'UpdateServerApi response error: {response.status_code}')
return response
def download_file(self, arch: str, file_name: str) -> bytes:
res = self.post("files", UpdateServerApi.ResourceFile(arch=arch, file=file_name).json())
return res.content
def get_hash(self, arch: str, file_name: str) -> str:
res = self.post("get_hash", UpdateServerApi.ResourceFile(arch=arch, file=file_name).json())
return res.text
class CrackResource:
ARCH_MAP = {
"x86": "x86",
"x86_64": "x86_64",
"armv7": "armeabi-v7a",
"arm": "armeabi-v7a",
"arm64": "arm64-v8a",
"aarch64": "arm64-v8a",
"aarch": "armeabi-v7a",
}
def __init__(self, config: AzurLaneConfig, device: Device):
self.config = config
self.device = device
arch_conf = config.full_config.Hook_HookGeneral_Architecture
if arch_conf == "auto":
logger.warning("Please Tools-HookOp to auto detect, use default x86_64")
config.full_config.Hook_HookGeneral_Architecture = "x86_64"
self.arch = "x86_64"
else:
self.arch = arch_conf
logger.info(f"Use arch: {self.arch}")
self.resource_root = f"./bin/hook"
self.resource_dir = f"{self.resource_root}/{self.arch}"
self.version_file = f"{self.resource_dir}/version.json"
self.first_init = False
self.has_new_version = False
self.update_server = self.__get_update_server()
self.upload_dir = f"/data/hook/{self.arch}"
lib_dir = config.full_config.Hook_HookGeneral_GameLibDir
if lib_dir:
self.game_lib_dir = Path(self.__format_str(lib_dir)).as_posix()
else:
self.game_lib_dir = None
def __file_hash(self, file_name):
with open(file_name, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
def __gen_version_file_from_local(self):
d = {}
for file_name in os.listdir(self.resource_dir):
f = f"{self.resource_dir}/{file_name}"
if not os.path.isfile(f):
continue
if file_name.endswith(".json"):
continue
hash_value = self.__file_hash(f).upper()
d[file_name] = hash_value
with open(self.version_file, "w") as f:
json.dump(d, f, indent=4)
def __ensure_local_resource(self):
if self.update_server is None:
logger.info("No update server, skip ensure local resource")
return
if not os.path.exists(f"{self.resource_dir}/libcracker.so"):
self.__download_resource("libcracker.so")
self.first_init = True
if not os.path.exists(f"{self.resource_dir}/patchelf"):
self.__download_resource("patchelf")
self.first_init = True
__update_local_version = __gen_version_file_from_local
def __ensure_crack_resource(self, ):
if not os.path.exists(self.resource_root):
os.makedirs(self.resource_root)
if not os.path.exists(self.resource_dir):
os.makedirs(self.resource_dir)
self.__ensure_local_resource()
if not os.path.exists(self.version_file):
self.__gen_version_file_from_local()
def __download_resource(self, file_name: str):
if self.update_server is None:
logger.info("No update server, skip download resource")
return
logger.info(f"Download resource: {self.arch}/{file_name}")
res = self.update_server.download_file(self.arch, file_name)
with open(f"{self.resource_dir}/{file_name}", "wb") as f:
f.write(res)
def __is_same_version(self, version_json: dict, file_name: str):
h = self.update_server.get_hash(self.arch, file_name)
return h == version_json.get(file_name, "")
def __check_update(self):
if self.update_server is None:
logger.info("No update server, skip update check, use local resource")
return
if not self.config.full_config.Hook_HookGeneral_UpdateEveryTime:
logger.info("Update skip, use local resource")
return
with open(self.version_file, "r") as f:
local_version = json.load(f)
if not self.__is_same_version(local_version, "libcracker.so"):
self.__download_resource("libcracker.so")
self.has_new_version = True
if not self.__is_same_version(local_version, "patchelf"):
self.__download_resource("patchelf")
self.has_new_version = True
if self.has_new_version:
self.__update_local_version()
def __format_str(self, s: str) -> str:
return ("".join([i for i in s if i.isprintable()])).replace(" ", "").replace("\n", "")
def __get_update_server(self) -> Union[UpdateServerApi, None]:
update_server_url: str = self.config.full_config.Hook_HookGeneral_UpdateServer
update_server_url = self.__format_str(update_server_url)
if not update_server_url:
return None
return UpdateServerApi(update_server_url)
def __push(self, file_name: str):
self.device.adb_shell(f"mkdir -p {self.upload_dir}")
self.device.adb_shell(f"rm {self.upload_dir}/{file_name}")
self.device.adb_push(f"{self.resource_dir}/{file_name}", f"{self.upload_dir}/{file_name}")
self.device.adb_shell(f"chmod 777 {self.upload_dir}/{file_name}")
def __push_resource(self):
logger.info("Push resource to device")
self.__push("libcracker.so")
self.__push("patchelf")
def __do_inject(self):
if not self.game_lib_dir:
raise CrackerError("GameLibDir not set")
logger.info(f"GameLibDir: {self.game_lib_dir}, do inject")
cmd = f"cd {self.upload_dir} && ./patchelf --local-patch --game-lib-dir '{self.game_lib_dir}'"
self.device.adb_shell(cmd)
def __is_exist(self, path):
cmd = f"if [ -f {path} ]; then echo 'exist'; else if [ -d {path} ]; then echo 'exist'; else echo 'not exist'; fi; fi"
return self.device.adb_shell(cmd) == "exist"
def __is_remote_file_exist(self) -> bool:
return self.__is_exist(self.upload_dir) and self.__is_exist(
f"{self.upload_dir}/libcracker.so") and self.__is_exist(f"{self.upload_dir}/patchelf")
def __adb_su_do(self, cmd: str):
return self.device.adb_shell(f"su -c '{cmd}'")
def __adb_root(self):
self.device.adb_command(["root"])
def __check_game_lib_dir(self):
return self.__is_exist(f"{self.game_lib_dir}/libil2cpp.so") and self.__is_exist(
f"{self.game_lib_dir}/libtolua.so")
def auto_detect(self):
if not self.device.app_is_running():
self.device.app_start()
time.sleep(5)
self.__adb_root()
pid = self.device.adb_shell(f"pidof {self.device.package}")
maps = self.device.adb_shell(f"cat /proc/{pid}/maps")
image_list = maps.splitlines()
libtolua_path = None
for image in image_list:
if image.find("libtolua.so") != -1:
libtolua_path = image.split(" ")[-1]
break
if libtolua_path is None:
logger.error("Cannot find libtolua.so path")
return
game_lib_path = libtolua_path.replace("/libtolua.so", "")
arch = CrackResource.ARCH_MAP[game_lib_path.rsplit("/", maxsplit=1)[-1]]
logger.info(f"Game lib path: {game_lib_path}")
logger.info(f"Game arch: {arch}")
full_config = self.config.full_config
full_config.Hook_HookGeneral_Architecture = arch
full_config.Hook_HookGeneral_GameLibDir = game_lib_path
def ensure(self):
if not self.config.full_config.Hook_HookGeneral_Enable:
return
if not self.__check_game_lib_dir():
raise CrackerError(f"GameLibDir '{self.game_lib_dir}' is invalid")
self.__adb_root()
self.__ensure_crack_resource()
if not self.first_init:
self.__check_update()
if self.has_new_version or not self.__is_remote_file_exist() or self.config.full_config.Hook_HookGeneral_PushEveryTime:
self.__push_resource()
self.device.app_stop()
self.config.task_call("Restart")
self.__do_inject()