金磊 发自 凹非寺
量子位 报道 | 公众号 QbitAI
渲染3D图像,一个「记事本」就够了。
最近,GitHub上一名叫“Kyle Halladay”的小哥,便上传了这样一个项目,用记事本来渲染图像。
效果是这样的:
立方体旋转、阴影变化,还挺有内味的。
还有贪吃蛇效果的:
那么,小哥是如何拿记事本,就做到这些效果的呢?
正确的「记事本」打开方式
据小哥介绍,所有的输入和渲染效果,都是在记事本中完成。
在此之前,需要做一些设置工作。
首先,是将键盘事件(Key Event),发送到正在运行的记事本。
这里就要用到 Visual Studio 提供的一个叫 Spy + + 的工具,可以列出组成给定应用程序的所有窗口。
Spy + + 显示了要找的记事本子窗口是“编辑”窗口。
一旦我知道了这一点,就只需要搞清楚 Win32函数调用的正确组合,用来获得该 UI 元素的 HWND,然后将输入发送过去。
得到的 HWND 是这样的:
HWND GetWindowForProcessAndClassName(DWORD pid, const char* className){ HWND curWnd = GetTopWindow(0); //0 arg means to get the window at the top of the Z order char classNameBuf[256]; while (curWnd != NULL){ DWORD curPid; DWORD dwThreadId = GetWindowThreadProcessId(curWnd, &curPid); if (curPid == pid){ GetClassName(curWnd, classNameBuf, 256); if (strcmp(className, classNameBuf) == 0) return curWnd; HWND childWindow = FindWindowEx(curWnd, NULL, className, NULL); if (childWindow != NULL) return childWindow; } curWnd = GetNextWindow(curWnd, GW_HWNDNEXT); } return NULL; }
一旦拿到了正确的控件 HWND,在记事本的编辑控件中绘制一个字符,便是使用 PostMessage 向它发送一个 WM char 事件的问题。
接下来,就是建一个内存扫描器 (Memory Scanner),这里要用到一个叫做 CheatEngine 的工具。
基本算法如下:
FOR EACH block of memory allocated by our target process IF that block is committed and read/write enabled Scan the contents of that block for our byte pattern IF WE FIND IT return that address
内存扫描程序需要做的第一件事,就是遍历进程分配的内存。
因为 Windows 上每个64位进程的虚拟内存范围是相同的,所以需要制作一个指向地址0的指针,然后使用 VirtualQueryEx 获取目标程序的虚拟地址信息。
将具有相同内存属性的内容页,组织到 MEMORY basic information 结构中,因此,可能是 VirtualQueryEx 为给定地址返回的结构包含超过1页的信息。
一旦有了第一个 MEMORY basic information 结构,在内存中进行迭代只需要将当前结构的 BaseAddress 和 RegionSize 成员添加到一起,并将新地址提供给 VirtualQueryEx 以获得下一组连续的页面。
char* FindBytePatternInProcessMemory(HANDLE process, const char* pattern, size_t patternLen){ char* basePtr = (char*)0x0; MEMORY_BASIC_INFORMATION memInfo; while (VirtualQueryEx(process, (void*)basePtr, &memInfo, sizeof(MEMORY_BASIC_INFORMATION))) { const DWORD mem_commit = 0x1000; const DWORD page_readwrite = 0x04; if (memInfo.State == mem_commit && memInfo.Protect == page_readwrite) { // search this memory for our pattern } basePtr = (char*)memInfo.BaseAddress + memInfo.RegionSize; } }
然后,是在进程内存中,搜索字节模式 (Byte Pattern)的工作,此处需要一个叫做 ReadProcessMemory 的工具。
一旦内存被复制到本地可见的缓冲区,搜索字节模式就很容易了。
char* FindPattern(char* src, size_t srcLen, const char* pattern, size_t patternLen){ char* cur = src; size_t curPos = 0; while (curPos
char* FindBytePatternInProcessMemory(HANDLE process, const char* pattern, size_t patternLen){ MEMORY_BASIC_INFORMATION memInfo; char* basePtr = (char*)0x0; while (VirtualQueryEx(process, (void*)basePtr, &memInfo, sizeof(MEMORY_BASIC_INFORMATION))){ const DWORD mem_commit = 0x1000; const DWORD page_readwrite = 0x04; if (memInfo.State == mem_commit && memInfo.Protect == page_readwrite){ char* remoteMemRegionPtr = (char*)memInfo.BaseAddress; char* localCopyContents = (char*)malloc(memInfo.RegionSize); SIZE_T bytesRead = 0; if (ReadProcessMemory(process, memInfo.BaseAddress, localCopyContents, memInfo.RegionSize, &bytesRead)){ char* match = FindPattern(localCopyContents, memInfo.RegionSize, pattern, patternLen); if (match){ uint64_t diff = (uint64_t)match - (uint64_t)(localCopyContents); char* processPtr = remoteMemRegionPtr + diff; return processPtr; } } free(localCopyContents); } basePtr = (char*)memInfo.BaseAddress + memInfo.RegionSize; } }
需要注意的是,记事本将屏幕上的文本缓冲区作为 UTF-16数据存储,因此提供给 FindBytePatternInMemory ()的字节模式也必须是 UTF-16。
更多细节描述,可以参考文末的参考链接。
更多的「记事本」玩法
当然,关于记事本的别样玩法,还有好多。
例如,有拿记事本完成「快排」的可视化。
还有用记事本自制绘图软件的。
那么,你还有更炫酷的「记事本」玩法吗?
欢迎在评论区留言推荐~