From fff7c67af6874608edd60e2b3e7d299721d14709 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Thu, 22 Oct 2026 16:52:48 +0200 Subject: [PATCH] Fix: Make the remote chat dialog Unicode and loader-independent Clients packaged as SC_AES/sRDI shellcode run the payload image as raw shellcode inside the loader stub, so the chat manager was handed the stub's module handle. The stub's resource section only carries a manifest, and Windows resource APIs reject manually mapped images outright, so creating IDD_CHAT failed and the chat thread exited before the window ever existed. The chat window now falls back to an embedded compiled dialog template that is byte-identical to the IDD_CHAT resource, and the title-bar icon is rebuilt directly from the module's own resource directory instead of LoadIcon, bypassing the loader on both paths. The PE walking code checks every offset against SizeOfImage and the resource directory size, so a malformed or truncated image returns NULL instead of faulting. Deployments where the resource lookup already works keep the original code path unchanged. The dialog is now created and driven with the wide-character API. The client is still an MBCS build, but an ANSI dialog renders text with the system code page, so GBK chat text turned into mojibake on German and other non-936 systems. Incoming text is converted CP936 to UTF-16 and outgoing text strictly back to CP936; characters GBK cannot represent (emoji, accented letters) are refused with a visible notice instead of silently becoming '?'. Static labels follow the system UI language, and IDD_CHAT now uses the scalable MS Shell Dlg face so font linking can supply CJK glyphs. The window is now positioned explicitly as well. DS_CENTER in the template put it at the work area origin on the three-monitor host: the centred position came out negative there and Windows clamps out-of-range creation coordinates to that origin. It is centred in the work area of the monitor under the cursor instead, falling back to SPI_GETWORKAREA. Teardown no longer leaves a hidden window behind either. The dialog thread blocked in GetMessageW and never saw m_bRun cleared, while the destructor waited for that thread before sending the message that would have destroyed the window, so a close command that never arrived (the sub-connection already gone, or WaitSendIdle timing out before CancelIO) leaked the window, the dialog thread and the manager loop. The destructor now destroys the window first, the loop checks m_bRun before blocking, and it destroys the window itself if it exits first. Co-Authored-By: deepseek-flash --- client/ChatDialogTemplate.h | 44 ++++ client/ChatManager.cpp | 272 +++++++++++++++++++----- client/ClientDll_vs2015.vcxproj | 2 + client/ClientDll_vs2015.vcxproj.filters | 2 + client/ImageResource.h | 147 +++++++++++++ client/Script.rc | 6 +- client/ghost_vs2015.vcxproj | 2 + client/ghost_vs2015.vcxproj.filters | 2 + 8 files changed, 425 insertions(+), 52 deletions(-) create mode 100644 client/ChatDialogTemplate.h create mode 100644 client/ImageResource.h diff --git a/client/ChatDialogTemplate.h b/client/ChatDialogTemplate.h new file mode 100644 index 0000000..27d9919 --- /dev/null +++ b/client/ChatDialogTemplate.h @@ -0,0 +1,44 @@ +// ChatDialogTemplate.h: IDD_CHAT 的编译后 DLGTEMPLATEEX(内嵌内存模板) +// +// 用途:加壳部署(EXE + SC_AES/sRDI)的客户端里,ChatManager 的 hInstance 是外壳进程 +// 映像(GetModuleHandle(NULL)),其资源里只有清单、没有 IDD_CHAT;而载荷 EXE 以裸 +// shellcode 在壳进程内运行,未注册进加载器,FindResource/LoadResource/CreateDialogParamW +// 一律取不到资源(实测 Win11 26200 返回 err=1812 / 59)。此时用下面的内存模板直接 +// CreateDialogIndirectParamW 建窗,与部署形态无关。 +// +// 数据来源:client/Script.rc 的 IDD_CHAT(DIALOGEX)。DLGTEMPLATEEX 的字符串字段 +// 一律是 UTF-16,同一份字节既可用于 A 接口也可用于 W 接口;聊天窗口固定用 W 接口 +// 创建成 Unicode 对话框,否则 MBCS 客户端在非中文系统(德语等)上会按系统代码页 +// 渲染 GBK 字节,中文全部乱码。 +// +// 重新生成(改动 Script.rc 的 IDD_CHAT 后必须同步): +// 1. rc /fo dlg.res dlg.rc (dlg.rc 只保留与 Script.rc 相同的 IDD_CHAT 块) +// 2. link /dll /noentry /machine:x64 /out:dlg.dll dlg.res +// 3. 从 dlg.dll 的 RT_DIALOG(5) → 106 → 任意语言 取数据块,按 16 字节一行导出。 +// 当前块 240 字节:CAPTION "Remote Chat"、FONT 9 "MS Shell Dlg" charset 1、 +// 控件 ID 1001/1002/1003/1004,与 Script.rc 完全一致。 + +#ifndef YAMA_CHAT_DIALOG_TEMPLATE_H +#define YAMA_CHAT_DIALOG_TEMPLATE_H + +// DLGTEMPLATEEX 的 DWORD 字段要求模板指针 4 字节对齐;显式声明对齐,避免依赖 +// 编译器把字节数组放在什么边界上。 +__declspec(align(4)) static const unsigned char kChatDialogTemplate[] = { + 0x01, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x08, 0xCA, 0x80, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEB, 0x00, 0x9B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x00, + 0x65, 0x00, 0x6D, 0x00, 0x6F, 0x00, 0x74, 0x00, 0x65, 0x00, 0x20, 0x00, 0x43, 0x00, 0x68, 0x00, + 0x61, 0x00, 0x74, 0x00, 0x00, 0x00, 0x09, 0x00, 0x90, 0x01, 0x00, 0x01, 0x4D, 0x00, 0x53, 0x00, + 0x20, 0x00, 0x53, 0x00, 0x68, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x20, 0x00, 0x44, 0x00, + 0x6C, 0x00, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x08, 0xA1, 0x50, 0x03, 0x00, 0x03, 0x00, 0xE5, 0x00, 0x6B, 0x00, 0xE9, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0x00, 0xA1, 0x50, 0x03, 0x00, 0x71, 0x00, 0xAA, 0x00, 0x26, 0x00, 0xEA, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x50, 0xB2, 0x00, 0x74, 0x00, 0x36, 0x00, 0x0E, 0x00, 0xEB, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0x80, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6E, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50, 0xB2, 0x00, 0x85, 0x00, + 0x36, 0x00, 0x0E, 0x00, 0xEC, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0x80, 0x00, 0x45, 0x00, 0x6E, 0x00, + 0x64, 0x00, 0x20, 0x00, 0x43, 0x00, 0x68, 0x00, 0x61, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +#endif // YAMA_CHAT_DIALOG_TEMPLATE_H diff --git a/client/ChatManager.cpp b/client/ChatManager.cpp index 54235fc..28ce7fc 100644 --- a/client/ChatManager.cpp +++ b/client/ChatManager.cpp @@ -4,9 +4,12 @@ #include "stdafx.h" #include "ChatManager.h" +#include "ChatDialogTemplate.h" +#include "ImageResource.h" #include "Common.h" #include "resource.h" #include +#include #if ENABLE_MESSAGE @@ -28,6 +31,27 @@ static SuspendOrResumeProcess g_lpfnNtResumeProcess = NULL; static HHOOK g_hKeyBoard = NULL; static int g_nLocked = 0; +// 加壳部署用:从本模块映像自建的聊天窗口图标,只建一次,进程内复用(不销毁) +static volatile LONG g_lOwnIconsState = 0; // 0=未建 1=建中 2=已建 +static HICON g_hOwnIconSmall = NULL; +static HICON g_hOwnIconBig = NULL; + +static void EnsureOwnChatIcons() +{ + if (InterlockedCompareExchange(&g_lOwnIconsState, 1, 0) == 0) { + g_hOwnIconSmall = CreateImageIcon(&__ImageBase, IDI_ICON_MSG, + GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); + g_hOwnIconBig = CreateImageIcon(&__ImageBase, IDI_ICON_MSG, + GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); + InterlockedExchange(&g_lOwnIconsState, 2); // 全屏障,发布句柄 + return; + } + // 另一个线程正在建:短暂等待其发布,避免读到尚未赋值的句柄 + for (int i = 0; i < 50 && g_lOwnIconsState != 2; i++) { + Sleep(1); + } +} + static DWORD GetWinlogonPid() { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); @@ -133,6 +157,113 @@ static void UnlockScreen() g_nLocked = 0; } +////////////////////////////////////////////////////////////////////// +// 聊天文字:Unicode 窗口与 CP936 转换 +////////////////////////////////////////////////////////////////////// + +// 聊天窗口是 Unicode 对话框:客户端整体是 MBCS 工程,但聊天窗口用 W 接口创建, +// 文字以 UTF-16 存在内存里,与系统 ANSI 代码页无关——德语/日语等非中文系统上 +// 中文不再乱码。静态标签按系统 UI 语言选择:中文系统保持中文,其余用英文。 +struct ChatUiStrings { + const wchar_t* caption; + const wchar_t* send; + const wchar_t* end; + const wchar_t* self; + const wchar_t* peer; + const wchar_t* errChars; + const wchar_t* errTooLong; +}; + +static const ChatUiStrings& ChatUi() +{ + static const ChatUiStrings zhChatUi = { + L"远程交谈", L"发送消息", L"结束交谈", L"自己:", L"对方:", + L"消息未发送:含 GBK 不支持的字符(emoji 或带变音符号的字母)", + L"消息未发送:超过 4000 字节上限" + }; + static const ChatUiStrings enChatUi = { + L"Remote Chat", L"Send", L"End Chat", L"Me:", L"Peer:", + L"Not sent: the message contains characters GBK cannot represent " + L"(emoji or accented letters)", + L"Not sent: the message exceeds the 4000-byte limit" + }; + static const bool bChineseUI = + (PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE); + return bChineseUI ? zhChatUi : enChatUi; +} + +// 服务端聊天正文一律是 CP936 字节;转成 UTF-16 交给 Unicode 编辑框显示。 +static bool Cp936ToWide(const char* src, int cbSrc, std::wstring& out) +{ + out.clear(); + if (!src || cbSrc <= 0) return true; + int cch = MultiByteToWideChar(936, 0, src, cbSrc, NULL, 0); + if (cch <= 0) return false; + out.resize((size_t)cch); + return MultiByteToWideChar(936, 0, src, cbSrc, &out[0], cch) > 0; +} + +static size_t ChatBoundedStrlen(const char* s, size_t nMax) +{ + size_t n = 0; + while (n < nMax && s[n] != '\0') ++n; + return n; +} + +// 追加一条聊天记录。控件是 Unicode 窗口,即使在 MBCS 工程里也必须用 W 接口, +// 否则文字会被按系统代码页转换。 +static void AppendChatLine(HWND hDlg, const wchar_t* who, const wchar_t* body) +{ + HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG); + if (!hEditChatLog) return; + + SYSTEMTIME st; + GetLocalTime(&st); + wchar_t stamp[64] = { 0 }; + swprintf_s(stamp, L"%d/%d/%d %d:%02d:%02d", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + + std::wstring text(who); + text += L' '; + text += stamp; + text += L"\r\n "; + if (body) text += body; + text += L"\r\n\r\n"; + + int nLen = GetWindowTextLengthW(hEditChatLog); + if (nLen >= 20000) { + SetWindowTextW(hEditChatLog, L""); + nLen = 0; + } + SendMessageW(hEditChatLog, EM_SETSEL, nLen, nLen); + SendMessageW(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)text.c_str()); +} + +// 把聊天窗口显式居中到鼠标所在显示器的工作区。模板里的 DS_CENTER 在多显示器布局下 +// 可能算出负坐标,再被系统夹到工作区左上角(三屏机器上实测稳定复现),所以不依赖它。 +static void CenterChatDialog(HWND hDlg) +{ + RECT rc; + if (!GetWindowRect(hDlg, &rc)) return; + const int cx = rc.right - rc.left; + const int cy = rc.bottom - rc.top; + + RECT rcWork; + POINT pt; + MONITORINFO mi = { sizeof(mi) }; + if (GetCursorPos(&pt) && GetMonitorInfo(MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY), &mi)) { + rcWork = mi.rcWork; + } else if (!SystemParametersInfo(SPI_GETWORKAREA, 0, &rcWork, 0)) { + return; + } + + int x = rcWork.left + (rcWork.right - rcWork.left - cx) / 2; + int y = rcWork.top + (rcWork.bottom - rcWork.top - cy) / 2; + if (x < rcWork.left) x = rcWork.left; + if (y < rcWork.top) y = rcWork.top; + SetWindowPos(hDlg, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); +} + ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// @@ -157,15 +288,19 @@ CChatManager::CChatManager(IOCPClient* ClientObject, int n, void* user) : CManag CChatManager::~CChatManager() { m_bRun = FALSE; + // 必须先销毁窗口再等线程:消息循环阻塞在 GetMessageW 里看不到 m_bRun,只有 + // 收到 WM_USER + 1(DestroyWindow → PostQuitMessage)才会退出。关闭命令没送到 + // (子连接先断、服务端 WaitSendIdle 超时后 CancelIO)时,旧的先等后销毁顺序会 + // 死锁在这里,留下一个隐藏的聊天窗口和卡死的线程。 + if (m_hWnd) { + SendMessage(m_hWnd, WM_USER + 1, NULL, NULL); + } while (m_hThread) Sleep(100); if (g_nLocked == 1) { UnlockScreen(); } - if (m_hWnd) { - SendMessage(m_hWnd, WM_USER + 1, NULL, NULL); - } Mprintf("Chat 析构\n"); } @@ -174,10 +309,25 @@ DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam) CChatManager* pThis = (CChatManager*)lParam; HANDLE& hThread(pThis->m_hThread); - pThis->m_hWnd = CreateDialogParamA(pThis->m_hInstance, MAKEINTRESOURCE(IDD_CHAT), NULL, + // 聊天窗口必须是 Unicode 窗口:ANSI 窗口会按系统代码页解释文字,非中文系统 + // (德语等)上 GBK 中文一律乱码。资源里的 IDD_CHAT 是 DIALOGEX,字符串本来 + // 就是 UTF-16,W 接口可直接加载。 + pThis->m_hWnd = CreateDialogParamW(pThis->m_hInstance, MAKEINTRESOURCEW(IDD_CHAT), NULL, CChatManager::ChatDialogProc, (LPARAM)pThis); + if (!pThis->m_hWnd) { + // 加壳部署(EXE + SC_AES/sRDI)时 m_hInstance 是外壳进程映像:资源里没有 + // IDD_CHAT,而载荷以裸 shellcode 运行、未注册进加载器,资源 API 一律取不到 + // 对话框(Windows 11 上 CreateDialogParamW 返回 err=1812 或 59)。 + // 回退:用内嵌的 IDD_CHAT 编译后模板建窗,与部署形态无关。 + DWORD dwError = GetLastError(); + Mprintf("CreateDialogParamW(%lu) 失败,改用内嵌对话框模板\n", dwError); + pThis->m_hWnd = CreateDialogIndirectParamW(pThis->m_hInstance, + (LPCDLGTEMPLATEW)kChatDialogTemplate, NULL, + CChatManager::ChatDialogProc, (LPARAM)pThis); + } if (pThis->m_hWnd) { - SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0); + SendMessageW(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0); + CenterChatDialog(pThis->m_hWnd); SetWindowPos(pThis->m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); // 移除最小化/最大化按钮 LONG lStyle = GetWindowLong(pThis->m_hWnd, GWL_STYLE); @@ -185,12 +335,22 @@ DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam) SetWindowLong(pThis->m_hWnd, GWL_STYLE, lStyle); MSG msg; - while (GetMessage(&msg, NULL, 0, 0) && pThis->m_bRun) { - if (!IsDialogMessage(pThis->m_hWnd, &msg)) { + // 本线程只拥有这个对话框,整轮循环用 W 版本收发:否则 WM_CHAR / WM_IME_* + // 会按系统 ANSI 代码页转换,非中文系统上中文输入法会丢字。 + // m_bRun 先判断:析构里置 FALSE 后即使没有新消息也要能退出,不能卡在 GetMessageW。 + while (pThis->m_bRun && GetMessageW(&msg, NULL, 0, 0)) { + if (!IsDialogMessageW(pThis->m_hWnd, &msg)) { TranslateMessage(&msg); - DispatchMessage(&msg); + DispatchMessageW(&msg); } } + // 兜底:若循环在窗口销毁之前就因 m_bRun 被清掉而退出(析构执行到销毁之前 + // 线程就已建好窗口),在同一线程里补一次销毁,避免残留隐藏窗口。 + if (pThis->m_hWnd) { + HWND hWnd = pThis->m_hWnd; + pThis->m_hWnd = NULL; + DestroyWindow(hWnd); + } } CloseHandle(hThread); @@ -205,43 +365,65 @@ INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wPara case WM_INITDIALOG: SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)lParam); pThis = (CChatManager*)lParam; - SetWindowText(hDlg, "远程交谈"); - SetDlgItemText(hDlg, IDC_BUTTON_SEND, "发送消息"); - SetDlgItemText(hDlg, IDC_BUTTON_END, "结束交谈"); + SetWindowTextW(hDlg, ChatUi().caption); + SetDlgItemTextW(hDlg, IDC_BUTTON_SEND, ChatUi().send); + SetDlgItemTextW(hDlg, IDC_BUTTON_END, ChatUi().end); { - HICON hIcon = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG)); - if (hIcon) { - SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); - SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); + HICON hIconSmall = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG)); + HICON hIconBig = hIconSmall; + if (!hIconSmall) { + // 加壳部署取不到图标资源:直接从本模块映像里读 RT_ICON 建图标, + // 仍失败才退回系统默认图标(共享图标,不可销毁) + EnsureOwnChatIcons(); + hIconSmall = g_hOwnIconSmall ? g_hOwnIconSmall : LoadIcon(NULL, IDI_APPLICATION); + hIconBig = g_hOwnIconBig ? g_hOwnIconBig : hIconSmall; } + if (hIconSmall) SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIconSmall); + if (hIconBig) SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIconBig); } return TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDC_BUTTON_SEND) { - char str[4096] = { 0 }; - GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str)); - if (str[0] == 0) { - SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG)); + HWND hEditNewMsg = GetDlgItem(hDlg, IDC_EDIT_NEWMSG); + int cch = GetWindowTextLengthW(hEditNewMsg); + if (cch <= 0) { + SetFocus(hEditNewMsg); return TRUE; // 消息为空不发送 } - pThis->Send((LPBYTE)str, lstrlenA(str) + 1); - - SYSTEMTIME st; - GetLocalTime(&st); - char Text[5120] = { 0 }; - sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "自己:", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, str); - HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG); - int nLen = GetWindowTextLength(hEditChatLog); - if (nLen >= 20000) { - SetWindowText(hEditChatLog, ""); - nLen = GetWindowTextLength(hEditChatLog); + std::wstring text((size_t)cch + 1, L'\0'); + GetWindowTextW(hEditNewMsg, &text[0], cch + 1); + text.resize(wcslen(text.c_str())); + if (text.empty()) { + SetFocus(hEditNewMsg); + return TRUE; } - SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen); - SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text); - SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, ""); - SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG)); + + // 协议仍是 CP936:UTF-16 → GBK 严格转换。GBK 表示不了的字符(emoji、 + // ö/ä/ß 等)直接拒发并提示,绝不静默变成 '?'。 + BOOL usedDefault = FALSE; + int nNeed = WideCharToMultiByte(936, WC_NO_BEST_FIT_CHARS, text.c_str(), -1, + NULL, 0, NULL, &usedDefault); + if (nNeed <= 1 || usedDefault) { + AppendChatLine(hDlg, ChatUi().self, ChatUi().errChars); + return TRUE; + } + if (nNeed - 1 > 4000) { + AppendChatLine(hDlg, ChatUi().self, ChatUi().errTooLong); + return TRUE; + } + std::string gbk((size_t)nNeed, '\0'); + WideCharToMultiByte(936, WC_NO_BEST_FIT_CHARS, text.c_str(), -1, + &gbk[0], nNeed, NULL, &usedDefault); + if (usedDefault) { + AppendChatLine(hDlg, ChatUi().self, ChatUi().errChars); + return TRUE; + } + + pThis->Send((LPBYTE)&gbk[0], (ULONG)gbk.size()); // 含结尾 NUL + AppendChatLine(hDlg, ChatUi().self, text.c_str()); + SetDlgItemTextW(hDlg, IDC_EDIT_NEWMSG, L""); + SetFocus(hEditNewMsg); return TRUE; } if (LOWORD(wParam) == IDC_BUTTON_END) { @@ -317,21 +499,13 @@ VOID CChatManager::OnReceive(PBYTE szBuffer, ULONG ulLength) } } - // 收到服务端发来的聊天文本 - SYSTEMTIME st; - GetLocalTime(&st); - char Text[8192] = { 0 }; - sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "对方:", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (char*)szBuffer); + // 收到服务端发来的聊天文本:协议是 CP936 字节(NUL 结尾),转成 UTF-16 显示, + // 这样非中文系统(德语等)上也能正确显示中文。 + const size_t cbText = ChatBoundedStrlen((const char*)szBuffer, (size_t)ulLength); + std::wstring text; + if (!Cp936ToWide((const char*)szBuffer, (int)cbText, text)) return; if (m_hWnd) { - HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG); - int nLen = GetWindowTextLength(hEditChatLog); - if (nLen >= 20000) { - SetWindowText(hEditChatLog, ""); - nLen = GetWindowTextLength(hEditChatLog); - } - SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen); - SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text); + AppendChatLine(m_hWnd, ChatUi().peer, text.c_str()); ShowWindow(m_hWnd, SW_RESTORE); } } diff --git a/client/ClientDll_vs2015.vcxproj b/client/ClientDll_vs2015.vcxproj index 09145ea..c59728b 100644 --- a/client/ClientDll_vs2015.vcxproj +++ b/client/ClientDll_vs2015.vcxproj @@ -289,6 +289,8 @@ + + diff --git a/client/ClientDll_vs2015.vcxproj.filters b/client/ClientDll_vs2015.vcxproj.filters index ff7eabf..89d21b5 100644 --- a/client/ClientDll_vs2015.vcxproj.filters +++ b/client/ClientDll_vs2015.vcxproj.filters @@ -119,6 +119,8 @@ + + diff --git a/client/ImageResource.h b/client/ImageResource.h new file mode 100644 index 0000000..1475355 --- /dev/null +++ b/client/ImageResource.h @@ -0,0 +1,147 @@ +// ImageResource.h: 不经加载器,直接从内存中的模块映像读取资源 +// +// 加壳部署(EXE + SC_AES/sRDI)下,载荷映像以裸 shellcode 运行、未注册进加载器, +// FindResource/LoadIcon/CreateDialogParamA 这类按 hInstance 找资源的 API 全部失败 +// (实测 Win11 返回 err=1812 / 59)。但映像的节(含 .rsrc)是按 PE 布局正常映射 +// 的,资源目录里的偏移又都是 RVA,因此可以直接遍历 IMAGE_RESOURCE_DIRECTORY +// 取到资源数据。本文件只读调用方给出的映像,不做任何映射。 +// +// 适用:正常 EXE/DLL、MemoryModule 手工映射 DLL、sRDI 包装的 shellcode 载荷。 +// 注意:调用方传入的必须是已映射映像的基址(如 &__ImageBase),不是磁盘文件。 +// +// 稳定性:所有偏移都按 SizeOfImage / 资源目录大小做越界检查,映像布局异常 +// (被第三方加壳改坏、资源被裁剪等)时一律返回 NULL,绝不越界读取。 + +#ifndef YAMA_IMAGE_RESOURCE_H +#define YAMA_IMAGE_RESOURCE_H + +#include + +#ifdef __cplusplus +extern "C" const IMAGE_DOS_HEADER __ImageBase; +#endif + +// RT_ICON / RT_GROUP_ICON 的数字 ID(与同名 RT_ 宏对应) +#define IMAGE_RES_TYPE_ICON 3 +#define IMAGE_RES_TYPE_GROUP_ICON 14 + +#pragma pack(push, 1) +typedef struct GRPICONDIRENTRY { + BYTE bWidth; + BYTE bHeight; + BYTE bColorCount; + BYTE bReserved; + WORD wPlanes; + WORD wBitCount; + DWORD dwBytesInRes; + WORD nID; +} GRPICONDIRENTRY; +#pragma pack(pop) + +#define IMAGE_RES_DIR_IN_BOUNDS(resOffset, resSize) \ + ((resOffset) <= (resSize) - sizeof(IMAGE_RESOURCE_DIRECTORY)) +#define IMAGE_RES_ENTRY_ARRAY_OK(resOffset, resCount, resSize) \ + ((resCount) <= ((resSize) - (resOffset) - sizeof(IMAGE_RESOURCE_DIRECTORY)) / sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY)) + +// 在已映射映像中查找 type/id(取第一个语言项)的资源数据,返回映像内指针 +static const BYTE* FindImageResource(const void* pvImage, WORD nType, WORD nId, DWORD* pdwSize) +{ + if (pdwSize) *pdwSize = 0; + if (!pvImage) return NULL; + + const BYTE* pBase = (const BYTE*)pvImage; + const DWORD dwLfanew = *(const DWORD*)(pBase + 0x3C); + // 正常 PE 头远小于一页;sRDI 的 CLEARHEADER 只保留 PE 头(e_lfanew 之后), + // 所以这里不能用 MZ 判断,只能靠 e_lfanew 与 PE 签名 + if (dwLfanew == 0 || dwLfanew > 0x1000) return NULL; + const PIMAGE_NT_HEADERS pNt = (const PIMAGE_NT_HEADERS)(pBase + dwLfanew); + if (pNt->Signature != IMAGE_NT_SIGNATURE) return NULL; + + const DWORD dwImageSize = pNt->OptionalHeader.SizeOfImage; + const DWORD dwResRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress; + const DWORD dwResSize = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size; + if (dwResRva == 0 || dwResSize < sizeof(IMAGE_RESOURCE_DIRECTORY) || + dwResRva > dwImageSize || dwResSize > dwImageSize - dwResRva) { + return NULL; + } + const BYTE* pRes = pBase + dwResRva; + // 已映射映像:资源目录与数据项偏移都是相对资源节起点的偏移 / RVA + + // 目录三层:类型 -> 名称/ID -> 语言 + const WORD keys[2] = { nType, nId }; + DWORD dwDirOff = 0; + for (int i = 0; i < 2; i++) { + if (!IMAGE_RES_DIR_IN_BOUNDS(dwDirOff, dwResSize)) return NULL; + const IMAGE_RESOURCE_DIRECTORY* pRd = (const IMAGE_RESOURCE_DIRECTORY*)(pRes + dwDirOff); + const DWORD dwCount = (DWORD)pRd->NumberOfNamedEntries + pRd->NumberOfIdEntries; + if (!IMAGE_RES_ENTRY_ARRAY_OK(dwDirOff, dwCount, dwResSize)) return NULL; + + const IMAGE_RESOURCE_DIRECTORY_ENTRY* pEntry = (const IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pRd + 1); + const IMAGE_RESOURCE_DIRECTORY_ENTRY* pFound = NULL; + for (DWORD j = 0; j < dwCount; j++) { + // 只按数字 ID 匹配;命名项(NameIsString)跳过 + if (!(pEntry[j].Name & IMAGE_RESOURCE_NAME_IS_STRING) && pEntry[j].Id == keys[i]) { + pFound = &pEntry[j]; + break; + } + } + if (!pFound || !pFound->DataIsDirectory) return NULL; + dwDirOff = pFound->OffsetToDirectory; + } + + if (!IMAGE_RES_DIR_IN_BOUNDS(dwDirOff, dwResSize)) return NULL; + const IMAGE_RESOURCE_DIRECTORY* pLangDir = (const IMAGE_RESOURCE_DIRECTORY*)(pRes + dwDirOff); + const DWORD dwLangCount = (DWORD)pLangDir->NumberOfNamedEntries + pLangDir->NumberOfIdEntries; + if (dwLangCount == 0 || !IMAGE_RES_ENTRY_ARRAY_OK(dwDirOff, dwLangCount, dwResSize)) return NULL; + + const IMAGE_RESOURCE_DIRECTORY_ENTRY* pLang = (const IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pLangDir + 1); + if (pLang->DataIsDirectory) return NULL; + + const DWORD dwDataOff = pLang->OffsetToData; + if (dwDataOff > dwResSize - sizeof(IMAGE_RESOURCE_DATA_ENTRY)) return NULL; + const IMAGE_RESOURCE_DATA_ENTRY* pData = (const IMAGE_RESOURCE_DATA_ENTRY*)(pRes + dwDataOff); + const DWORD dwRva = pData->OffsetToData; + const DWORD dwBytes = pData->Size; + if (dwRva > dwImageSize || dwBytes > dwImageSize - dwRva) return NULL; + + if (pdwSize) *pdwSize = dwBytes; + return pBase + dwRva; // OffsetToData 是 RVA +} + +// 按图标组(RT_GROUP_ICON)ID 建 HICON:选尺寸最接近 nWidth x nHeight 的一帧, +// 用 CreateIconFromResourceEx 创建。返回的 HICON 由调用方负责 DestroyIcon。 +static HICON CreateImageIcon(const void* pvImage, WORD nGroupId, int nWidth, int nHeight) +{ + DWORD dwGroupSize = 0; + const BYTE* pGroup = FindImageResource(pvImage, IMAGE_RES_TYPE_GROUP_ICON, nGroupId, &dwGroupSize); + if (!pGroup || dwGroupSize < 6 + sizeof(GRPICONDIRENTRY)) return NULL; + + // GRPICONDIR: WORD idReserved, idType, idCount + const WORD nCount = *(const WORD*)(pGroup + 4); + if (*(const WORD*)(pGroup + 2) != 1 || nCount == 0) return NULL; + if (dwGroupSize < 6 + (DWORD)nCount * sizeof(GRPICONDIRENTRY)) return NULL; + + const GRPICONDIRENTRY* pBest = NULL; + int nBestDiff = 0x7FFFFFFF; + for (WORD i = 0; i < nCount; i++) { + const GRPICONDIRENTRY* pEntry = (const GRPICONDIRENTRY*)(pGroup + 6 + i * sizeof(GRPICONDIRENTRY)); + const int w = pEntry->bWidth ? pEntry->bWidth : 256; // 0 表示 256 + const int h = pEntry->bHeight ? pEntry->bHeight : 256; + const int dx = w > nWidth ? w - nWidth : nWidth - w; + const int dy = h > nHeight ? h - nHeight : nHeight - h; + if (dx + dy < nBestDiff) { + nBestDiff = dx + dy; + pBest = pEntry; + } + } + if (!pBest) return NULL; + + DWORD dwIconSize = 0; + const BYTE* pIcon = FindImageResource(pvImage, IMAGE_RES_TYPE_ICON, pBest->nID, &dwIconSize); + if (!pIcon || dwIconSize == 0) return NULL; + + return (HICON)CreateIconFromResourceEx((PBYTE)pIcon, dwIconSize, TRUE, 0x00030000, + nWidth, nHeight, LR_DEFAULTCOLOR); +} + +#endif // YAMA_IMAGE_RESOURCE_H diff --git a/client/Script.rc b/client/Script.rc index af34a28..3954d8c 100644 --- a/client/Script.rc +++ b/client/Script.rc @@ -34,13 +34,13 @@ END IDD_CHAT DIALOGEX 0, 0, 235, 155 STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Chat" -FONT 10, "System", 0, 0, 0x0 +CAPTION "Remote Chat" +FONT 9, "MS Shell Dlg", 400, 0, 0x1 BEGIN EDITTEXT IDC_EDIT_CHATLOG,3,3,229,107,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL EDITTEXT IDC_EDIT_NEWMSG,3,113,170,38,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL DEFPUSHBUTTON "Send",IDC_BUTTON_SEND,178,116,54,14 - PUSHBUTTON "End",IDC_BUTTON_END,178,133,54,14 + PUSHBUTTON "End Chat",IDC_BUTTON_END,178,133,54,14 END diff --git a/client/ghost_vs2015.vcxproj b/client/ghost_vs2015.vcxproj index 70713e1..c219114 100644 --- a/client/ghost_vs2015.vcxproj +++ b/client/ghost_vs2015.vcxproj @@ -302,6 +302,8 @@ + + diff --git a/client/ghost_vs2015.vcxproj.filters b/client/ghost_vs2015.vcxproj.filters index 3759f1b..7af216b 100644 --- a/client/ghost_vs2015.vcxproj.filters +++ b/client/ghost_vs2015.vcxproj.filters @@ -147,6 +147,8 @@ + +