Fix: reissue client_id on clone collision and harden V1 file-transfer command

撞 ID 的克隆机(同镜像 MachineGuid 相同导致 client_id 相同)此前会被服务端暂存为
替补、永远无法转正,从而不显示在主机列表。现在服务端检测到「client_id 相同但主机名
不同」时,主动下发 COMMAND_REGEN_CLIENT_ID,客户端在旧 ID 基础上掺随机 UUID 重算后
重连,以新 ID 正常上线;触发条件仅看主机名,排除 IP 漂移与纯用户名变化,避免误伤
正常重连的机器。

同时加固 V1 服务端→客户端文件传输:COMMAND_GET_FILE 指令的一次性 md5 令牌绑定登记
时的客户端源 IP(PutCmd/PopCmd 均带 peerIP),防止异常客户端冒用;并对指令包增加边界
校验(len 与 NUL 结尾),杜绝无 NUL 包导致的越界读。

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-17 09:29:33 +02:00
1 parent d3a339247b
commit e6b4fe3a85
7 files changed
+96 -13

No files matched your search

+34 -6
View File
@@ -1771,6 +1771,16 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
" 已在线: 主机名=%s IP=%s 用户名=%s\n",
id, strPCName.GetString(), data[ONLINELIST_IP].GetString(), newUser.GetString(),
oldName.GetString(), oldIP.GetString(), oldUser.GetString());
// 主机名不同 = 确定是不同物理机(克隆机撞 ID,而非同一台的 IP 漂移/重连)。
// 主动下发「重生成 client_id」命令:克隆机掺随机 UUID 重算后重连,以新 ID
// 重新上线脱离撞 ID 状态。纯 IP 不同、纯用户名不同不下发,避免误伤正常重连
// 的机器(其 ID 合法,重生成反而丢备注/授权/历史)。
if (strPCName.CompareNoCase(oldName) != 0) {
BYTE bToken = COMMAND_REGEN_CLIENT_ID;
ContextObject->Send2Client(&bToken, sizeof(BYTE));
Mprintf("下发重生成 client_id 命令给撞 ID 克隆机 %llu(主机名=%s)\n",
id, strPCName.GetString());
}
}
Mprintf("上线消息 - 主机重复上线,暂存替补 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
// 网络抖动导致客户端用新连接重复上线:这里不立即替换/关闭旧连接,
@@ -6108,15 +6118,33 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
}
case COMMAND_GET_FILE: {
// 发送文件
std::string dir = (char*)(szBuffer + 1);
char* ptr = (char*)szBuffer + 1 + dir.length() + 1;
auto md5 = CalcMD5FromBytes((BYTE*)ptr, len - 2 - dir.length());
if (!m_CmdList.PopCmd(md5)) {
// 边界校验:dir 必须在 len 内以 NUL 结尾,否则 std::string 越界读,且
// len - 2 - dir.length() 会下溢导致后续越界读。异常/恶意客户端可能构造无 NUL 的包。
if (len < 2) {
Mprintf("【警告】文件传输指令包过短: %u\n", len);
ContextObject->CancelIO();
break;
}
const char* pDir = (const char*)(szBuffer + 1);
const char* pDirEnd = (const char*)memchr(pDir, '\0', len - 1);
if (!pDirEnd) {
Mprintf("【警告】文件传输指令 dir 未以 NUL 结尾,丢弃\n");
ContextObject->CancelIO();
break;
}
size_t dirLen = pDirEnd - pDir;
size_t listLen = (len - 1) - dirLen - 1; // 文件列表字节数,已由 memchr 保证 >= 0
std::string dir(pDir, dirLen);
const char* ptr = pDirEnd + 1;
auto md5 = CalcMD5FromBytes((const BYTE*)ptr, (DWORD)listLen);
if (!m_CmdList.PopCmd(md5, ContextObject->GetPeerName())) {
Mprintf("【警告】文件传输指令非法或已过期: %s\n", md5.c_str());
ContextObject->CancelIO();
break;
}
auto files = *ptr ? ParseMultiStringPath(ptr, len - 2 - dir.length()) : std::vector<std::string> {};
// listLen==0 时 ptr 可能越过缓冲区末尾(空文件列表无独立结尾 NUL),先判 listLen>0
// 再解引用 *ptr,避免空列表时越界读 1 字节。
auto files = (listLen > 0 && *ptr) ? ParseMultiStringPath(ptr, listLen) : std::vector<std::string> {};
if (!files.empty()) {
CDlgFileSend* dlg = new CDlgFileSend(this, ContextObject->GetServer(), ContextObject, TRUE);
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
@@ -11560,7 +11588,7 @@ LRESULT CALLBACK CMy2015RemoteDlg::LowLevelKeyboardProc(int nCode, WPARAM wParam
memcpy((char*)szBuffer + 1 + masterId.length(), hmac.c_str(), hmac.length());
memcpy(szBuffer + 1 + 80, str.data(), str.size());
auto md5 = CalcMD5FromBytes((BYTE*)str.data(), str.size());
g_2015RemoteDlg->m_CmdList.PutCmd(md5);
g_2015RemoteDlg->m_CmdList.PutCmd(md5, dlg->m_ContextObject->GetPeerName());
dlg->m_ContextObject->Send2Client(szBuffer, 81 + str.size());
SAFE_DELETE_ARRAY(szBuffer);
Mprintf("【Ctrl+V】 [本地 -> 远程] V1 传输: %s\n", md5.c_str());
+18 -6
View File
@@ -43,18 +43,30 @@ typedef struct DllInfo {
typedef struct FileTransformCmd {
CLock Lock;
std::map<std::string, uint64_t> CmdTime;
void PutCmd(const std::string& str)
// md5 -> (目标客户端源 IP, 登记时间)。源 IP 绑定:只有与 PutCmd 登记时同源 IP 的
// 子连接才能消费该 md5,防止异常客户端在别的 IP 上冒用文件传输指令。
std::map<std::string, std::pair<std::string, uint64_t>> CmdInfo;
void PutCmd(const std::string& str, const std::string& peerIP)
{
Lock.Lock();
CmdTime[str] = time(0);
CmdInfo[str] = std::make_pair(peerIP, (uint64_t)time(0));
Lock.Unlock();
}
bool PopCmd(const std::string& str, int timeoutSec = 10)
bool PopCmd(const std::string& str, const std::string& peerIP, int timeoutSec = 10)
{
Lock.Lock();
bool valid = CmdTime.find(str) != CmdTime.end() && time(0) - CmdTime[str] < timeoutSec;
CmdTime.erase(str);
auto it = CmdInfo.find(str);
if (it == CmdInfo.end()) {
Lock.Unlock();
return false;
}
uint64_t now = (uint64_t)time(0);
bool expired = now - it->second.second >= (uint64_t)timeoutSec;
bool matched = it->second.first == peerIP;
// 过期或成功消费才删除;源 IP 不匹配(冒用)保留,让合法客户端在 TTL 内仍可取回。
bool valid = !expired && matched;
if (expired || valid)
CmdInfo.erase(it);
Lock.Unlock();
return valid;
}
+1 -1
View File
@@ -3643,7 +3643,7 @@ void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
memcpy((char*)szBuffer + 1 + masterId.length(), hmac.c_str(), hmac.length());
memcpy(szBuffer + 1 + 80, str.data(), str.size());
auto md5 = CalcMD5FromBytes((BYTE*)str.data(), str.size());
m_pParent->m_CmdList.PutCmd(md5);
m_pParent->m_CmdList.PutCmd(md5, m_ContextObject->GetPeerName());
m_ContextObject->Send2Client(szBuffer, 81 + str.size());
Mprintf("【拖拽】 [本地 -> 远程] V1 传输: %s\n", md5.c_str());
SAFE_DELETE_ARRAY(szBuffer);