initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
*.p12
|
||||
*.mobileprovision
|
||||
*.ipa
|
||||
certs/password.txt
|
||||
build/
|
||||
tools/
|
||||
src/__pycache__/
|
||||
/.idea/
|
||||
@@ -0,0 +1,40 @@
|
||||
# L4 API Hook
|
||||
|
||||
把已解密的 Link! Like! Love Live! iOS 包接到 `https://api-alfa-l4.hasu-link.club`,并用自己的开发者证书重签。Bundle ID 保持 `com.oddno.lovelive`。开屏时顶部居中显示小号白字 `API-HOOKED`,大约 5 秒后消失。
|
||||
|
||||
## 准备
|
||||
|
||||
需要 Python 3。Zig 和 zsign 放在 `tools/`,这两个目录被 git 忽略,不会进提交。本机已经放好的话,打包时直接使用。
|
||||
|
||||
如果换了一台电脑、这两个工具不在,在项目根目录执行:
|
||||
|
||||
```
|
||||
python tools/zig.py
|
||||
python tools/zsign.py
|
||||
```
|
||||
|
||||
脚本会按当前系统打印下载地址,并自动下载、解压。压缩包已经下完时,直接解压。Zig 解压到 `tools/`,zsign 解压到 `tools/zsign/`。
|
||||
|
||||
`tools/zsign/zsign-noent.exe` 会在第一次打包时自动生成。它和原版的差别是:动态库和 UnityFramework 不再被写上空的 entitlements,否则 iOS 27 会在启动前杀掉进程。
|
||||
|
||||
## 放入自己的文件
|
||||
|
||||
1. 开发证书 `.p12` 放到 `certs/`。
|
||||
2. 描述文件 `.mobileprovision` 放到 `certs/`。
|
||||
3. 证书密码为空就不用管。有密码时,把密码写在 `certs/password.txt` 的第一行。这个文件不会进 git。
|
||||
|
||||
已解密的游戏包在打包时检查。`build/LLLL_5.1.0.ipa` 已存在就直接使用,没有则自动下载。每个证书目录只放一份对应文件。手机必须在这份描述文件的设备列表里。不要改 Bundle ID。
|
||||
|
||||
API 改写规则在 `config/api-hook.txt`。只改游戏 API 主机,资源 CDN 和官网不要写进去。新旧地址必须一样长。
|
||||
|
||||
## 打包
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```
|
||||
python src/build.py
|
||||
```
|
||||
|
||||
成功后,可安装包在 `output/`,文件名是原来的 IPA 名加上 `-apihook-signed`。装之前先删掉手机上的旧包。
|
||||
|
||||
`output/` 里还会留下一份未签名的中间包,安装时用带 `signed` 的那一份。
|
||||
@@ -0,0 +1,2 @@
|
||||
把自己的 .p12 和 .mobileprovision 放在这个目录,各一份。
|
||||
p12 密码为空则不用额外文件。有密码时,把密码写在同目录的 password.txt 第一行。
|
||||
@@ -0,0 +1,3 @@
|
||||
# 只改写游戏 API 主机。资源 CDN(assets.link-like-lovelive.app)和官网不要写进来。
|
||||
# 格式:原主机 替换后的源(含 https://,不要末尾斜杠)
|
||||
api.link-like-lovelive.app https://api-alfa-l4.hasu-link.club
|
||||
@@ -0,0 +1 @@
|
||||
python src/build.py 生成的安装包会放在这里。安装带 signed 的那一份。
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
/* JIT-free API host rewrite. Swizzles NSURL construction only. */
|
||||
#include <ctype.h>
|
||||
#include <dlfcn.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define RTLD_NOW 0x2
|
||||
#define RTLD_GLOBAL 0x8
|
||||
|
||||
typedef void *id;
|
||||
typedef void *Class;
|
||||
typedef void *SEL;
|
||||
typedef void *IMP;
|
||||
typedef void *Method;
|
||||
typedef struct {
|
||||
double x;
|
||||
double y;
|
||||
double w;
|
||||
double h;
|
||||
} CGRect;
|
||||
|
||||
typedef id (*msg_id_t)(id, SEL);
|
||||
typedef id (*msg_id_id_t)(id, SEL, id);
|
||||
typedef id (*msg_id_cstr_t)(id, SEL, const char *);
|
||||
typedef id (*msg_id_f64_t)(id, SEL, double);
|
||||
typedef id (*msg_id_rgba_t)(id, SEL, double, double, double, double);
|
||||
typedef const char *(*msg_cstr_t)(id, SEL);
|
||||
typedef double (*msg_f64_t)(id, SEL);
|
||||
typedef void (*msg_void_id_t)(id, SEL, id);
|
||||
typedef void (*msg_void_bool_t)(id, SEL, signed char);
|
||||
typedef void (*msg_void_long_t)(id, SEL, long);
|
||||
typedef void (*msg_void_rect_t)(id, SEL, CGRect);
|
||||
typedef id (*msg_id_long_t)(id, SEL, long);
|
||||
typedef unsigned long (*msg_ulong_t)(id, SEL);
|
||||
typedef id (*msg_id_ulong_t)(id, SEL, unsigned long);
|
||||
|
||||
static Class (*p_objc_getClass)(const char *);
|
||||
static SEL (*p_sel_registerName)(const char *);
|
||||
static Method (*p_class_getClassMethod)(Class, SEL);
|
||||
static Method (*p_class_getInstanceMethod)(Class, SEL);
|
||||
static IMP (*p_method_getImplementation)(Method);
|
||||
static IMP (*p_method_setImplementation)(Method, IMP);
|
||||
static id (*p_objc_msgSend)(id, SEL, ...);
|
||||
|
||||
typedef uint64_t dispatch_time_t;
|
||||
typedef void *dispatch_queue_t;
|
||||
static dispatch_time_t (*p_dispatch_time)(dispatch_time_t, int64_t);
|
||||
static void (*p_dispatch_after_f)(dispatch_time_t, dispatch_queue_t, void *, void (*)(void *));
|
||||
static dispatch_queue_t (*p_dispatch_get_main_queue)(void);
|
||||
|
||||
#define DISPATCH_TIME_NOW ((dispatch_time_t)0)
|
||||
#define NSEC_PER_SEC 1000000000ll
|
||||
#define MAX_RULES 8
|
||||
#define LABEL_TAG 0x41504948L
|
||||
|
||||
static char g_from_host[MAX_RULES][256];
|
||||
static char g_to_origin[MAX_RULES][512];
|
||||
static int g_rule_count = 0;
|
||||
|
||||
static SEL s_UTF8String;
|
||||
static SEL s_stringWithUTF8String;
|
||||
|
||||
static IMP orig_url_with_string;
|
||||
static IMP orig_url_with_string_rel;
|
||||
static IMP orig_url_with_string_enc;
|
||||
static IMP orig_init_string;
|
||||
static IMP orig_init_string_rel;
|
||||
static IMP orig_req_with_url;
|
||||
static IMP orig_req_init;
|
||||
static IMP orig_req_init_policy;
|
||||
static IMP orig_set_url;
|
||||
static IMP orig_task_url;
|
||||
static IMP orig_task_url_block;
|
||||
static IMP orig_task_req;
|
||||
static IMP orig_task_req_block;
|
||||
|
||||
static int load_symbol(void *handle, const char *name, void **out) {
|
||||
void *sym = dlsym(handle, name);
|
||||
if (!sym) {
|
||||
fprintf(stderr, "[ApiHook] missing symbol %s\n", name);
|
||||
return 0;
|
||||
}
|
||||
*out = sym;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static SEL sel(const char *name) { return p_sel_registerName(name); }
|
||||
|
||||
static id cls(const char *name) { return (id)p_objc_getClass(name); }
|
||||
|
||||
static id nsstr(const char *utf8) {
|
||||
return ((msg_id_cstr_t)p_objc_msgSend)(cls("NSString"), s_stringWithUTF8String, utf8);
|
||||
}
|
||||
|
||||
static const char *cstr(id string) {
|
||||
if (!string) return NULL;
|
||||
return ((msg_cstr_t)p_objc_msgSend)(string, s_UTF8String);
|
||||
}
|
||||
|
||||
static void trim(char *s) {
|
||||
size_t n = strlen(s);
|
||||
size_t i = 0;
|
||||
while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n')) i++;
|
||||
if (i > 0) memmove(s, s + i, n - i + 1);
|
||||
n = strlen(s);
|
||||
while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r' || s[n - 1] == '\n')) {
|
||||
s[--n] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int host_eq(const char *host, size_t len, const char *rule) {
|
||||
size_t n = strlen(rule);
|
||||
if (n != len) return 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (tolower((unsigned char)host[i]) != tolower((unsigned char)rule[i])) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static id rewrite_string(id string) {
|
||||
const char *src = cstr(string);
|
||||
if (!src || !src[0] || g_rule_count == 0) return string;
|
||||
const char *sep = strstr(src, "://");
|
||||
if (!sep) return string;
|
||||
|
||||
const char *host = sep + 3;
|
||||
const char *host_end = host;
|
||||
while (*host_end && *host_end != '/' && *host_end != '?' && *host_end != '#') host_end++;
|
||||
|
||||
const char *host_begin = host;
|
||||
for (const char *p = host; p < host_end; p++) {
|
||||
if (*p == '@') host_begin = p + 1;
|
||||
}
|
||||
const char *colon = host_begin;
|
||||
while (colon < host_end && *colon != ':') colon++;
|
||||
size_t host_len = (size_t)(colon - host_begin);
|
||||
|
||||
for (int i = 0; i < g_rule_count; i++) {
|
||||
if (!host_eq(host_begin, host_len, g_from_host[i])) continue;
|
||||
size_t origin_len = strlen(g_to_origin[i]);
|
||||
size_t rest_len = strlen(host_end);
|
||||
char *buf = (char *)malloc(origin_len + rest_len + 1);
|
||||
if (!buf) return string;
|
||||
memcpy(buf, g_to_origin[i], origin_len);
|
||||
memcpy(buf + origin_len, host_end, rest_len + 1);
|
||||
id replaced = nsstr(buf);
|
||||
free(buf);
|
||||
return replaced ? replaced : string;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
static id rewrite_url(id url) {
|
||||
if (!url) return url;
|
||||
id abs = ((msg_id_t)p_objc_msgSend)(url, sel("absoluteString"));
|
||||
id replaced = rewrite_string(abs);
|
||||
if (replaced == abs) return url;
|
||||
return ((id (*)(id, SEL, id))orig_url_with_string)(cls("NSURL"), sel("URLWithString:"), replaced);
|
||||
}
|
||||
|
||||
static id rewrite_request(id request) {
|
||||
if (!request) return request;
|
||||
id url = ((msg_id_t)p_objc_msgSend)(request, sel("URL"));
|
||||
id new_url = rewrite_url(url);
|
||||
if (new_url == url) return request;
|
||||
id mutable = ((msg_id_t)p_objc_msgSend)(request, sel("mutableCopy"));
|
||||
if (!mutable) return request;
|
||||
((void (*)(id, SEL, id))orig_set_url)(mutable, sel("setURL:"), new_url);
|
||||
return mutable;
|
||||
}
|
||||
|
||||
static id hook_url_with_string(id self, SEL _cmd, id string) {
|
||||
return ((id (*)(id, SEL, id))orig_url_with_string)(self, _cmd, rewrite_string(string));
|
||||
}
|
||||
static id hook_url_with_string_rel(id self, SEL _cmd, id string, id base) {
|
||||
return ((id (*)(id, SEL, id, id))orig_url_with_string_rel)(self, _cmd, rewrite_string(string), base);
|
||||
}
|
||||
static id hook_url_with_string_enc(id self, SEL _cmd, id string, signed char invalid) {
|
||||
return ((id (*)(id, SEL, id, signed char))orig_url_with_string_enc)(self, _cmd, rewrite_string(string), invalid);
|
||||
}
|
||||
static id hook_init_string(id self, SEL _cmd, id string) {
|
||||
return ((id (*)(id, SEL, id))orig_init_string)(self, _cmd, rewrite_string(string));
|
||||
}
|
||||
static id hook_init_string_rel(id self, SEL _cmd, id string, id base) {
|
||||
return ((id (*)(id, SEL, id, id))orig_init_string_rel)(self, _cmd, rewrite_string(string), base);
|
||||
}
|
||||
static id hook_req_with_url(id self, SEL _cmd, id url) {
|
||||
return ((id (*)(id, SEL, id))orig_req_with_url)(self, _cmd, rewrite_url(url));
|
||||
}
|
||||
static id hook_req_init(id self, SEL _cmd, id url) {
|
||||
return ((id (*)(id, SEL, id))orig_req_init)(self, _cmd, rewrite_url(url));
|
||||
}
|
||||
static id hook_req_init_policy(id self, SEL _cmd, id url, unsigned long policy, double timeout) {
|
||||
return ((id (*)(id, SEL, id, unsigned long, double))orig_req_init_policy)(self, _cmd, rewrite_url(url), policy, timeout);
|
||||
}
|
||||
static void hook_set_url(id self, SEL _cmd, id url) {
|
||||
((void (*)(id, SEL, id))orig_set_url)(self, _cmd, rewrite_url(url));
|
||||
}
|
||||
static id hook_task_url(id self, SEL _cmd, id url) {
|
||||
return ((id (*)(id, SEL, id))orig_task_url)(self, _cmd, rewrite_url(url));
|
||||
}
|
||||
static id hook_task_url_block(id self, SEL _cmd, id url, id block) {
|
||||
return ((id (*)(id, SEL, id, id))orig_task_url_block)(self, _cmd, rewrite_url(url), block);
|
||||
}
|
||||
static id hook_task_req(id self, SEL _cmd, id request) {
|
||||
return ((id (*)(id, SEL, id))orig_task_req)(self, _cmd, rewrite_request(request));
|
||||
}
|
||||
static id hook_task_req_block(id self, SEL _cmd, id request, id block) {
|
||||
return ((id (*)(id, SEL, id, id))orig_task_req_block)(self, _cmd, rewrite_request(request), block);
|
||||
}
|
||||
|
||||
static int swizzle(const char *class_name, const char *sel_name, int instance, IMP repl, IMP *orig_out) {
|
||||
Class c = p_objc_getClass(class_name);
|
||||
if (!c) return 0;
|
||||
SEL s = sel(sel_name);
|
||||
Method m = instance ? p_class_getInstanceMethod(c, s) : p_class_getClassMethod(c, s);
|
||||
if (!m) return 0;
|
||||
*orig_out = p_method_getImplementation(m);
|
||||
p_method_setImplementation(m, repl);
|
||||
fprintf(stderr, "[ApiHook] swizzled %s[%s %s]\n", instance ? "-" : "+", class_name, sel_name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void add_default_rule(void) {
|
||||
snprintf(g_from_host[0], sizeof g_from_host[0], "api.link-like-lovelive.app");
|
||||
snprintf(g_to_origin[0], sizeof g_to_origin[0], "https://api-alfa-l4.hasu-link.club");
|
||||
g_rule_count = 1;
|
||||
}
|
||||
|
||||
static void load_config(void) {
|
||||
char exe[1024];
|
||||
uint32_t sz = (uint32_t)sizeof exe;
|
||||
if (_NSGetExecutablePath(exe, &sz) != 0) {
|
||||
add_default_rule();
|
||||
return;
|
||||
}
|
||||
char *slash = strrchr(exe, '/');
|
||||
if (!slash) {
|
||||
add_default_rule();
|
||||
return;
|
||||
}
|
||||
*slash = 0;
|
||||
char path[1200];
|
||||
snprintf(path, sizeof path, "%s/api-hook.txt", exe);
|
||||
FILE *fp = fopen(path, "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "[ApiHook] no config, using built-in host\n");
|
||||
add_default_rule();
|
||||
return;
|
||||
}
|
||||
char line[800];
|
||||
while (fgets(line, sizeof line, fp) && g_rule_count < MAX_RULES) {
|
||||
trim(line);
|
||||
if (line[0] == 0 || line[0] == '#') continue;
|
||||
char *sp = strchr(line, ' ');
|
||||
if (!sp) continue;
|
||||
*sp = 0;
|
||||
char *origin = sp + 1;
|
||||
trim(origin);
|
||||
if (line[0] == 0 || origin[0] == 0) continue;
|
||||
snprintf(g_from_host[g_rule_count], sizeof g_from_host[0], "%s", line);
|
||||
snprintf(g_to_origin[g_rule_count], sizeof g_to_origin[0], "%s", origin);
|
||||
g_rule_count++;
|
||||
}
|
||||
fclose(fp);
|
||||
if (g_rule_count == 0) add_default_rule();
|
||||
fprintf(stderr, "[ApiHook] %d rewrite rule(s)\n", g_rule_count);
|
||||
}
|
||||
|
||||
static id g_banner_label;
|
||||
static id g_banner_window;
|
||||
static int g_banner_tries;
|
||||
static int g_banner_done;
|
||||
static int g_owns_window;
|
||||
|
||||
typedef struct {
|
||||
double top;
|
||||
double left;
|
||||
double bottom;
|
||||
double right;
|
||||
} Insets;
|
||||
|
||||
static CGRect obj_bounds(id obj) {
|
||||
return ((CGRect (*)(id, SEL))p_objc_msgSend)(obj, sel("bounds"));
|
||||
}
|
||||
|
||||
static double top_inset(id view) {
|
||||
Insets insets = ((Insets (*)(id, SEL))p_objc_msgSend)(view, sel("safeAreaInsets"));
|
||||
if (insets.top >= 20.0 && insets.top < 160.0) return insets.top;
|
||||
return 54.0;
|
||||
}
|
||||
|
||||
static dispatch_queue_t main_queue(void) {
|
||||
if (p_dispatch_get_main_queue) {
|
||||
dispatch_queue_t q = p_dispatch_get_main_queue();
|
||||
if (q) return q;
|
||||
}
|
||||
return (dispatch_queue_t)dlsym(((void *)(intptr_t)-2), "_dispatch_main_q");
|
||||
}
|
||||
|
||||
static id foreground_scene(id app) {
|
||||
id scenes = ((msg_id_t)p_objc_msgSend)(app, sel("connectedScenes"));
|
||||
if (!scenes) return (id)0;
|
||||
id all = ((msg_id_t)p_objc_msgSend)(scenes, sel("allObjects"));
|
||||
if (!all) return (id)0;
|
||||
unsigned long count = ((msg_ulong_t)p_objc_msgSend)(all, sel("count"));
|
||||
Class window_scene = p_objc_getClass("UIWindowScene");
|
||||
id fallback = (id)0;
|
||||
for (unsigned long i = 0; i < count; i++) {
|
||||
id scene = ((msg_id_ulong_t)p_objc_msgSend)(all, sel("objectAtIndex:"), i);
|
||||
if (!scene || !window_scene) continue;
|
||||
signed char kind = ((signed char (*)(id, SEL, id))p_objc_msgSend)(scene, sel("isKindOfClass:"), (id)window_scene);
|
||||
if (!kind) continue;
|
||||
if (!fallback) fallback = scene;
|
||||
long state = ((long (*)(id, SEL))p_objc_msgSend)(scene, sel("activationState"));
|
||||
if (state == 0) return scene;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static id host_window(id app, id scene) {
|
||||
if (scene) {
|
||||
id window = ((msg_id_t)p_objc_msgSend)(scene, sel("keyWindow"));
|
||||
if (window) return window;
|
||||
id windows = ((msg_id_t)p_objc_msgSend)(scene, sel("windows"));
|
||||
unsigned long count = windows ? ((msg_ulong_t)p_objc_msgSend)(windows, sel("count")) : 0;
|
||||
if (count > 0) return ((msg_id_ulong_t)p_objc_msgSend)(windows, sel("objectAtIndex:"), 0);
|
||||
}
|
||||
if (!app) return (id)0;
|
||||
id window = ((msg_id_t)p_objc_msgSend)(app, sel("keyWindow"));
|
||||
if (window) return window;
|
||||
id windows = ((msg_id_t)p_objc_msgSend)(app, sel("windows"));
|
||||
unsigned long count = windows ? ((msg_ulong_t)p_objc_msgSend)(windows, sel("count")) : 0;
|
||||
if (count == 0) return (id)0;
|
||||
return ((msg_id_ulong_t)p_objc_msgSend)(windows, sel("objectAtIndex:"), 0);
|
||||
}
|
||||
|
||||
static id make_label(id container) {
|
||||
id label = ((msg_id_t)p_objc_msgSend)(((msg_id_t)p_objc_msgSend)(cls("UILabel"), sel("alloc")), sel("init"));
|
||||
if (!label) return (id)0;
|
||||
((msg_void_id_t)p_objc_msgSend)(label, sel("setText:"), nsstr("API-HOOKED"));
|
||||
((msg_void_long_t)p_objc_msgSend)(label, sel("setTextAlignment:"), 1);
|
||||
((msg_void_long_t)p_objc_msgSend)(label, sel("setTag:"), LABEL_TAG);
|
||||
((msg_void_long_t)p_objc_msgSend)(label, sel("setAutoresizingMask:"), 37);
|
||||
((msg_void_bool_t)p_objc_msgSend)(label, sel("setUserInteractionEnabled:"), 0);
|
||||
id clear = ((msg_id_t)p_objc_msgSend)(cls("UIColor"), sel("clearColor"));
|
||||
id white = ((msg_id_t)p_objc_msgSend)(cls("UIColor"), sel("whiteColor"));
|
||||
id shadow = ((msg_id_rgba_t)p_objc_msgSend)(cls("UIColor"), sel("colorWithRed:green:blue:alpha:"), 0, 0, 0, 0.85);
|
||||
id font = ((msg_id_f64_t)p_objc_msgSend)(cls("UIFont"), sel("systemFontOfSize:"), 12.0);
|
||||
((msg_void_id_t)p_objc_msgSend)(label, sel("setTextColor:"), white);
|
||||
((msg_void_id_t)p_objc_msgSend)(label, sel("setBackgroundColor:"), clear);
|
||||
((msg_void_id_t)p_objc_msgSend)(label, sel("setShadowColor:"), shadow);
|
||||
{
|
||||
typedef struct { double w, h; } CGSize;
|
||||
((void (*)(id, SEL, CGSize))p_objc_msgSend)(label, sel("setShadowOffset:"), (CGSize){0.0, 1.0});
|
||||
}
|
||||
if (font) ((msg_void_id_t)p_objc_msgSend)(label, sel("setFont:"), font);
|
||||
((void (*)(id, SEL))p_objc_msgSend)(label, sel("sizeToFit"));
|
||||
|
||||
CGRect text = obj_bounds(label);
|
||||
CGRect screen = obj_bounds(container);
|
||||
double width = screen.w;
|
||||
if (width < 200.0 || width > 2000.0) width = 390.0;
|
||||
double label_w = text.w > 20.0 ? text.w : 88.0;
|
||||
double label_h = text.h > 8.0 ? text.h : 14.0;
|
||||
CGRect frame;
|
||||
frame.w = label_w;
|
||||
frame.h = label_h;
|
||||
frame.x = (width - label_w) / 2.0;
|
||||
frame.y = top_inset(container);
|
||||
((msg_void_rect_t)p_objc_msgSend)(label, sel("setFrame:"), frame);
|
||||
return label;
|
||||
}
|
||||
|
||||
static void hide_banner(void *unused) {
|
||||
(void)unused;
|
||||
if (g_banner_done) return;
|
||||
g_banner_done = 1;
|
||||
if (g_banner_label) {
|
||||
((void (*)(id, SEL))p_objc_msgSend)(g_banner_label, sel("removeFromSuperview"));
|
||||
g_banner_label = (id)0;
|
||||
}
|
||||
if (g_owns_window && g_banner_window) {
|
||||
((msg_void_bool_t)p_objc_msgSend)(g_banner_window, sel("setHidden:"), 1);
|
||||
}
|
||||
g_banner_window = (id)0;
|
||||
}
|
||||
|
||||
static void arm_hide(void) {
|
||||
dispatch_queue_t q = main_queue();
|
||||
if (!q || !p_dispatch_after_f || !p_dispatch_time) return;
|
||||
dispatch_time_t when = p_dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
|
||||
p_dispatch_after_f(when, q, NULL, hide_banner);
|
||||
}
|
||||
|
||||
static void show_banner(void *unused) {
|
||||
(void)unused;
|
||||
if (g_banner_done) return;
|
||||
if (g_banner_label) return;
|
||||
|
||||
if (g_banner_tries < 20 && p_dispatch_after_f && p_dispatch_time) {
|
||||
dispatch_queue_t q = main_queue();
|
||||
if (q) {
|
||||
g_banner_tries++;
|
||||
dispatch_time_t when = p_dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC));
|
||||
p_dispatch_after_f(when, q, NULL, show_banner);
|
||||
}
|
||||
}
|
||||
|
||||
dlopen("/System/Library/Frameworks/UIKit.framework/UIKit", RTLD_NOW);
|
||||
if (!p_objc_getClass("UILabel") || !p_objc_getClass("UIApplication") || !p_objc_getClass("UIWindow")) return;
|
||||
id app = ((msg_id_t)p_objc_msgSend)(cls("UIApplication"), sel("sharedApplication"));
|
||||
if (!app) return;
|
||||
id scene = foreground_scene(app);
|
||||
id host = host_window(app, scene);
|
||||
if (!scene && !host) return;
|
||||
|
||||
id container = host;
|
||||
id overlay = (id)0;
|
||||
if (scene) {
|
||||
overlay = ((msg_id_id_t)p_objc_msgSend)(((msg_id_t)p_objc_msgSend)(cls("UIWindow"), sel("alloc")), sel("initWithWindowScene:"), scene);
|
||||
if (overlay) {
|
||||
((void (*)(id, SEL, double))p_objc_msgSend)(overlay, sel("setWindowLevel:"), 100000.0);
|
||||
id clear = ((msg_id_t)p_objc_msgSend)(cls("UIColor"), sel("clearColor"));
|
||||
((msg_void_id_t)p_objc_msgSend)(overlay, sel("setBackgroundColor:"), clear);
|
||||
((msg_void_bool_t)p_objc_msgSend)(overlay, sel("setUserInteractionEnabled:"), 0);
|
||||
((msg_void_bool_t)p_objc_msgSend)(overlay, sel("setHidden:"), 0);
|
||||
container = overlay;
|
||||
}
|
||||
}
|
||||
if (!container) return;
|
||||
|
||||
id label = make_label(container);
|
||||
if (!label) return;
|
||||
((msg_void_id_t)p_objc_msgSend)(container, sel("addSubview:"), label);
|
||||
g_banner_label = label;
|
||||
g_banner_window = container;
|
||||
g_owns_window = overlay ? 1 : 0;
|
||||
arm_hide();
|
||||
fprintf(stderr, "[ApiHook] API-HOOKED banner added\n");
|
||||
}
|
||||
|
||||
static void schedule_banner(void) {
|
||||
dispatch_queue_t q = main_queue();
|
||||
if (!p_dispatch_after_f || !p_dispatch_time || !q) {
|
||||
fprintf(stderr, "[ApiHook] banner timer unavailable\n");
|
||||
return;
|
||||
}
|
||||
dispatch_time_t when = p_dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC));
|
||||
p_dispatch_after_f(when, q, NULL, show_banner);
|
||||
}
|
||||
|
||||
static void install_hooks(void) {
|
||||
swizzle("NSURL", "URLWithString:", 0, (IMP)hook_url_with_string, &orig_url_with_string);
|
||||
swizzle("NSURL", "URLWithString:relativeToURL:", 0, (IMP)hook_url_with_string_rel, &orig_url_with_string_rel);
|
||||
swizzle("NSURL", "URLWithString:encodingInvalidCharacters:", 0, (IMP)hook_url_with_string_enc, &orig_url_with_string_enc);
|
||||
swizzle("NSURL", "initWithString:", 1, (IMP)hook_init_string, &orig_init_string);
|
||||
swizzle("NSURL", "initWithString:relativeToURL:", 1, (IMP)hook_init_string_rel, &orig_init_string_rel);
|
||||
swizzle("NSURLRequest", "requestWithURL:", 0, (IMP)hook_req_with_url, &orig_req_with_url);
|
||||
swizzle("NSURLRequest", "initWithURL:", 1, (IMP)hook_req_init, &orig_req_init);
|
||||
swizzle("NSURLRequest", "initWithURL:cachePolicy:timeoutInterval:", 1, (IMP)hook_req_init_policy, &orig_req_init_policy);
|
||||
swizzle("NSMutableURLRequest", "setURL:", 1, (IMP)hook_set_url, &orig_set_url);
|
||||
swizzle("NSURLSession", "dataTaskWithURL:", 1, (IMP)hook_task_url, &orig_task_url);
|
||||
swizzle("NSURLSession", "dataTaskWithURL:completionHandler:", 1, (IMP)hook_task_url_block, &orig_task_url_block);
|
||||
swizzle("NSURLSession", "dataTaskWithRequest:", 1, (IMP)hook_task_req, &orig_task_req);
|
||||
swizzle("NSURLSession", "dataTaskWithRequest:completionHandler:", 1, (IMP)hook_task_req_block, &orig_task_req_block);
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void api_hook_init(void) {
|
||||
void *objc = dlopen("/usr/lib/libobjc.A.dylib", RTLD_NOW | RTLD_GLOBAL);
|
||||
dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_NOW);
|
||||
dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", RTLD_NOW);
|
||||
if (!objc) {
|
||||
fprintf(stderr, "[ApiHook] libobjc failed to load\n");
|
||||
return;
|
||||
}
|
||||
if (!load_symbol(objc, "objc_getClass", (void **)&p_objc_getClass)) return;
|
||||
if (!load_symbol(objc, "sel_registerName", (void **)&p_sel_registerName)) return;
|
||||
if (!load_symbol(objc, "class_getClassMethod", (void **)&p_class_getClassMethod)) return;
|
||||
if (!load_symbol(objc, "class_getInstanceMethod", (void **)&p_class_getInstanceMethod)) return;
|
||||
if (!load_symbol(objc, "method_getImplementation", (void **)&p_method_getImplementation)) return;
|
||||
if (!load_symbol(objc, "method_setImplementation", (void **)&p_method_setImplementation)) return;
|
||||
if (!load_symbol(objc, "objc_msgSend", (void **)&p_objc_msgSend)) return;
|
||||
void *self = ((void *)(intptr_t)-2);
|
||||
p_dispatch_time = dlsym(self, "dispatch_time");
|
||||
p_dispatch_after_f = dlsym(self, "dispatch_after_f");
|
||||
p_dispatch_get_main_queue = dlsym(self, "dispatch_get_main_queue");
|
||||
|
||||
s_UTF8String = sel("UTF8String");
|
||||
s_stringWithUTF8String = sel("stringWithUTF8String:");
|
||||
load_config();
|
||||
install_hooks();
|
||||
schedule_banner();
|
||||
fprintf(stderr, "[ApiHook] loaded\n");
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
"""Build a signed L4 IPA from the decrypted game package, certs/, and local tools."""
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CERTS_DIR = ROOT / "certs"
|
||||
OUTPUT_DIR = ROOT / "output"
|
||||
BUILD_DIR = ROOT / "build"
|
||||
TOOLS_DIR = ROOT / "tools"
|
||||
CONFIG = ROOT / "config" / "api-hook.txt"
|
||||
ZIG_INCLUDE = "lib/libc/include/any-darwin-any"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from fetch import archive_complete, ensure_archive # noqa: E402
|
||||
from inject_ipa import inject_ipa # noqa: E402
|
||||
from macho_fix import patch_dylib # noqa: E402
|
||||
|
||||
IPA_URL = "https://www.senluopan.com/f/VKd2ha/LLLL_5.1.0.ipa"
|
||||
IPA_FILE = BUILD_DIR / "LLLL_5.1.0.ipa"
|
||||
|
||||
|
||||
def pick_one(folder: Path, suffix: str, title: str) -> Path | None:
|
||||
found = sorted(p for p in folder.glob(f"*{suffix}") if p.is_file())
|
||||
if not found:
|
||||
return None
|
||||
if len(found) > 1:
|
||||
names = "\n".join(f" - {p.name}" for p in found)
|
||||
raise SystemExit(f"{title} 里有多份 {suffix},请只留一份:\n{names}")
|
||||
return found[0]
|
||||
|
||||
|
||||
def require_signing_material() -> tuple[Path, Path, str]:
|
||||
CERTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p12 = pick_one(CERTS_DIR, ".p12", "certs")
|
||||
profile = pick_one(CERTS_DIR, ".mobileprovision", "certs")
|
||||
if p12 and profile:
|
||||
password = ""
|
||||
password_file = CERTS_DIR / "password.txt"
|
||||
if password_file.is_file():
|
||||
text = password_file.read_text(encoding="utf-8")
|
||||
password = text.splitlines()[0] if text else ""
|
||||
return p12, profile, password
|
||||
|
||||
print("还不能签名。请把你自己的开发者材料放进 certs/ :")
|
||||
print(" 1. 证书文件,扩展名是 .p12")
|
||||
print(" 2. 描述文件,扩展名是 .mobileprovision")
|
||||
print("密码为空就不用额外操作。如果 p12 有密码,把密码单独写在 certs/password.txt 的第一行。")
|
||||
print("描述文件的设备列表里必须包含这台手机,Bundle ID 保持游戏原来的 com.oddno.lovelive。")
|
||||
if not p12:
|
||||
print("当前缺少 .p12")
|
||||
if not profile:
|
||||
print("当前缺少 .mobileprovision")
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def require_ipa() -> Path:
|
||||
if archive_complete(IPA_FILE):
|
||||
return IPA_FILE
|
||||
print(IPA_URL, flush=True)
|
||||
ensure_archive(IPA_URL, IPA_FILE)
|
||||
if not archive_complete(IPA_FILE):
|
||||
raise SystemExit(2)
|
||||
return IPA_FILE
|
||||
|
||||
|
||||
def fetch_tool(script: str) -> None:
|
||||
result = subprocess.run([sys.executable, str(ROOT / "tools" / script)], check=False)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def find_zig() -> Path:
|
||||
found = sorted(TOOLS_DIR.glob("zig*/zig.exe"))
|
||||
if found:
|
||||
return found[0]
|
||||
fetch_tool("zig.py")
|
||||
found = sorted(TOOLS_DIR.glob("zig*/zig.exe"))
|
||||
if not found:
|
||||
raise SystemExit(2)
|
||||
return found[0]
|
||||
|
||||
|
||||
def ensure_zsign() -> Path:
|
||||
folder = TOOLS_DIR / "zsign"
|
||||
patched = folder / "zsign-noent.exe"
|
||||
if patched.is_file():
|
||||
return patched
|
||||
stock = folder / "zsign.exe"
|
||||
if not stock.is_file():
|
||||
fetch_tool("zsign.py")
|
||||
stock = folder / "zsign.exe"
|
||||
if not stock.is_file():
|
||||
raise SystemExit(2)
|
||||
data = bytearray(stock.read_bytes())
|
||||
off = 0xFD45C
|
||||
expect = bytes([0x41, 0xB8, 0xB5, 0x00, 0x00, 0x00])
|
||||
if data[off - 2 : off + 4] != expect:
|
||||
raise SystemExit("tools/zsign/zsign.exe 不是已验证的 1.1.2,不能自动去掉动态库上的空 entitlements")
|
||||
data[off] = 0
|
||||
patched.write_bytes(data)
|
||||
print(f"已生成 {patched.name}:非主程序不再写入空 entitlements")
|
||||
return patched
|
||||
|
||||
|
||||
def compile_dylib(zig: Path, output: Path) -> None:
|
||||
include = zig.parent / ZIG_INCLUDE
|
||||
if not include.is_dir():
|
||||
raise SystemExit(f"Zig 头文件目录不存在:{include}")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
cmd = [
|
||||
str(zig),
|
||||
"cc",
|
||||
"-target",
|
||||
"aarch64-macos",
|
||||
"-isystem",
|
||||
str(include),
|
||||
"-dynamiclib",
|
||||
"-fno-stack-protector",
|
||||
"-O2",
|
||||
"-Wl,-install_name,@executable_path/ApiHook.dylib",
|
||||
"-o",
|
||||
str(output),
|
||||
str(ROOT / "src" / "api_hook.c"),
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def verify_signed_dylib(ipa: Path) -> None:
|
||||
with zipfile.ZipFile(ipa) as archive:
|
||||
dylib = archive.read("Payload/ProductName.app/ApiHook.dylib")
|
||||
if dylib[:4] != struct.pack("<I", 0xFEEDFACF):
|
||||
raise SystemExit("签名后的 ApiHook.dylib 不是 Mach-O")
|
||||
ncmds = struct.unpack_from("<I", dylib, 16)[0]
|
||||
off = 32
|
||||
signature = None
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", dylib, off)
|
||||
if cmd == 0xE:
|
||||
raise SystemExit("签名后的动态库仍带有 LC_LOAD_DYLINKER")
|
||||
if cmd == 0x19:
|
||||
vmsize = struct.unpack_from("<Q", dylib, off + 32)[0]
|
||||
filesize = struct.unpack_from("<Q", dylib, off + 48)[0]
|
||||
if vmsize % 0x4000 or filesize > vmsize:
|
||||
raise SystemExit("签名后的动态库段对齐不正确")
|
||||
if cmd == 0x1D:
|
||||
signature = struct.unpack_from("<II", dylib, off + 8)
|
||||
off += cmdsize
|
||||
if not signature:
|
||||
raise SystemExit("签名后的动态库没有代码签名")
|
||||
dataoff, datasize = signature
|
||||
blob = dylib[dataoff : dataoff + datasize]
|
||||
count = struct.unpack_from(">I", blob, 8)[0]
|
||||
kinds = [struct.unpack_from(">I", blob, 12 + i * 8)[0] for i in range(count)]
|
||||
if 5 in kinds or 7 in kinds:
|
||||
raise SystemExit("签名后的动态库仍带有 entitlements")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ipa = require_ipa()
|
||||
p12, profile, password = require_signing_material()
|
||||
if not CONFIG.is_file():
|
||||
raise SystemExit(f"缺少配置:{CONFIG}")
|
||||
zig = find_zig()
|
||||
zsign = ensure_zsign()
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
dylib = BUILD_DIR / "ApiHook.dylib"
|
||||
unsigned = OUTPUT_DIR / f"{ipa.stem}-apihook-unsigned.ipa"
|
||||
signed = OUTPUT_DIR / f"{ipa.stem}-apihook-signed.ipa"
|
||||
|
||||
print(f"编译动态库:{zig}")
|
||||
compile_dylib(zig, dylib)
|
||||
patch_dylib(dylib)
|
||||
inject_ipa(ipa, dylib, CONFIG, unsigned)
|
||||
|
||||
if signed.exists():
|
||||
signed.unlink()
|
||||
temp = BUILD_DIR / "zsign-tmp"
|
||||
if temp.exists():
|
||||
shutil.rmtree(temp)
|
||||
temp.mkdir(parents=True)
|
||||
cmd = [
|
||||
str(zsign),
|
||||
"-k",
|
||||
str(p12),
|
||||
"-p",
|
||||
password,
|
||||
"-m",
|
||||
str(profile),
|
||||
"-o",
|
||||
str(signed),
|
||||
"-t",
|
||||
str(temp),
|
||||
"-z",
|
||||
"6",
|
||||
str(unsigned),
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
shutil.rmtree(temp, ignore_errors=True)
|
||||
verify_signed_dylib(signed)
|
||||
print(f"可安装包:{signed}")
|
||||
print("Bundle ID 保持 com.oddno.lovelive。装到手机前先删掉旧包。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(ROOT)
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Inject ApiHook.dylib into a decrypted L4 IPA and leave it unsigned."""
|
||||
import argparse
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
LC_LOAD_DYLIB = 0xC
|
||||
MH_MAGIC_64 = 0xFEEDFACF
|
||||
DYLIB_NAME = "@executable_path/ApiHook.dylib"
|
||||
METADATA = "Payload/ProductName.app/Data/Managed/Metadata/global-metadata.dat"
|
||||
MAIN_BIN = "Payload/ProductName.app/ProductName"
|
||||
APP_DYLIB = "Payload/ProductName.app/ApiHook.dylib"
|
||||
APP_CONFIG = "Payload/ProductName.app/api-hook.txt"
|
||||
|
||||
|
||||
def load_rule(config: Path) -> tuple[bytes, bytes]:
|
||||
for raw in config.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split(None, 1)
|
||||
if len(parts) != 2:
|
||||
raise SystemExit(f"配置行格式应为“原主机 新地址”:{line}")
|
||||
host, origin = parts
|
||||
old = host.encode("ascii") if host.startswith("http") else b"https://" + host.encode("ascii")
|
||||
new = origin.strip().encode("ascii")
|
||||
if len(old) != len(new):
|
||||
raise SystemExit(
|
||||
f"API 地址替换必须等长(现在 {len(old)} 和 {len(new)}):{old.decode()} -> {new.decode()}"
|
||||
)
|
||||
return old, new
|
||||
raise SystemExit(f"{config} 里没有改写规则")
|
||||
|
||||
|
||||
def align8(n: int) -> int:
|
||||
return (n + 7) & ~7
|
||||
|
||||
|
||||
def add_load_dylib(binary: bytearray, name: str) -> bytearray:
|
||||
if binary[:4] != struct.pack("<I", MH_MAGIC_64):
|
||||
raise SystemExit("main executable is not a 64-bit Mach-O")
|
||||
header = struct.unpack_from("<IIIIIIII", binary, 0)
|
||||
magic, cputype, cpusub, filetype, ncmds, sizeofcmds, flags, reserved = header
|
||||
if filetype != 2:
|
||||
raise SystemExit(f"unexpected Mach-O filetype {filetype}")
|
||||
|
||||
encoded = name.encode("ascii") + b"\x00"
|
||||
cmdsize = align8(24 + len(encoded))
|
||||
header_end = 32 + sizeofcmds
|
||||
first_section = None
|
||||
off = 32
|
||||
for _ in range(ncmds):
|
||||
cmd, size = struct.unpack_from("<II", binary, off)
|
||||
if cmd == 0x19:
|
||||
nsects = struct.unpack_from("<I", binary, off + 64)[0]
|
||||
sect = off + 72
|
||||
for _s in range(nsects):
|
||||
sect_off = struct.unpack_from("<I", binary, sect + 48)[0]
|
||||
if sect_off and (first_section is None or sect_off < first_section):
|
||||
first_section = sect_off
|
||||
sect += 80
|
||||
if cmd == LC_LOAD_DYLIB:
|
||||
nameoff = struct.unpack_from("<I", binary, off + 8)[0]
|
||||
existing = binary[off + nameoff : off + size].split(b"\x00", 1)[0]
|
||||
if existing.decode("ascii", "replace") == name:
|
||||
print("LC_LOAD_DYLIB already present")
|
||||
return binary
|
||||
off += size
|
||||
|
||||
if first_section is None or header_end + cmdsize > first_section:
|
||||
raise SystemExit(
|
||||
f"no header padding for load command ({header_end}+{cmdsize} > {first_section})"
|
||||
)
|
||||
|
||||
cmd = struct.pack("<IIIIII", LC_LOAD_DYLIB, cmdsize, 24, 0, 0x00010000, 0x00010000)
|
||||
cmd += encoded
|
||||
cmd += b"\x00" * (cmdsize - len(cmd))
|
||||
binary[header_end : header_end + cmdsize] = cmd
|
||||
struct.pack_into("<II", binary, 16, ncmds + 1, sizeofcmds + cmdsize)
|
||||
print(f"added LC_LOAD_DYLIB {name} at offset {header_end}")
|
||||
return binary
|
||||
|
||||
|
||||
def patch_metadata(data: bytes, old: bytes, new: bytes) -> bytes:
|
||||
count = data.count(old)
|
||||
if count == 0:
|
||||
raise SystemExit("global-metadata.dat 里找不到要替换的 API 地址")
|
||||
print(f"replaced {count} API URL literal(s) in global-metadata.dat")
|
||||
return data.replace(old, new)
|
||||
|
||||
|
||||
def inject_ipa(ipa: Path, dylib: Path, config: Path, out: Path) -> None:
|
||||
old, new = load_rule(config)
|
||||
dylib_bytes = dylib.read_bytes()
|
||||
if not dylib_bytes.startswith(struct.pack("<I", MH_MAGIC_64)):
|
||||
raise SystemExit("ApiHook.dylib is not a 64-bit Mach-O")
|
||||
config_bytes = config.read_bytes()
|
||||
|
||||
with zipfile.ZipFile(ipa) as src:
|
||||
binary = add_load_dylib(bytearray(src.read(MAIN_BIN)), DYLIB_NAME)
|
||||
metadata = patch_metadata(src.read(METADATA), old, new)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
if out.exists():
|
||||
out.unlink()
|
||||
with zipfile.ZipFile(out, "w") as dst:
|
||||
for info in src.infolist():
|
||||
if info.filename in (MAIN_BIN, METADATA):
|
||||
continue
|
||||
if info.filename.endswith("/") or info.is_dir():
|
||||
continue
|
||||
payload = src.read(info.filename)
|
||||
new_info = zipfile.ZipInfo(filename=info.filename, date_time=info.date_time)
|
||||
new_info.compress_type = info.compress_type
|
||||
new_info.external_attr = info.external_attr
|
||||
dst.writestr(new_info, payload)
|
||||
for name, payload, mode in (
|
||||
(MAIN_BIN, bytes(binary), 0o100755),
|
||||
(METADATA, metadata, 0o100644),
|
||||
(APP_DYLIB, dylib_bytes, 0o100755),
|
||||
(APP_CONFIG, config_bytes, 0o100644),
|
||||
):
|
||||
info = zipfile.ZipInfo(filename=name)
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = mode << 16
|
||||
dst.writestr(info, payload)
|
||||
print(f"wrote {out} ({out.stat().st_size} bytes)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--ipa", type=Path, required=True)
|
||||
parser.add_argument("--dylib", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
inject_ipa(args.ipa, args.dylib, args.config, args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Make a Zig macOS dylib loadable as an iOS arm64 dylib."""
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
PAGE = 0x4000
|
||||
MH_MAGIC_64 = 0xFEEDFACF
|
||||
LC_SEGMENT_64 = 0x19
|
||||
LC_LOAD_DYLINKER = 0xE
|
||||
LC_BUILD_VERSION = 0x32
|
||||
|
||||
|
||||
def patch_dylib(path: Path) -> None:
|
||||
data = bytearray(path.read_bytes())
|
||||
if data[:4] != struct.pack("<I", MH_MAGIC_64):
|
||||
raise SystemExit(f"{path} is not a 64-bit Mach-O")
|
||||
ncmds, sizeofcmds = struct.unpack_from("<II", data, 16)
|
||||
off = 32
|
||||
cmds = []
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", data, off)
|
||||
cmds.append((cmd, bytearray(data[off : off + cmdsize])))
|
||||
off += cmdsize
|
||||
|
||||
segments = []
|
||||
for cmd, chunk in cmds:
|
||||
if cmd != LC_SEGMENT_64:
|
||||
continue
|
||||
name = bytes(chunk[8:24]).split(b"\x00", 1)[0].decode()
|
||||
vmaddr, vmsize, _fileoff, filesize = struct.unpack_from("<QQQQ", chunk, 24)
|
||||
segments.append((name, vmaddr, vmsize, filesize, chunk))
|
||||
|
||||
for index, (name, vmaddr, vmsize, filesize, chunk) in enumerate(segments):
|
||||
if name == "__LINKEDIT":
|
||||
aligned = 0x3000
|
||||
while aligned < filesize:
|
||||
aligned += PAGE
|
||||
else:
|
||||
aligned = (vmsize + PAGE - 1) & ~(PAGE - 1)
|
||||
next_vm = segments[index + 1][1]
|
||||
if vmaddr + aligned > next_vm:
|
||||
raise SystemExit(f"{name} cannot be aligned without overlapping the next segment")
|
||||
if aligned < filesize:
|
||||
raise SystemExit(f"{name} aligned size {aligned:#x} is smaller than file size {filesize:#x}")
|
||||
struct.pack_into("<Q", chunk, 32, aligned)
|
||||
print(f"{name} vmsize {vmsize:#x} -> {aligned:#x}")
|
||||
|
||||
rebuilt = bytearray()
|
||||
removed_dylinker = False
|
||||
patched_platform = False
|
||||
for cmd, chunk in cmds:
|
||||
if cmd == LC_BUILD_VERSION:
|
||||
struct.pack_into("<III", chunk, 8, 2, 0x000F0000, 0x000F0000)
|
||||
patched_platform = True
|
||||
if cmd == LC_LOAD_DYLINKER:
|
||||
removed_dylinker = True
|
||||
print("removed LC_LOAD_DYLINKER")
|
||||
continue
|
||||
rebuilt += chunk
|
||||
if not removed_dylinker:
|
||||
raise SystemExit("LC_LOAD_DYLINKER was not found; the dylib layout changed")
|
||||
if not patched_platform:
|
||||
raise SystemExit("LC_BUILD_VERSION was not found")
|
||||
|
||||
data[32 : 32 + len(rebuilt)] = rebuilt
|
||||
data[32 + len(rebuilt) : 32 + sizeofcmds] = b"\x00" * (sizeofcmds - len(rebuilt))
|
||||
struct.pack_into("<II", data, 16, ncmds - 1, len(rebuilt))
|
||||
path.write_bytes(data)
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
"""Download a release archive and extract it. A complete local file is extracted without downloading again."""
|
||||
import tarfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
UA = "LLLL-tools"
|
||||
|
||||
|
||||
def _archive_name(path: Path) -> str:
|
||||
name = path.name.lower()
|
||||
if name.endswith(".part"):
|
||||
name = name[: -len(".part")]
|
||||
return name
|
||||
|
||||
|
||||
def archive_complete(path: Path) -> bool:
|
||||
if not path.is_file() or path.stat().st_size == 0:
|
||||
return False
|
||||
name = _archive_name(path)
|
||||
try:
|
||||
if name.endswith(".zip") or name.endswith(".ipa"):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
return bool(archive.infolist())
|
||||
if ".tar." in name or name.endswith(".tgz"):
|
||||
mode = "r:xz" if name.endswith(".tar.xz") else "r:gz"
|
||||
with tarfile.open(path, mode) as archive:
|
||||
for _ in archive:
|
||||
pass
|
||||
return True
|
||||
except (OSError, tarfile.TarError, zipfile.BadZipFile):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def _total_size(resp, have: int, code: int) -> int | None:
|
||||
if code == 206:
|
||||
content_range = resp.headers.get("Content-Range")
|
||||
if content_range and "/" in content_range:
|
||||
total = content_range.rsplit("/", 1)[-1]
|
||||
if total.isdigit():
|
||||
return int(total)
|
||||
length = resp.headers.get("Content-Length")
|
||||
if length and length.isdigit():
|
||||
return have + int(length)
|
||||
return None
|
||||
length = resp.headers.get("Content-Length")
|
||||
if length and length.isdigit():
|
||||
return int(length)
|
||||
return None
|
||||
|
||||
|
||||
def ensure_archive(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
part = dest.with_name(dest.name + ".part")
|
||||
if archive_complete(dest):
|
||||
return
|
||||
if archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
if dest.exists() and not part.exists():
|
||||
dest.replace(part)
|
||||
elif dest.exists():
|
||||
dest.unlink()
|
||||
|
||||
have = part.stat().st_size if part.exists() else 0
|
||||
headers = {"User-Agent": UA}
|
||||
if have:
|
||||
headers["Range"] = f"bytes={have}-"
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
try:
|
||||
response = urllib.request.urlopen(request, timeout=120)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 416 and archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
print(f"下载失败:{exc.code}")
|
||||
raise SystemExit(2)
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
try:
|
||||
with response:
|
||||
code = getattr(response, "status", 200)
|
||||
if code == 200:
|
||||
have = 0
|
||||
elif code != 206:
|
||||
print(f"下载失败:{code}")
|
||||
raise SystemExit(2)
|
||||
total = _total_size(response, have, code)
|
||||
shown = -1
|
||||
with part.open("wb" if code == 200 else "ab") as output:
|
||||
done = have if code == 206 else 0
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
output.write(chunk)
|
||||
done += len(chunk)
|
||||
if not total:
|
||||
continue
|
||||
pct = min(100, done * 100 // total)
|
||||
if pct // 10 != shown:
|
||||
shown = pct // 10
|
||||
print(f"\r{pct}%", end="", flush=True)
|
||||
if shown >= 0:
|
||||
print()
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
if archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
if total and part.exists() and part.stat().st_size == total:
|
||||
part.unlink(missing_ok=True)
|
||||
print("下载内容无法解压")
|
||||
raise SystemExit(2)
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def extract_archive(archive: Path, dest: Path) -> None:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
root = dest.resolve()
|
||||
name = archive.name.lower()
|
||||
if name.endswith(".zip"):
|
||||
with zipfile.ZipFile(archive) as zipped:
|
||||
for info in zipped.infolist():
|
||||
target = (dest / info.filename).resolve()
|
||||
if not target.is_relative_to(root):
|
||||
raise SystemExit("压缩包路径不安全")
|
||||
zipped.extractall(dest)
|
||||
return
|
||||
with tarfile.open(archive) as packed:
|
||||
packed.extractall(dest, filter="data")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Download and extract Zig for this operating system."""
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from fetch import ensure_archive, extract_archive
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = ROOT / "tools"
|
||||
VERSION = "0.16.0"
|
||||
RELEASE = f"https://github.com/ziglang/zig/releases/tag/{VERSION}"
|
||||
|
||||
ARCH = {
|
||||
"amd64": "x86_64",
|
||||
"x86_64": "x86_64",
|
||||
"arm64": "aarch64",
|
||||
"aarch64": "aarch64",
|
||||
}
|
||||
OS_FILE = {
|
||||
"windows": ("windows", "zip", "zig.exe"),
|
||||
"darwin": ("macos", "tar.xz", "zig"),
|
||||
"linux": ("linux", "tar.xz", "zig"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
system = platform.system().lower()
|
||||
arch = ARCH.get(platform.machine().lower())
|
||||
spec = OS_FILE.get(system)
|
||||
if not arch or not spec:
|
||||
print(RELEASE)
|
||||
raise SystemExit(2)
|
||||
|
||||
os_name, ext, binary = spec
|
||||
folder = f"zig-{arch}-{os_name}-{VERSION}"
|
||||
if any(TOOLS.glob(f"zig*/{binary}")):
|
||||
return
|
||||
url = f"https://ziglang.org/download/{VERSION}/{folder}.{ext}"
|
||||
archive = TOOLS / f"{folder}.{ext}"
|
||||
print(url, flush=True)
|
||||
ensure_archive(url, archive)
|
||||
extract_archive(archive, TOOLS)
|
||||
found = sorted(TOOLS.glob(f"zig*/{binary}"))
|
||||
if not found:
|
||||
raise SystemExit(2)
|
||||
if os.name != "nt":
|
||||
for path in found:
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Download and extract zsign for this operating system."""
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from fetch import ensure_archive, extract_archive
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = ROOT / "tools" / "zsign"
|
||||
VERSION = "1.1.2"
|
||||
RELEASE = f"https://github.com/zhlynn/zsign/releases/tag/v{VERSION}"
|
||||
|
||||
ARCH = {
|
||||
"amd64": "x86_64",
|
||||
"x86_64": "x86_64",
|
||||
"arm64": "aarch64",
|
||||
"aarch64": "aarch64",
|
||||
}
|
||||
# Official v1.1.2 assets. There is no Windows arm64 or macOS x64 build.
|
||||
ASSETS = {
|
||||
("windows", "x86_64"): ("zsign-windows-x64.zip", "zsign.exe"),
|
||||
("darwin", "aarch64"): ("zsign-macos-arm64.tar.gz", "zsign"),
|
||||
("linux", "x86_64"): ("zsign-linux-x86_64.tar.gz", "zsign"),
|
||||
("linux", "aarch64"): ("zsign-linux-aarch64.tar.gz", "zsign"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
system = platform.system().lower()
|
||||
arch = ARCH.get(platform.machine().lower())
|
||||
asset = ASSETS.get((system, arch))
|
||||
if not asset:
|
||||
print(RELEASE)
|
||||
raise SystemExit(2)
|
||||
|
||||
filename, binary = asset
|
||||
if (TOOLS / binary).is_file():
|
||||
return
|
||||
url = f"https://github.com/zhlynn/zsign/releases/download/v{VERSION}/{filename}"
|
||||
archive = TOOLS.parent / filename
|
||||
print(url, flush=True)
|
||||
ensure_archive(url, archive)
|
||||
extract_archive(archive, TOOLS)
|
||||
installed = TOOLS / binary
|
||||
if not installed.is_file():
|
||||
raise SystemExit(2)
|
||||
if os.name != "nt":
|
||||
installed.chmod(0o755)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user