让任务管理器播放动画
2018-06-17 23:47:14来源:未知 阅读 ()
一、源起
原先在B站上看到各式各样拿Windows任务管理器播放动画的视频,感觉很新奇,也有人无私分享代码。有些视频中的动画是后期加上的,也有些是实时渲染的。不管怎样,像实时渲染这类程序就非常“奇特”,它是怎么让任务管理器播放视频的呢?
自制高仿山寨视频
工程源码
二、探秘
“拿来主义”
如要完成一个程序,抑或是一个功能,不会写,怎么办?拿来!
得到源码,解读ing,然后重写,消化吸收,这是“高效”的学习方法。
剖析源码
剖析源码,理解思路。
这个程序大致的工作流程如下:
- 启动目标子进程,即taskmgr.exe
- 注入到子进程,让子进程加载自己编写的dll
- 在dll中实现运行逻辑
启动子进程
常用的启动进程无非是双击一下程序,但在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; }
那么我们就实现了跟随鼠标找到目标窗口的功能。
大体思路:
- 启用Hook监听鼠标移动消息
- 根据鼠标位置实时查找窗口,并对其加粗
- 如鼠标中键按下,保存所在窗口句柄,开始渲染动画
屏蔽重绘消息
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后,还需下一个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)为例。
主进程-控制台(管道读):
- 创建命名管道
- 等待其他程序连接
- 如连接成功,则读管道信息并输出
子进程-应用程序(管道写):
- 获取管道文件句柄
- 写文件
四、尾声
一个非常有趣的程序就这么完成了。
通过此次实验,我们接触了:
- 进程注入-DLL
- 消息拦截-Hook
- 窗口查找-Spy
- 进程间通信-管道
- 逐帧渲染-SDL
- 图像处理-二值化、边缘检测
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- 完全依赖QML实现播放器 2020-03-03
- 实验作业 2019-10-25
- 超级简单的跨平台高性能音视频播放框架QtAv编译指南 2019-08-16
- 福州大学算法作业题 - 加法运算 2018-10-29
- 福州大学算法作业题 - 数列 2018-10-29
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash