无限阳光
-
打开CE连接游戏程序,通过改变阳光找到阳光值的内存地址
-
把该地址添加到候选地址,右击找出是什么改写了这个地址,再回到游戏改变阳光
-
得到指令mov [edi+00005560],esi,指针基址可能是 edi=135F7788
-
CE新的扫描,勾上Hex,数值填135F7788,新的扫描
-
扫描类型改未变动的数值,然后按再次扫描若干次
-
取左上角筛选出来的地址中的第一个为候选地址,本例为0487A6C8,右击找出是什么访问了这个地址
-
会发现很多+00000768的指令,点开第一条指令
-
mov esi,[edi+00000768],指针基址可能是 edi=04879F60
-
CE新的扫描,勾上Hex,查找04879F60,首次扫描
-
左上角筛选出来的地址中有四个是绿色的地址,下面列出来了:
- PlantsVsZombies.exe+2A9EC0
- PlantsVsZombies.exe+2A9F38
- PlantsVsZombies.exe+2A9F78
- PlantsVsZombies.exe+2AA00C
-
我这里取了第一个作为基地址,双击加入到下面的候选地址栏,得到他的地址006A9EC0
-
当然,你把00400000代入PlantsVsZombies.exe去计也是一样的
-
至此,基地址006A9EC0,一级偏移地址+768,二级偏移地址+5560,全部找出
-
创建MFC,拉一个按钮并双击,写实现代码
void CplantsprojectDlg::OnBnClickedButton1() {
/*
阳光基地址:0x6A9EC0
阳光第1级偏移地址:[0x6A9EC0]0x768
阳光第2级偏移地址:[[0x6A9EC0]+0x768]+0x5560
*/
UpdateData(TRUE);
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开失败"), NULL, 0);
return;
}
DWORD dwTemp = 0; //基地址
if (!ReadProcessMemory(hProcess, (LPVOID)0x6A9EC0, (LPVOID)&dwTemp, sizeof(DWORD), &dwPid)) {
MessageBox(_T("读取失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
dwTemp += 0x768; //加偏移地址
if (!ReadProcessMemory(hProcess, (LPVOID)dwTemp, (LPVOID)&dwTemp, sizeof(DWORD), &dwPid)) {
MessageBox(_T("读取失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
dwTemp += 0x5560; //加偏移地址
DWORD dwShine = 9999; //是传入的值,表示修改后的阳光数
DWORD dwLength = 0; //是传出的值,表示修改了的字节数
if (!::WriteProcessMemory(hProcess, (LPVOID)dwTemp, &dwShine, sizeof(DWORD),&dwLength)){
MessageBox(_T("写入失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
CloseHandle(hProcess);
}
无限金币
思路同无限阳光,CE连接游戏程序,通过金币改变找地金币的值的内存地址,加入到下面候选地址
右击,找出是什么改写了这个地址,然后回到游戏改变金币,得到更改金币指令,从而得到候选地址=某寄存器值+偏移地址28
新的扫描,16进制,刚刚得到的寄存器值,然后转未改变的数值再扫描若干次
取出左上角筛选地址的第一个到候选地址,右击,找出是什么访问了这个地址,会发现很多某寄存器值+偏移地址82C
新的扫描,16进制,刚刚得到的寄存器值,然后转未改变的数值再扫描若干次
取出左上角筛选地址的绿色地址(有四个任取其一即可),或者说是“本程序名”+0级偏移地址(程序基地址偏移)
至此,得到了金币基地址,一级偏移,二级偏移,在MFC程序中增加按钮,双击,写实现代码:
void CplantsprojectDlg::OnBnClickedButton2(){
/*
金币基地址:006A9EC0(或者说是:程序基地址00400000+0级偏移地址002A9EC0)
金币1级偏移地址:[006A9EC0]+0x82C
金币2级偏移地址:[[006A9EC0]+82C]+0x28
*/
UpdateData(TRUE);
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开失败"), NULL, 0);
return;
}
DWORD dwTemp = 0; //基地址
if (!ReadProcessMemory(hProcess, (LPVOID)0x6A9EC0, (LPVOID)&dwTemp, sizeof(DWORD), &dwPid)) {
MessageBox(_T("读取失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
dwTemp += 0x82C; //加偏移地址
if (!ReadProcessMemory(hProcess, (LPVOID)dwTemp, (LPVOID)&dwTemp, sizeof(DWORD), &dwPid)) {
MessageBox(_T("读取失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
dwTemp += 0x28; //加偏移地址
DWORD dwShine = 9999; //是传入的值,表示修改后的金币数
DWORD dwLength = 0; //是传出的值,表示修改了的字节数
if (!::WriteProcessMemory(hProcess, (LPVOID)dwTemp, &dwShine, sizeof(DWORD), &dwLength)) {
MessageBox(_T("写入失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
CloseHandle(hProcess);
}
无CD
- 通过改变某一植物的CD状态,寻找CD值所在的内存地址,右击,找到是什么改变了这个地址,回到游戏改变CD,再回到CE看到mov eax,dword ptr ds:[edi+0x24] ,其内存位置是00487290 ,然后用OD打开游戏程序,查看该位置的反汇编代码
00487290 |. 8B47 24 mov eax,dword ptr ds:[edi+0x24] ; PlantsVs.00690062
00487293 |. 3B47 28 cmp eax,dword ptr ds:[edi+0x28] ; PlantsVs.00730065
00487296 ^ 7E 90 jle short PlantsVs.00487228
-
我们在OD测试,把 jle short PlantsVs.00487228 这行代码改成nop nop,发现游戏所有植物都没有了CD,爆破完成
-
下面开始在MFC中写实现代码,有几点要注意
- 读写数据与读写指令是一样的,都是机器码,计算机识别是数据还是指令全靠寄存器,所以下面MFC程序中的读写0x00487296的值同之前无限阳光无限金币的写法一样
- 00487296不用考虑偏移地址,他是指令,没有局部变量的说法
- nop nop是90 90 ,原指令jle short 00487228的机器码是7E 14
void CplantsprojectDlg::OnBnClickedCheck1(){
UpdateData(TRUE);
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开失败"), NULL, 0);
return;
}
UCHAR buf[2] = {
0 };
if (m_edit_nocd) {
buf[0] = 0x90;
buf[1] = 0x90;
}else {
buf[0] = 0x7E;
buf[1] = 0x14;
}
if (!::WriteProcessMemory(hProcess, (LPVOID)0x487296, buf, sizeof(buf), NULL)) {
MessageBox(_T("写入失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
/*
这里说明一下UpdateData(FALSE)与UpdateData(TRUE)的用法:
在建立了控件和变量之间的联系后,
如果后端修改了变量的值,希望前端对话框更新显示,就应该在修改变量后调用UpdateData(FALSE)
如果前端对话框输入了值,希望后端获得对话框内容,就应该在访问变量前调用UpdateData(TRUE)
*/
}
大嘴花无限吞食
- 在场上放一个大嘴花,利用他吞吃状态与等待状态,用CE寻找到其计时的值的内存地址
- 找到是什么改写了这个地址,等大嘴花状态改变,得到改变的指令地址
- 到OD定位到该地址(可能有多个,逐个试),对附近的条件跳转类指令逐个改成nop测试,会发现0x461565处是关键跳转
- 然后写实现代码,与上面的一样,只是(LPVOID)0x487296的地址变成了(LPVOID)0x461565
自动种植
- 找call函数思路:从植物的类型入手,第一下点击植物时是写入内存记录类型,第二下点击空位时是读出内存记录记的类型
- OD打开游戏程序,CE连接,首次搜索未知的值
- 点击植物,CE找变动的值,未放下,CE找没变动的值若干次
- 放置植物,CE找变动的值,未点击新植物,CE找没变动的值若干次
- 如此循环得到记录当前鼠标选中的植物的内存地址
- 在OD中给这个内存地址打内存读断点,继续运行游戏并操作
- 对断点断到的指令,依次进行分析其后面的call,我们预测call之前会push进去xy坐标还有植物种类id
- 分析方法一:我们重复用同一种植物在同一位置种植,则call前的push中应完全相同
- 分析方法二:我们重复用不同种植物在同一位置种植,则call前的push中应变化一个值
- 分析方法三:我们重复用不同种植物在不同位置种植,则call前的push中应变化全部值
!!!重点思想:控制传入的参数找CALL!!!
最后定位到00410A91处的五行代码就是我们要操作的:
00410A94 |. 52 push edx ; PlantsVs.<ModuleEntryPoint>
00410A95 |. 50 push eax
00410A96 |. 8B4424 20 mov eax,dword ptr ss:[esp+0x20]
00410A9A |. 57 push edi ; PlantsVs.<ModuleEntryPoint>
00410A9B |. 55 push ebp
00410A9C |. E8 7FC6FFFF call PlantsVs.0040D120
00410A94 固定是-1
00410A95 植物种类ID
00410A96 X坐标
00410A9A y坐标
00410A9B 未知的参数,每次运行都有变化
对于未知参数,我们要用前面找阳光与找金币的方法找到他的偏移地址:[[6A9EC0]+768]
!!!重点思想:对不确定意义的传入参数,我们要用偏移地址来表示他!!!
下面是实现代码:
__declspec(naked) void putPlant() {
//__declspec(naked)告诉编译器不要改我写的汇编代码,注意最后要手动ret
//源码位置00410A91,下面的2是植物类型,3是X坐标,5是Y坐标
__asm{
pushad
push -1
push 2
mov eax, 3
push 5
mov ebx,ds:[0x006A9EC0]
mov ebx,ds:[ebx + 0x768]
push ebx
mov edx, 0x40D120
call edx
popad
ret
}
}
void CplantsprojectDlg::OnBnClickedButton5() {
//一、窗口名获PID
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
//二、PID获进程话柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开程序失败"), NULL, 0);
return;
}
//三、分配内存(进程话柄/上一步获取,内存地址/随意,大小/4K,提交/立马使用,权限/读写执行)
LPVOID ThreadFunAdd=VirtualAllocEx(hProcess,NULL,4096,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (!ThreadFunAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
//四、写入代码
DWORD tmpWrite = 0;
DWORD tmpSize = 4096;
BOOL wpm = WriteProcessMemory(hProcess, ThreadFunAdd, putPlant, tmpSize, &tmpWrite);
if (!wpm) {
MessageBox(_T("写入代码失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
//五、远程调用
HANDLE hThread=CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, NULL, NULL, NULL);
if (!hThread) {
MessageBox(_T("远程调用失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
//六、等待远程线程执行完成
DWORD dwWait=WaitForSingleObject(hThread, INFINITE);
//七、释放目标进程空间
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
//八、关闭目标进程句柄
CloseHandle(hProcess);
}
void CplantsprojectDlg::AddPlantsToComBox() {
int tmp = 0;
tmp = typeOfPlants.AddString(_T("葵花"));//类型是1
typeOfPlants.SetItemData(tmp, 1);
tmp = typeOfPlants.AddString(_T("樱桃"));//类型是2
typeOfPlants.SetItemData(tmp, 2);
tmp = typeOfPlants.AddString(_T("坚石"));//类型是3
typeOfPlants.SetItemData(tmp, 3);
typeOfPlants.SetCurSel(1);
}
typedef struct _PutPlantsPareame {
UINT u_x;
UINT u_y;
UINT u_plantid;
}PutPlantsPareame,*PPutPlantsPareame;
DWORD __stdcall putPlant2(LPVOID lpThreadParame) {
PPutPlantsPareame pParame = (PPutPlantsPareame)lpThreadParame;
UINT u_x = pParame->u_x;
UINT u_y = pParame->u_y;
UINT u_plantid = pParame->u_plantid;
__asm {
pushad
push -1
push u_plantid
mov eax, u_x
push u_y
mov ebx, ds: [0x006A9EC0]
mov ebx, ds : [ebx + 0x768]
push ebx
mov edx, 0x40D120
call edx
popad
}
return 0;
}
void CplantsprojectDlg::OnBnClickedButton6(){
//获取参数
UpdateData(TRUE);
CString strPlantName = _T("");
typeOfPlants.GetWindowTextW(strPlantName);
int tmp = typeOfPlants.FindString(-1, strPlantName);
UINT plantid = typeOfPlants.GetItemData(tmp);
PutPlantsPareame parame;
parame.u_plantid = plantid;
parame.u_x = m_x;
parame.u_y = m_y;
//远程注入
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //需要判空
LPVOID ThreadFunAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //需要判空
DWORD tmpWrite = 0;
BOOL wpm1 = WriteProcessMemory(hProcess, ThreadFunAdd, putPlant2, 4096, &tmpWrite); //需要判假
LPVOID ParamAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //需要判空
BOOL wpm2 = WriteProcessMemory(hProcess, ParamAdd, ¶me, sizeof(parame), &tmpWrite); //需要判假
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, ParamAdd, NULL, NULL); //需要判空
DWORD dwWait = WaitForSingleObject(hThread, INFINITE); //用INFINITE参数则函数会在等不到返回时卡死
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
void CplantsprojectDlg::OnBnClickedButton7(){
UpdateData(TRUE);
CString strPlantName = _T("");
typeOfPlants.GetWindowTextW(strPlantName);
int tmp = typeOfPlants.FindString(-1, strPlantName);
UINT plantid = typeOfPlants.GetItemData(tmp);
for (UINT i = 0; i <= 5; i++) {
for (UINT j = 0; j <= 8; j++) {
PutPlantsPareame parame;
parame.u_plantid = plantid;
parame.u_x = i;
parame.u_y = j;
DWORD dwPid = FindGageProcessIdByWndTitle(_T("植物大战僵尸中文版"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //需要判空
LPVOID ThreadFunAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //需要判空
DWORD tmpWrite = 0;
BOOL wpm1 = WriteProcessMemory(hProcess, ThreadFunAdd, putPlant2, 4096, &tmpWrite); //需要判假
LPVOID ParamAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //需要判空
BOOL wpm2 = WriteProcessMemory(hProcess, ParamAdd, ¶me, sizeof(parame), &tmpWrite); //需要判假
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, ParamAdd, NULL, NULL); //需要判空
DWORD dwWait = WaitForSingleObject(hThread, INFINITE); //用INFINITE参数则函数会在等不到返回时卡死
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
}
}
- 补充找call的重要思路:使用代码注入工具,自己改push的参数来call,验证参数的意义
- 当然,你直接在od改push指令中要传入的参数值,加断点跑也是可以的
方法总结
- CE找多重偏移地址(基本功),实现改值
- 根据CE找到的值的内存地址,在OD给该地址加内存断点,在中断指令附近找关键跳转并进行测试,实现改jmp
- 根据CE找到的值的内存地址,在OD给该地址加内存断点,在中断指令附近找call并进行测试,测试的方法是跟观察call前的push的值
- 重复进行游戏观察每一次该call前的push的值的变化,猜测参数意义
- 在OD修改push的值观察执行效果,验证参数意义
- 使用代码注入工具,自己改push的参数来call,确保可行后
- 找不到意义的值要用偏移地址来定
对于改值与改jmp,是比较简单的内存读写
对于追加执行call,是比较复杂的代码注入