mirror of
https://github.com/a244573118/WeChatIntercept.git
synced 2026-06-14 05:25:08 +08:00
增加特征码自动寻址机制,兼容性更强
This commit is contained in:
parent
e1047bcc8b
commit
608934165d
51
README.md
51
README.md
|
|
@ -4,14 +4,15 @@ macOS 微信防撤回工具,持续更新,欢迎star。
|
||||||
|
|
||||||
## 最新版本
|
## 最新版本
|
||||||
|
|
||||||
**支持微信 4.1.9 ~ 4.1.10**,适配微信全新 C++ 架构,通过 DYLD 运行时注入实现防撤回。
|
**支持微信 4.1.x 系列**,适配微信全新 C++ 架构,通过 DYLD 运行时注入实现防撤回。内置**特征码自动寻址**机制,应对微信动态更新。
|
||||||
|
|
||||||
### 功能
|
### 功能
|
||||||
|
|
||||||
- 对方撤回的消息保留可见
|
- 对方撤回的消息保留可见
|
||||||
- 自己撤回正常工作
|
- 自己撤回正常工作
|
||||||
- 撤回时弹出 macOS 系统通知(显示谁撤回了消息)
|
- 撤回时弹出 macOS 系统通知(显示谁撤回了消息,含用户昵称)
|
||||||
- 通知开关可随时切换
|
- 通知开关可随时切换
|
||||||
|
- 微信动态更新后自动适配(特征码搜索)
|
||||||
|
|
||||||
### 原理
|
### 原理
|
||||||
|
|
||||||
|
|
@ -22,11 +23,16 @@ macOS 微信防撤回工具,持续更新,欢迎star。
|
||||||
|
|
||||||
通过读取当前登录用户 ID(完整字符串匹配),精确区分自己与对方。
|
通过读取当前登录用户 ID(完整字符串匹配),精确区分自己与对方。
|
||||||
|
|
||||||
|
Hook 安装采用三级查找策略:
|
||||||
|
1. **快速路径**:硬编码地址 + 完整 5 条指令特征码验证
|
||||||
|
2. **特征码搜索**:扫描 `wechat.dylib __TEXT` 段,自动定位新版本函数地址
|
||||||
|
3. **fallback**:4.1.9 dispatch slot 写入
|
||||||
|
|
||||||
### 适用范围
|
### 适用范围
|
||||||
|
|
||||||
- macOS 微信 4.1.9、4.1.10
|
- macOS 微信 4.1.x 系列(4.1.9 / 4.1.10 已验证,更新版本依赖运行时自动寻址)
|
||||||
- Apple Silicon(arm64)及 Intel(x86_64)
|
- Apple Silicon(arm64)及 Intel(x86_64)
|
||||||
- macOS Sequoia / Sonoma / Ventura 等(自动处理 provenance 限制)
|
- macOS Sequoia / Sonoma / Ventura / Tahoe 等(自动处理 provenance 限制)
|
||||||
|
|
||||||
### 使用
|
### 使用
|
||||||
|
|
||||||
|
|
@ -79,6 +85,37 @@ macOS 系统自带工具,无需额外安装:
|
||||||
cat /tmp/antirevoke_debug.log # 查看运行时日志
|
cat /tmp/antirevoke_debug.log # 查看运行时日志
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 微信版本更新后
|
||||||
|
|
||||||
|
微信支持动态更新(无需经过 App Store),更新后 build 号变化可能导致 hook 失效。
|
||||||
|
|
||||||
|
脚本会自动应对:
|
||||||
|
|
||||||
|
1. **特征码动态搜索**:当硬编码地址失效时,自动扫描 `wechat.dylib` 的 `__TEXT` 段查找 `isRevokeMessage` 函数特征码。微信小版本更新(仅函数地址改变、实现不变)的情况下,**自动适配,无需任何操作**。
|
||||||
|
|
||||||
|
2. **大版本校验放宽**:脚本只校验 `CFBundleShortVersionString` 是否为 `4.1.x`,不再依赖精确 build 号。新 build 号(如 4.1.11)也能正常安装,运行时通过特征码自动寻址。
|
||||||
|
|
||||||
|
3. **失败提示**:如果特征码也匹配不到(微信改了实现),系统通知会弹出提示:
|
||||||
|
- `WeChatIntercept 需更新` — 微信版本号变化(如 4.1.10 → 4.1.11),需要更新脚本
|
||||||
|
- `WeChatIntercept 异常` — 已适配的 build 号但仍失败(极罕见)
|
||||||
|
|
||||||
|
4. **消息对象偏移失效**:如果 `msg+0x18` 偏移读取的 sender ID 持续出现非可打印 ASCII 内容,累计达到阈值后会弹出 `"快去催 WeChatIntercept 作者更新适配"` 通知。日常对方撤回的多次调用不会误触发。
|
||||||
|
|
||||||
|
### 排查步骤
|
||||||
|
|
||||||
|
如果防撤回功能不生效:
|
||||||
|
|
||||||
|
1. 查看运行时日志:`cat /tmp/antirevoke_debug.log`
|
||||||
|
2. 关键日志说明:
|
||||||
|
- `[已适配]` — build 号在已知列表,安装应该成功
|
||||||
|
- `[未适配]` — build 号未知,靠运行时自动寻址
|
||||||
|
- `快速路径命中` — 硬编码地址有效,正常工作
|
||||||
|
- `特征码搜索找到` — 自动找到新地址,正常工作(说明微信地址变了但实现没变)
|
||||||
|
- `hook 安装失败` — 需要更新脚本,请前往 GitHub 检查最新版本或提交 issue
|
||||||
|
3. 提交 issue 时附带:
|
||||||
|
- 微信版本:`defaults read /Applications/WeChat.app/Contents/Info.plist CFBundleShortVersionString` 与 `CFBundleVersion`
|
||||||
|
- 调试日志:`/tmp/antirevoke_debug.log`
|
||||||
|
|
||||||
### 已知限制
|
### 已知限制
|
||||||
|
|
||||||
- **聊天框内无撤回提示**:由于微信 4.x 的架构限制(C++ 实现 + 符号 strip + 数据库加密),无法在聊天界面内插入系统消息。替代方案为 macOS 系统通知。
|
- **聊天框内无撤回提示**:由于微信 4.x 的架构限制(C++ 实现 + 符号 strip + 数据库加密),无法在聊天界面内插入系统消息。替代方案为 macOS 系统通知。
|
||||||
|
|
@ -89,11 +126,11 @@ cat /tmp/antirevoke_debug.log # 查看运行时日志
|
||||||
|
|
||||||
### 风险说明
|
### 风险说明
|
||||||
|
|
||||||
1.微信每次升级后,地址、结构体字段、运行时行为都可能变化,补丁可能立即失效。
|
1. 微信每次升级后,地址、结构体字段、运行时行为都可能变化,补丁可能立即失效。脚本内置特征码自动寻址机制可应对函数地址变化,但无法应对函数实现/消息对象结构的根本变化。
|
||||||
|
|
||||||
2.本项目只承诺仓库内标明的支持版本,不承诺自动兼容未来版本。
|
2. 本项目仅承诺仓库内标明的已验证版本(4.1.9 / 4.1.10);4.1.x 系列其他 build 号通过运行时自动寻址尽力支持,但不保证 100% 兼容。
|
||||||
|
|
||||||
3.本项目仅用于技术研究与兼容性分析,请自行承担使用风险。
|
3. 本项目仅用于技术研究与兼容性分析,请自行承担使用风险。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
355
patch.sh
355
patch.sh
|
|
@ -33,15 +33,11 @@ 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"
|
||||||
|
|
||||||
# 支持的版本列表
|
|
||||||
VERSION_419="268602"
|
|
||||||
VERSION_4110="268824"
|
|
||||||
|
|
||||||
print_banner() {
|
print_banner() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo " 微信防撤回安装工具"
|
echo " 微信防撤回安装工具"
|
||||||
echo " 适用: macOS / 微信 4.1.9 & 4.1.10"
|
echo " 适用: macOS / 微信 4.1.9+"
|
||||||
echo " 支持: Apple Silicon + Intel"
|
echo " 支持: Apple Silicon + Intel"
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -53,12 +49,28 @@ check_environment() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
SHORT_VER=$(defaults read "$WECHAT_APP/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null)
|
||||||
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" != "$VERSION_419" ] && [ "$VERSION" != "$VERSION_4110" ]; then
|
|
||||||
echo "[ERROR] 不支持的微信版本 (实际 $VERSION,支持 $VERSION_419 / $VERSION_4110)"
|
if [ -z "$SHORT_VER" ]; then
|
||||||
|
echo "[ERROR] 无法读取微信版本号,请检查 /Applications/WeChat.app 是否完整"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 大版本校验:仅支持 4.1.x 系列(C++ 架构)
|
||||||
|
case "$SHORT_VER" in
|
||||||
|
4.1.*)
|
||||||
|
echo "[INFO] 微信版本: $SHORT_VER ($VERSION)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[ERROR] 不支持的微信大版本: $SHORT_VER"
|
||||||
|
echo " 本工具仅支持 4.1.x 系列"
|
||||||
|
echo " 旧版 3.x 请使用 Install.sh"
|
||||||
|
echo " 如果你认为这是误判,请提交 issue"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if ! command -v clang &>/dev/null; then
|
if ! command -v clang &>/dev/null; then
|
||||||
echo "[ERROR] 未找到 clang,请安装 Xcode Command Line Tools:"
|
echo "[ERROR] 未找到 clang,请安装 Xcode Command Line Tools:"
|
||||||
echo " xcode-select --install"
|
echo " xcode-select --install"
|
||||||
|
|
@ -99,10 +111,12 @@ compile_dylib() {
|
||||||
echo "[INFO] 编译 hook 动态库..."
|
echo "[INFO] 编译 hook 动态库..."
|
||||||
|
|
||||||
# 内嵌 hook.m 源码
|
# 内嵌 hook.m 源码
|
||||||
local SRC_FILE=$(mktemp /tmp/hook_XXXXXX.m)
|
local SRC_FILE="/tmp/antirevoke_hook_src.m"
|
||||||
|
rm -f "$SRC_FILE"
|
||||||
cat > "$SRC_FILE" << 'HOOK_SOURCE'
|
cat > "$SRC_FILE" << 'HOOK_SOURCE'
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <mach-o/dyld.h>
|
#import <mach-o/dyld.h>
|
||||||
|
#import <mach-o/loader.h>
|
||||||
#import <mach/mach.h>
|
#import <mach/mach.h>
|
||||||
#import <sys/mman.h>
|
#import <sys/mman.h>
|
||||||
#import <libkern/OSCacheControl.h>
|
#import <libkern/OSCacheControl.h>
|
||||||
|
|
@ -233,6 +247,18 @@ static void send_notification(const char *text) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 检查 sender 偏移是否仍有效 ───────────────────────────────
|
||||||
|
// 仅检查前 4 字节是否为可打印 ASCII(微信 ID 总以可打印字符开头)
|
||||||
|
// 缩小检查范围避免误判撤回流程中的二次调用(其 sender 可能是 std::string 元数据)
|
||||||
|
static _Bool is_valid_sender(const char *s) {
|
||||||
|
if (s[0] == '\0') return 1; // 空字符串 = 自己撤回确认
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
unsigned char c = (unsigned char)s[i];
|
||||||
|
if (c < 0x20 || c > 0x7E) return 0; // 非可打印字符
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// ── hook 函数 ────────────────────────────────────────────────
|
// ── hook 函数 ────────────────────────────────────────────────
|
||||||
__attribute__((visibility("default")))
|
__attribute__((visibility("default")))
|
||||||
_Bool hook_isRevokeMessage(void *msg) {
|
_Bool hook_isRevokeMessage(void *msg) {
|
||||||
|
|
@ -245,6 +271,32 @@ _Bool hook_isRevokeMessage(void *msg) {
|
||||||
|
|
||||||
const char *sender = (const char *)((uint8_t *)msg + 0x18);
|
const char *sender = (const char *)((uint8_t *)msg + 0x18);
|
||||||
|
|
||||||
|
// 检查 sender 偏移是否仍有效(前 4 字节必须可打印 ASCII)
|
||||||
|
// 失效时静默放行(return 1),避免影响撤回流程的内部状态
|
||||||
|
// 真正的偏移失效会持续触发,达到阈值时弹一次"催更新"通知
|
||||||
|
if (!is_valid_sender(sender)) {
|
||||||
|
ARLOG("WARN: sender 区域非可打印 ASCII,跳过此次调用");
|
||||||
|
|
||||||
|
// 累计失效次数,达到阈值时弹通知催更新(仅一次)
|
||||||
|
static int g_invalid_count = 0;
|
||||||
|
static _Bool g_warned = 0;
|
||||||
|
g_invalid_count++;
|
||||||
|
if (g_invalid_count >= 5 && !g_warned) {
|
||||||
|
g_warned = 1;
|
||||||
|
char *cmd = (char *)malloc(1024);
|
||||||
|
if (cmd) {
|
||||||
|
snprintf(cmd, 1024,
|
||||||
|
"osascript -e 'display notification \"sender 偏移已失效,快去催 WeChatIntercept 作者更新适配\" "
|
||||||
|
"with title \"WeChatIntercept 需更新\"' &");
|
||||||
|
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||||
|
system(cmd);
|
||||||
|
free(cmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1; // 静默放行,不影响业务流程
|
||||||
|
}
|
||||||
|
|
||||||
// 自己撤回 → 放行
|
// 自己撤回 → 放行
|
||||||
if (sender[0] == '\0') return 1;
|
if (sender[0] == '\0') return 1;
|
||||||
if (g_my_id[0] != '\0' && strncmp(sender, g_my_id, strlen(g_my_id)) == 0) return 1;
|
if (g_my_id[0] != '\0' && strncmp(sender, g_my_id, strlen(g_my_id)) == 0) return 1;
|
||||||
|
|
@ -285,11 +337,12 @@ _Bool hook_isRevokeMessage(void *msg) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 查找 wechat.dylib 的 ASLR slide ─────────────────────────
|
// ── 查找 wechat.dylib 的 ASLR slide 和 mach_header ───────────
|
||||||
// 优先匹配 Resources/wechat.dylib(核心库),Frameworks/ 为 stub 不可用
|
// 优先匹配 Resources/wechat.dylib(核心库),Frameworks/ 为 stub 不可用
|
||||||
static uintptr_t find_wechat_slide(void) {
|
static uintptr_t find_wechat_slide(const struct mach_header **out_header) {
|
||||||
uint32_t count = _dyld_image_count();
|
uint32_t count = _dyld_image_count();
|
||||||
uintptr_t fallback = 0;
|
uintptr_t fallback = 0;
|
||||||
|
const struct mach_header *fallback_header = NULL;
|
||||||
size_t resLen = strlen(kDylibSuffix_Resources);
|
size_t resLen = strlen(kDylibSuffix_Resources);
|
||||||
size_t fwLen = strlen(kDylibSuffix_Frameworks);
|
size_t fwLen = strlen(kDylibSuffix_Frameworks);
|
||||||
|
|
||||||
|
|
@ -297,14 +350,133 @@ static uintptr_t find_wechat_slide(void) {
|
||||||
const char *name = _dyld_get_image_name(i);
|
const char *name = _dyld_get_image_name(i);
|
||||||
if (!name) continue;
|
if (!name) continue;
|
||||||
size_t len = strlen(name);
|
size_t len = strlen(name);
|
||||||
if (len >= resLen && strcmp(name + len - resLen, kDylibSuffix_Resources) == 0)
|
if (len >= resLen && strcmp(name + len - resLen, kDylibSuffix_Resources) == 0) {
|
||||||
|
if (out_header) *out_header = _dyld_get_image_header(i);
|
||||||
return (uintptr_t)_dyld_get_image_vmaddr_slide(i);
|
return (uintptr_t)_dyld_get_image_vmaddr_slide(i);
|
||||||
if (len >= fwLen && strcmp(name + len - fwLen, kDylibSuffix_Frameworks) == 0)
|
}
|
||||||
|
if (len >= fwLen && strcmp(name + len - fwLen, kDylibSuffix_Frameworks) == 0) {
|
||||||
fallback = (uintptr_t)_dyld_get_image_vmaddr_slide(i);
|
fallback = (uintptr_t)_dyld_get_image_vmaddr_slide(i);
|
||||||
|
fallback_header = _dyld_get_image_header(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (out_header) *out_header = fallback_header;
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 解析 wechat.dylib 的 __TEXT 段范围 ───────────────────────
|
||||||
|
// 返回 1 = 成功,0 = 失败
|
||||||
|
static _Bool find_text_segment(const struct mach_header *header, uintptr_t slide,
|
||||||
|
uintptr_t *out_start, size_t *out_size) {
|
||||||
|
if (!header) return 0;
|
||||||
|
|
||||||
|
const uint8_t *p = (const uint8_t *)header;
|
||||||
|
uint32_t ncmds;
|
||||||
|
if (header->magic == MH_MAGIC_64) {
|
||||||
|
p += sizeof(struct mach_header_64);
|
||||||
|
ncmds = ((const struct mach_header_64 *)header)->ncmds;
|
||||||
|
} else if (header->magic == MH_MAGIC) {
|
||||||
|
p += sizeof(struct mach_header);
|
||||||
|
ncmds = header->ncmds;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < ncmds; i++) {
|
||||||
|
const struct load_command *lc = (const struct load_command *)p;
|
||||||
|
if (lc->cmd == LC_SEGMENT_64) {
|
||||||
|
const struct segment_command_64 *seg = (const struct segment_command_64 *)p;
|
||||||
|
if (strcmp(seg->segname, "__TEXT") == 0) {
|
||||||
|
*out_start = (uintptr_t)seg->vmaddr + slide;
|
||||||
|
*out_size = (size_t)seg->vmsize;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (lc->cmd == LC_SEGMENT) {
|
||||||
|
const struct segment_command *seg = (const struct segment_command *)p;
|
||||||
|
if (strcmp(seg->segname, "__TEXT") == 0) {
|
||||||
|
*out_start = (uintptr_t)seg->vmaddr + slide;
|
||||||
|
*out_size = (size_t)seg->vmsize;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p += lc->cmdsize;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 特征码搜索:在 __TEXT 段中查找 isRevokeMessage 函数 ──────
|
||||||
|
// arm64 特征:5 条指令的 isRevokeMessage(无 dispatch slot 的 4.1.10 形态)
|
||||||
|
// LDR W8, [X0, #0xC]; MOV W9, #0x2712; CMP W8, W9; CSET W0, EQ; RET
|
||||||
|
// 返回函数 VA(slide + offset),未找到返回 0
|
||||||
|
static uintptr_t scan_isRevokeMessage_arm64(uintptr_t text_start, size_t text_size) {
|
||||||
|
static const uint32_t pattern[5] = {
|
||||||
|
0xB9400C08u, 0x5284E249u, 0x6B09011Fu, 0x1A9F17E0u, 0xD65F03C0u
|
||||||
|
};
|
||||||
|
const uint32_t *base = (const uint32_t *)text_start;
|
||||||
|
size_t count = text_size / 4;
|
||||||
|
if (count < 5) return 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i + 5 <= count; i++) {
|
||||||
|
if (base[i] == pattern[0] &&
|
||||||
|
base[i+1] == pattern[1] &&
|
||||||
|
base[i+2] == pattern[2] &&
|
||||||
|
base[i+3] == pattern[3] &&
|
||||||
|
base[i+4] == pattern[4]) {
|
||||||
|
return text_start + i * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x86_64 特征:完整函数(16 字节)
|
||||||
|
// 55 48 89 E5 (push rbp; mov rbp,rsp)
|
||||||
|
// 81 7F 0C 12 27 00 00 (cmp [rdi+0xC], 0x2712)
|
||||||
|
// 0F 94 C0 (sete al)
|
||||||
|
// 5D C3 (pop rbp; ret)
|
||||||
|
static uintptr_t scan_isRevokeMessage_x86_64(uintptr_t text_start, size_t text_size) {
|
||||||
|
static const uint8_t pattern[] = {
|
||||||
|
0x55, 0x48, 0x89, 0xE5,
|
||||||
|
0x81, 0x7F, 0x0C, 0x12, 0x27, 0x00, 0x00,
|
||||||
|
0x0F, 0x94, 0xC0,
|
||||||
|
0x5D, 0xC3
|
||||||
|
};
|
||||||
|
const uint8_t *base = (const uint8_t *)text_start;
|
||||||
|
if (text_size < sizeof(pattern)) return 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i + sizeof(pattern) <= text_size; i++) {
|
||||||
|
if (base[i] == pattern[0] &&
|
||||||
|
memcmp(base + i, pattern, sizeof(pattern)) == 0) {
|
||||||
|
return text_start + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 版本检测 ─────────────────────────────────────────────────
|
||||||
|
// 已知支持的 build:4.1.9 (268602)、4.1.10 (268824)
|
||||||
|
static const char *kKnownBuilds[] = { "268602", "268824", NULL };
|
||||||
|
|
||||||
|
static _Bool is_known_build(const char *build) {
|
||||||
|
if (!build) return 0;
|
||||||
|
for (int i = 0; kKnownBuilds[i]; i++) {
|
||||||
|
if (strcmp(build, kKnownBuilds[i]) == 0) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 Info.plist 中的 CFBundleVersion + CFBundleShortVersionString
|
||||||
|
static void read_wechat_version(char *short_ver, size_t short_sz,
|
||||||
|
char *build, size_t build_sz) {
|
||||||
|
short_ver[0] = '\0';
|
||||||
|
build[0] = '\0';
|
||||||
|
@autoreleasepool {
|
||||||
|
NSDictionary *info = [[NSBundle bundleWithPath:@"/Applications/WeChat.app"] infoDictionary];
|
||||||
|
NSString *sv = info[@"CFBundleShortVersionString"];
|
||||||
|
NSString *bv = info[@"CFBundleVersion"];
|
||||||
|
if (sv) strncpy(short_ver, [sv UTF8String], short_sz - 1);
|
||||||
|
if (bv) strncpy(build, [bv UTF8String], build_sz - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 内存保护工具 ─────────────────────────────────────────────
|
// ── 内存保护工具 ─────────────────────────────────────────────
|
||||||
static kern_return_t make_rw(uintptr_t addr, size_t len) {
|
static kern_return_t make_rw(uintptr_t addr, size_t len) {
|
||||||
uintptr_t page = addr & ~(uintptr_t)0x3FFF;
|
uintptr_t page = addr & ~(uintptr_t)0x3FFF;
|
||||||
|
|
@ -360,6 +532,49 @@ static _Bool install_x86_64_trampoline(uintptr_t func_addr, uintptr_t hook_addr)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hook 安装失败时通知用户 ─────────────────────────────────
|
||||||
|
static void notify_install_failed(const char *short_ver, const char *build, _Bool known_build) {
|
||||||
|
if (!is_notify_enabled()) return;
|
||||||
|
|
||||||
|
char *cmd = (char *)malloc(2048);
|
||||||
|
if (!cmd) return;
|
||||||
|
|
||||||
|
char title[64];
|
||||||
|
char body[512];
|
||||||
|
|
||||||
|
if (known_build) {
|
||||||
|
// 已知 build 但仍失败(极罕见)
|
||||||
|
snprintf(title, sizeof(title), "WeChatIntercept 异常");
|
||||||
|
snprintf(body, sizeof(body),
|
||||||
|
"已知版本 %s (%s) hook 安装失败,请查看 /tmp/antirevoke_debug.log",
|
||||||
|
short_ver, build);
|
||||||
|
} else {
|
||||||
|
// 未知 build:可能是版本变化或仅 build 号变化
|
||||||
|
snprintf(title, sizeof(title), "WeChatIntercept 需更新");
|
||||||
|
snprintf(body, sizeof(body),
|
||||||
|
"微信版本 %s (build %s) 未适配,防撤回功能已失效。请前往 GitHub 获取最新脚本",
|
||||||
|
short_ver, build);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转义 body 中的双引号和反斜杠
|
||||||
|
char escaped[1024];
|
||||||
|
int j = 0;
|
||||||
|
for (int i = 0; body[i] && j < (int)sizeof(escaped) - 2; i++) {
|
||||||
|
if (body[i] == '"' || body[i] == '\\') escaped[j++] = '\\';
|
||||||
|
escaped[j++] = body[i];
|
||||||
|
}
|
||||||
|
escaped[j] = '\0';
|
||||||
|
|
||||||
|
snprintf(cmd, 2048,
|
||||||
|
"osascript -e 'display notification \"%s\" with title \"%s\"' &",
|
||||||
|
escaped, title);
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_global_queue(0, 0), ^{
|
||||||
|
system(cmd);
|
||||||
|
free(cmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── 主 constructor ───────────────────────────────────────────
|
// ── 主 constructor ───────────────────────────────────────────
|
||||||
__attribute__((constructor))
|
__attribute__((constructor))
|
||||||
static void hook_init(void) {
|
static void hook_init(void) {
|
||||||
|
|
@ -371,43 +586,125 @@ static void hook_init(void) {
|
||||||
init_config_path();
|
init_config_path();
|
||||||
ARLOG("hook_init 启动");
|
ARLOG("hook_init 启动");
|
||||||
|
|
||||||
uintptr_t slide = find_wechat_slide();
|
// 读取微信版本
|
||||||
if (slide == 0) { ARLOG("ERROR: 未找到 wechat.dylib"); return; }
|
char short_ver[32] = {0};
|
||||||
|
char build[32] = {0};
|
||||||
|
read_wechat_version(short_ver, sizeof(short_ver), build, sizeof(build));
|
||||||
|
_Bool known_build = is_known_build(build);
|
||||||
|
ARLOG("微信版本: %s (build %s) %s", short_ver, build,
|
||||||
|
known_build ? "[已适配]" : "[未适配]");
|
||||||
|
|
||||||
|
const struct mach_header *header = NULL;
|
||||||
|
uintptr_t slide = find_wechat_slide(&header);
|
||||||
|
if (slide == 0) {
|
||||||
|
ARLOG("ERROR: 未找到 wechat.dylib");
|
||||||
|
notify_install_failed(short_ver, build, known_build);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 __TEXT 段范围(用于特征码搜索)
|
||||||
|
uintptr_t text_start = 0;
|
||||||
|
size_t text_size = 0;
|
||||||
|
_Bool has_text = find_text_segment(header, slide, &text_start, &text_size);
|
||||||
|
ARLOG("slide=0x%lx __TEXT=[0x%lx, +0x%zx) found=%d",
|
||||||
|
(unsigned long)slide, (unsigned long)text_start, text_size, has_text);
|
||||||
|
|
||||||
uintptr_t hook = (uintptr_t)&hook_isRevokeMessage;
|
uintptr_t hook = (uintptr_t)&hook_isRevokeMessage;
|
||||||
ARLOG("slide=0x%lx hook=0x%lx", (unsigned long)slide, (unsigned long)hook);
|
_Bool installed = 0;
|
||||||
|
|
||||||
#if defined(__arm64__) || defined(__aarch64__)
|
#if defined(__arm64__) || defined(__aarch64__)
|
||||||
|
// 1. 先尝试硬编码地址(快速路径)
|
||||||
|
uintptr_t func_addr = 0;
|
||||||
uintptr_t func_4110 = slide + k4110_FuncVA_arm64;
|
uintptr_t func_4110 = slide + k4110_FuncVA_arm64;
|
||||||
uint32_t head_insn = *(volatile uint32_t *)func_4110;
|
uint32_t head_insn = *(volatile uint32_t *)func_4110;
|
||||||
|
|
||||||
if (head_insn == 0xB9400C08u) {
|
if (head_insn == 0xB9400C08u) {
|
||||||
if (install_arm64_trampoline(func_4110, hook))
|
// 进一步验证完整 5 条指令特征码(避免误判)
|
||||||
ARLOG("4.1.10 arm64 trampoline 安装成功");
|
uint32_t *p = (uint32_t *)func_4110;
|
||||||
|
if (p[1] == 0x5284E249u && p[2] == 0x6B09011Fu &&
|
||||||
|
p[3] == 0x1A9F17E0u && p[4] == 0xD65F03C0u) {
|
||||||
|
func_addr = func_4110;
|
||||||
|
ARLOG("快速路径命中: 0x%lx", (unsigned long)func_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 快速路径失败 → 尝试 4.1.9 slot
|
||||||
|
if (func_addr == 0) {
|
||||||
|
void **slot = (void **)(slide + k419_SlotVA_arm64);
|
||||||
|
// 简单验证:检查 slot 周围是否在 __DATA 段(不严格)
|
||||||
|
// 先记录,后面如果特征码搜索也失败再尝试 slot
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 特征码搜索(兜底)
|
||||||
|
if (func_addr == 0 && has_text) {
|
||||||
|
ARLOG("快速路径未命中,开始特征码搜索...");
|
||||||
|
uintptr_t found = scan_isRevokeMessage_arm64(text_start, text_size);
|
||||||
|
if (found) {
|
||||||
|
func_addr = found;
|
||||||
|
ARLOG("特征码搜索找到: 0x%lx (offset 0x%lx)",
|
||||||
|
(unsigned long)func_addr, (unsigned long)(func_addr - slide));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 安装 trampoline
|
||||||
|
if (func_addr != 0) {
|
||||||
|
if (install_arm64_trampoline(func_addr, hook)) {
|
||||||
|
ARLOG("arm64 trampoline 安装成功");
|
||||||
|
installed = 1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 5. 最后尝试 4.1.9 slot 方式
|
||||||
void **slot = (void **)(slide + k419_SlotVA_arm64);
|
void **slot = (void **)(slide + k419_SlotVA_arm64);
|
||||||
uintptr_t page = (uintptr_t)slot & ~(uintptr_t)0x3FFF;
|
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);
|
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)page, 0x4000,
|
||||||
*slot = (void *)hook;
|
0, VM_PROT_READ | VM_PROT_WRITE);
|
||||||
ARLOG("4.1.9 arm64 slot 写入成功");
|
if (kr == KERN_SUCCESS) {
|
||||||
|
*slot = (void *)hook;
|
||||||
|
ARLOG("4.1.9 arm64 slot 写入(fallback)");
|
||||||
|
installed = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(__x86_64__)
|
#elif defined(__x86_64__)
|
||||||
|
uintptr_t func_addr = 0;
|
||||||
uintptr_t func_4110_x86 = slide + k4110_FuncVA_x86_64;
|
uintptr_t func_4110_x86 = slide + k4110_FuncVA_x86_64;
|
||||||
uintptr_t func_419_x86 = slide + k419_FuncVA_x86_64;
|
uintptr_t func_419_x86 = slide + k419_FuncVA_x86_64;
|
||||||
const uint32_t kFuncHead = 0xE5894855u;
|
const uint32_t kFuncHead = 0xE5894855u;
|
||||||
|
|
||||||
|
// 1. 快速路径
|
||||||
if (*(volatile uint32_t *)func_4110_x86 == kFuncHead) {
|
if (*(volatile uint32_t *)func_4110_x86 == kFuncHead) {
|
||||||
if (install_x86_64_trampoline(func_4110_x86, hook))
|
func_addr = func_4110_x86;
|
||||||
ARLOG("4.1.10 x86_64 trampoline 安装成功");
|
|
||||||
} else if (*(volatile uint32_t *)func_419_x86 == kFuncHead) {
|
} else if (*(volatile uint32_t *)func_419_x86 == kFuncHead) {
|
||||||
if (install_x86_64_trampoline(func_419_x86, hook))
|
func_addr = func_419_x86;
|
||||||
ARLOG("4.1.9 x86_64 trampoline 安装成功");
|
}
|
||||||
} else {
|
|
||||||
ARLOG("ERROR: x86_64 函数地址未匹配");
|
// 2. 特征码搜索
|
||||||
|
if (func_addr == 0 && has_text) {
|
||||||
|
ARLOG("快速路径未命中,开始特征码搜索...");
|
||||||
|
uintptr_t found = scan_isRevokeMessage_x86_64(text_start, text_size);
|
||||||
|
if (found) {
|
||||||
|
func_addr = found;
|
||||||
|
ARLOG("特征码搜索找到: 0x%lx (offset 0x%lx)",
|
||||||
|
(unsigned long)func_addr, (unsigned long)(func_addr - slide));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 安装 trampoline
|
||||||
|
if (func_addr != 0) {
|
||||||
|
if (install_x86_64_trampoline(func_addr, hook)) {
|
||||||
|
ARLOG("x86_64 trampoline 安装成功");
|
||||||
|
installed = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
ARLOG("就绪,等待撤回消息...");
|
|
||||||
|
if (!installed) {
|
||||||
|
ARLOG("ERROR: hook 安装失败 - 微信版本 %s (build %s) 未适配",
|
||||||
|
short_ver, build);
|
||||||
|
notify_install_failed(short_ver, build, known_build);
|
||||||
|
} else {
|
||||||
|
ARLOG("就绪,等待撤回消息...");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
HOOK_SOURCE
|
HOOK_SOURCE
|
||||||
|
|
@ -654,10 +951,6 @@ do_install() {
|
||||||
print_banner
|
print_banner
|
||||||
check_environment
|
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
|
if [ -f "$DYLIB_DST" ]; then
|
||||||
echo "[INFO] 检测到已安装,将重新安装..."
|
echo "[INFO] 检测到已安装,将重新安装..."
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user