原先在B站上看到各式各樣拿Windows任務管理器播放動畫的視頻,感覺很新奇,也有人無私分享代碼。有些視頻中的動畫是後期加上的,也有些是實時渲染的。不管怎樣,像實時渲染這類程序就非常“奇特”,它是怎麼讓任務管理器播放視頻的呢?
自制高仿山寨視頻
工程源碼
如要完成一個程序,抑或是一個功能,不會寫,怎麼辦?拿來!
得到源碼,解讀ing,然後重寫,消化吸收,這是“高效”的學習方法。
剖析源碼,理解思路。
這個程序大致的工作流程如下:
常用的啟動進程無非是雙擊一下程序,但在C++中如何調用呢,用ShellExecute。
運行命令行:
ShellExecute(NULL, "open", "taskmgr", NULL, NULL, SW_SHOWNORMAL);
第三個參數相當於“開始-運行”中的命令。
由於taskmgr是子進程,那麼我們的父進程對它進行控制就有很高的權限。
網上常見的一段提權代碼:
HANDLE hToken; if (!OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { return;//ERROR } TOKEN_PRIVILEGES tkp; tkp.PrivilegeCount = 1; if (!LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &tkp.Privileges[0].Luid)) { return;//ERROR } if (!AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), nullptr, nullptr)) { return;//ERROR }
啟動子進程後,等待一段時間,開始注入。
“注入”的意思,比方說“注水豬肉”,將自己刻意編寫的代碼注入到目標進程中。
既然我們對taskmgr擁有最高權限,那麼注入也就不成問題。
首先,需要找到子進程(taskmgr,下面略),在茫茫進程樹中,如何找到它呢?
我們就用一個比較便捷的方法。
HWND hWnd = FindWindow("TaskManagerWindow", "任務管理器");
根據類名和窗口名稱找到它,做到這些,只需下一個Spy++,自制Spy。
有人問,如果有開了多個任務管理器怎麼辦?那只能全部關掉重新試了,因為這裡有權限的問題。
接下來獲取它的句柄:
DWORD dwPId; GetWindowThreadProcessId(hWnd, &dwPId);
以最高權限打開進程:
HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPId);
現在需要將縮寫的dll的路徑寫入到子進程:
LPCTSTR szLibPath = "DLL的絕對路徑"; LPVOID pLibRemoteSrc = VirtualAllocEx(hRemoteProcess, nullptr, nLibPathLength, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hRemoteProcess, pLibRemoteSrc, LPVOID(*szLibPath), nLibPathLength, &dwPathLength);
現在,子進程的pLibRemoteSrc中就存放了DLL的路徑。
下面需要讓子進程根據路徑加載DLL。
進程加載DLL的函數是LoadLibraryA,我們需要讓子進程執行這個函數,首先需要獲取這個函數的地址。
LoadLibraryA位於kernel32.dll中,由於其特殊性,不同進程會將kernel32.dll加載到同一個地址,此外還有user32.dll等等,這給我們帶來了方便。
獲取LoadLibraryA的地址:
HMODULE hKernel32 = GetModuleHandleA("Kernel32"); FARPROC fpcLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
在子進程中執行LoadLibraryA:
CreateRemoteThread(hRemoteProcess, NULL, NULL, LPTHREAD_START_ROUTINE(fpcLoadLibrary), pLibRemoteSrc, NULL, NULL);
調用VirtualFreeEx釋放內存,CloseHandle關閉句柄,不用多說。
目前是程序最核心的階段——“教會”taskmgr去播放動畫。
動畫都是由一幀幀組成,現在我們已經擁有幾千幀的圖片,放置在某個文件夾中,編號如0000.jpg-9999.jpg。
我們需要找到播放動畫的那個子窗口,怎麼找?用Spy?我們來個高大上的Hook。
Hook在這裡的用處就是過濾或是截獲消息。
首先,我們要替換的CPU圖表為什麼會不停地動?就是因為定時器對它發送重繪消息,我們可以用Hook截獲它,或是SetWindowLong過濾掉它。在嘗試過程中,發現Hook有點問題,因此采用SetWindowLong。
其次,我們要高仿個Spy的功能——實時定位鼠標所在窗口。怎麼做呢?鼠標在移動過程中會發送移動消息WM_MOUSEMOVE,那麼我們規定按下鼠標中鍵WM_MBUTTONDOWN,鼠標此時所在的窗口就播放動畫。
那麼啟用Hook:
SetWindowsHookEx(WH_GETMESSAGE, HOOKPROC(MsgHookProc), NULL, GetWindowThreadProcessId(g_hWnd, NULL));
監聽消息:
LRESULT CALLBACK MsgHookProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode < 0) { return CallNextHookEx(g_hHook, nCode, wParam, lParam); } if (nCode == HC_ACTION) { auto lpMsg = LPMSG(lParam); POINT pt; switch (lpMsg->message) { case WM_MOUSEMOVE: if (g_bHooking) { pt = lpMsg->pt; ScreenToClient(g_hWnd, &pt); SpyExecScanning(pt); } break; case WM_MBUTTONDOWN: if (g_bHooking) { pt = lpMsg->pt; g_prevHwnd = SpyFindSmallestWindow(pt); //找到當前鼠標所在窗口 g_bHooking = FALSE; uHook = SetTimer(g_hWnd, WM_USER + 401, 1000, TIMERPROC(HookProc)); //利用定時器創建渲染線程 } break; default: break; } } return CallNextHookEx(g_hHook, nCode, wParam, lParam); }
在這裡面有SpyFindSmallestWindow(找到鼠標所在窗口)和SpyExecScanning(對鼠標所在窗口邊框進行加粗)。
找到鼠標所在窗口:
HWND SpyFindSmallestWindow(const POINT &pt) { auto hWnd = WindowFromPoint(pt); // 鼠標所在窗口 if (hWnd) { // 得到本窗口大小和父窗口句柄,以便比較 RECT rect; ::GetWindowRect(hWnd, &rect); auto parent = ::GetParent(hWnd); // 父窗口 // 只有該窗口有父窗口才繼續比較 if (parent) { // 按Z方向搜索 auto find = hWnd; // 遞歸調用句柄 RECT rect_find; while (1) // 循環 { find = ::GetWindow(find, GW_HWNDNEXT); // 得到下一個窗口的句柄 ::GetWindowRect(find, &rect_find); // 得到下一個窗口的大小 if (::PtInRect(&rect_find, pt) // 鼠標所在位置是否在新窗口裡 && ::GetParent(find) == parent // 新窗口的父窗口是否是鼠標所在主窗口 && ::IsWindowVisible(find)) // 窗口是否可視 { // 比較窗口,看哪個更小 if (RECT_SIZE(rect_find) < RECT_SIZE(rect)) { // 找到更小窗口 hWnd = find; // 計算新窗口的大小 ::GetWindowRect(hWnd, &rect); } } // hWnd的子窗口find為NULL,則hWnd為最小窗口 if (!find) { break; // 退出循環 } } } } return hWnd; }
對鼠標所在窗口邊框進行加粗:
void SpyInvertBorder(const HWND &hWnd) { // 若非窗口則返回 if (!IsWindow(hWnd)) return; RECT rect; // 窗口矩形 // 得到窗口矩形 ::GetWindowRect(hWnd, &rect); auto hDC = ::GetWindowDC(hWnd); // 窗口設備上下文 // 設置窗口當前前景色的混合模式為R2_NOT // R2_NOT - 當前的像素值為屏幕像素值的取反,這樣可以覆蓋掉上次的繪圖 SetROP2(hDC, R2_NOT); // 創建畫筆 HPEN hPen; // PS_INSIDEFRAME - 產生封閉形狀的框架內直線,指定一個限定矩形 // 3 * GetSystemMetrics(SM_CXBORDER) - 三倍邊界粗細 // RGB(0,0,0) - 黑色 hPen = ::CreatePen(PS_INSIDEFRAME, 3 * GetSystemMetrics(SM_CXBORDER), RGB(0, 0, 0)); // 選擇畫筆 auto old_pen = ::SelectObject(hDC, hPen); // 設定畫刷 auto old_brush = ::SelectObject(hDC, GetStockObject(NULL_BRUSH)); // 畫矩形 Rectangle(hDC, 0, 0, RECT_WIDTH(rect), RECT_HEIGHT(rect)); // 恢復原來的設備環境 ::SelectObject(hDC, old_pen); ::SelectObject(hDC, old_brush); DeleteObject(hPen); ReleaseDC(hWnd, hDC); } void SpyExecScanning(POINT &pt) { ClientToScreen(g_hWnd, &pt); // 轉換到屏幕坐標 auto current_window = SpyFindSmallestWindow(pt); //找到當前位置的最小窗口 if (current_window) { // 若是新窗口,就把舊窗口的邊界去掉,畫新窗口的邊界 if (current_window != g_prevHwnd) { SpyInvertBorder(g_prevHwnd); g_prevHwnd = current_window; SpyInvertBorder(g_prevHwnd); } } g_savedHwnd = g_prevHwnd; }
那麼我們就實現了跟隨鼠標找到目標窗口的功能。
大體思路:
LRESULT CALLBACK PaintProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_PAINT) return TRUE; if (msg == WM_LBUTTONUP) bUpdate = !bUpdate; return CallWindowProc(oldProc, hWnd, msg, wParam, lParam); } SetWindowLong(hWnd, GWL_WNDPROC, LONG(PaintProc));
替換窗口子過程,如是重繪消息,不予處理。
原先的啟動Hook、替換子過程等任務是在OnAttach中做的,即DLL剛加載至子進程中,而在OnAttach這個初始化線程中,是無法創建新線程的,這是因為此時DLL尚未初始化完成。
然而有解決方法,即用SetTimer運行任務,當任務被Timer喚醒時,DLL已加載完畢。
此時開始渲染。
在播放動畫之前,做了許多繁瑣的工作。其實涉及渲染的代碼反而不是很多。
我們這裡用SDL完成渲染任務,一方面為了方便,另一方面體驗一把SDL。
去官網上下載SDL後,還需下一個SDL_TTF插件,用來顯示文字。
下面就初始化:
Print("Create SDL Window..."); if (SDL_Init(SDL_INIT_VIDEO) < 0) { Print("Create SDL Window... FAILED"); Print(SDL_GetError()); return; } sdlWindow = SDL_CreateWindowFrom(static_cast<void*>(hWnd)); if (sdlWindow) Print("Create SDL Window... OK"); else { Print("Create SDL Window... FAILED"); return; } Print("Create SDL Surface... OK"); sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (!sdlRenderer) { Print("Create SDL Renderer... FAILED"); return; } Print("Create SDL Renderer... OK"); sdlSurface = SDL_GetWindowSurface(sdlWindow); if (!sdlSurface) { Print("Create SDL Surface... FAILED"); return; } Print("Create SDL Surface... OK"); if (TTF_Init() == -1) { Print("Create SDL TTF... FAILED"); Print(TTF_GetError()); return; } Print("Create SDL TTF... OK"); SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 255, 255); SDL_RenderClear(sdlRenderer); SDL_RenderPresent(sdlRenderer); auto font = TTF_OpenFont("C:\\windows\\fonts\\msyh.ttf", 32);//微軟雅黑 assert(font); SDL_Color color = { 17, 152, 187 }; auto surface = TTF_RenderUNICODE_Blended(font, PUINT16(L"准備播放動畫!"), color); auto texture = SDL_CreateTextureFromSurface(sdlRenderer, surface); SDL_Rect rt; rt.x = 0; rt.y = 0; SDL_QueryTexture(texture, nullptr, nullptr, &rt.w, &rt.h); SDL_RenderClear(sdlRenderer); SDL_RenderCopy(sdlRenderer, texture, &rt, &rt); SDL_RenderPresent(sdlRenderer); SDL_DestroyTexture(texture); SDL_FreeSurface(surface); TTF_CloseFont(font); SDL_Delay(2000); Prepare(); GetWindowTextA(g_hWnd, oldCaption, sizeof(oldCaption)); uSDL = SetTimer(g_hWnd, WM_USER + 402, REFRESH_RATE, SDLProc);//啟動計時器,開始播放逐幀動畫
我們將目標窗口的句柄直接傳給了SDL,這樣有個好處——窗口大小變化時,動畫的大小也會相應變化。
在每一幀中,我們要加載一幀圖片,處理後再渲染。
VOID CALLBACK SDLProc(HWND, UINT, UINT_PTR, DWORD) { static char filename[100]; if (bUpdate) nSDLTime++; else return; sprintf_s(filename, g_strImagePathFormat, nSDLTime); int x, y, comp; auto data = stbi_load(filename, &x, &y, &comp, 0); if (!data) return; ProcessingImage(data, x, y, comp, x * comp); auto image = SDL_CreateRGBSurfaceFrom(data, x, y, comp << 3, x * comp, 0, 0, 0, 0); if (!image) { SetWindowTextA(g_hWnd, SDL_GetError()); return; } auto texture = SDL_CreateTextureFromSurface(sdlRenderer, image); SDL_RenderClear(sdlRenderer); SDL_RenderCopy(sdlRenderer, texture, nullptr, nullptr); SDL_RenderPresent(sdlRenderer); SDL_DestroyTexture(texture); SDL_FreeSurface(image); stbi_image_free(data); if (nSDLTime > MAX_FRAME) { //對SDL的清掃工作 if (uSDL) KillTimer(g_hWnd, uSDL); return; } }
大家發現,顯示的動畫跟保存的jpg畫風相差太大,這是因為程序對圖片進行了處理。
簡單來說,做的工作有:
void ProcessingImage(stbi_uc* data, int width, int height, int comp, int pitch) { int i, j; BYTE c, prev; //二值化 for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { //auto B = data[j * pitch + i * comp]; //auto G = data[j * pitch + i * comp + 1]; //auto R = data[j * pitch + i * comp + 2]; auto Gray = 0.212671f * data[j * pitch + i * comp + 2] + 0.715160f * data[j * pitch + i * comp + 1] + 0.072169f * data[j * pitch + i * comp]; if (Gray < 128.0f) { data[j * pitch + i * comp] = 0; data[j * pitch + i * comp + 1] = 0; data[j * pitch + i * comp + 2] = 0; } else { data[j * pitch + i * comp] = 255; data[j * pitch + i * comp + 1] = 255; data[j * pitch + i * comp + 2] = 255; } } } //邊緣檢測 prev = 0; for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { c = data[j * pitch + i * comp]; if (c != prev) { data[j * pitch + i * comp] = DISPLAY_B; data[j * pitch + i * comp + 1] = DISPLAY_G; data[j * pitch + i * comp + 2] = DISPLAY_R; } prev = c; } } //邊緣檢測 prev = 0; for (i = 0; i < width; i++) { for (j = 0; j < height; j++) { c = data[j * pitch + i * comp]; if (c != prev) { data[j * pitch + i * comp] = DISPLAY_B; data[j * pitch + i * comp + 1] = DISPLAY_G; data[j * pitch + i * comp + 2] = DISPLAY_R; } prev = c; } } //背景 for (i = 0; i < width; i++) { for (j = 0; j < height; j++) { c = data[j * pitch + i * comp]; if (c == DISPLAY_B) continue; if ((j % (height / 10) == 0) && j != 0)//橫線 { data[j * pitch + i * comp] = LINE_B; data[j * pitch + i * comp + 1] = LINE_G; data[j * pitch + i * comp + 2] = LINE_R; } else if ((i % (width / 5) == (((MAX_FRAME - nSDLTime) / 30 * (width / 20))) % (width / 5)) && i != 0)//豎線 { data[j * pitch + i * comp] = LINE_B; data[j * pitch + i * comp + 1] = LINE_G; data[j * pitch + i * comp + 2] = LINE_R; } else if (c == 255) { data[j * pitch + i * comp] = BG_B; data[j * pitch + i * comp + 1] = BG_G; data[j * pitch + i * comp + 2] = BG_R; } else if (c == 0) { data[j * pitch + i * comp] = FILL_B; data[j * pitch + i * comp + 1] = FILL_G; data[j * pitch + i * comp + 2] = FILL_R; } } } //邊框 for (i = 0; i < width; i++) { for (j = 0; j < height; j++) { if ((i == 0 || i == width - 1) || (j == 0 || j == height - 1)) { data[j * pitch + i * comp] = EDGE_B; data[j * pitch + i * comp + 1] = EDGE_G; data[j * pitch + i * comp + 2] = EDGE_R; } } } }
我們需要子進程把渲染的信息實時在控制台中顯示出來。
這就涉及到進程間通訊(IPC)了,方法有很多,這裡以管道(Pipe)為例。
主進程-控制台(管道讀):
子進程-應用程序(管道寫):
一個非常有趣的程序就這麼完成了。
通過此次實驗,我們接觸了: