• 让任务管理器播放动画


    一、源起

    原先在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. 图像处理-二值化、边缘检测
  • 相关阅读:
    springboot Quartz 定时任务工具类
    java 发邮件 代码
    spring boot 整合 shiro 权限框架
    java 读取xml
    flowable 报错 Waiting for changelog lock....
    微服务 springcloud Alibaba 阿里组件 项目源码
    Java 读取汉字拼音
    syslog how to
    c++ 字符串总结
    aptget 总结
  • 原文地址:https://www.cnblogs.com/bajdcc/p/5868125.html
Copyright © 2020-2023  润新知