14 Commits

Author SHA1 Message Date
zhangyang131 faf7db0482 拦截到撤回时弹出 macOS 系统通知(显示谁撤回了消息) 2026-05-29 19:10:53 +08:00
a244573118 fe51b1f1fe Update README.md 2026-05-29 12:29:47 +08:00
a244573118 31d96c8820 Update README.md 2026-05-29 12:29:25 +08:00
zhangyang131 47e59afaed readme 2026-05-29 12:29:01 +08:00
zhangyang131 ad49ec693c 适配微信4.1.10,兼容性增强 2026-05-29 11:16:34 +08:00
a244573118 0802b092e7 Update README.md 2026-05-27 20:59:43 +08:00
a244573118 8c055418de Update README.md 2026-05-27 20:58:06 +08:00
a244573118 a678ae0047 Update README.md 2026-05-27 20:17:18 +08:00
a244573118 781fdc3560 Update README to clarify message revocation behavior
Clarified the behavior of the message revocation feature.
2026-05-27 20:10:11 +08:00
zhangyang131 be26e43549 修复自己不能撤回的bug 2026-05-27 20:07:47 +08:00
a244573118 eeeec7547b Update README.md 2026-05-27 17:34:21 +08:00
a244573118 8607a6337a Update README.md 2026-05-27 16:57:36 +08:00
zhangyang131 d2ae29e2cd 支持微信4.1.9防撤回 2026-05-27 16:18:40 +08:00
a244573118 3b01d71c73 Update README.md 2025-05-09 11:47:55 +08:00
2 changed files with 952 additions and 17 deletions
+114 -17
View File
@@ -1,36 +1,133 @@
# WeChatIntercept
### 微信防撤回插件,一键安装,仅MAC可用。
1.支持最新版微信3.7.0防撤回;
macOS 微信防撤回工具。
2.新增免认证登录;
## 最新版本
3.新增拦截提示语自定义前缀功能
**支持微信 4.1.9 ~ 4.1.10**,适配微信全新 C++ 架构,通过 DYLD 运行时注入实现防撤回
### 功能
- 对方撤回的消息保留可见
- 自己撤回正常工作
- 撤回时弹出 macOS 系统通知(显示谁撤回了消息)
- 通知开关可随时切换
### 原理
通过注入运行时 hook 动态库(`WeChatAntiRevoke.dylib`),拦截微信内部的 `isRevokeMessage()` 函数:
- 对方撤回 → 返回 false(消息保留)+ 弹出通知
- 自己撤回 → 返回 true(正常处理)
通过读取当前登录用户 ID(完整字符串匹配),精确区分自己与对方。
### 适用范围
- macOS 微信 4.1.9、4.1.10
- Apple Siliconarm64)及 Intelx86_64
- macOS Sequoia / Sonoma / Ventura 等(自动处理 provenance 限制)
### 使用
```bash
chmod +x patch.sh # 添加可执行权限
./patch.sh # 安装防撤回
./patch.sh openNotify # 开启撤回通知
./patch.sh closeNotify # 关闭撤回通知
./patch.sh --uninstall # 卸载
./patch.sh --help # 帮助
```
首次运行可能需要约 30 秒(自动解除系统文件保护并重签名)。
### 配置
配置文件路径:`~/.config/antirevoke/config`
```ini
notify=1 # 1=开启撤回通知, 0=关闭
```
安装时默认开启通知,也可通过命令随时切换:
```bash
./patch.sh openNotify # 等同于设置 notify=1
./patch.sh closeNotify # 等同于设置 notify=0
```
修改后立即生效,无需重启微信。
### 依赖
macOS 系统自带工具,无需额外安装:
- clangXcode Command Line Tools
- python3
- codesign
- tar
如未安装 Xcode Command Line Tools,运行:`xcode-select --install`
### 调试
```bash
./patch.sh --debug # 调试模式(不安装 hook,仅签名允许 lldb attach
cat /tmp/antirevoke_debug.log # 查看运行时日志
```
### 已知限制
- **聊天框内无撤回提示**:由于微信 4.x 的架构限制(C++ 实现 + 符号 strip + 数据库加密),无法在聊天界面内插入系统消息。替代方案为 macOS 系统通知。
- **为什么不能像旧版那样在聊天框内显示提示?**
旧版微信 macOS3.x)使用 Objective-C,可通过 Method Swizzling 调用内部消息插入 API。4.x 版本核心逻辑全部迁移到 C++(仅剩 65 个 ObjC 类,90MB+ 代码段,符号已 strip),撤回处理通过虚函数 + 加密数据库 + 协程调度完成,无法稳定地从外部构造调用链插入消息。
### 技术文档
详细的逆向分析过程和适配指南见 [`doc/reverse-engineering-guide.md`](doc/reverse-engineering-guide.md)。
---
## 旧版本
支持微信 3.7.0 及更早版本,基于 DYLD 注入 + Method Swizzling,支持撤回提示和自定义前缀。
### 功能
1. 防撤回(支持聊天框内拦截提示)
2. 免认证登录
3. 拦截提示语自定义前缀
<img width="301" alt="image" src="https://user-images.githubusercontent.com/18585610/159691061-3f24b69f-a494-4549-a530-7724b1b40060.png">
### 微信 v3.7.0 下载
[微信 v3.7.0 版本](https://dldir1.qq.com/weixin/mac/WeChatMac.dmg)
### 使用方法
自定义防撤回提示前缀:安装成功后重启微信,屏幕左上角微信菜单栏有个小助手菜单,修改后点击关闭即可
安装方法:cd到WeChatIntercept文件夹,将Install.sh文件拖到终端,输入密码回车,然后重启微信即可。
卸载方法:将Uninstall.sh文件拖到终端,然后回车。
此插件将一直更新,欢迎Star
- **安装**cd 到 WeChatIntercept 文件夹,将 `Install.sh` 拖到终端,输入密码回车,重启微信
- **卸载**:将 `Uninstall.sh` 拖到终端回车。
- **自定义前缀**:安装成功后重启微信,屏幕左上角微信菜单栏有个小助手菜单,修改后点击关闭即可。
### 常见问题
1.无法打开insert_dylib,因为无法验证开发者
1. **无法打开 "insert_dylib",因为无法验证开发者**
在系统安全性与隐私处点击允许
在系统安全性与隐私处点击允许
2.如果系统提示遇到截屏无法使用,已添加微信的屏幕录制权限仍不行
2. **截屏无法使用,已添加屏幕录制权限仍不行**
在系统安全性与隐私中删除掉WeChat/微信,然后重新添加进来,重启WeChat/微信,就可以正常截图(感谢[Kylelkh](https://github.com/Kylelkh)
在系统安全性与隐私中删除微信,重新添加,重启微信即可。(感谢 [Kylelkh](https://github.com/Kylelkh)
3.M1芯片怎么使用
3. **M1 芯片怎么使用**
安装Rosetta软件,然后在属性勾选使用Rosetta打开微信就可以了(感谢[Mercury2699](https://github.com/Mercury2699)、[bolosea](https://github.com/bolosea)
安装 Rosetta,在微信属性勾选"使用 Rosetta 打开"。(感谢 [Mercury2699](https://github.com/Mercury2699)、[bolosea](https://github.com/bolosea)
---
## 注意
- 微信更新后需重新运行 `./patch.sh`
- 仅供学习研究用途
Executable
+838
View File
@@ -0,0 +1,838 @@
#!/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"
# 递归清除残留 xattrbest-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 <Foundation/Foundation.h>
#import <mach-o/dyld.h>
#import <mach/mach.h>
#import <sys/mman.h>
#import <libkern/OSCacheControl.h>
#import <stdint.h>
#import <string.h>
#import <stdio.h>
#import <sys/stat.h>
// ── 日志 ─────────────────────────────────────────────────────
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);
// 提取 replacemsg 并发通知
const char *xml_body = NULL;
uint64_t xml_ptr = *(uint64_t *)((uint8_t *)msg + 0x130);
uint64_t xml_len = *(uint64_t *)((uint8_t *)msg + 0x138);
if (xml_ptr != 0 && xml_len > 0 && xml_len < 4096)
xml_body = (const char *)xml_ptr;
char notify_text[256] = {0};
if (xml_body) {
const char *cs = strstr(xml_body, "<![CDATA[");
const char *ce = cs ? strstr(cs, "]]>") : 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';
}
}
}
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 trampoline20 字节)─────────────────────────
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 trampoline16 字节)───────────────────────
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('<I', f.read(4))[0]
if magic != 0xFEEDFACF:
continue
f.read(12)
ncmds_pos = f.tell()
ncmds = struct.unpack('<I', f.read(4))[0]
sizeofcmds_pos = f.tell()
sizeofcmds = struct.unpack('<I', f.read(4))[0]
f.read(8)
# Check if already injected
header_end = slice_offset + 32
f.seek(header_end)
already = False
for i in range(ncmds):
pos = f.tell()
cmd = struct.unpack('<I', f.read(4))[0]
cs = struct.unpack('<I', f.read(4))[0]
if cmd == 0xC:
no = struct.unpack('<I', f.read(4))[0]
f.seek(pos + no)
name = b''
while True:
b = f.read(1)
if b == b'\x00': break
name += b
if b'WeChatAntiRevoke' in name:
already = True
break
f.seek(pos + cs)
if already:
continue
insert_pos = slice_offset + 32 + sizeofcmds
lc = struct.pack('<I', 0xC)
lc += struct.pack('<I', cmd_size)
lc += struct.pack('<I', 24)
lc += struct.pack('<I', 2)
lc += struct.pack('<I', 0x10000)
lc += struct.pack('<I', 0x10000)
lc += dylib_name
while len(lc) < cmd_size:
lc += b'\x00'
f.seek(insert_pos)
f.write(lc)
f.seek(ncmds_pos)
f.write(struct.pack('<I', ncmds + 1))
f.seek(sizeofcmds_pos)
f.write(struct.pack('<I', sizeofcmds + cmd_size))
print("ok")
INJECT_SCRIPT
echo "[INFO] 注入完成"
}
resign_app() {
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. 签名 dylibadhoc
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
# 清除 xattrbest-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'
<?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/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
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 <slide+0x4D5FD70>"
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