From ad49ec693ca7253c52a875bacac262453f058a74 Mon Sep 17 00:00:00 2001 From: zhangyang131 Date: Fri, 29 May 2026 11:16:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8D=E5=BE=AE=E4=BF=A14.1.10?= =?UTF-8?q?=EF=BC=8C=E5=85=BC=E5=AE=B9=E6=80=A7=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch.sh | 444 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 400 insertions(+), 44 deletions(-) diff --git a/patch.sh b/patch.sh index f2f7a57..d599519 100755 --- a/patch.sh +++ b/patch.sh @@ -5,13 +5,14 @@ # ============================================================ # # 适用版本: 微信 4.1.9 (CFBundleVersion: 268602) +# 微信 4.1.10 (CFBundleVersion: 268824) # 适用平台: macOS (Apple Silicon + Intel) # 依赖工具: clang, codesign, python3 (macOS 系统自带) # # 使用方法: -# chmod +x install.sh -# ./install.sh # 安装防撤回 -# ./install.sh --uninstall # 卸载(恢复原始微信) +# chmod +x patch.sh +# ./patch.sh # 安装防撤回 +# ./patch.sh --uninstall # 卸载(恢复原始微信) # # 原理: # 通过 DYLD 注入一个运行时 hook 动态库, @@ -20,6 +21,9 @@ # - 对方撤回 → 返回 false(消息保留不被删除) # - 自己撤回 → 返回 true(正常处理,不会闪退) # +# 4.1.9: 通过写入内建 hook dispatch slot (BSS 区域) 实现 +# 4.1.10: dispatch slot 机制已移除,改用 inline trampoline patch +# # ============================================================ set -e @@ -28,13 +32,16 @@ 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" -EXPECTED_VERSION="268602" + +# 支持的版本列表 +VERSION_419="268602" +VERSION_4110="268824" print_banner() { echo "" echo "==============================" echo " 微信防撤回安装工具" - echo " 适用: macOS / 微信 4.1.9" + echo " 适用: macOS / 微信 4.1.9 & 4.1.10" echo " 支持: Apple Silicon + Intel" echo "==============================" echo "" @@ -47,8 +54,8 @@ check_environment() { fi VERSION=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleVersion 2>/dev/null) - if [ "$VERSION" != "$EXPECTED_VERSION" ]; then - echo "[ERROR] 微信版本不匹配 (期望 $EXPECTED_VERSION, 实际 $VERSION)" + if [ "$VERSION" != "$VERSION_419" ] && [ "$VERSION" != "$VERSION_4110" ]; then + echo "[ERROR] 不支持的微信版本 (实际 $VERSION,支持 $VERSION_419 / $VERSION_4110)" exit 1 fi @@ -68,13 +75,24 @@ kill_wechat() { } remove_provenance() { - echo "[INFO] 解除系统文件保护(约 30 秒)..." + 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" - echo "[INFO] 保护已解除" + + # 递归清除残留 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() { @@ -87,45 +105,249 @@ compile_dylib() { #import #import #import +#import #import +#import +#import -static const uintptr_t kSlotVA = 0x9301838; -static const char *kDylibSuffix = "Resources/wechat.dylib"; -static const int kMsgTypeOffset = 0x0C; -static const int kRevokeType = 0x2712; +// ── 日志(写入 /tmp/antirevoke_debug.log)──────────────────── +static FILE *g_logFile = NULL; -static _Bool hook_isRevokeMessage(void *msg) { - if (msg == NULL) return 0; - int32_t msgType = *(int32_t *)((uint8_t *)msg + kMsgTypeOffset); - if (msgType != kRevokeType) return 0; - uint32_t field18 = *(uint32_t *)((uint8_t *)msg + 0x18); - if (field18 == 0x64697877) return 0; // "wxid" = 对方撤回 → 阻止 - return 1; // 自己撤回 → 放行 +static void log_open(void) { + g_logFile = fopen("/tmp/antirevoke_debug.log", "w"); } -static uintptr_t find_wechat_slide(void) { - uint32_t count = _dyld_image_count(); - for (uint32_t i = 0; i < count; i++) { - const char *name = _dyld_get_image_name(i); - if (name == NULL) continue; - size_t len = strlen(name); - size_t suffixLen = strlen(kDylibSuffix); - if (len >= suffixLen && strcmp(name + len - suffixLen, kDylibSuffix) == 0) - return (uintptr_t)_dyld_get_image_vmaddr_slide(i); +#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 + +// ── 版本地址表 ─────────────────────────────────────────────── +// 4.1.9 (CFBundleVersion 268602) +// arm64: hook dispatch slot VA = 0x9301838 (BSS,运行时可写) +// x86_64: 暂不需要 slot(x86_64 直接 inline patch,同 4.1.10) +static const uintptr_t k419_SlotVA_arm64 = 0x9301838; + +// 4.1.10 (CFBundleVersion 268824) — dispatch slot 已移除,改用 inline trampoline +static const uintptr_t k4110_FuncVA_arm64 = 0x44FFE20; +static const uintptr_t k4110_FuncVA_x86_64 = 0x4B4E9A0; + +// 4.1.9 x86_64 函数 VA(inline trampoline,与 4.1.10 流程相同) +static const uintptr_t k419_FuncVA_x86_64 = 0x4AF08D0; + +// ── 获取当前登录用户 ID(完整字符串)───────────────────────── +// 懒加载:首次遇到撤回消息时从 app_data/login/ 读取最近登录的用户目录名 +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) { + ARLOG("WARN: login 目录为空或不存在"); + 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("当前用户 ID: %s", g_my_id); + } else { + ARLOG("WARN: 未能获取当前用户 ID"); + } } +} + +// ── 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; + + // 懒加载当前用户 ID(首次遇到撤回消息时,登录一定已完成) + load_my_user_id(); + + // msg+0x18: 撤回操作发起者的 ID(std::string SSO buffer,直接存储字符内容) + const char *sender = (const char *)((uint8_t *)msg + 0x18); + + // 判断逻辑: + // 1. field18 为空 → 自己撤回确认 → 放行 + // 2. field18 == 自己 ID → 自己撤回 → 放行 + // 3. 其他 → 对方撤回 → 阻止 + if (sender[0] == '\0') { + ARLOG("自己撤回(field=空),放行"); + return 1; + } + + if (g_my_id[0] != '\0' && strncmp(sender, g_my_id, strlen(g_my_id)) == 0) { + ARLOG("自己撤回(%s),放行", g_my_id); + return 1; + } + + ARLOG("对方撤回(%.20s),已阻止", sender); 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(), ^{ + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + + log_open(); + ARLOG("hook_init 启动"); + uintptr_t slide = find_wechat_slide(); - if (slide == 0) return; - void **slot = (void **)(slide + kSlotVA); - uintptr_t page = (uintptr_t)slot & ~0x3FFF; - vm_protect(mach_task_self(), (vm_address_t)page, 0x4000, 0, VM_PROT_READ | VM_PROT_WRITE); - *slot = (void *)&hook_isRevokeMessage; + 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 @@ -234,18 +456,147 @@ INJECT_SCRIPT } resign_app() { - echo "[INFO] 重签名..." + echo "[INFO] 重签名(注入 entitlements 绕过 Library Validation)..." + + # 创建 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 + + + +ENTITLEMENTS + + # 1. 签名 dylib(adhoc) codesign --force --sign - "$DYLIB_DST" 2>/dev/null - codesign --force --sign - "$WECHAT_BIN" 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 - echo "[INFO] 微信版本: 4.1.9 ($EXPECTED_VERSION)" + 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 @@ -254,15 +605,15 @@ do_install() { kill_wechat - # 尝试写入测试 - if ! touch "$DYLIB_DST" 2>/dev/null; then - remove_provenance - fi + # 无条件清除 provenance(即使 .app 顶层无标记,内层文件也可能有) + # 重打包是幂等操作,不会造成损坏 + remove_provenance rm -f "$DYLIB_DST" 2>/dev/null || true compile_dylib inject_dylib resign_app + verify_install echo "" echo "==============================" @@ -272,7 +623,12 @@ do_install() { echo " 功能: 对方撤回的消息将保留可见" echo " 自己撤回消息正常工作" echo "" - echo " 打开微信: open /Applications/WeChat.app" + echo " 验证步骤:" + echo " 1. 让别人发一条消息,然后撤回" + echo " 2. 如果消息保留可见 → 防撤回生效" + echo " 3. 如果消息仍被撤回 → 执行以下命令查看调试日志:" + echo " cat /tmp/antirevoke_debug.log" + echo "" echo " 卸载: $0 --uninstall" echo "" }