#!/bin/bash # # ============================================================ # 微信防撤回一键安装脚本 # ============================================================ # # 适用版本: 微信 4.1.9 (CFBundleVersion: 268602) # 微信 4.1.10 (CFBundleVersion: 268824) # 适用平台: macOS (Apple Silicon + Intel) # 依赖工具: clang, codesign, python3 (macOS 系统自带) # # 使用方法: # chmod +x patch.sh # ./patch.sh # 安装防撤回 # ./patch.sh --uninstall # 卸载(恢复原始微信) # # 原理: # 通过 DYLD 注入一个运行时 hook 动态库, # 拦截微信的 isRevokeMessage() 函数, # 区分对方撤回和自己撤回: # - 对方撤回 → 返回 false(消息保留不被删除) # - 自己撤回 → 返回 true(正常处理,不会闪退) # # 4.1.9: 通过写入内建 hook dispatch slot (BSS 区域) 实现 # 4.1.10: dispatch slot 机制已移除,改用 inline trampoline patch # # ============================================================ set -e WECHAT_APP="/Applications/WeChat.app" WECHAT_BIN="$WECHAT_APP/Contents/MacOS/WeChat" DYLIB_DST="$WECHAT_APP/Contents/Resources/WeChatAntiRevoke.dylib" DYLIB_INSTALL_NAME="@executable_path/../Resources/WeChatAntiRevoke.dylib" # 支持的版本列表 VERSION_419="268602" VERSION_4110="268824" print_banner() { echo "" echo "==============================" echo " 微信防撤回安装工具" echo " 适用: macOS / 微信 4.1.9 & 4.1.10" echo " 支持: Apple Silicon + Intel" echo "==============================" echo "" } check_environment() { if [ ! -d "$WECHAT_APP" ]; then echo "[ERROR] 未找到微信: $WECHAT_APP" exit 1 fi VERSION=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleVersion 2>/dev/null) if [ "$VERSION" != "$VERSION_419" ] && [ "$VERSION" != "$VERSION_4110" ]; then echo "[ERROR] 不支持的微信版本 (实际 $VERSION,支持 $VERSION_419 / $VERSION_4110)" exit 1 fi if ! command -v clang &>/dev/null; then echo "[ERROR] 未找到 clang,请安装 Xcode Command Line Tools:" echo " xcode-select --install" exit 1 fi } kill_wechat() { if pgrep -x WeChat >/dev/null 2>&1; then echo "[INFO] 关闭微信..." killall WeChat 2>/dev/null || true sleep 2 fi } remove_provenance() { echo "[INFO] 尝试解除系统文件保护..." TMP_DIR=$(mktemp -d) tar --no-xattrs -cf - -C /Applications WeChat.app | tar -xf - -C "$TMP_DIR/" rm -rf "$WECHAT_APP" mv "$TMP_DIR/WeChat.app" "$WECHAT_APP" rm -rf "$TMP_DIR" # 递归清除残留 xattr(best-effort) xattr -cr "$WECHAT_APP" 2>/dev/null || true sudo xattr -cr "$WECHAT_APP" 2>/dev/null || true # 检查结果(仅警告,不阻断安装) if xattr "$WECHAT_APP" 2>/dev/null | grep -q "com.apple.provenance"; then echo "[WARN] provenance 未能完全清除(macOS Sequoia 可能会自动重新附加)" echo "[INFO] 将通过 entitlements 绕过此限制" else echo "[INFO] 文件保护已解除" fi } compile_dylib() { echo "[INFO] 编译 hook 动态库..." # 内嵌 hook.m 源码 local SRC_FILE=$(mktemp /tmp/hook_XXXXXX.m) cat > "$SRC_FILE" << 'HOOK_SOURCE' #import #import #import #import #import #import #import #import #import // ── 日志 ───────────────────────────────────────────────────── static FILE *g_logFile = NULL; static void log_open(void) { g_logFile = fopen("/tmp/antirevoke_debug.log", "w"); } #define ARLOG(fmt, ...) do { \ if (g_logFile) { fprintf(g_logFile, "[AntiRevoke] " fmt "\n", ##__VA_ARGS__); fflush(g_logFile); } \ } while(0) // ── 常量 ───────────────────────────────────────────────────── static const char *kDylibSuffix_Resources = "Resources/wechat.dylib"; static const char *kDylibSuffix_Frameworks = "Frameworks/wechat.dylib"; static const int32_t kRevokeType = 0x2712; // 10002 // 配置文件路径:~/.config/antirevoke/config // 格式:每行一个 key=value // notify=1 开启通知(默认) // notify=0 关闭通知 static char g_config_path[512] = {0}; // ── 版本地址表 ─────────────────────────────────────────────── static const uintptr_t k419_SlotVA_arm64 = 0x9301838; static const uintptr_t k4110_FuncVA_arm64 = 0x44FFE20; static const uintptr_t k4110_FuncVA_x86_64 = 0x4B4E9A0; static const uintptr_t k419_FuncVA_x86_64 = 0x4AF08D0; // ── 获取当前登录用户 ID ────────────────────────────────────── static char g_my_id[64] = {0}; static _Bool g_my_id_loaded = 0; static void load_my_user_id(void) { if (g_my_id_loaded) return; g_my_id_loaded = 1; const char *home = getenv("HOME"); if (!home) return; char loginDir[1024]; snprintf(loginDir, sizeof(loginDir), "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/login", home); @autoreleasepool { NSFileManager *fm = [NSFileManager defaultManager]; NSString *dirPath = [NSString stringWithUTF8String:loginDir]; NSArray *contents = [fm contentsOfDirectoryAtPath:dirPath error:nil]; if (!contents || [contents count] == 0) return; NSString *latestName = nil; NSDate *latestDate = nil; for (NSString *name in contents) { if ([name hasPrefix:@"."]) continue; NSString *fullPath = [dirPath stringByAppendingPathComponent:name]; BOOL isDir = NO; if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || !isDir) continue; NSString *keyInfo = [fullPath stringByAppendingPathComponent:@"key_info.dat"]; NSDictionary *attrs = [fm fileExistsAtPath:keyInfo] ? [fm attributesOfItemAtPath:keyInfo error:nil] : [fm attributesOfItemAtPath:fullPath error:nil]; NSDate *modDate = attrs[NSFileModificationDate]; if (!latestDate || (modDate && [modDate compare:latestDate] == NSOrderedDescending)) { latestDate = modDate; latestName = name; } } if (latestName && [latestName length] >= 3 && [latestName length] < sizeof(g_my_id)) { strncpy(g_my_id, [latestName UTF8String], sizeof(g_my_id) - 1); ARLOG("用户: %s", g_my_id); } } } // ── 配置 ───────────────────────────────────────────────────── static void init_config_path(void) { const char *home = getenv("HOME"); if (home) { snprintf(g_config_path, sizeof(g_config_path), "%s/.config/antirevoke/config", home); } } static _Bool is_notify_enabled(void) { if (g_config_path[0] == '\0') return 1; // 配置路径未初始化,默认开启 FILE *f = fopen(g_config_path, "r"); if (!f) return 1; // 配置文件不存在,默认开启 char line[128]; _Bool enabled = 1; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "notify=0", 8) == 0) { enabled = 0; break; } } fclose(f); return enabled; } static void send_notification(const char *text) { if (!is_notify_enabled()) return; // 对英文双引号和反斜杠做转义 char *escaped = (char *)malloc(1024); if (!escaped) return; int j = 0; for (int i = 0; text[i] && j < 1022; i++) { if (text[i] == '"' || text[i] == '\\') escaped[j++] = '\\'; escaped[j++] = text[i]; } escaped[j] = '\0'; dispatch_async(dispatch_get_global_queue(0, 0), ^{ FILE *sf = fopen("/tmp/antirevoke_notify.scpt", "w"); if (sf) { fprintf(sf, "display notification \"%s\" with title \"WeChatIntercept\"\n", escaped); fclose(sf); system("osascript /tmp/antirevoke_notify.scpt"); } free(escaped); }); } // ── hook 函数 ──────────────────────────────────────────────── __attribute__((visibility("default"))) _Bool hook_isRevokeMessage(void *msg) { if (msg == NULL) return 0; int32_t msgType = *(int32_t *)((uint8_t *)msg + 0x0C); if (msgType != kRevokeType) return 0; load_my_user_id(); const char *sender = (const char *)((uint8_t *)msg + 0x18); // 自己撤回 → 放行 if (sender[0] == '\0') return 1; if (g_my_id[0] != '\0' && strncmp(sender, g_my_id, strlen(g_my_id)) == 0) return 1; // 对方撤回 → 阻止 ARLOG("拦截: %.20s", sender); // 提取通知内容 char notify_text[256] = {0}; #if defined(__arm64__) || defined(__aarch64__) // arm64:从 msg+0x130 读取 XML body,提取 replacemsg(含用户昵称) uint64_t xml_ptr = *(uint64_t *)((uint8_t *)msg + 0x130); uint64_t xml_len = *(uint64_t *)((uint8_t *)msg + 0x138); if (xml_ptr > 0x100000000ULL && xml_len > 0 && xml_len < 4096) { const char *xml_body = (const char *)xml_ptr; const char *cs = strstr(xml_body, "") : NULL; if (cs && ce) { cs += 9; size_t len = ce - cs; if (len > 0 && len < sizeof(notify_text) - 1) { memcpy(notify_text, cs, len); notify_text[len] = '\0'; } } } #endif char content[512] = {0}; if (notify_text[0] != '\0') snprintf(content, sizeof(content), "拦截到%s", notify_text); else snprintf(content, sizeof(content), "拦截到 %s 撤回了一条消息", sender); send_notification(content); return 0; } // ── 查找 wechat.dylib 的 ASLR slide ───────────────────────── // 优先匹配 Resources/wechat.dylib(核心库),Frameworks/ 为 stub 不可用 static uintptr_t find_wechat_slide(void) { uint32_t count = _dyld_image_count(); uintptr_t fallback = 0; size_t resLen = strlen(kDylibSuffix_Resources); size_t fwLen = strlen(kDylibSuffix_Frameworks); for (uint32_t i = 0; i < count; i++) { const char *name = _dyld_get_image_name(i); if (!name) continue; size_t len = strlen(name); if (len >= resLen && strcmp(name + len - resLen, kDylibSuffix_Resources) == 0) return (uintptr_t)_dyld_get_image_vmaddr_slide(i); if (len >= fwLen && strcmp(name + len - fwLen, kDylibSuffix_Frameworks) == 0) fallback = (uintptr_t)_dyld_get_image_vmaddr_slide(i); } return fallback; } // ── 内存保护工具 ───────────────────────────────────────────── static kern_return_t make_rw(uintptr_t addr, size_t len) { uintptr_t page = addr & ~(uintptr_t)0x3FFF; size_t sz = (addr + len - page + 0x3FFF) & ~(size_t)0x3FFF; return vm_protect(mach_task_self(), (vm_address_t)page, sz, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); } static kern_return_t make_rx(uintptr_t addr, size_t len) { uintptr_t page = addr & ~(uintptr_t)0x3FFF; size_t sz = (addr + len - page + 0x3FFF) & ~(size_t)0x3FFF; return vm_protect(mach_task_self(), (vm_address_t)page, sz, 0, VM_PROT_READ | VM_PROT_EXECUTE); } // ── arm64 inline trampoline(20 字节)───────────────────────── static _Bool install_arm64_trampoline(uintptr_t func_addr, uintptr_t hook_addr) { kern_return_t kr = make_rw(func_addr, 20); if (kr != KERN_SUCCESS) { ARLOG("ERROR: make_rw kr=%d", kr); return 0; } uint32_t *p = (uint32_t *)func_addr; p[0] = 0x58000050u; // LDR X16, #8 p[1] = 0xD61F0200u; // BR X16 *(uint64_t *)(func_addr + 8) = (uint64_t)hook_addr; p[4] = 0xD503201Fu; // NOP // 回读验证 if (*(volatile uint32_t *)func_addr != 0x58000050u) { ARLOG("ERROR: 写入验证失败"); return 0; } sys_icache_invalidate((void *)func_addr, 20); make_rx(func_addr, 20); return 1; } // ── x86_64 inline trampoline(16 字节)─────────────────────── static _Bool install_x86_64_trampoline(uintptr_t func_addr, uintptr_t hook_addr) { kern_return_t kr = make_rw(func_addr, 16); if (kr != KERN_SUCCESS) { ARLOG("ERROR: x86_64 make_rw kr=%d", kr); return 0; } uint8_t *p = (uint8_t *)func_addr; p[0] = 0xFF; p[1] = 0x25; // JMP [RIP+0] p[2] = p[3] = p[4] = p[5] = 0x00; *(uint64_t *)(func_addr + 6) = (uint64_t)hook_addr; p[14] = 0x90; p[15] = 0xC3; if (*(volatile uint8_t *)func_addr != 0xFF) { ARLOG("ERROR: x86_64 写入验证失败"); return 0; } __builtin___clear_cache((char *)func_addr, (char *)(func_addr + 16)); make_rx(func_addr, 16); return 1; } // ── 主 constructor ─────────────────────────────────────────── __attribute__((constructor)) static void hook_init(void) { dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ log_open(); init_config_path(); ARLOG("hook_init 启动"); uintptr_t slide = find_wechat_slide(); if (slide == 0) { ARLOG("ERROR: 未找到 wechat.dylib"); return; } uintptr_t hook = (uintptr_t)&hook_isRevokeMessage; ARLOG("slide=0x%lx hook=0x%lx", (unsigned long)slide, (unsigned long)hook); #if defined(__arm64__) || defined(__aarch64__) uintptr_t func_4110 = slide + k4110_FuncVA_arm64; uint32_t head_insn = *(volatile uint32_t *)func_4110; if (head_insn == 0xB9400C08u) { if (install_arm64_trampoline(func_4110, hook)) ARLOG("4.1.10 arm64 trampoline 安装成功"); } else { void **slot = (void **)(slide + k419_SlotVA_arm64); uintptr_t page = (uintptr_t)slot & ~(uintptr_t)0x3FFF; vm_protect(mach_task_self(), (vm_address_t)page, 0x4000, 0, VM_PROT_READ | VM_PROT_WRITE); *slot = (void *)hook; ARLOG("4.1.9 arm64 slot 写入成功"); } #elif defined(__x86_64__) uintptr_t func_4110_x86 = slide + k4110_FuncVA_x86_64; uintptr_t func_419_x86 = slide + k419_FuncVA_x86_64; const uint32_t kFuncHead = 0xE5894855u; if (*(volatile uint32_t *)func_4110_x86 == kFuncHead) { if (install_x86_64_trampoline(func_4110_x86, hook)) ARLOG("4.1.10 x86_64 trampoline 安装成功"); } else if (*(volatile uint32_t *)func_419_x86 == kFuncHead) { if (install_x86_64_trampoline(func_419_x86, hook)) ARLOG("4.1.9 x86_64 trampoline 安装成功"); } else { ARLOG("ERROR: x86_64 函数地址未匹配"); } #endif ARLOG("就绪,等待撤回消息..."); }); } HOOK_SOURCE clang -arch arm64 -arch x86_64 -shared -framework Foundation \ -o "$DYLIB_DST" \ -install_name "$DYLIB_INSTALL_NAME" \ "$SRC_FILE" 2>&1 rm -f "$SRC_FILE" if [ ! -f "$DYLIB_DST" ]; then echo "[ERROR] 编译失败" exit 1 fi echo "[INFO] 编译成功" } inject_dylib() { echo "[INFO] 注入动态库到微信..." python3 << 'INJECT_SCRIPT' import struct wechat_path = '/Applications/WeChat.app/Contents/MacOS/WeChat' dylib_name = b'@executable_path/../Resources/WeChatAntiRevoke.dylib\x00' while len(dylib_name) % 4 != 0: dylib_name += b'\x00' cmd_size = 24 + len(dylib_name) while cmd_size % 4 != 0: cmd_size += 1 dylib_name += b'\x00' with open(wechat_path, 'r+b') as f: fat_magic = struct.unpack('>I', f.read(4))[0] assert fat_magic == 0xCAFEBABE narch = struct.unpack('>I', f.read(4))[0] slices = [] for i in range(narch): cpu = struct.unpack('>I', f.read(4))[0] sub = struct.unpack('>I', f.read(4))[0] offset = struct.unpack('>I', f.read(4))[0] size = struct.unpack('>I', f.read(4))[0] align = struct.unpack('>I', f.read(4))[0] slices.append((cpu, offset, size)) for cpu, slice_offset, size in slices: f.seek(slice_offset) magic = struct.unpack(' "$ENT_FILE" << 'ENTITLEMENTS' com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-unsigned-executable-memory ENTITLEMENTS # 1. 签名 dylib(adhoc) codesign --force --sign - "$DYLIB_DST" 2>/dev/null # 2. 整体 deep 签名(先处理所有子组件) codesign --force --deep --sign - "$WECHAT_APP" 2>/dev/null # 3. 最后单独给主程序签名并注入 entitlements(覆盖 deep 签名的结果) # 这样 entitlements 不会被后续操作覆盖 codesign --force --sign - --entitlements "$ENT_FILE" "$WECHAT_BIN" 2>/dev/null # 清除 xattr(best-effort) xattr -cr "$WECHAT_APP" 2>/dev/null || true # 验证 entitlements 是否注入成功 if codesign -d --entitlements - "$WECHAT_BIN" 2>&1 | grep -q "disable-library-validation"; then echo "[INFO] 重签名完成(Library Validation 已禁用)" else echo "[WARN] entitlements 可能未生效,请确认 SIP 状态" fi rm -f "$ENT_FILE" } verify_install() { echo "[INFO] 验证安装..." local FAIL=0 # 1. dylib 文件存在 if [ ! -f "$DYLIB_DST" ]; then echo "[ERROR] dylib 文件不存在: $DYLIB_DST" FAIL=1 fi # 2. LC_LOAD_DYLIB 注入成功 if ! otool -l "$WECHAT_BIN" 2>/dev/null | grep -q "WeChatAntiRevoke"; then echo "[ERROR] LC_LOAD_DYLIB 未注入到主程序" FAIL=1 fi # 3. provenance 已清除 if xattr "$WECHAT_APP" 2>/dev/null | grep -q "com.apple.provenance"; then echo "[WARN] WeChat.app 仍有 provenance 标记(重签名可能重新添加)" xattr -d com.apple.provenance "$WECHAT_APP" 2>/dev/null || true fi if xattr "$WECHAT_BIN" 2>/dev/null | grep -q "com.apple.provenance"; then echo "[WARN] 主程序仍有 provenance 标记" xattr -d com.apple.provenance "$WECHAT_BIN" 2>/dev/null || true fi if xattr "$DYLIB_DST" 2>/dev/null | grep -q "com.apple.provenance"; then echo "[WARN] dylib 仍有 provenance 标记" xattr -d com.apple.provenance "$DYLIB_DST" 2>/dev/null || true fi # 4. 签名验证 if ! codesign -v "$DYLIB_DST" 2>/dev/null; then echo "[ERROR] dylib 签名无效" FAIL=1 fi if ! codesign -v "$WECHAT_BIN" 2>/dev/null; then echo "[ERROR] 主程序签名无效" FAIL=1 fi # 5. 运行时加载测试(启动微信、等待后检查 dylib 是否在内存中) echo "[INFO] 启动微信进行加载验证(约 8 秒)..." open "$WECHAT_APP" sleep 8 local PID=$(pgrep -x WeChat 2>/dev/null) if [ -z "$PID" ]; then echo "[ERROR] 微信未能启动" FAIL=1 else if vmmap "$PID" 2>/dev/null | grep -q "AntiRevoke"; then echo "[INFO] dylib 已成功加载到微信进程" else echo "[ERROR] dylib 未加载到微信进程!可能原因:" echo " - macOS 安全策略阻止" echo " - 签名不一致" FAIL=1 fi fi # 6. 检查 hook 安装日志(/tmp/antirevoke_debug.log) local LOG_FILE="/tmp/antirevoke_debug.log" if [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then local LOG_OUTPUT=$(cat "$LOG_FILE") if echo "$LOG_OUTPUT" | grep -q "trampoline 安装完成"; then echo "[INFO] Hook 安装成功(trampoline 已写入)" elif echo "$LOG_OUTPUT" | grep -q "slot 写入完成"; then echo "[INFO] Hook 安装成功(slot 方式)" elif echo "$LOG_OUTPUT" | grep -q "写入验证失败"; then echo "[ERROR] trampoline 写入验证失败" FAIL=1 elif echo "$LOG_OUTPUT" | grep -q "make_rw 失败"; then echo "[ERROR] 代码页写入被系统拒绝(vm_protect 失败)" FAIL=1 elif echo "$LOG_OUTPUT" | grep -q "未找到 wechat.dylib"; then echo "[ERROR] 未找到 wechat.dylib" FAIL=1 elif echo "$LOG_OUTPUT" | grep -q "均未匹配\|hook 失败"; then echo "[ERROR] hook 安装失败" FAIL=1 fi else echo "[WARN] hook 日志文件未生成,hook_init 可能尚未执行" fi echo "[INFO] 调试日志: cat /tmp/antirevoke_debug.log" if [ "$FAIL" -ne 0 ]; then echo "" echo "[WARN] 安装验证未完全通过,请检查上述错误" echo "" fi } do_install() { print_banner check_environment VERSION=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleVersion 2>/dev/null) SHORT_VER=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null) echo "[INFO] 微信版本: $SHORT_VER ($VERSION)" # 检查是否已安装 if [ -f "$DYLIB_DST" ]; then echo "[INFO] 检测到已安装,将重新安装..." fi kill_wechat # 无条件清除 provenance(即使 .app 顶层无标记,内层文件也可能有) # 重打包是幂等操作,不会造成损坏 remove_provenance rm -f "$DYLIB_DST" 2>/dev/null || true compile_dylib inject_dylib resign_app verify_install # 创建默认配置(开启通知) local CONFIG_DIR="$HOME/.config/antirevoke" mkdir -p "$CONFIG_DIR" if [ ! -f "$CONFIG_DIR/config" ]; then echo "notify=1" > "$CONFIG_DIR/config" fi echo "" echo "==============================" echo " 安装成功!" echo "==============================" echo "" echo " 功能: 对方撤回的消息将保留可见" echo " 自己撤回消息正常工作" echo "" echo " 通知开关:" echo " $0 openNotify 开启撤回通知" echo " $0 closeNotify 关闭撤回通知" echo "" echo " 卸载: $0 --uninstall" echo "" } do_debug() { print_banner echo "[INFO] 调试模式(不安装 hook,仅签名允许 lldb attach)" check_environment kill_wechat remove_provenance # 删除已有的 hook dylib(确保无 hook) rm -f "$DYLIB_DST" 2>/dev/null || true # 签名(带 get-task-allow,允许 lldb attach) echo "[INFO] 重签名(注入调试 entitlements)..." local ENT_FILE=$(mktemp /tmp/entitlements_XXXXXX.plist) cat > "$ENT_FILE" << 'ENTITLEMENTS' com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.get-task-allow ENTITLEMENTS codesign --force --deep --sign - "$WECHAT_APP" 2>/dev/null codesign --force --sign - --entitlements "$ENT_FILE" "$WECHAT_BIN" 2>/dev/null xattr -cr "$WECHAT_APP" 2>/dev/null || true rm -f "$ENT_FILE" echo "[INFO] 启动微信..." open "$WECHAT_APP" sleep 3 echo "" echo "==============================" echo " 调试模式已启用" echo "==============================" echo "" echo " 微信无 hook,撤回流程完整执行" echo " 可使用 lldb attach 进行逆向分析" echo "" echo " 命令:" echo " lldb -p \$(pgrep -x WeChat)" echo " image list wechat.dylib" echo " # Resources 行地址 = slide" echo " br set -a " echo " c" echo "" echo " 恢复防撤回: $0" echo "" } do_uninstall() { print_banner echo "[INFO] 卸载防撤回插件..." kill_wechat # 删除 dylib rm -f "$DYLIB_DST" 2>/dev/null || true # 重新安装微信是最干净的卸载方式 echo "[INFO] 建议重新安装微信以完全恢复原始状态" echo "[INFO] 或者删除 $DYLIB_DST 并重新签名" if [ -f "$DYLIB_DST" ]; then echo "[WARN] 无法删除 dylib,请手动重新安装微信" else resign_app 2>/dev/null || true echo "" echo "==============================" echo " 已卸载(dylib 已删除)" echo " 建议重新安装微信以彻底恢复" echo "==============================" fi echo "" } CONFIG_DIR="$HOME/.config/antirevoke" CONFIG_FILE="$CONFIG_DIR/config" do_open_notify() { mkdir -p "$CONFIG_DIR" if grep -q "^notify=" "$CONFIG_FILE" 2>/dev/null; then sed -i '' 's/^notify=.*/notify=1/' "$CONFIG_FILE" else echo "notify=1" >> "$CONFIG_FILE" fi echo "[INFO] 撤回通知已开启" } do_close_notify() { mkdir -p "$CONFIG_DIR" if grep -q "^notify=" "$CONFIG_FILE" 2>/dev/null; then sed -i '' 's/^notify=.*/notify=0/' "$CONFIG_FILE" else echo "notify=0" >> "$CONFIG_FILE" fi echo "[INFO] 撤回通知已关闭" } # ======================== 入口 ======================== case "${1:-}" in openNotify) do_open_notify ;; closeNotify) do_close_notify ;; --debug|-d) do_debug ;; --uninstall|-u) do_uninstall ;; --help|-h) print_banner echo "用法:" echo " $0 安装防撤回" echo " $0 openNotify 开启撤回通知" echo " $0 closeNotify 关闭撤回通知" echo " $0 --debug 调试模式(无 hook,允许 lldb)" echo " $0 --uninstall 卸载" echo " $0 --help 帮助" ;; "") do_install ;; *) echo "[ERROR] 未知参数: $1" echo "用法: $0 [openNotify|closeNotify|--uninstall|--debug|--help]" exit 1 ;; esac