本文已参与「新人创作礼」活动,一起开启掘金创作之路。 网上的X64dbg新手入门的教程很少,因为自己也是个新手菜狗并且直接跳过了OD用的X64dbg,所以想记录一下方便后来想直接上手X64dbg的萌新,把过程写下来也是想加深一遍理解,用QT编写辅助是因为最近在学QT (目标制作游戏辅助,实现扫雷一键通关等各种功能)
最终效果
- 实现了远程控制难度
- 修改旗子的数量
- 显示雷在雷区中所在位置
- 一键扫雷
成品及源码download.csdn.net/download/L2…
涉及知识
X64dbg的使用 CE工具的使用 Win32API C++的QT编程 32位汇编
模拟菜单点击和修改旗子数量
前置知识——WIN32API消息处理函数 当菜单被点击时会发送WM_COMMAND消息 WIN32API消息处理函数的原型
LRESULT CALLBACK WndProc(
HWND hWnd, //窗口句柄
UINT msgID, //消息ID
WPARAM wParam, //消息参数
LPARAM lParam //消息参数
)
当点击菜单的时候,windowProc会被系统调用 msgID = WM_COMMAND wParam = 对应的自定义的菜单ID
功能分析 若要模拟菜单点击需要得到菜单点击时发送消息的类型,以及所带参数 win32API菜单点击消息的原型
在这里WM_COMMAND
wNotifyCode = HIWORD(wParam); // notification code
wID = LOWORD(wParam); // item, control, or accelerator identifier
hwndCtl = (HWND) lParam; // handle of control 插入代码片
获取菜单消息ID
- 用X32dbg打开扫雷
- 打开后按F9直到出现扫雷界面(若不然无法在句柄视图查看到扫雷)
-
切换到句柄视图并点击反键选择刷新 ,选中扫雷反键选择 在汇编中转到窗口过程 (也就是消息处理函数)
-
即可定位到消息处理函数的地址 可以在此位置设置断点 然后在扫雷界面上晃动鼠标 即可发现程序在此断下 说明消息处理函数定位成功
-
这里推荐一款插件叫xAnalyzer的插件来显示WIN32API调用时传递的参数 这点X64dbg没有OD好用 虽然自己手动分析也能看出来 但好用的插件可以让人事半功倍 看雪上的大佬已经把常用的插件打包好了bbs.pediy.com/thread-2664…
-
因为函数参数的传递是通过栈传递的 windows的消息处理函数采用的是__std调用约定 再结合通过xAnalyzer分析出的
01001C41 | 50 | PUSH EAX | LPPAINTSTRUCT lpPaint
01001C42 | FF75 08 | PUSH DWORD PTR SS:[EBP+8] | HWND hwnd
01001C45 | FF15 44110001 | CALL DWORD PTR DS:[<&BeginPaint>] | BeginPaint
01001C4B | 50 | PUSH EAX |
01001C4C | E8 720E0000 | CALL <winmine.sub_1002AC3> |
01001C51 | 8D45 C0 | LEA EAX,DWORD PTR SS:[EBP-40] | [EBP-40] 也就是CALL winmine.sub_1002AC3函数的返回值 PAINTSTRUCT* lpPaint
01001C54 | 50 | PUSH EAX | PAINTSTRUCT* lpPaint
01001C55 | FF75 08 | PUSH DWORD PTR SS:[EBP+8] | HWND hWnd
01001C58 | FF15 40110001 | CALL DWORD PTR DS:[<&EndPaint>] | EndPaint
可以得出
01001C55 | FF75 08 | PUSH DWORD PTR SS:[EBP+8] | 为窗口句柄
01001BCF | 8B55 0C | MOV EDX,DWORD PTR SS:[EBP+C] | 为参数Msg消息ID
01001BD2 | 8B4D 14 | MOV ECX,DWORD PTR SS:[EBP+14] | 为参数lParam消息参数
则 SS:[EBP+12] 指向的两个字型为参数wParam的值
反键 在MOV ECX,DWORD PTR SS:[EBP+14]处设置条件断点EDX==WM_COMMAND判断是否是在此处理菜单点击消息
点击扫雷游戏发现只有在点击菜单中的选项时程序才被截断 说明定位成功
程序截断后查看栈面板
熟悉函数调用的应该都知道此时栈面板中的值都代表着那些参数了,不明白这些参数怎么来的请移步【逆向工程】C/C++的反汇编表示详解(1)
000DFCD8 000DFD04 提升栈空间前EBP寄存器的值
000DFCDC 752F47AB 函数返回值地址
000DFCE0 00380D08 HWND hWnd, //窗口句柄
000DFCE4 00000111 UINT msgID, //消息ID
000DFCE8 00000209 WPARAM wParam, //消息参数
000DFCEC 00000000 LPARAM lParam //消息参数
重复上述步骤,我们可以获取扫雷初级中级高级三个菜单的ID
- 初级 0x00000209
- 中级 0x0000020A
- 高级 0x0000020B
注入代码调试
这里用的工具是CodeInject,注入后扫雷难度成功改变 说明所得各数据正确
确认旗子基地址 CE开扫 以剩余旗子为数值 不断改变其值,取得唯一地址 0x01005194
验证是否为基地址 在命令行中输入dump 01005194 跳转到扫描得到的地址
设置内存断点 这点就很难受x64dbg是针对内存页下的断点的不是跟OD一样是针对准确的地址值来下断点的
,但根据我的调试 当剩余雷数减少时最终发现会在一下位置调用 直接查看内存视图也可以看到这个指令
可以看到此时EAX的值为FFFFFFFF也就是-1 表明剩余旗子数-1,像这种立即数寻址的通常都是基地址
实现切换难度和修改旗子数量的主要代码
//获取窗口句柄
this->hWnd=FindWindow(NULL,L扫雷);
//获取进程句柄
this->hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pID);
connect(ui->comboBox,SIGNAL(currentIndexChanged(int)),this,SLOT(Level(int)));
//绑定修改难度槽函数
GetWindowThreadProcessId(this->hWnd,&pID);
void MainWindow::Level(int _level)
{
//切换扫雷难度
switch (_level)
{
case 0:SendMessage(this->hWnd,WM_COMMAND,0x209,0);break;//sendmessage发送消息可以使程序立刻处理此消息
case 1:SendMessage(this->hWnd,WM_COMMAND,0x20A,0);break;
case 2:SendMessage(this->hWnd,WM_COMMAND,0x20B,0);break;
}
}
//读取剩余红旗数基地址内存
ReadProcessMemory(this->hProcess,(LPCVOID)0x01005194,&this->flagNumber,sizeof (flagNumber),&pID);//进程句柄 基地址
//显示旗帜个数
ui->mineText->setText(QString::number(this->flagNumber));
//绑定修改剩余红旗数信号和槽函数 这里是自定义的信号和槽
connect(ui->updateButton, &QPushButton::clicked, this,[=](){emit AlterFlagNumberSignal(hProcess);} );
connect(this,&MainWindow::AlterFlagNumberSignal,this,&MainWindow::AlterFlagNumber);
//修改旗子个数
void MainWindow::AlterFlagNumber(HANDLE _hProcess)
{
//获取用户输入旗帜个数
QString flagNumTmp=ui->mineText->text();
int flagNum= flagNumTmp.toInt();
WriteProcessMemory(_hProcess,(LPVOID)0x01005194,&flagNum,sizeof (flagNum),&this->flagNumber);
}
一键扫雷功能实现
分析:若要实现一键扫雷的功能,需要获取雷区的大小,位置,以及不同状态在内存中的值
CE开扫 不断改变第一课雷的状态确认经过筛选后雷区首地址为0x01005361
经过多次点击尝试发现各种状态在内存之中的值
不断切换雷区的高宽数 用CE获取雷区的高和宽的基地址 可以发现高宽皆有两个基地址任取其一即可
观察内存地址可知无论扫雷难度及雷区的宽和高如何变化 每行雷总是占满32字节 以0x10为每行雷的结束符 以第二个0x10换行
扫雷游戏最大高宽为24,30
猜测以二维数组的方式存放,所以在编程的时候用一个二维数组array[24][30+两个结束符]来存放雷区
我们通过模拟鼠标点击消息 来进行排雷 这里用到了Spy++这款工具 如果下载了VS 或者 VC 可以在其工具中找到
用Spy++来确定每次排雷时鼠标所在位置以及鼠标点击时所传递的消息参数
可以看到每次排雷 传递了 WM_LBUTTNDOWN 和WM_LBUTTONUP 消息
附带的参数有鼠标当前的相对坐标 以及 附加键 MK_LBUTTON
实现一键扫雷功能的主要代码
HWND hWnd;
DWORD flagNumber; //旗子数量
DWORD width; //雷区宽
DWORD height; //雷区高
BYTE mineArr[24][32]; //保存读取的雷区内存值
HANDLE hProcess; //进程句柄
DWORD pID;; //进程ID
void MainWindow::ClearMine(HANDLE _hProcess,DWORD _pID)
{
//获取雷区高和宽
ReadProcessMemory(_hProcess,(LPCVOID)0x01005334,&this->width,sizeof (width),&_pID);
ReadProcessMemory(_hProcess,(LPCVOID)0x01005338,&this->height,sizeof (height),&_pID);
//雷区首地址的基地址
int index=0x01005361;
//每次扫雷前清除上一次的雷区视图
ui->textEdit->clear();
ui->textEdit_2->clear();
QString flag[]={"[雷]","[ ]"};
//判断是否在中途关闭扫雷
if(!ReadProcessMemory(_hProcess,(LPCVOID)index,&this->mineArr,24*32,&_pID))
{
int result= QMessageBox::question(this,"错误","未找到扫雷进程是否重新扫描",QMessageBox::Yes|QMessageBox::Close);
if(result==QMessageBox::Close)
{
PostQuitMessage(0);
qDebug()<<result;
}
else
{
return;
}
}
//扫雷算法 0x10 为雷 0x8F为非雷
for(int i=0;i<=height;i++)
{
for(int j=0 ; j<=32; j++)
{
if(mineArr[i][j]==0x10)
{
ui->textEdit->append("");
ui->textEdit_2->append("");
break;
}
else if(mineArr[i][j]==0x8F)
{
ui->textEdit_2->insertPlainText(flag[0]);
}
else
{
//发送鼠标点击请求 模拟鼠标扫雷
SendMessage(this->hWnd, WM_LBUTTONDOWN,MK_LBUTTON, MAKELONG(28+j*20, 74+i*20));
SendMessage(this->hWnd, WM_LBUTTONUP, 0, MAKELONG(28+j*20, 74+i*20));
ui->textEdit_2->insertPlainText(flag[1]);
}
ui->textEdit->insertPlainText(QString::number(this->mineArr[i][j])+" ");
}
}
}