刚开始是预备知识,如果熟悉的话,可以直接跳到第二部分阅读
在 Windows API 中,SetTimer
函数用于创建一个定时器,并在指定的时间间隔后触发一个定时器消息。以下是关于 SetTimer
函数的介绍:
功能:创建一个定时器,并在指定的时间间隔后触发定时器消息。
参数:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 获取当前时间
SYSTEMTIME sysTime;
GetLocalTime(&sysTime);
// 将时间转换为字符串
char timeString[64];
sprintf(timeString, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
// 绘制时间文本
TextOut(hdc, 10, 10, timeString, strlen(timeString));
EndPaint(hwnd, &ps);
break;
}
要在窗口中使用计时器(Timer)来定期绘制时间,可以按照以下步骤进行操作:
1、在窗口类中添加一个计时器标识符:在窗口类的定义中,添加一个计时器标识符作为类的成员变量,用于标识计时器的ID。
class WindowClass
{
private:
static const UINT_PTR TIMER_ID = 1; // 计时器标识符
// 其他成员变量和方法
};
2、在窗口的创建过程中启动计时器:在窗口的创建过程中,通过调用 SetTimer
函数来启动计时器。这个函数将设置一个定时器,每隔一定时间触发一个 WM_TIMER
消息。
HWND hwnd = CreateWindow(/* 窗口参数 */);
SetTimer(hwnd, TIMER_ID, 1000, NULL); // 在这里启动计时器,每隔1秒触发一次 WM_TIMER 消息
3、处理 WM_TIMER
消息:在窗口的消息处理函数中,处理 WM_TIMER
消息,并在该消息中绘制时间。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
// 其他消息处理代码
case WM_TIMER:
{
if (wParam == TIMER_ID) // 根据计时器标识符判断是哪个计时器触发的消息
{
// 获取当前时间
SYSTEMTIME sysTime;
GetLocalTime(&sysTime);
// 将时间转换为字符串
char timeString[64];
sprintf(timeString, "%02d:%02d:%02d", sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
// 绘制时间文本
HDC hdc = GetDC(hwnd);
TextOut(hdc, 10, 10, timeString, strlen(timeString));
ReleaseDC(hwnd, hdc);
}
break;
}
// 其他消息处理代码
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
另外,不要忘记在窗口销毁时停止计时器,可以在 WM_DESTROY
消息中调用 KillTimer
函数来停止计时器的触发。
case WM_DESTROY:
KillTimer(hwnd, TIMER_ID); // 停止计时器
PostQuitMessage(0);
break;
首先准备本次实验需要用到的工具:
如果大家需要扫雷的资源,可以私信我获取噢~
附加到扫雷的进程:
首次扫描未知的初始值,类型选择字节
点击首次扫描,发现有10W+的搜索结果,,,然后毫无疑问我们需要过滤掉一部分数据:
点击第一个格子,再次扫描变动的数据
点击旁边的格子,扫描不变的数据,再次扫描
重新开始,如果数字如果发生变化就选择变动的数值,再次扫描
然后继续点击几个格子,不是地雷的话就可以选择未变动的值,再次扫描
很快就找到了我们所需要的雷区的数据
浏览这块区域所在的内存位置,观察一下里面都是什么数据
可以手动测试一下:
"0x0f"表示不是地雷
"0x8f"表示是地雷
"0x0e"表示小旗子
"0x10"表示边界
"40"表示空格
"41-49"表示数字
注意:这里说的高度和宽度是不计算无关数据的,就是真实的高、宽占多少个格子
比如对于初级模式就是9*9:
切换为中级、高级,分别搜索,很快就可以找到高度所在内存:
同理,还可以找到宽度和地雷数量 :
我们找存储时间内存的目的,就是观察谁修改了它,具体来说就是哪条指令把这块内存的数据inc了,这样我们通过把这句指令nop掉,就可以实现时间停止的功能,从而实现0秒扫雷
其实搜索起来十分简单,只要一直追那个变化的数据就好了
这样我们就找到了inc时间的指令,位于0x1002ff5的位置,把他nop掉,时间就静止了~
但是我们一开始点击格子的时候,它还是会自动设置时间为1,所有我们还需要把这一条指令也nop掉,实现真正的0秒扫雷!!!
我采用的思路是在左键弹起的地方设置一个硬件断点,然后一路f8下去,观察到时间发生变化,就在最近的那个call打个断点继续跟----->
看到修改全局的内存,高度怀疑是这里修改时间,经过测试果然如此:
跟进去发现果真调用了绘制函数!!!
所以我们最终确定,设置时间的地址位于0x1003830位置处!
1、获取扫雷窗口句柄,然后获得pid
2、nop掉时间inc的函数,实现时间静止
3、读取雷区有效数据
4、模拟鼠标给窗口发消息
5、释放掉申请的内存和句柄
#include
using namespace std;
#include
#define increaseTimeAddr 0x01002FF5
#define setTimeAddr 0x01003830
#define mineStartAddr 0x01005340
int main() {
//打开扫雷进程
HWND hWnd = FindWindow(nullptr, L"扫雷");
if (hWnd != nullptr)
{
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == nullptr)
{
cout << "fail to find the winmine process" << endl;
CloseHandle(hProcess);
return -1;
}
//测试是否正确打开扫雷进程,如果关闭了,就说明句柄是对的
//TerminateProcess(hProcess, 0);
//把时间增加的指令nop掉,实现0秒扫雷
DWORD oldProtect = 0;
SIZE_T nBytesWritten1 = 0;
SIZE_T nBytesWritten2 = 0;
bool bRet1, bRet2;
BYTE replaceBuf[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
VirtualProtectEx(hProcess, (LPVOID)increaseTimeAddr, 6, MEDIA_READ_WRITE, &oldProtect);
bRet1=WriteProcessMemory(hProcess, (LPVOID)increaseTimeAddr, replaceBuf, sizeof(replaceBuf), &nBytesWritten1);
bRet2=WriteProcessMemory(hProcess, (LPVOID)setTimeAddr, replaceBuf, sizeof(replaceBuf), &nBytesWritten2);
VirtualProtectEx(hProcess, (LPVOID)increaseTimeAddr, 6, oldProtect, &oldProtect);
if (bRet1 == FALSE || bRet2 == FALSE || nBytesWritten1 <= 0 || nBytesWritten2 <= 0) {
cout << "修改时间失败!\n";
CloseHandle(hProcess);
return -1;
}
//读取整个雷区的数据
//设置读取雷区的最大字节数
DWORD maxSize = 832;
char* pByte = NULL;
SIZE_T nBytesRead = 0;
pByte = (char*)malloc(sizeof(BYTE) * maxSize);
bool bReadData = ReadProcessMemory(hProcess, (LPVOID)mineStartAddr, pByte, maxSize, &nBytesRead);
if (!bReadData) {
CloseHandle(hProcess);
delete pByte;
return -1;
}
//打印一下观察是否读取正确
/*for (int i = 0; i < maxSize;i++) {
printf("%x ", pByte[i]);
}*/
//获取当前雷区的大小
DWORD sizeAddr = 0x01005330;
DWORD dwHeight = 0, dwWidth = 0;
ReadProcessMemory(hProcess, (LPVOID)(sizeAddr + 4), &dwWidth, sizeof(DWORD),0);
ReadProcessMemory(hProcess, (LPVOID)(sizeAddr + 8), &dwHeight, sizeof(DWORD), 0);
//读取当前雷区的有效数据
char* pCurByte = NULL;
pCurByte = (char*)malloc(sizeof(BYTE) * dwHeight * dwWidth);
int h = 0;
int index = 0;
while (1) {
if (pByte[index] == 0x10 && pByte[index + 1] != 0x10 && pByte[index + dwWidth + 1] == 0x10) {
for (int j = 0; j < dwWidth; j++) {
pCurByte[dwWidth * h + j] = pByte[index + 1];
index++;
}
h++;
if (h >= dwHeight)
break;
}
index++;
}
//模拟鼠标点击格子
for (int i = 0; i < dwWidth*dwHeight; i++) {
if (i % dwWidth == 0) {
cout << endl;
}
printf("%x ", *(pCurByte+i));
}
int x1 = 0, y1 = 0;
int x = 0, y = 0;
for (int i = 0; i < dwHeight * dwWidth; i++) {
if (*(pCurByte+i) != 0xffffff8f) {
x1 = i % dwWidth;
y1 = i / dwWidth;
x = x1 * 16 + 16;
y = y1 * 16 + 61;
SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG(x, y));
SendMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELONG(x, y));
}
}
free(pByte);
free(pCurByte);
pByte = NULL;
pCurByte = NULL;
CloseHandle(hProcess);
}
printf("???就这???\n");
printf("???就这???\n");
printf("???就这???\n");
printf("???就这???\n");
system("pause");
return 0;
}
扫雷窗口最大是24*30
留下你的鼎鼎大名
当然还要有适当的凡尔赛:
好了,今天的逆向辅助实战就到这里了,如果大家喜欢的话,可以私信我,去更新你们想要的游戏噢~喜欢的话就多多点赞、收藏、关注吧!!!