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 @@ + +