OD打开winmine.exe程序后crl+g查找MessageBoxA,即可到达MessageBoxA call地址,我们用代码注入工具测试(详见《注入工具学习笔记》),无误。
//话柄(窗口指针),消息文本(字符串的内存首地址),标题(字符串的内存首地址),消息类型(error/warning/ok)
int MessageBox{
__in HWND hWnd,
__in LPSTSTR lpText,
__in LPCTSTR lpCaption,
__in UNIT uType
}
//话柄(窗口指针),消息类型,菜单消息参数,窗口消息函数
LRESULT CALLBACK WindowProc{
__in HWND hwnd,
__in UINT uMsg,
__in WPARAM wParam,
__in LPARAM lParam
}
//当点击菜单时,WindowProc会被系统调用,uMsg=WM_COMMAND,wParam赋值为对应菜单ID(重点分析)
启动:OD打开winmine.exe程序,运行,按上面w按钮,刷新,右击根窗口,跟随ClassProc,即可找到WindowProc地址,本例程测试得到的地址是01001BD2
希望:我们希望做到在WindowProc函数处下条件断点:uMsg==WM_COMMAND,以过滤掉不必要的消息
观察:因exe程序不认识uMsg,故观察反汇编有mov edx,dword ptr ss:[ebp+0xC],顺便留意右上方寄存器区edx的值是00000111
加点:在下一行mov ecx,dword ptr ss:[ebp+0x14]右击,断点,条件断点,edx==WM_COMMAND
测试:按雷框不触发断点,按菜单切换初/中/高级会触发断点,实现条件中断
双击mov edx,[arg,2],看到mov edx,dword ptr ss:[ebp+0xC]
右下角堆栈地址右击,地址,相对于EBP,看到EBP+C的值是00000111
EBP+8,+C,+10,+14分别就是local.1,2,3,4,注意这里local是顺序调出,因为我们知道函数调用前压栈是倒序压入的,本例程测试得到的信息如下:
EBP+8 >|00230A22
EBP+C >|00000111
EBP+10 >|00000209
EBP+14 >|00000000
读出EBP+10的值,就是当前菜单项的ID,多次测试可以得到:
开局:0x1FE
初级:0x209
中级:0x20A
高级:0x20B
基地址概念:全局变量,字符常量等地址,每次打开
我们来找扫雷界面左上方剩余雷数的值的内存地址,OE打开winmine.exe运行,CE再连接winmine.exe
CE搜索10,然后在雷区右击一雷位,CE搜索9,得到值的内存地址0x01005194
OD左下方用db 0x01005194找到数据位置,右击,断点,内存写入
再右击一雷位,会中断于0100346E,指令是add dword ptr ds:[0x1005194],eax,查看OD右上方EAX的值,是FFFFFFFF,结合add也就是减1,无误
看到指令中0x1005194立即数寻址是绝对地址,不是ebp±或local之类,可以确定是基地址
/*打开VS后按ctrl+f1即可跳到VS帮助网页,搜索关键字如SendMessage或FindWindow即可查到相关API语法
以SendMessage为例:
LRESULT WINAPI SendMessage(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
*/
//创建MFC项目并创建4个按钮,双击按钮写实现代码
void CwinmineDlg::OnBnClickedButton1(){
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));
if (NULL == hWnd) {
::MessageBox(NULL, _T("扫雷游戏未打开"),_T("错误"),MB_OK);
return ;
}
::SendMessage(hWnd, WM_COMMAND, 0x209, 0);
}
void CwinmineDlg::OnBnClickedButton2(){
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));
if (NULL == hWnd) {
::MessageBox(NULL, _T("扫雷游戏未打开"),_T("错误"), MB_OK);
return;
}
::SendMessage(hWnd, WM_COMMAND, 0x20A, 0);
}
void CwinmineDlg::OnBnClickedButton3(){
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));
if (NULL == hWnd) {
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
::SendMessage(hWnd, WM_COMMAND, 0x20B, 0);
}
void CwinmineDlg::OnBnClickedButton4(){
for (int i = 0; i < 3; i++) {
OnBnClickedButton1();
Sleep(1000);
OnBnClickedButton2();
Sleep(1000);
OnBnClickedButton3();
Sleep(1000);
}
}
//运行即可实现按钮对扫雷游戏的控制
//创建编辑框与一个按钮,编辑框绑定变量,类别是值,类型是int,名称是m_num_mine
//双击按钮写实现代码,下面会用到的函数,可以先在VS帮助文档中查看
void CwinmineDlg::OnBnClickedButton5(){
DWORD pid;
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));
if (NULL == hWnd) {
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
GetWindowThreadProcessId(hWnd, &pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (NULL == hProcess) {
::MessageBox(NULL, _T("扫雷游戏打开失败"), _T("错误"), MB_OK);
return;
}
ReadProcessMemory(hProcess, (LPCVOID)0x1005194, &m_num_mine, sizeof(m_num_mine), &pid);
UpdateData(FALSE);
}
打开扫雷游戏,打开CE并导入扫雷游戏,现在的目标是找到最左上角的格子的内存地址
按一下第一个格,首次查找是未知的初始值
然后按笑脸,再按第一个格,如果两次第一个格的数字相同就再次查找未变动的数值,如果不相同就再次查找变动的数值
循环上一步,直到只剩下一个候选地址,本笔记测试地址是01005361
CE中添加候选地址01005361到下面,更改描述为雷区基地址,保存CT表以便下次读取
右击下面候选地址,浏览相关内存区域,观察与雷区格子的关系,不难发现:
0F是非雷+未操作
0E是非雷+插旗
0D是非雷+问号
8F是雷+未操作
8E是雷+插旗
8D是雷+问号
CC是踩中雷
40是0
41是1
42是2
43是3
44是4
45是5
46是6
47是7
48是8(可以自定义行列雷都是999来测试,不过最大只能是24,30,667)
10是无效区域标示
补充:如果第一下按到雷会重新布置雷的位置,如果第一下没按到雷则不会重新布置
补充:从01005361起32个字节为第一组,代表第一行的雷,最多前30个字节有效,即扫雷窗口最多有30列雷
补充:30列雷区时最后两个字节是10,如果一行不够30列,则在两个10之间补充0F
补充:从01005381起32个字节是第二组,代表第二行的雷,如此类推,最多24组,即扫雷窗口最多有24行雷
char a[24][32];//不难推断这个就是定义雷区的全局变量
利用自定义,不断设置不同的行数与列数并用CE筛选得到行列值的地址,因为本游戏辅助要针对不同级别皆适用
winmine.exe+5338与winmine.exe+56A8是雷区高
winmine.exe+5334与winmine.exe+56AC是雷区宽
winmine.exe的基地址是01000000,注意:你即使同时开多个扫雷,每个扫雷都是这个基地址,各个扫雷之间的地址是相互独立的!就是说都有4GB内存
(而且00000000到FFFFFFFF就是4G个地址,以字节为单位,可以说明这一点)
创建编辑框,设置属性多行为True,只读为True,生成绑定变量,变量名m_strshowdata,类别value
创建按钮,描述文字:自动扫雷,双击写实现代码
BOOL PostMessage(
__in HWND hWnd,
__in UINT Msg,
__in WPARAM wParam,
__in LPARAM lParam,
)//堵塞,SendMessage是非堵塞的
void CwinmineDlg::OnBnClickedButton6(){
DWORD pid;
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));
if (NULL == hWnd) {
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
GetWindowThreadProcessId(hWnd, &pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (NULL == hProcess) {
::MessageBox(NULL, _T("扫雷游戏打开失败"), _T("错误"), MB_OK);
return;
}
/*
0x01005361雷区数据,0x8F是雷
0x01005338雷区高,雷格距离16
0x01005334雷区宽,雷格距离16
*/
unsigned char gamedata[24][32] = { 0 };
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005361, &gamedata, 24 * 32, &pid)) {
::MessageBox(NULL, _T("读取数据失败"), _T("错误"), MB_OK);
return;
}
DWORD rows;
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005338, &rows, sizeof(rows), &pid)) {
::MessageBox(NULL, _T("读取行数失败"), _T("错误"), MB_OK);
return;
}
DWORD columns;
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005334, &columns, sizeof(columns), &pid)) {
::MessageBox(NULL, _T("读取列数失败"), _T("错误"), MB_OK);
return;
}
m_strshowdata.Empty();
CString strTemp = _T("");
short gamex = 20;
short gamey = 60;
unsigned short xypos[2] = { 0 };
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 32; j++) {
if (0x10 == gamedata[i][j]) {
break;
}
xypos[0] = gamex + j * 16;
xypos[1] = gamey + i * 16;
if (0x8F != gamedata[i][j]) {
::PostMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, *(int*)xypos);
::PostMessage(hWnd, WM_LBUTTONUP, 0, *(int*)xypos);
}
strTemp.Format(_T("%02x "), gamedata[i][j]);
m_strshowdata += strTemp;
}
m_strshowdata += _T("\r\n");
}
UpdateData(FALSE);
}