mirror of
https://github.com/a244573118/WeChatIntercept.git
synced 2026-06-14 13:35:08 +08:00
适配微信4.1.10,兼容性增强
This commit is contained in:
parent
0802b092e7
commit
ad49ec693c
434
patch.sh
434
patch.sh
|
|
@ -5,13 +5,14 @@
|
||||||
# ============================================================
|
# ============================================================
|
||||||
#
|
#
|
||||||
# 适用版本: 微信 4.1.9 (CFBundleVersion: 268602)
|
# 适用版本: 微信 4.1.9 (CFBundleVersion: 268602)
|
||||||
|
# 微信 4.1.10 (CFBundleVersion: 268824)
|
||||||
# 适用平台: macOS (Apple Silicon + Intel)
|
# 适用平台: macOS (Apple Silicon + Intel)
|
||||||
# 依赖工具: clang, codesign, python3 (macOS 系统自带)
|
# 依赖工具: clang, codesign, python3 (macOS 系统自带)
|
||||||
#
|
#
|
||||||
# 使用方法:
|
# 使用方法:
|
||||||
# chmod +x install.sh
|
# chmod +x patch.sh
|
||||||
# ./install.sh # 安装防撤回
|
# ./patch.sh # 安装防撤回
|
||||||
# ./install.sh --uninstall # 卸载(恢复原始微信)
|
# ./patch.sh --uninstall # 卸载(恢复原始微信)
|
||||||
#
|
#
|
||||||
# 原理:
|
# 原理:
|
||||||
# 通过 DYLD 注入一个运行时 hook 动态库,
|
# 通过 DYLD 注入一个运行时 hook 动态库,
|
||||||
|
|
@ -20,6 +21,9 @@
|
||||||
# - 对方撤回 → 返回 false(消息保留不被删除)
|
# - 对方撤回 → 返回 false(消息保留不被删除)
|
||||||
# - 自己撤回 → 返回 true(正常处理,不会闪退)
|
# - 自己撤回 → 返回 true(正常处理,不会闪退)
|
||||||
#
|
#
|
||||||
|
# 4.1.9: 通过写入内建 hook dispatch slot (BSS 区域) 实现
|
||||||
|
# 4.1.10: dispatch slot 机制已移除,改用 inline trampoline patch
|
||||||
|
#
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
@ -28,13 +32,16 @@ WECHAT_APP="/Applications/WeChat.app"
|
||||||
WECHAT_BIN="$WECHAT_APP/Contents/MacOS/WeChat"
|
WECHAT_BIN="$WECHAT_APP/Contents/MacOS/WeChat"
|
||||||
DYLIB_DST="$WECHAT_APP/Contents/Resources/WeChatAntiRevoke.dylib"
|
DYLIB_DST="$WECHAT_APP/Contents/Resources/WeChatAntiRevoke.dylib"
|
||||||
DYLIB_INSTALL_NAME="@executable_path/../Resources/WeChatAntiRevoke.dylib"
|
DYLIB_INSTALL_NAME="@executable_path/../Resources/WeChatAntiRevoke.dylib"
|
||||||
EXPECTED_VERSION="268602"
|
|
||||||
|
# 支持的版本列表
|
||||||
|
VERSION_419="268602"
|
||||||
|
VERSION_4110="268824"
|
||||||
|
|
||||||
print_banner() {
|
print_banner() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo " 微信防撤回安装工具"
|
echo " 微信防撤回安装工具"
|
||||||
echo " 适用: macOS / 微信 4.1.9"
|
echo " 适用: macOS / 微信 4.1.9 & 4.1.10"
|
||||||
echo " 支持: Apple Silicon + Intel"
|
echo " 支持: Apple Silicon + Intel"
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -47,8 +54,8 @@ check_environment() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleVersion 2>/dev/null)
|
VERSION=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleVersion 2>/dev/null)
|
||||||
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
if [ "$VERSION" != "$VERSION_419" ] && [ "$VERSION" != "$VERSION_4110" ]; then
|
||||||
echo "[ERROR] 微信版本不匹配 (期望 $EXPECTED_VERSION, 实际 $VERSION)"
|
echo "[ERROR] 不支持的微信版本 (实际 $VERSION,支持 $VERSION_419 / $VERSION_4110)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -68,13 +75,24 @@ kill_wechat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_provenance() {
|
remove_provenance() {
|
||||||
echo "[INFO] 解除系统文件保护(约 30 秒)..."
|
echo "[INFO] 尝试解除系统文件保护..."
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
tar --no-xattrs -cf - -C /Applications WeChat.app | tar -xf - -C "$TMP_DIR/"
|
tar --no-xattrs -cf - -C /Applications WeChat.app | tar -xf - -C "$TMP_DIR/"
|
||||||
rm -rf "$WECHAT_APP"
|
rm -rf "$WECHAT_APP"
|
||||||
mv "$TMP_DIR/WeChat.app" "$WECHAT_APP"
|
mv "$TMP_DIR/WeChat.app" "$WECHAT_APP"
|
||||||
rm -rf "$TMP_DIR"
|
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() {
|
compile_dylib() {
|
||||||
|
|
@ -87,45 +105,249 @@ compile_dylib() {
|
||||||
#import <mach-o/dyld.h>
|
#import <mach-o/dyld.h>
|
||||||
#import <mach/mach.h>
|
#import <mach/mach.h>
|
||||||
#import <sys/mman.h>
|
#import <sys/mman.h>
|
||||||
|
#import <libkern/OSCacheControl.h>
|
||||||
#import <stdint.h>
|
#import <stdint.h>
|
||||||
|
#import <string.h>
|
||||||
|
#import <stdio.h>
|
||||||
|
|
||||||
static const uintptr_t kSlotVA = 0x9301838;
|
// ── 日志(写入 /tmp/antirevoke_debug.log)────────────────────
|
||||||
static const char *kDylibSuffix = "Resources/wechat.dylib";
|
static FILE *g_logFile = NULL;
|
||||||
static const int kMsgTypeOffset = 0x0C;
|
|
||||||
static const int kRevokeType = 0x2712;
|
|
||||||
|
|
||||||
static _Bool hook_isRevokeMessage(void *msg) {
|
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
|
||||||
|
|
||||||
|
// ── 版本地址表 ───────────────────────────────────────────────
|
||||||
|
// 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;
|
if (msg == NULL) return 0;
|
||||||
int32_t msgType = *(int32_t *)((uint8_t *)msg + kMsgTypeOffset);
|
|
||||||
|
int32_t msgType = *(int32_t *)((uint8_t *)msg + 0x0C);
|
||||||
if (msgType != kRevokeType) return 0;
|
if (msgType != kRevokeType) return 0;
|
||||||
uint32_t field18 = *(uint32_t *)((uint8_t *)msg + 0x18);
|
|
||||||
if (field18 == 0x64697877) return 0; // "wxid" = 对方撤回 → 阻止
|
// 懒加载当前用户 ID(首次遇到撤回消息时,登录一定已完成)
|
||||||
return 1; // 自己撤回 → 放行
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uintptr_t find_wechat_slide(void) {
|
if (g_my_id[0] != '\0' && strncmp(sender, g_my_id, strlen(g_my_id)) == 0) {
|
||||||
uint32_t count = _dyld_image_count();
|
ARLOG("自己撤回(%s),放行", g_my_id);
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
return 1;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ARLOG("对方撤回(%.20s),已阻止", sender);
|
||||||
return 0;
|
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))
|
__attribute__((constructor))
|
||||||
static void hook_init(void) {
|
static void hook_init(void) {
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)),
|
dispatch_after(
|
||||||
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)),
|
||||||
dispatch_get_main_queue(), ^{
|
dispatch_get_main_queue(), ^{
|
||||||
|
|
||||||
|
log_open();
|
||||||
|
ARLOG("hook_init 启动");
|
||||||
|
|
||||||
uintptr_t slide = find_wechat_slide();
|
uintptr_t slide = find_wechat_slide();
|
||||||
if (slide == 0) return;
|
if (slide == 0) { ARLOG("ERROR: 未找到 wechat.dylib"); return; }
|
||||||
void **slot = (void **)(slide + kSlotVA);
|
|
||||||
uintptr_t page = (uintptr_t)slot & ~0x3FFF;
|
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);
|
vm_protect(mach_task_self(), (vm_address_t)page, 0x4000, 0, VM_PROT_READ | VM_PROT_WRITE);
|
||||||
*slot = (void *)&hook_isRevokeMessage;
|
*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
|
HOOK_SOURCE
|
||||||
|
|
@ -234,18 +456,147 @@ INJECT_SCRIPT
|
||||||
}
|
}
|
||||||
|
|
||||||
resign_app() {
|
resign_app() {
|
||||||
echo "[INFO] 重签名..."
|
echo "[INFO] 重签名(注入 entitlements 绕过 Library Validation)..."
|
||||||
|
|
||||||
|
# 创建 entitlements 文件
|
||||||
|
local ENT_FILE=$(mktemp /tmp/entitlements_XXXXXX.plist)
|
||||||
|
cat > "$ENT_FILE" << 'ENTITLEMENTS'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
ENTITLEMENTS
|
||||||
|
|
||||||
|
# 1. 签名 dylib(adhoc)
|
||||||
codesign --force --sign - "$DYLIB_DST" 2>/dev/null
|
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
|
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
|
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() {
|
do_install() {
|
||||||
print_banner
|
print_banner
|
||||||
check_environment
|
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
|
if [ -f "$DYLIB_DST" ]; then
|
||||||
|
|
@ -254,15 +605,15 @@ do_install() {
|
||||||
|
|
||||||
kill_wechat
|
kill_wechat
|
||||||
|
|
||||||
# 尝试写入测试
|
# 无条件清除 provenance(即使 .app 顶层无标记,内层文件也可能有)
|
||||||
if ! touch "$DYLIB_DST" 2>/dev/null; then
|
# 重打包是幂等操作,不会造成损坏
|
||||||
remove_provenance
|
remove_provenance
|
||||||
fi
|
|
||||||
rm -f "$DYLIB_DST" 2>/dev/null || true
|
rm -f "$DYLIB_DST" 2>/dev/null || true
|
||||||
|
|
||||||
compile_dylib
|
compile_dylib
|
||||||
inject_dylib
|
inject_dylib
|
||||||
resign_app
|
resign_app
|
||||||
|
verify_install
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
|
|
@ -272,7 +623,12 @@ do_install() {
|
||||||
echo " 功能: 对方撤回的消息将保留可见"
|
echo " 功能: 对方撤回的消息将保留可见"
|
||||||
echo " 自己撤回消息正常工作"
|
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 " 卸载: $0 --uninstall"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user