一:背景
1. 講故事
今天本來想寫一篇 非託管洩露
的生產事故分析,但想著昨天就上了一篇非託管文章,連著寫也沒什麼意思,換個口味吧,剛好前些天有位朋友也找到我,說他們的拍攝監控軟體卡死了,讓我幫忙分析下為什麼會卡死,聽到這種軟體,讓我不禁想起了前些天 在程式設計師桌子上安裝監控 的新聞,參考如下:
我在想我這不是尼瑪作惡嗎... ??? 和朋友確認了下還好不是幹這個事的。
二:WinDbg 分析
1. 為什麼會卡死
因為這種監控軟體是窗體程式,所以它的卡死理應看主執行緒的呼叫棧即可, 在windbg中有一個 k
命令。
0:000:x86> kb 8
# ChildEBP RetAddr Args to Child
00 00dbedc0 77835329 0fd54c08 00000000 0fd54c08 ntdll_777d0000!NtWaitForAlertByThreadId+0xc
01 00dbedc0 7783505c 00000000 00000000 0fd54c08 ntdll_777d0000!RtlpWaitOnAddressWithTimeout+0x64
02 00dbee60 77813fd8 0fd543f0 0fd54c04 0000000c ntdll_777d0000!RtlpWaitOnCriticalSection+0x1ac
03 00dbeea8 77813d99 00000000 00dbef04 09d72f87 ntdll_777d0000!RtlpEnterCriticalSectionContended+0x228
04 00dbeeb4 09d72f87 0fd54c04 09d38131 ee66de6e ntdll_777d0000!RtlEnterCriticalSection+0x49
WARNING: Stack unwind information not available. Following frames may be wrong.
05 00dbef04 09d38036 ee66de46 000001fd 00000111 scvncctrl!DllUnregisterServer+0x4ed7
06 00dbef2c 09d3304d 00000111 000001fd 00000111 scvncctrl+0x48036
07 00dbef50 09d341f3 00000111 000001fd 00000001 scvncctrl+0x4304d
從卦象來看,程式在 scvncctrl!DllUnregisterServer+0x4ed7
方法中等待 臨界區鎖
,即 RtlEnterCriticalSection
處。
可能有些朋友有疑問,為什麼 scvncctrl
後面的偏移值那麼大,這是因為 scvncctrl 沒有提供公有和私有符號,所以無法對應函式名,windbg 只能以 module 為參考點設定偏移,這對 dump 分析產生了很大的阻礙!
接下來繼續看,既然主執行緒在等待鎖,那必然有人在持有鎖,那到底是誰在持有呢?
2. 尋找持有執行緒
要想找到持有者,可以提取 RtlEnterCriticalSection
方法中的第一個引數 0fd54c04
,我們使用 dt _RTL_CRITICAL_SECTION 命令即可。
0:000:x86> dt _RTL_CRITICAL_SECTION 0fd54c04
ntdll_777d0000!_RTL_CRITICAL_SECTION
+0x000 DebugInfo : 0x07ba4428 _RTL_CRITICAL_SECTION_DEBUG
+0x004 LockCount : 0n-6
+0x008 RecursionCount : 0n1
+0x00c OwningThread : 0x0000621c Void
+0x010 LockSemaphore : 0xffffffff Void
+0x014 SpinCount : 0x200064a
上面的 OwningThread
就是當前的持有執行緒,找到了之後切過去看下它的執行緒棧,它到底在幹嘛?
0:005:x86> ~~[0x0000621c]s
ntdll_777d0000!NtWaitForSingleObject+0xc:
7784619c c20c00 ret 0Ch
0:005:x86> kb
CvRegToMachine(x86) conversion failure for 0x14f
X86MachineInfo::SetVal: unknown register 0 requested
# ChildEBP RetAddr Args to Child
00 0a8cf1ac 747ccfd5 00000924 00000001 00000000 ntdll_777d0000!NtWaitForSingleObject+0xc
01 0a8cf1ac 747ddb12 00000002 00000006 ae23e128 mswsock!SockWaitForSingleObject+0x125
02 0a8cf220 75c05fe5 000007e8 0a8cf258 00000001 mswsock!WSPRecv+0x232
03 0a8cf26c 09ddd32f 000007e8 011a5a30 00002000 ws2_32!recv+0x95
WARNING: Stack unwind information not available. Following frames may be wrong.
04 0a8cf3b4 09ddd0a6 011a5a30 00002000 00000003 scvncctrl!DllUnregisterServer+0x6f27f
05 0a8cf4d4 09ddd625 00000001 00000001 07ac4ae0 scvncctrl!DllUnregisterServer+0x6eff6
06 0a8cf5f0 09ddd72f 0fd1f350 07ac4ae0 00000000 scvncctrl!DllUnregisterServer+0x6f575
07 0a8cf708 09d70626 00000003 00000001 0fd543f0 scvncctrl!DllUnregisterServer+0x6f67f
08 0a8cf958 09d71b56 00000075 000001f7 0000070b scvncctrl!DllUnregisterServer+0x2576
09 0a8cf9a4 09d3140c 00000075 000001f7 0000070b scvncctrl!DllUnregisterServer+0x3aa6
0a 0a8cfa18 09d35b89 e431cbea 0fd5fbf0 0fd543f0 scvncctrl+0x4140c
0b 0a8cfa80 09d73189 00000000 09d73120 0a8cfacc scvncctrl+0x45b89
0c 0a8cfa90 09e09434 0fd543f0 e431cba6 09e093dd scvncctrl!DllUnregisterServer+0x50d9
0d 0a8cfacc 75c77ba9 0fd5fbf0 75c77b90 0a8cfb34 scvncctrl!DllUnregisterServer+0x9b384
0e 0a8cfadc 7783b79b 0fd5fbf0 c738a5e9 00000000 kernel32!BaseThreadInitThunk+0x19
0f 0a8cfb34 7783b71f ffffffff 778689f7 00000000 ntdll_777d0000!__RtlUserThreadStart+0x2b
卦中的 ws2_32!recv
是一個win32體系內的方法,用於 接收客戶端傳送資料
,可能有些朋友對 recv
方法不是很清楚,方法簽名大概如下:
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
因為是主控端,我在網上找了一段 win32 實現的 server 版的 recv 完整程式碼。
#define _WINSOCK_DEPRECATED_NO_WARNINGS
//1.標頭檔案
#include <stdio.h>
#include <Winsock2.h>
#pragma comment (lib,"ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("請求版本失敗!\n");
return -1;
}
printf("請求版本成功!\n");
SOCKET serverScoket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == serverScoket)
{
printf("建立套接字失敗!\n");
WSACleanup();
return -1;
}
printf("建立套接字成功!\n");
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.107");
if (SOCKET_ERROR == bind(serverScoket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)))
{
printf("繫結失敗!\n");
closesocket(serverScoket);
WSACleanup();
return -1;
}
printf("繫結成功!\n");
if (SOCKET_ERROR == listen(serverScoket, 10))
{
printf("監聽失敗!\n");
closesocket(serverScoket);
WSACleanup();
return -1;
}
printf("監聽成功!\n");
SOCKADDR_IN clientAddr = { 0 };
int len = sizeof(clientAddr);
SOCKET clientSocket = accept(serverScoket, (sockaddr*)&clientAddr, &len);
if (INVALID_SOCKET == clientSocket)
{
printf("接受連結失敗!\n");
closesocket(serverScoket);
WSACleanup();
return -1;
}
printf("接受客戶連結成功!\n");
printf("客戶ip為:%s", inet_ntoa(clientAddr.sin_addr));
//8.開始通訊
char recvbuff[1024] = {};
char sendbuff[1024] = {};
//引數一:代表客戶端的socket,表示從客戶端進行收取資料
//引數二:接受的資料存放地址
//引數三:接受資料的長度
//引數四:表示收發方式,0表示預設,一次收完
while (true)
{
//儲存資料清空
memset(recvbuff, 0, sizeof(recvbuff));
//從客戶端接受資料
if (recv(clientSocket, recvbuff, sizeof(recvbuff) - 1, 0) > 0)
{
printf("客戶說:%s\n", recvbuff);
}
else
{
break;
}
memset(sendbuff, 0, sizeof(sendbuff));
printf("我說:");
scanf_s("%s", sendbuff, sizeof(sendbuff) - 1);
//傳送資料給客戶端
send(clientSocket, sendbuff, strlen(sendbuff), 0);
}
//9.關閉連結
closesocket(clientSocket);//關閉客戶端socket
closesocket(serverScoket);//關閉服務端socket
WSACleanup(); //關閉套接字請求
return 0;
}
結合上面的完整程式碼,業務邏輯應該是 while (true)
裡的 send
和 recv
區間內的某句程式碼持有了鎖,但因為某種異常導致持有的 臨界區鎖
沒有釋放,出現了一種 鎖汙染
的情況。
朋友提供的資訊也進一步佐證了這種說法。
- 大截圖
- 受控端偶發斷網
這些情況組合在一起導致了 send
和 recv
之間的某處程式碼異常汙染了 臨界區鎖
。
本來想提取下 recv 中的 socket 資訊,結果發現是一個網路控制程式碼號,真正的socket資訊在核心層,沒法提出來只能作罷,截圖如下:
也即執行緒棧上的 000007e8
欄位。
0a8cf26c 09ddd32f 000007e8 011a5a30 00002000 ws2_32!recv+0x95
那這個問題怎麼解決呢? 通篇分析下來應該就是 scvncctrl 的 bug,能做的就是升級到最新版本,畢竟程式裡還是 2020 年的。
0:005:x86> lmvm scvncctrl
Browse full module list
start end module name
09cf0000 09f06000 scvncctrl (export symbols) scvncctrl.dll
Loaded symbol image file: scvncctrl.dll
Image name: scvncctrl.dll
Browse all global symbols functions data
Timestamp: Sat Oct 10 15:14:33 2020 (5F815F59)
CheckSum: 001CA728
ImageSize: 00216000
File version: 3.9.2.0
Product version: 3.9.2.0
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
Information from resource tables:
CompanyName: SmartCode Pte. Ltd.
ProductName: SmartCode VNC Viewer ActiveX
OriginalFilename: scvncctrl.dll
ProductVersion: 3.9.2.0
FileVersion: 3.9.2.0
FileDescription: SmartCode VNC Viewer ActiveX
LegalCopyright: Copyright (c) 2003-2020 SmartCode Pte. Ltd. All rights reserved.
Comments: https://www.s-code.com
三:總結
這次卡死事故還是挺有教育意義的,告訴我們第三方外掛儘量應升盡升,同時也考察了對 臨界區鎖 和 socket 的基礎知識。