让任务管理器播放动画

一、源起

原先在B站上看到各式各样拿Windows任务管理器播放动画的视频,感觉很新奇,也有人无私分享代码。有些视频中的动画是后期加上的,也有些是实时渲染的。不管怎样,像实时渲染这类程序就非常“奇特”,它是怎么让任务管理器播放视频的呢?

自制高仿山寨视频

工程源码

 

二、探秘

“拿来主义”

如要完成一个程序,抑或是一个功能,不会写,怎么办?拿来!

得到源码,解读ing,然后重写,消化吸收,这是“高效”的学习方法。

剖析源码

剖析源码,理解思路。

这个程序大致的工作流程如下:

  1. 启动目标子进程,即taskmgr.exe
  2. 注入到子进程,让子进程加载自己编写的dll
  3. 在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;
}

那么我们就实现了跟随鼠标找到目标窗口的功能。

大体思路:

  1. 启用Hook监听鼠标移动消息
  2. 根据鼠标位置实时查找窗口,并对其加粗
  3. 如鼠标中键按下,保存所在窗口句柄,开始渲染动画

屏蔽重绘消息

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画风相差太大,这是因为程序对图片进行了处理。

简单来说,做的工作有:

  1. 二值化
  2. 边缘检测
  3. 画背景
  4. 画边框
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)为例。

主进程-控制台(管道读):

  1. 创建命名管道
  2. 等待其他程序连接
  3. 如连接成功,则读管道信息并输出

子进程-应用程序(管道写):

  1. 获取管道文件句柄
  2. 写文件

四、尾声

一个非常有趣的程序就这么完成了。

通过此次实验,我们接触了:

  1. 进程注入-DLL
  2. 消息拦截-Hook
  3. 窗口查找-Spy
  4. 进程间通信-管道
  5. 逐帧渲染-SDL
  6. 图像处理-二值化、边缘检测
原文地址:https://www.cnblogs.com/bajdcc/p/5868125.html