// 通用文本桥接。复用已经实测的版本指纹、字符串分配器、任务调度和卸载保护。 // 单独生成 text_bridge.dll,不替换仍在运行的 selftest_bridge.dll。 #define WXBOT_EXTENDED_RECEIVE #include "selftest_bridge.cpp" struct TextConfig { TestConfig base; char target[128]; }; static_assert(sizeof(TextConfig) == 1688, "Unexpected text config ABI"); struct AtTextConfig { TextConfig base; char members[1024]; }; static_assert(sizeof(AtTextConfig) == 2712, "Unexpected at-text config ABI"); struct MentionContext { const char* members; bool applied; }; using MessageFactory = SharedMessage* (__fastcall*)(SharedMessage*); static MessageFactory original_factory = nullptr; static SharedMessage* __fastcall mention_factory(SharedMessage* output) { _InterlockedIncrement(&active_callbacks); SharedMessage* result = nullptr; __try { result = original_factory(output); const auto context = factory_tls == TLS_OUT_OF_INDEXES ? nullptr : static_cast(TlsGetValue(factory_tls)); if (context) { auto object = output ? static_cast(output->object) : nullptr; if (!object || *reinterpret_cast(object) != core_base + 0x8e53588) RaiseException(ERROR_INVALID_DATA, 0, 0, nullptr); // 实机真正艾特样本:最后两个 std::string 分别为正文 +0x758、成员列表 +0x778。 auto members = reinterpret_cast(object + 0x778); if (members->length != 0 || members->capacity != 15 || !set_native_string(members, context->members)) RaiseException(ERROR_INVALID_DATA, 0, 0, nullptr); context->applied = true; } } __finally { _InterlockedDecrement(&active_callbacks); } return result; } static DWORD ensure_mention_factory() { if (factory_hook_created) return 0; MH_STATUS status = MH_Initialize(); const bool initialized_here = status == MH_OK; if (status != MH_OK && status != MH_ERROR_ALREADY_INITIALIZED) return 600 + status; factory_tls = TlsAlloc(); if (factory_tls == TLS_OUT_OF_INDEXES) { if (initialized_here) MH_Uninitialize(); return ERROR_NOT_ENOUGH_MEMORY; } void* target = reinterpret_cast(core_base + 0x6f4c70); status = MH_CreateHook(target, reinterpret_cast(&mention_factory), reinterpret_cast(&original_factory)); if (status == MH_OK) { status = MH_EnableHook(target); if (status != MH_OK) MH_RemoveHook(target); } if (status != MH_OK) { TlsFree(factory_tls); factory_tls = TLS_OUT_OF_INDEXES; if (initialized_here) MH_Uninitialize(); return 600 + status; } factory_hook_created = true; return 0; } static bool valid_target(const char* target) { size_t length = strnlen_s(target, 128); if (!length || length >= 128) return false; for (size_t index = 0; index < length; ++index) { unsigned char ch = target[index]; if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '.' || ch == '@' || ch == '-')) return false; } return true; } struct TextCompletion { volatile LONG references; uint32_t padding; void (__fastcall* invoke)(TextCompletion*, const unsigned char*); void (__fastcall* destroy)(TextCompletion*); TextConfig config; char members[1024]; }; static void __fastcall text_completion_invoke(TextCompletion* self, const unsigned char* result) { _InterlockedIncrement(&active_callbacks); __try { FILE* log = nullptr; bool success = result && result[0] == 1; if (_wfopen_s(&log, self->config.base.log_path, L"wb") == 0 && log) { std::fprintf(log, "{\"stage\":21,\"exception\":0,\"native_status\":%u,\"target\":", success ? 0 : 1); write_json_string(log, self->config.target); std::fputs(",\"text\":", log); write_json_string(log, self->config.base.text); std::fputs(",\"at_members\":", log); write_json_string(log, self->members); std::fprintf(log, ",\"thread_id\":%lu,\"completion_success\":%s}\n", GetCurrentThreadId(), success ? "true" : "false"); std::fclose(log); } } __finally { _InterlockedDecrement(&active_callbacks); } } static void __fastcall text_completion_destroy(TextCompletion* self) { delete self; _InterlockedDecrement(&live_completions); } static DWORD send_text_on_task(const TextConfig* config, const char* members = nullptr) { if (!config || live_completions != 0) return ERROR_BUSY; struct TextRequest { SmallString receiver; SmallString body; } request{}; request.receiver.capacity = request.body.capacity = 15; SmallString description{}; description.capacity = 15; if (!set_native_string(&request.receiver, config->target) || !set_native_string(&request.body, config->base.text) || !set_inline(&description, "wxbot.text")) { free_native_string(&request.receiver); free_native_string(&request.body); return ERROR_INVALID_PARAMETER; } TextCompletion* completion = new TextCompletion{1, 0, text_completion_invoke, text_completion_destroy, *config, {}}; if (members) strcpy_s(completion->members, members); _InterlockedIncrement(&live_completions); TestResult report{}; MentionContext mention{members, false}; if (members && !TlsSetValue(factory_tls, &mention)) { text_completion_destroy(completion); free_native_string(&request.receiver); free_native_string(&request.body); return ERROR_INVALID_DATA; } __try { using Builder = void (__fastcall*)(void*, TextRequest*, SmallString*, TextCompletion**); reinterpret_cast(core_base + 0x54b02f0)(nullptr, &request, &description, &completion); report.stage = 20; } __except (record_exception(GetExceptionInformation(), &report)) { write_report(config->base, report, "WXBOT_TEXT_SEND_EXCEPTION"); } if (members) TlsSetValue(factory_tls, nullptr); if (completion) text_completion_destroy(completion); // 好友和群 ID 通常超过 SSO 的 15 字节,必须释放两份请求字符串。 free_native_string(&request.receiver); free_native_string(&request.body); return report.exception ? 5 : 0; } static bool valid_mentions(const char* members) { size_t length = strnlen_s(members, 1024); if (!length || length >= 1024) return false; unsigned int count = 1; size_t part = 0; for (size_t index = 0; index < length; ++index) { unsigned char ch = members[index]; if (ch == ',') { if (!part || part >= 128 || ++count > 8) return false; part = 0; } else { if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '.' || ch == '-')) return false; ++part; } } return part > 0 && part < 128; } struct AtTaskLease { AtTextConfig config; explicit AtTaskLease(const AtTextConfig& value) : config(value) { _InterlockedIncrement(&live_tasks); } ~AtTaskLease() { _InterlockedDecrement(&live_tasks); } }; extern "C" __declspec(dllexport) DWORD WINAPI WxSendAtText(void* parameter) { if (!parameter || live_completions != 0 || live_tasks != 0) return ERROR_BUSY; const AtTextConfig config = *static_cast(parameter); size_t target_length = strnlen_s(config.base.target, sizeof(config.base.target)); if (config.base.base.size != sizeof(AtTextConfig) || config.base.base.pid != GetCurrentProcessId() || !valid_target(config.base.target) || target_length < 9 || std::strcmp(config.base.target + target_length - 9, "@chatroom") || !valid_mentions(config.members)) return ERROR_INVALID_PARAMETER; size_t length = strnlen_s(config.base.base.text, sizeof(config.base.base.text)); if (!length || length >= sizeof(config.base.base.text) || !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, config.base.base.text, static_cast(length), nullptr, 0)) return ERROR_INVALID_PARAMETER; HMODULE core = GetModuleHandleW(L"Weixin.dll"); if (!core || !core_fingerprint(core)) return 4; core_base = reinterpret_cast(core); DWORD status = ensure_mention_factory(); if (status != 0) return status; auto lease = std::make_shared(config); std::function task = [lease]() { _InterlockedIncrement(&active_callbacks); __try { send_text_on_task(&lease->config.base, lease->config.members); } __finally { _InterlockedDecrement(&active_callbacks); } }; status = schedule_task(config.base.base, &task); return status; } struct TextTaskLease { TextConfig config; explicit TextTaskLease(const TextConfig& value) : config(value) { _InterlockedIncrement(&live_tasks); } ~TextTaskLease() { _InterlockedDecrement(&live_tasks); } }; extern "C" __declspec(dllexport) DWORD WINAPI WxSendText(void* parameter) { if (!parameter || live_completions != 0 || live_tasks != 0) return ERROR_BUSY; const TextConfig config = *static_cast(parameter); if (config.base.size != sizeof(TextConfig) || config.base.pid != GetCurrentProcessId() || !valid_target(config.target)) return ERROR_INVALID_PARAMETER; size_t length = strnlen_s(config.base.text, sizeof(config.base.text)); if (!length || length >= sizeof(config.base.text) || !MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, config.base.text, static_cast(length), nullptr, 0)) return ERROR_INVALID_PARAMETER; HMODULE core = GetModuleHandleW(L"Weixin.dll"); if (!core || !core_fingerprint(core)) return 4; core_base = reinterpret_cast(core); auto lease = std::make_shared(config); std::function task = [lease]() { _InterlockedIncrement(&active_callbacks); __try { send_text_on_task(&lease->config); } __finally { _InterlockedDecrement(&active_callbacks); } }; return schedule_task(config.base, &task); } static bool try_read_source(const unsigned char* message, size_t offset, char* output, size_t capacity) { __try { // 真正艾特小号样本已核对:msgsource +0x38 是直接 std::string 指针。 // 与 from/to/content 的 StringValue 包装不同。 const auto string = *reinterpret_cast(message + offset); if (!string || !string->length || string->length >= capacity || string->capacity < string->length || !(string->capacity == 15 || (string->capacity >= 16 && string->capacity <= 1024 * 1024))) return false; const char* bytes = string->capacity > 15 ? *reinterpret_cast(string->data) : string->data; std::memcpy(output, bytes, static_cast(string->length)); output[string->length] = 0; return std::strstr(output, " 512) return; for (uintptr_t address = batch[0]; address < batch[1]; address += 0x78) { const auto message = reinterpret_cast(address); if (*reinterpret_cast(message + 0x14) != 1) continue; char sender[128]{}, receiver[128]{}, text[1024]{}; if (!read_pb_string(message, 8, sender, sizeof(sender)) || !valid_target(sender) || !read_pb_string(message, 0x18, receiver, sizeof(receiver)) || !valid_target(receiver) || !read_pb_string(message, 0x20, text, sizeof(text))) continue; // 接收瞬间复制元数据,不持有任何微信消息对象或字符串指针。 char source[4096]{}; if (!try_read_source(message, 0x38, source, sizeof(source))) source[0] = 0; enqueue_received(sender, receiver, text, source); } } __except (EXCEPTION_EXECUTE_HANDLER) { // 布局不匹配时放弃采样,真实客户端仍按原逻辑收消息。 } } static uintptr_t __fastcall text_receive(void* manager, const uintptr_t* batch, uint8_t flag1, uint8_t flag2) { _InterlockedIncrement(&active_callbacks); uintptr_t result = 0; __try { sample_all_text(batch); result = original_receive(manager, batch, flag1, flag2); } __finally { _InterlockedDecrement(&active_callbacks); } return result; } extern "C" __declspec(dllexport) DWORD WINAPI WxStartTextReceiver(void* parameter) { if (!parameter || hook_created || receive_hook_created) return ERROR_BUSY; const TextConfig config = *static_cast(parameter); if (config.base.size != sizeof(TextConfig) || config.base.pid != GetCurrentProcessId()) return 3; HMODULE core = GetModuleHandleW(L"Weixin.dll"); if (!core || !core_fingerprint(core)) return 4; core_base = reinterpret_cast(core); AcquireSRWLockExclusive(&receive_lock); receive_head = receive_count = receive_dropped = 0; receive_sequence = 0; ReleaseSRWLockExclusive(&receive_lock); MH_STATUS status = MH_Initialize(); if (status != MH_OK) return 600 + status; void* target = reinterpret_cast(core_base + 0x17b9010); status = MH_CreateHook(target, reinterpret_cast(&text_receive), reinterpret_cast(&original_receive)); if (status != MH_OK) { MH_Uninitialize(); return 600 + status; } status = MH_EnableHook(target); if (status != MH_OK) { MH_RemoveHook(target); MH_Uninitialize(); return 600 + status; } receive_hook_created = true; return 0; } extern "C" __declspec(dllexport) DWORD WINAPI WxPollTextMessage(void* parameter) { // 保留旧接口的 1312 字节边界,旧调用者不会被 V2 写越界。 if (!parameter) return ERROR_INVALID_PARAMETER; ReceivedMessage message{}; const DWORD code = WxPollFileHelperMessage(&message); std::memcpy(parameter, &message, 1312); *static_cast(parameter) = 1312; return code; } extern "C" __declspec(dllexport) DWORD WINAPI WxPollTextMessageV2(void* parameter) { return WxPollFileHelperMessage(parameter); } // 定向只读探针:仅在手动发送 WXBOT_AT_PROBE 的消息中记录 msgsource 字符串。 // 不发送消息、不改写消息对象,用于核对该版本真实艾特的元数据布局。 static TextConfig mention_probe_config{}; static bool copy_small_string(const unsigned char* object, size_t offset, char* output, size_t size) { __try { const auto string = reinterpret_cast(object + offset); if (!string->length || string->length >= size || string->capacity < string->length || !(string->capacity == 15 || (string->capacity >= 16 && string->capacity <= 1024 * 1024))) return false; const char* data = string->capacity == 15 ? string->data : *reinterpret_cast(string->data); std::memcpy(output, data, static_cast(string->length)); output[string->length] = 0; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static void capture_mention_source(SharedMessage* message) { __try { if (!message || !message->object) return; const auto object = static_cast(message->object); if (*reinterpret_cast(object) != core_base + 0x8e53588) return; char body[1024]{}; if (!copy_small_string(object, 0x758, body, sizeof(body)) || !std::strstr(body, "WXBOT_AT_PROBE")) return; if (_InterlockedCompareExchange(&triggered_once, 1, 0) != 0) return; FILE* log = nullptr; if (_wfopen_s(&log, mention_probe_config.base.log_path, L"wb") != 0 || !log) return; std::fputs("{\"vtable_rva\":\"0x8e53588\",\"fields\":[", log); bool comma = false; for (size_t offset = 0; offset + sizeof(SmallString) <= 0x798; offset += 8) { char value[4096]{}; if (!copy_small_string(object, offset, value, sizeof(value))) continue; if (!std::strstr(value, "msgsource") && !std::strstr(value, "atuserlist") && !std::strstr(value, "WXBOT_AT_PROBE") && !std::strstr(value, "wxid_") && !std::strstr(value, "@chatroom")) continue; if (comma) std::fputc(',', log); std::fprintf(log, "{\"offset\":%llu,\"value\":", static_cast(offset)); write_json_string(log, value); std::fputc('}', log); comma = true; } std::fputs("],\"vectors\":[", log); comma = false; for (size_t offset = 0; offset + 24 <= 0x798; offset += 8) { const auto begin = *reinterpret_cast(object + offset); const auto end = *reinterpret_cast(object + offset + 8); const auto capacity = *reinterpret_cast(object + offset + 16); if (begin < 0x10000 || end <= begin || end > capacity || end - begin > 256 || (end - begin) % 32 || capacity - begin > 1024 * 1024) continue; char member[128]{}; if (!copy_small_string(reinterpret_cast(begin), 0, member, sizeof(member)) || std::strncmp(member, "wxid_", 5)) continue; if (comma) std::fputc(',', log); std::fprintf(log, "{\"offset\":%llu,\"member\":", static_cast(offset)); write_json_string(log, member); std::fputc('}', log); comma = true; } std::fputs("],\"object_hex\":\"", log); for (size_t offset = 0; offset < 0x798; ++offset) std::fprintf(log, "%02x", object[offset]); std::fputs("\"}\n", log); std::fclose(log); } __except (EXCEPTION_EXECUTE_HANDLER) { } } static void* __fastcall mention_probe_send(void* manager, void* output, SharedMessage* message, uint8_t flag) { _InterlockedIncrement(&active_callbacks); void* result = nullptr; __try { capture_mention_source(message); result = original_send(manager, output, message, flag); } __finally { _InterlockedDecrement(&active_callbacks); } return result; } extern "C" __declspec(dllexport) DWORD WINAPI WxArmMentionProbe(void* parameter) { if (!parameter || hook_created || receive_hook_created) return ERROR_BUSY; const TextConfig config = *static_cast(parameter); if (config.base.size != sizeof(TextConfig) || config.base.pid != GetCurrentProcessId()) return 3; HMODULE core = GetModuleHandleW(L"Weixin.dll"); if (!core || !core_fingerprint(core)) return 4; core_base = reinterpret_cast(core); mention_probe_config = config; triggered_once = 0; MH_STATUS status = MH_Initialize(); if (status != MH_OK) return 600 + status; void* target = reinterpret_cast(core_base + 0x178f100); status = MH_CreateHook(target, reinterpret_cast(&mention_probe_send), reinterpret_cast(&original_send)); if (status != MH_OK) { MH_Uninitialize(); return 600 + status; } status = MH_EnableHook(target); if (status != MH_OK) { MH_RemoveHook(target); MH_Uninitialize(); return 600 + status; } hook_created = true; return 0; }