笑傲江湖逆向

血量查找

血量来源:
00AEC7CC | 8B7E 1C                  | mov edi,dword ptr ds:[esi+1C]           | 血量 esi + 1c  esi:525354D000A22BE0 | 8D8E A0020000            | lea ecx,dword ptr ds:[esi+2A0]          | esi:5253523000ED5C84 | 8B1C03                   | mov ebx,dword ptr ds:[ebx+eax]          | ebx:00000010  eax:820247B000ED5C6A | 8B86 C8000000            | mov eax,dword ptr ds:[esi+C8]           | esi:52510D58008328A9 | 8B8C86 C0030000          | mov ecx,dword ptr ds:[esi+eax*4+3C0]    | eax:1 esi:2590781000454B05 | 8B4E 08                  | mov ecx,dword ptr ds:[esi+8]            | esi:0F63C1680043581B | 8B4E 30                  | mov ecx,dword ptr ds:[esi+30]           | esi:015995E0004623C5 | A3 88BE5901              | mov dword ptr ds:[159BE88],eax          |
004623CA | A3 8CBE5901              | mov dword ptr ds:[159BE8C],eax          |
004623CF | A3 90BE5901              | mov dword ptr ds:[159BE90],eax          |
004623D4 | A3 94BE5901              | mov dword ptr ds:[159BE94],eax          |
004623D9 | A3 98BE5901              | mov dword ptr ds:[159BE98],eax          |
004623DE | A3 9CBE5901              | mov dword ptr ds:[159BE9C],eax          |
004623E3 | A3 A0BE5901              | mov dword ptr ds:[159BEA0],eax          |
004623E8 | A3 A4BE5901              | mov dword ptr ds:[159BEA4],eax          |
004623ED | B9 E0955901              | mov ecx,xajh.15995E0                    |血量来源:
[[[[[15995E0+30]+8]+1*4+3C0] +C8]+0x10]+2A0+1C

通过发包函数找功能CALL

网络游戏和单机游戏的区别:
网络游戏中几乎所有的功能都会先发送封包数据到服务器,由服务器做出判断后相应给客户端,客户端才会产生对应功能的动作。所以在网络游戏中要寻找功能call可以通过在法宝函数处下断来回溯找功能CALL。客户端在准备好内容,发送给服务端之前,通常会进行加密
就跟我们古代两军交战,要给领导传递消息的时候肯定会先经过加密的
如果不加密,信件被人拦截下来了,那不就什么都暴露了。其实就跟我们自己写客户端服务端一样
比如,喝药瓶,那我肯定要给服务端发送一个喝药品的消息,服务端收到以后还要去修改我们药品的数量,人物的血量等等。不过也不一定是写好东西就直接加密,可能会转手很多次,才加密。
但是无论转手多少次,最后都会经过发包函数。
所以我们直接给发包函数下段,然后往回找。ws2_32.send
发包函数一共有三个:-send  tcp-sendto udp-WSASend


CALL 喊话
push后堆栈图:
0019DA70  54147900
0019DA74  618F64B4  L"222222"
0019DA78  00000000
0019DA7C  00000000  0019DA70  54147900
0019DA74  618E9024  L"444444"
0019DA78  00000000
0019DA7C  00000000  00931E8F | 52                             | push edx                                |    edx 是0
00931E90 | 8B5424 40                | mov edx,dword ptr ss:[esp+40]
00931E94 | 53                             | push ebx                                |  ebx是0
00931E95 | 51                             | push ecx                                | ecx是喊话内容【Unicode编码】
00931E96 | 52                             | push edx                                | edx我们发现是固定的54147900
00931E97 | 8BC8                         | mov ecx,eax                             | ecx = eax = 2C017398
00931E99 | E8 82B43C00             | call 0x00CFD320                        | 所以我们可以发现只有一个参数是变化的,其他东西是不变的。
然后我们要分析edx和eax的来源【游戏启动以后会发生变化】。记录eax寄存器的来源
00931E4E | 51                       | push ecx                                |
00931E4F | C74424 40 04000000       | mov dword ptr ss:[esp+40],4             |
00931E57 | E8 F4872B00          | call xajh.BEA650                        |
00931E5C | 83C4 0C                | add esp,C                               |
00931E5F | C64424 34 05       | mov byte ptr ss:[esp+34],5              |
00931E64 | E8 B7E9F0FF         |  call 0x00840820                       | eax来源
00931E69 | 3BC3                     | cmp eax,ebx                             |
00931E6B | 74 31                    | je xajh.931E9E                          |
00931E6D | 8B5424 24             | mov edx,dword ptr ss:[esp+24]           |
00931E71 | 8B4C24 20             | mov ecx,dword ptr ss:[esp+20]           |
00931E75 | 2BD1                      | sub edx,ecx                             |
00931E77 | C1FA 03                  | sar edx,3                               |
00931E7A | 3BD3                      | cmp edx,ebx                             |现在有两种方式:-1.跟进去看eax怎么产生的-2.直接调用这个函数,看他需要什么参数,然后我们看看能不能直接通过参数拿到eax
然后我们发现不用参数然后分析最后那个edx从哪里来的
00931E90 | 8B5424 40                | mov edx,dword ptr ss:[esp+40]           |  esp:0019DA7C 【esp是上一个函数的第一个参数】009330D5 | 8B4424 10                | mov eax,dword ptr ss:[esp+10]           | esp:0019DAC8然后我们向上找,我们发现这个[esp + 10]是这个函数中的一个局部变量,因为在当前函数开辟的栈空间里面
然后我们通过硬件写入端点,我们发现是把bl的值给这个内存,并不是我们想的四个字节,而是一个
那么就是说写这个代码的人,没有初始化局部变量,这个栈空间里面还是残留值。然后经过测试我们发现bl的值一直是0.
0019DA70  00
0019DA74  618F64B4  L"222222"
0019DA78  00000000
0019DA7C  00000000  push 0
push 0
push 06460000
push 0
call 0x00840820
mov ecx,eax
call 0x00CFD320然后测试,我们的喊话正常执行,也就是说edx的值,其实的确就是占用一个字节,就是写代码的人没有清空栈空间,导致问题存在。
还好我们通过硬件断点访问,发现是从bl传递给edx的,然后bl每次固定的值就是0.
所以调用喊话的功能。我们实际只需要两个参数-1.通过开辟一块内存空间,存放我们的字符串-2.通过调用call 0x00840820,拿到我们的eax。

然后使用MFC编写窗口辅助程序

// 笑傲江湖游戏辅助.cpp: 定义 DLL 的初始化例程。
//#include "pch.h"
#include "framework.h"
#include "笑傲江湖游戏辅助.h"
#include "CMainDlg.h"#ifdef _DEBUG
#define new DEBUG_NEW
#endif//
//TODO:  如果此 DLL 相对于 MFC DLL 是动态链接的,
//      则从此 DLL 导出的任何调入
//      MFC 的函数必须将 AFX_MANAGE_STATE 宏添加到
//      该函数的最前面。
//
//      例如:
//
//      extern "C" BOOL PASCAL EXPORT ExportedFunction()
//      {
//          AFX_MANAGE_STATE(AfxGetStaticModuleState());
//          // 此处为普通函数体
//      }
//
//      此宏先于任何 MFC 调用
//      出现在每个函数中十分重要。  这意味着
//      它必须作为以下项中的第一个语句:
//      出现,甚至先于所有对象变量声明,
//      这是因为它们的构造函数可能生成 MFC
//      DLL 调用。
//
//      有关其他详细信息,
//      请参阅 MFC 技术说明 33 和 58。
//// C笑傲江湖游戏辅助AppBEGIN_MESSAGE_MAP(C笑傲江湖游戏辅助App, CWinApp)
END_MESSAGE_MAP()CMainDlg* pMainDlg;// C笑傲江湖游戏辅助App 构造C笑傲江湖游戏辅助App::C笑傲江湖游戏辅助App()
{// TODO:  在此处添加构造代码,// 将所有重要的初始化放置在 InitInstance 中
}// 唯一的 C笑傲江湖游戏辅助App 对象C笑傲江湖游戏辅助App theApp;DWORD WINAPI MainProc(LPVOID args) {pMainDlg->DoModal();delete pMainDlg;return 0;
}// C笑傲江湖游戏辅助App 初始化BOOL C笑傲江湖游戏辅助App::InitInstance()
{CWinApp::InitInstance();pMainDlg = new CMainDlg;this->m_pMainWnd = pMainDlg;// 然后为了避免阻塞UI线程,我们创建子线程显示窗口::CreateThread(NULL,0, MainProc,NULL,0,NULL);return TRUE;
}/*@function 喊话功能的按钮
*/
void CMainDlg::OnBnClickedButtonSpeack()
{UpdateData(TRUE); // 窗口到变量LPCWSTR pSpeackContent = this->m_speack.GetString();__asm {push 0push 0push pSpeackContent // 直接push字符串地址push 0mov eax, 0x00840820call eax // 调用函数拿到eax的值mov ecx, eax // 然后把eax的值给ecxmov eax, 0x00CFD320call eax // 调用喊话};
}

线程发包执行流程:

和前面的封包数据不太一样,就是不能通过函数一直向下传递了。
比如下面的图,到函数5就不能继续向下传了。
剩下的工作在线程B那边。
【
厂商把游戏的功能代码和发包代码放在了不同的线程里面。通过一个一个不变的地址或者结构互相联系。子线程发包,这个线程里面恐怕是一个死循环,一直在查看全局变量里面是否有游戏主线程写入的封包内容,如果有那就发送出去。
】前面我们是通过对send下断点,然后一层一层向上函数追溯,直接追溯到我们的明文CALL里面。
但是现在我们就做不到了。我们追溯到函数1就无法继续进行了。那么我们现在要解决的问题,就是如何跳出线程B,到达线程A。
我们发现他们有一个沟通的桥梁。就是那个封包数据。所以我们就可以通过对封包数据下断点。下一个写入断点,一旦停下来了,那么我们就看一看追溯到函数5 4 3 2 1了。
为什么是写入呢?因为子线程,即线程B他是负责发送,主要是访问封包数据。

这里还是对喊话功能进行实现。
因为实际上他会发两个包。我们前面是通过分析第一个包,找到的功能。
其实是巧合。实际上真正的数据在第二个包
第一个包是一个长度固定为1的包,不是真正的功能包。然后发现确实往上最终到了一个死循环,程序出不去了。
所以就可以断定是通过线程发包的了。
怎么判断呢?因为线程发包是循环,它的流程是固定的,无论我们使用什么功能。
所以如果我们发现它是死循环,而且什么功能的流程还是固定的就肯定是线程发包了。线程发包:
00DCF88E | 8B48 08                  | mov ecx,dword ptr ds:[eax+8]            |
00DCF891 | 2B48 04                  | sub ecx,dword ptr ds:[eax+4]            |
00DCF894 | 8B40 04                  | mov eax,dword ptr ds:[eax+4]            |
00DCF897 | 8B56 04                  | mov edx,dword ptr ds:[esi+4]            |
00DCF89A | 53                       | push ebx                                |
00DCF89B | 6A 00                    | push 0                                  |
00DCF89D | 51                       | push ecx                                |
00DCF89E | 50                       | push eax                                |
00DCF89F | 52                       | push edx                                |
00DCF8A0 | FF15 E09C2E01            | call dword ptr ds:[<&send>]             |  子线程第一层send函数的四个参数
int WSAAPI send([in] SOCKET     s,[in] const char *buf,[in] int        len,[in] int        flags
);
0757FE70  00000E68  socket
0757FE74  2C025018  buf
0757FE78  0000002C   len
0757FE7C  00000000  flags然后我们发现无论我们使用什么功能这个buf的地址都是固定的,就跟我们说的一样,主线程和子线程通信的封包数据是固定的。那么我们就给这个数据下一个写入断点,因为是固定的,所以我们猜测很可能就是主线程写过来的。
那么我们进行尝试。但是遗憾的是我们发现并不能追踪到前面的地方,还是进入死循环了。
而且可以看出来是线程B在往这个固定的地址里面在写,因为进入了死循环,而不是线程A。
所以就证明线程A写入的数据并不在线程B读取到那个里面,就证明我们刚刚找的那个变量不是他们共享的那个。

它肯定是从真正共享的内存拷贝过来的。
0043D064 | 53                       | push ebx                                |
0043D065 | 51                       | push ecx                                |
0043D066 | 57                       | push edi                                |
0043D067 | FFD5                    | call ebp                                | memmove
然后我们发现它调用了一个memmove函数
下面是 memmove() 函数的声明。
void *memmove(void *str1, const void *str2, size_t n)
参数
str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
n -- 要被复制的字节数。那么第二个参数就是源头,我们就是从第二个参数拷贝过来的,也就是ecx。0043D060 | 8B4C24 24                | mov ecx,dword ptr ss:[esp+24]           |
然后我们发现ecx来源[esp+24] 继续向上跟踪然后发现来源上一个函数的第二个参数,继续跟踪00D0C440 | 8B46 04                  | mov eax,dword ptr ds:[esi+4]            | esi:358BF480然后esi+0x4保存的内容是变化的,但是esi是不变的。
可以看出是一个结构体
struct{dword b; // 这个指向的地址是变化的,结构体的地址是不变的
}
所以我们下手的地方就是这个dword b。通过下硬件写入断点,看看谁往esi+0x4里面写入东西
0043D3C2 | 8946 08                  | mov dword ptr ds:[esi+8],eax            | [esi+8]:"der"
然后断下来之后我们一层一层向上,发现追踪到之前的喊话call的位置了。00931E8F | 52                       | push edx                                |
00931E90 | 8B5424 40                | mov edx,dword ptr ss:[esp+40]           |
00931E94 | 53                       | push ebx                                |
00931E95 | 51                       | push ecx                                |
00931E96 | 52                       | push edx                                |
00931E97 | 8BC8                     | mov ecx,eax                             |
00931E99 | E8 82B43C00              | call xajh.CFD320                        | 5所以证明esi+0x4就是主线程和子线程通信封包数据。通过esi+0x4这个位置下一个硬件写入断点,就可以追踪主线程的流程了。
所以如果遇到封包数据追不上去,就证明这个封包数据不是共享的,研究一下这个封包数据是从哪里来的。
从而找到真正的封包数据

使用背包物品CALL


使用背包物品
00A4D805 | 8B4C24 18                   | mov ecx,dword ptr ss:[esp+18]           |
00A4D809 | 8B89 541F0000            | mov ecx,dword ptr ds:[ecx+1F54]         | ecx:53846D40
00A4D80F | 6A 01                         | push 1                                  |
00A4D811 | 55                             | push ebp                                |
00A4D812 | 57                              | push edi                                |
00A4D813 | E8 4897C6FF              | call 0x006B6F60                        | 使用物品6
然后我们f8观察堆栈状态, 发现是三个参数0019E868  00000002
0019E86C  00000002
0019E870  00000001  然后1是固定的,那么就是分析edi和ebp。然后使用不同的物品测试,我们发现ebp即第二个参数是背包数组的索引。也就是我们当前使用的是第几个物品,从0开始。
edi好像也基本是2.
先不理他。我们先分析一下ecx。
因为ecx一般是指向对象。使用代码注入器,发现成功换了鞋子,那么就是使用物品成功。push 1
push 3
push 2
mov ecx,53846D40
call 0x006B6F60所以我们要追溯ecx的来源
00A4D805 | 8B4C24 18                   | mov ecx,dword ptr ss:[esp+18]           |
00A4D809 | 8B89 541F0000            | mov ecx,dword ptr ds:[ecx+1F54]         |
我们还是用之前的老办法,一般用esp ebp进行寻址,不是局部变量就是函数参数。
然后观察我们猜测应该是一个局部变量。然后使用多次物品观察esp+18的值。
我们发现是一个固定的地址。
0019E88C。
然后我们就下一个硬件写入断点,看是什么给esp+18赋值的。00A4D25C | 8BC8                     | mov ecx,eax                             |
00A4D25E | E8 FDB04800              | call xajh.ED8360                        |
00A4D263 | 84C0                     | test al,al                              |
00A4D265 | 75 0F                    | jne xajh.A4D276                         |
00A4D267 | 8BCB                     | mov ecx,ebx                             |
00A4D269 | E8 220AA7FF              | call xajh.4BDC90                        |
00A4D26E | 84C0                     | test al,al                              |
00A4D270 | 0F85 A2050000            | jne xajh.A4D818                         |
00A4D276 | E8 0595A0FF              | call 0x00456780                    |
00A4D27B | 894424 18                | mov dword ptr ss:[esp+18],eax           |然后我们发现是eax的值给esp + 18,那么我们就要找eax的来源。
然后上面刚好有个call我们知道一般函数的返回值是放在eax中的。
然后我们经过测试返回eax的确是上面一个函数的返回值。然后我们又测试,发现上面那个函数并不需要参数。也就是说我们可以通过上面调用上面那个函数拿到eax的值。[esp + 18] = eax。所以call完以后我们就相当于拿到了[esp + 18]00A4D805 | 8B4C24 18                   | mov ecx,dword ptr ss:[esp+18]           |
00A4D809 | 8B89 541F0000            | mov ecx,dword ptr ds:[ecx+1F54]         |
那么我们就是call,然后把eax给ecx。然后再把[ecx + 1F54]给ecx,那么我们的四个参数都有了。push 1
push 3 // 使用第几个物品
push 2
call 0x00456780  // 通过call这个函数拿到esp + 18的值
mov ecx,eax // 然后给ecx
mov ecx,[ecx+1F54]  // 再把ecx+1F54给ecx
call 0x006B6F60然后测试发现没有问题但是存在一个问题,这个是基于数组索引使用物品的。那么我们写出来的辅助,是写死的。
比如我们写一个吃药。那么就要求玩家把药品放在固定的格子里面。不科学。然后先给MFC添加功能。
/*@function 使用物品
*/
void CMainDlg::OnBnClickedButtonPackuuse()
{UpdateData(TRUE); // 窗口到变量int packNo = _ttoi(this->m_packNo);__asm {mov eax,0x00456780call eaxmov ecx, eaxmov ecx,dword ptr [ecx + 0x1F54]push 1push packNopush 2mov eax, 0x006B6F60call eax};
}

人物背包分析

分析人物背包。
背包中的物品和背包是相关联的,找背包可能没什么思路。不过物品我们上面分析了。所以我们通过物品来找背包。然后我们通过CE工具,搜索到了背包某个物品的地址【51831928】。
然后如果是我们自己写代码,在打开背包的时候我们肯定会去找这些物品,然后把他们显示出来。
那么我们就可以给这个地址下一个硬件访问断点,看看是谁访问了它。当我们点开背包的时候,一定会触发访问的。00A4B58B | 837E 70 01               | cmp dword ptr ds:[esi+70],1             |
然后我们发现esi+70每次断下来就代表遍历一个物品,它存放着这个物品有多少个,从背包下标0开始遍历。也就是说我们猜测是准确的。当我们打开背包的时候它有一个循环去遍历所有的物品进行显示。那么我们就尝试从这里一直往上找,看看能不能找到背包的基址。00A4B50D | 8BB424 84000000          | mov esi,dword ptr ss:[esp+84]           |  esp:0019EC34然后又看见esp寻址,那么我们又猜测是局部变量或者上一个函数的参数。
0019ECB4  00A4BB2D  返回到 xajh.00A4BB2D 自 xajh.00A4B4B0
0019ECB8  518317D0
然后我们发现很可能是上一个函数传递的第一个参数。所以发现esp+84来源自上一个函数的参数,即eax,那么eax = esi。我们通过eax+70依旧可以找到我们的物品发现。00A4BB1A | 50                       | push eax                                |  esi来源
00A4BB1B | EB 0B                    | jmp xajh.A4BB28                         |
00A4BB1D | 8B5424 18                | mov edx,dword ptr ss:[esp+18]           |
00A4BB21 | 8B07                     | mov eax,dword ptr ds:[edi]              |
00A4BB23 | 52                       | push edx                                |
00A4BB24 | 50                       | push eax                                |
00A4BB25 | 53                       | push ebx                                |
00A4BB26 | 6A 00                    | push 0                                  |
00A4BB28 | E8 83F9FFFF              | call 0x00A4B4B0                        |然后继续向上,我们发现一个call,一般call的返回保存在eax。
00A4BAEC | E8 5F58AAFF              | call 0x004F1350                        |
然后我们发现的确通过这个call可以拿到eax。 不过这个call需要两个参数捋一下:我们找[esi+70],然后发现esi来源[esp+84]
然后看到esp寻址一般猜测是局部变量或者函数参数。
然后我们发现是上一个函数的参数eax,所以eax = [esp + 84] = esi
然后我们通过eax寻址,发现的确找得到我们的物品。
然后追踪eax的来源,发现有一个call。
通过这个call可以拿到eax,但是它需要接收两个参数。00A4BAE9 | 6A 00                    | push 0                                  |
00A4BAEB | 53                       | push ebx                                |
00A4BAEC | E8 5F58AAFF              | call 0x00A4B4B0                                                |
一个参数固定是0,那么我们只要追踪ebx。
然后通过观察我们发现ebx从0一直递增,可以猜测这就是背包物品的索引 循环变量。
那么我们就可以拿到eax了。
即通过这个函数我们可以取出来每一个物品,但是我还是不知道背包。但是我们不能通过这个call直接拿出物品来,因为我们想找的是背包的基址。并不是找到物品的基址就完事了。
所以我们进入这个函数,追踪eax的来源。004F1350 | 8B4424 04                | mov eax,dword ptr ss:[esp+4]            |  eax物品格子的序号
004F1354 | 85C0                     | test eax,eax                            |
004F1356 | 7C 1D                    | jl xajh.4F1375                          |
004F1358 | 3B41 28                  | cmp eax,dword ptr ds:[ecx+28]           |   ecx:7126EB80
004F135B | 7D 18                    | jge xajh.4F1375                         |
004F135D | 807C24 08 00             | cmp byte ptr ss:[esp+8],0               |
004F1362 | 8B49 24                  | mov ecx,dword ptr ds:[ecx+24]           |
004F1365 | 8D0C81                   | lea ecx,dword ptr ds:[ecx+eax*4]        |
004F1368 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
004F136A | 74 0B                    | je xajh.4F1377                          |
004F136C | C701 00000000            | mov dword ptr ds:[ecx],0                |
004F1372 | C2 0800                  | ret 8                                   |
004F1375 | 33C0                     | xor eax,eax                             |
004F1377 | C2 0800                  | ret 8                                   |分析这个函数:-1.取出esp + 4即第一个参数给eax,第一个参数就是背包物品的索引-2.然后判断是否位0,如果小于等于0,那么就直接等于0,就是判断数组边界-3.然后又把eax跟[ecx + 28]进行比较,大于等于0,也直接等于0,所以猜测也是判断数组的边界-然后经过测试发现ecx+28保存的就是背包的最大容量,那么下标应该是最大容量-1,所以大于等于也直接0-4.....
反正就是用来获取背包的物品的。我们去追踪ecx。
00A4BAE5 | 8B4C24 2C                | mov ecx,dword ptr ss:[esp+2C]           | esp:0019ECC8
然后看见esp寻址,我们猜测是局部变量或者参数,然后我们发现距离返回地址很远,可能是一个局部变量。然后通过给函数头部,和刚刚哪里下断点。然后给esp+2C下一个硬件写入断点。我们看看是哪里往esp+2C写入东西的。
00A4B9E1 | 894C24 34                | mov dword ptr ss:[esp+34],ecx           |
00A4B9DC | 8D4F 08                  | lea ecx,dword ptr ds:[edi+8]            |  edi:71AB963800A4B93F | E8 5CAEA0FF              | call xajh.4567A0                        |
00A4B947 | 8B78 14                  | mov edi,dword ptr ds:[eax+14]           | eax:5E97DBF0:
00A4B944 | 8B40 08                  | mov eax,dword ptr ds:[eax+8]            |
然后发现eax最终来源一个call,并且不用参数004567A0 | A1 507F5901              | mov eax,dword ptr ds:[1597F50]          |
004567A5 | 85C0                     | test eax,eax                            |
004567A7 | 74 0E                    | je xajh.4567B7                          |
004567A9 | 8B40 30                  | mov eax,dword ptr ds:[eax+30]           |
004567AC | 85C0                     | test eax,eax                            |
004567AE | 74 07                    | je xajh.4567B7                          |
004567B0 | 8B80 90000000            | mov eax,dword ptr ds:[eax+90]           |
004567B6 | C3                       | ret                                     |
004567B7 | 33C0                     | xor eax,eax                             |
004567B9 | C3                       | ret                                     |cmp dword ptr ds:[esi+70],1  mov eax,dword ptr ds:[ecx]lea ecx,dword ptr ds:[ecx+eax*4]mov ecx,dword ptr ds:[ecx+24]  lea ecx,dword ptr ds:[edi+8]mov edi,dword ptr ds:[eax+14] mov eax,dword ptr ds:[eax+8] mov eax,dword ptr ds:[eax+90] mov eax,dword ptr ds:[eax+30]mov eax,dword ptr ds:[1597F50] 某一个格子的物品的数量
[[[[[[[1597F50] +30]+90] +8] +14] +8+24] +eax*4]+70  eax表示背包里面的第几个格子背包基址
[[[[[[1597F50] +30]+90] +8] +14] +8+24]

遍历背包

那么我们肯定要通过背包基址来遍历背包。也就是说,我们现在其实就只需要拿到背包,和背包的容量就可以遍历背包了。然后我们通过观察内存。
发现
[[[[[[1597F50] +30]+90] +8] +14] +8+24]
里面保存的的确是背包中的每一个物品元素。然后我们前面有分析到这里。
004F1350 | 8B4424 04                | mov eax,dword ptr ss:[esp+4]            |  eax物品格子的序号
004F1354 | 85C0                     | test eax,eax                            |
004F1356 | 7C 1D                    | jl xajh.4F1375                          |
004F1358 | 3B41 28                  | cmp eax,dword ptr ds:[ecx+28]           |   ecx:7126EB80
004F135B | 7D 18                    | jge xajh.4F1375                         |
004F135D | 807C24 08 00             | cmp byte ptr ss:[esp+8],0               |
004F1362 | 8B49 24                  | mov ecx,dword ptr ds:[ecx+24]           |
004F1365 | 8D0C81                   | lea ecx,dword ptr ds:[ecx+eax*4]        |
004F1368 | 8B01                     | mov eax,dword ptr ds:[ecx]              |
004F136A | 74 0B                    | je xajh.4F1377                          |
004F136C | C701 00000000            | mov dword ptr ds:[ecx],0                |
004F1372 | C2 0800                  | ret 8                                   |
004F1375 | 33C0                     | xor eax,eax                             |
004F1377 | C2 0800                  | ret 8                                   |分析这个函数:-1.取出esp + 4即第一个参数给eax,第一个参数就是背包物品的索引-2.然后判断是否位0,如果小于等于0,那么就直接等于0,就是判断数组边界-3.然后又把eax跟[ecx + 28]进行比较,大于等于0,也直接等于0,所以猜测也是判断数组的边界-然后经过测试发现ecx+28保存的就是背包的最大容量,那么下标应该是最大容量-1,所以大于等于也直接0-4.....
反正就是用来获取背包的物品的。004F1358 | 3B41 28                  | cmp eax,dword ptr ds:[ecx+28]
就是说这里[ecx+28]保存着背包的容量。[[[[[[[1597F50] +30]+90] +8] +14] +8+24] +eax*4]+70 +70 当前物品的数量
+48 当前物品最多能放多少
+C  当前物品的ID但是要遍历背包,我们除了要知道物品的数量,我们还要知道物品的名字。4708FBD4 通过CE找到了炼金石的,然后我们去这个地址,看看是怎么来的。遍历背包:004EE858 | E8 694FC700              | call <JMP.&memset>                      |
004EE85D | 8B47 08                     | mov eax,dword ptr ds:[edi+8]            |
004EE860 | 50                               | push eax                                |
004EE861 | 53                               | push ebx                                |  物品名称地址的来源
004EE862 | 56                               | push esi                                |
004EE863 | E8 0251C700              | call <JMP.&memcpy>                      |004EE83A | 03D8                     | add ebx,eax                             | ebx=ebx+4  ebx:4708FBD0004EE746 | 8BD8                     | mov ebx,eax                             |004EE746 | 8BD8                     | mov ebx,eax                             |004EE738 | 8D4C24 2C                   | lea ecx,dword ptr ss:[esp+2C]           | 局部变量
004EE73C | 51                                | push ecx                                | 感觉是一个输出参数,因为是在栈空间里面的
004EE73D | 56                              | push esi                                | 固定是0
004EE73E | 52                               | push edx                                | 物品id
004EE73F | 8BC8                          | mov ecx,eax                                   | ecx:2BA68010
004EE741 | E8 3AB27A00              | call 0x00C99980                        | 3个参数那么现在只要分析ecx寄存器的来源,我们就可以拿到物品的名称了。 ecx来源eax。然后向上追踪发现一个call。然后发现确实来源这里004EE60D | 8B0D 507F5901            | mov ecx,dword ptr ds:[1597F50]          |
004EE613 | E8 9838F4FF                 | call 0x00431EB0                        |// 遍历背包
void CMainDlg::OnBnClickedPackAll()
{// [[[[[[1597F50] +30]+90] +8] +14] +8+24] DWORD******* bagBaseAddr = (DWORD*******)0x1597F50;// 背包的基地址// 那么这样就拿到背包地址了DWORD* bagArr = (DWORD*)bagBaseAddr[0][0x30 / 4][0x90 / 4][0x8 / 4][0x14 / 4][0x2C / 4];CString str;DWORD pOutNum = 0;LPWSTR pName = nullptr;// 接下来遍历背包数组/*+70 当前物品的数量+48 当前物品最多能放多少+C  当前物品的ID*/for (int i = 0; i < 30; i++) {if (bagArr[i] == 0) {continue;}DWORD pID = *(DWORD*)(bagArr[i] + 0xC);DWORD pMax = *(DWORD*)(bagArr[i] + 0x48);DWORD pnumber = *(DWORD*)(bagArr[i] + 0x70);__asm {pushad;pushf;mov ecx, dword ptr ds : [0x1597F50];mov eax, 0x00431EB0;call eax;mov ecx, eax; // 拿到ecxlea ebx, pOutNum;push ebx;push 0;push pID;mov eax, 0x00C99980;call eax;lea ebx, pName; // 获取pName的地址mov dword ptr [ebx], eax; // 保存名字修改pName的指向popf;popad;};str.Format(L"物品名称:%ls\r\n物品id为:%d\r\n物品数量为:%d\r\n物品最大数量为:%d\r\n", pName,pID, pnumber, pMax);this->m_bagContent.Append(str);}UpdateData(FALSE); // 变量到空间
}有点bug hh 不搞了

数据结构分析-list

#include <list>
#include <iostream>using namespace std;int main() {list<int> mylist;mylist.push_back(5);mylist.push_back(7);mylist.push_back(9);mylist.push_back(20);return 0;
}

所以我们可以看出来list是一个双向循环链表。然后最后一个节点的下一个指向虚拟头结点。
虚拟头结点-指向首元素-指向尾元素稍微总结:-list的第二个指针域指向一个虚拟头结点-虚拟头结点有两个指针域,第一个指向首节点,第二个指向尾节点-然后后面每个元素有三个域,第一个指针域指向下一个节点,第二个指向上一个节点,第三个数据-最后一个元素的下一个节点,即第一个指针域指向虚拟头结点

数据结构分析vector

Vector:Vector通过一个连续的数组存放元素,如何集合已经满了,在新增元素的时候,就要分配一块更大的内存,将原本的数据复制过来,释放之前的数据,再插入新的数据。#include <vector>
#include <iostream>using namespace std;int main() {vector<int> myVector;myVector.push_back(1);myVector.push_back(2);myVector.push_back(3);myVector.push_back(4);myVector.push_back(5);myVector.push_back(6);myVector.push_back(7);return 0;
}通过分析:-1.vector一共有四个指针域-第一个指向内存块1-第二个指向数组的首地址-第三个指向数组最后一个元素的下一个地址【这个减去首地址/元素大小 = 实际元素个数】-第四个指向数组开辟的空间的结束地址【这个减去首地址/元素大小 = 实际开辟的空间大小,因为我们指导vector有2倍扩容的机制】-2.内存块1跟list一样没啥用,指向自身然后通过观察跟我们自己写数组是一样的。-1.如果删除的是最后一个元素,我们直接修改尾指针就好了,就是实际结束地址。-2.如果删除的是中间的元素,我们需要把后面的元素往上提,覆盖前面的元素。

数据结构分析-map

map:典型的二叉树结构,准确的说是红黑树。
它的特点是数据存储是有序的。
map存储数据是以键值对的形式存储的key-value
优势:查找数据效率高
缺点:插入数据效率非常低,因为要排序。#include <map>
#include <iostream>using namespace std;int main() {map<int, int> intMap;intMap.insert(pair<int,int>(1,0x10));intMap.insert(pair<int,int>(2,0x11));intMap.insert(pair<int,int>(3,0x12));intMap.insert(pair<int,int>(4,0x13));return 0;
}通过分析:-1.map有三个指针域目的一个是内存块1,第二个是内存块2,第三个是元素个数-2.内存块1没什么用指向自己,stl容器的标志-3.内存块2指向一个类似虚拟头节点的东西,第二个指针域保存根节点的地址-4.节点分析:-节点的第二个指针域表示上一个元素-节点的第三个指针域表示下一个元素 【如果到头了指向虚拟头节点】-节点的第四个指针域,1表示跟节点,0表示普通节点-节点的第五个指针域,key-节点的第六个指针与,val

周围对象分析

先来回顾之前分析的数据:
1.最简单的直接血值来源,通过CE搜索当前面板属性血值,通过改变血量定位存放血值的内存,由于该内存是堆内存,所以需要寻找基址,通过追踪堆栈内存地址的来源最终找到了基址
2.人物背包分析,分析数据的关键是通过数据变化定位数据,但是背包是一个不变的东西,没有办法让背包发生变化,此时我们可以通过背包的物品入手。
因为物品跟背包是有关联的,通过背包物品数量的一个变化定位到了物品,因为物品是存放在背包中的,最终追踪到了背包的基址。
那么周围对象如何分析呢?
同背包一样,不能直接入手分析NPC,我们可以找一找跟NPC相关,并且我们可以利用的数据。周围对象分析:
0C897EE8 选中人物
通过下硬件写入断点,找到 mov dword ptr ds:[eax+1EB0],ecx选中的NPC id006B3E7C | 8B98 B41E0000            | mov ebx,dword ptr ds:[eax+1EB4]         |
006B3E82 | 3BDA                          | cmp ebx,edx                             |
006B3E84 | 74 67                           | je xajh.6B3EED                          |
006B3E86 | 8988 B01E0000            | mov dword ptr ds:[eax+1EB0],ecx         | ecx选中的NPC id
006B3E8C | 8990 B41E0000            | mov dword ptr ds:[eax+1EB4],edx         |
006B3E92 | 837E 08 00                    | cmp dword ptr ds:[esi+8],0              |
006B3E96 | 75 1E                            | jne xajh.6B3EB6                         |
006B3E98 | 8B7F 08                        | mov edi,dword ptr ds:[edi+8]            |
006B3E9B | 8B9F B01E0000            | mov ebx,dword ptr ds:[edi+1EB0]         |
006B3EA1 | 8BBF B41E0000            | mov edi,dword ptr ds:[edi+1EB4]         |接下来就是最终NPC ID的来源,看看能不能找到对象的地址。
006B3E6D | 8B0E                     | mov ecx,dword ptr ds:[esi]              | esi:0019EEF4然后继续追踪esi的来源。
006B3E31 | 8B7424 08                | mov esi,dword ptr ss:[esp+8]            | esp:0019EE94然后看到esp+8我们又可以猜测是局部变量或者函数参数了。006BDB83 | 83FE 07                  | cmp esi,7                               |
006BDB86 | 74 05                      | je xajh.6BDB8D                          |
006BDB88 | 83FE 01                    | cmp esi,1                               |
006BDB8B | 75 17                        | jne xajh.6BDBA4                         |
006BDB8D | 8B4C24 14                | mov ecx,dword ptr ss:[esp+14]           |
006BDB91 | 8D5424 54                 | lea edx,dword ptr ss:[esp+54]           | edx来源 esp + 54. edx:0019EEF4
006BDB95 | 52                               | push edx                                |  发现参数来源是edx
006BDB96 | C64424 17 01             | mov byte ptr ss:[esp+17],1              |
006BDB9B | E8 9062FFFF               | call xajh.6B3E30                        |然后是一个局部变量。然后我们发现这个是不变的地址,即esp+54,0x0019EEF4这个地址是固定的
那么我们就给函数头部和这里下一个断点。然后给内存写入一个硬件写入断点。看看是哪里写入的。006BDB43 | 894C24 54                | mov dword ptr ss:[esp+54],ecx           | esp:0019EEA0通过查找局部变量值的来源,发现局部变量来源于ecx寄存器。然后继续追踪ecx寄存器的来源。006BDB37 | 8B4C24 44                | mov ecx,dword ptr ss:[esp+44]           | esp:0019EEA0然后发现esp继续研究是局部变量还是函数参数,然后发现是一个局部变量。006BDA86 | 894424 44                | mov dword ptr ss:[esp+44],eax           | 然后发现来源eax,继续追踪eax的来源。
006BDA80 | 8B85 78010000            | mov eax,dword ptr ss:[ebp+178]          |  ebp:64BC9008那我们就要追踪ebp的来源了。006BDA05 | 8B6C24 74                | mov ebp,dword ptr ss:[esp+74]           | esp:0019EEA8发现ebp来源esp+74,所以猜测是局部变量或者函数参数。是传递过来的第6个参数。006C0048 | 52                       | push edx                                | 第6个参数来源edx
006C0049 | 50                       | push eax                                |
006C004A | 51                       | push ecx                                |
006C004B | 8D5424 30                | lea edx,dword ptr ss:[esp+30]           |
006C004F | 52                            | push edx                                |
006C0050 | 53                            | push ebx                                |
006C0051 | 55                            | push ebp                                |
006C0052 | 8BCE                         | mov ecx,esi                             |
006C0054 | E8 A7D9FFFF              | call xajh.6BDA00                        |那么我们继续追踪edx寄存器的来源006C003D | 8B57 40                  | mov edx,dword ptr ds:[edi+40]           | edi:300C9B00发现来源edi,edi的地址是固定的,edi+40存放的是当前NPC对象地址。
办法1:追踪edi的来源
006BFD05 | 8B7A 04                  | mov edi,dword ptr ds:[edx+4]            |  edx:4331A620然后最终edx
006BFCFD | 8B5424 1C                | mov edx,dword ptr ss:[esp+1C]           | esp:0019EF20然后发现esp+的形式,去看是局部变量还是函数参数,发现是局部变量。
那么老规矩,首尾加上断点,然后给内存下一个硬件写入断点,看看是哪里写入的。
006BFBDF | 894424 14                | mov dword ptr ss:[esp+14],eax           |  eax:4331A620然后发现上面有个call,一般返回值放在eax里面
006BFBDA | E8 816BD9FF              | call call 0x00456760                        |
然后发现这个函数没有参数,即可以通过这个函数拿到eax。但是我们要查找是对象数组,现在拿到的eax和对象数组一点关系都没有。说明某些步骤错了。方法2:我们发现edi+40保存的是NPC对象的地址,点击不同的NPC对象,存放的地址不同。所以我们应该追踪是谁把地址写进来的。
因为我们在游戏中,鼠标选中了某个NPC他就会写进来,那他肯定会遍历所有的对象,看我们到底选中的是那个。00722169 | 895A 38                  | mov dword ptr ds:[edx+38],ebx           |  ebx:1031F008跟踪ebx来源eax00722015 | 8BD9                     | mov ebx,ecx                             | ecx:1031F008然后发现到头了,ecx应该是上一个函数的,继续向上找。0072241D | 8BCE                     | mov ecx,esi                             | esi:1031F008然后esi又来源ecx00722209 | 8BF1                     | mov esi,ecx                             | ecx:1031F008 但是我们发现这个ecx值会一直变化也就是说我们猜测上一层在不停的循环遍历周围对象,然后把周围对象的地址给ecx传递值。00594F0E | 8BCE                     | mov ecx,esi                             |  ecx来源esi,esi就是传递进入的每一个对象的地址。00594EE6 | 8B30                     | mov esi,dword ptr ds:[eax]              |   然后发现来源于eax,继续跟踪eax来源。
因为是一个循环,所以也不一定是在上面改变的eax的值,然后我们执行一下,看看eax最后一次改变值是在哪里改变的。00594F39 | 8D42 04                  | lea eax,dword ptr ds:[edx+4]            | 最后一次改变eax的值那我们要在循环里面找edx的来源00594F27 | 8B5424 20                 | mov edx,dword ptr ss:[esp+20]           | edx的来源又是esp+偏移的形式,可能局部变量,可能参数。
发现可能是局部变量,所以我们在首部和这一行下一个断点,看看中间哪里改写了。
但是我们发现循环不好使,可能会写入超级多次,我们无法确定。
那么我们只能在循环里面一行一行走了。00594F22 | E8 498CFFFF              | call xajh.58DB70                        | 修改esp+20然后发现这个call里面修改了esp+20
那么我们现在就要分析这个call里面的内容。
这个call不停的输出对象的地址给我们,而且进入这个call我们这个call里面也是一个循环。
所以很有可能这个call就在遍历我们的对象数组。0058DB70 | 8BC1                     | mov eax,ecx                             | 结构体的首地址,一共12字节
0058DB72 | 8378 04 00               | cmp dword ptr ds:[eax+4],0              | 判断是否进行遍历,eax+4 存放的是对象集合的首地址
0058DB76 | 74 47                    | je xajh.58DBBF                          |
0058DB78 | 57                       | push edi                                | 保存寄存器环境
0058DB79 | 8DA424 00000000          | lea esp,dword ptr ss:[esp]              | push了,把esp地址给esp地址,不变
0058DB80 | 8B48 08                  | mov ecx,dword ptr ds:[eax+8]            |
0058DB83 | 85C9                     | test ecx,ecx                            | 判断结构体第三个属性是否为0
0058DB85 | 75 23                    | jne xajh.58DBAA                         |
0058DB87 | 8340 04 04               | add dword ptr ds:[eax+4],4              | 取集合的下一个地址
0058DB8B | 8B08                     | mov ecx,dword ptr ds:[eax]              |
0058DB8D | 8B79 14                  | mov edi,dword ptr ds:[ecx+14]           |
0058DB90 | 8B50 04                  | mov edx,dword ptr ds:[eax+4]            | eax+4是当前正在遍历的对象集合地址
0058DB93 | 83C1 08                  | add ecx,8                               |
0058DB96 | 8B09                     | mov ecx,dword ptr ds:[ecx]              |
0058DB98 | 8D0CB9                   | lea ecx,dword ptr ds:[ecx+edi*4]        | 这个地方很明显看出来,这是数组的结束地址
0058DB9B | 3BD1                     | cmp edx,ecx                             | 是否遍历结束了
0058DB9D | 74 18                    | je xajh.58DBB7                          |
0058DB9F | 8B12                     | mov edx,dword ptr ds:[edx]              |
0058DBA1 | 8950 08                  | mov dword ptr ds:[eax+8],edx            |
0058DBA4 | 85D2                     | test edx,edx                            |
0058DBA6 | 75 16                    | jne xajh.58DBBE                         |
0058DBA8 | EB 05                    | jmp xajh.58DBAF                         |
0058DBAA | 8B11                     | mov edx,dword ptr ds:[ecx]              | 取出来对象集合的x元素
0058DBAC | 8950 08                  | mov dword ptr ds:[eax+8],edx            | [eax+8]==[esp+20]
0058DBAF | 8378 08 00               | cmp dword ptr ds:[eax+8],0              | 判断集合的xx元素是否为0
0058DBB3 | 74 CB                    | je xajh.58DB80                          |
0058DBB5 | 5F                          | pop edi                                 |
0058DBB6 | C3                         | ret                                     |
0058DBB7 | C740 04 00000000         | mov dword ptr ds:[eax+4],0              |
0058DBBE | 5F                         | pop edi                                 |
0058DBBF | C3                         | ret                                     |然后我们最终ecx这个结构体的首地址来源esp+18.然后追踪esp+18的来源,通过首尾断点+硬件写入
这个应该就是我们在循环里面看到的结构体的12个字节了。其中esp+18是首地址。
00594E87 | 8D74B5 00                | lea esi,dword ptr ss:[ebp+esi*4]        |
00594E8B | 885C24 13                | mov byte ptr ss:[esp+13],bl             |
00594E8F | C64424 12 00             | mov byte ptr ss:[esp+12],0              |
00594E94 | 894424 18                | mov dword ptr ss:[esp+18],eax           |
00594E98 | 895424 20                | mov dword ptr ss:[esp+20],edx           |
00594E9C | 894C24 1C                | mov dword ptr ss:[esp+1C],ecx           |  此处就是对结构体成员初始化,赋值。
然后通过观察,我们要找的就是结构体+0的位置的地址的来源,因为+0的地址指向的一个结构体,里面保存的数组的起始地址 结束地址 最大长度等,00594E6E | 8D79 14                  | lea edi,dword ptr ds:[ecx+14]           |
00594E71 | 8BC7                     | mov eax,edi                             |
00594E73 | 8B70 14                  | mov esi,dword ptr ds:[eax+14]           |
00594E76 | 894C24 14                | mov dword ptr ss:[esp+14],ecx           |
00594E7A | 8B48 08                  | mov ecx,dword ptr ds:[eax+8]            |然后发现edi最终还是来源上一层的ecx。【ecx:65061360】
007C99D8 | 8B0F                     | mov ecx,dword ptr ds:[edi]              |  edi:43844C3C007C99C9 | 8D78 6C                  | lea edi,dword ptr ds:[eax+6C]           |007C99B0 | E8 ABCDC8FF              | call xajh.456760                        |
然后我们发现来源于这个call,但是并不是直接就拿到了我们的基址。而是循环了不知道多少遍才取出来我们的集合基址。
那么我们猜测我们的NPC集合的基值是从另一个集合中来的,就是有一个集合保存了很多个集合的基址。
然后我们测试了一下,循环了两次取出来的才是我们要的。也就是说call出来的值,+6C 拿到edi,然后再+8才能拿到我们要的。人物名字CE搜索:3702A4BC 血条上面的名字:36CDDF34
eax:36CDDF36
00F091AC | 57                       | push edi                                |
00F091AD | 33DB                     | xor ebx,ebx                             |
00F091AF | 8D78 02                  | lea edi,dword ptr ds:[eax+2]            |
00F091B2 | 66:8B10                  | mov dx,word ptr ds:[eax]                |
00F091B5 | 83C0 02                  | add eax,2                               |00F09191 | 8B6C24 08                | mov ebp,dword ptr ss:[esp+8]            | esp:0019CD1C00A3DE5C | 8B6C24 2C                | mov ebp,dword ptr ss:[esp+2C]           |
00A3DE60 | 8B55 00                  | mov edx,dword ptr ss:[ebp]              |  ebp:31F74E28  虚函数表,第一个参数就是虚函数指针
00A3DE63 | 8B82 88000000            | mov eax,dword ptr ds:[edx+88]           |  对应的获取对象名称的虚函数
00A3DE69 | 8BCD                          | mov ecx,ebp                             | this指针,所以ebp就是首地址。
00A3DE6B | FFD0                           | call eax                                |然后发现函数里面其实就一句话,+90C就是拿到名字
0072A260 | 8B81 0C090000            | mov eax,dword ptr ds:[ecx+90C]          |
所以对象的首地址+90C就是首地址。// 遍历周围对象
void CMainDlg::OnBnClickedButtonNpc()
{DWORD objectBeginAddr = 0;DWORD arrLen = 0;__asm {pushad;pushf;mov eax, 0x00456760;call eax;mov eax, [eax + 0x74];// 这样拿到了数组那个结构的地址 然后 +1C 首地址 +20结束 24大小lea ebx, objectBeginAddr; // 拿到变量地址mov edx, [eax + 0x1C]; // +0x1C的地址里面的内容保存的就是首地址mov dword ptr [ebx], edx; // 把首地址给变量lea ebx, arrLen; // 拿到变量地址mov edx, [eax + 0x24]; // +0x24的地址里面的内容保存的就是长度mov dword ptr [ebx], edx; // 把长度给变量popf;popad;};// 遍历CString str;for (int i = 0; i < arrLen; i++) {DWORD npcStruct = *(DWORD*)(objectBeginAddr + i * 4); // 取出来了数组第i个元素的内容if (npcStruct == 0) {continue;}DWORD npcRealStruct = *(DWORD*)(npcStruct + 0x4); // NPC结构体指针 首地址DWORD npcId = *(DWORD*)(npcRealStruct + 0x178);DWORD virtualFunTable = ((DWORD*)npcRealStruct)[0]; // 获取虚函数表DWORD getNameFunc = *(DWORD*)(virtualFunTable + 0x88); // 这里就是拿到获取名称的虚函数LPWSTR npcName = L"";__asm {pushad;pushf;mov ecx, npcRealStruct; // 传递this指针mov eax, getNameFunc;call eax; // 获取名字mov npcName, eax; // 然后把名字地址保存popf;popad;};str.Format(L"npcId为:%d\tnpc名字:%ls\r\n",npcId, npcName);this->m_Npcs.Append(str);}UpdateData(FALSE); // 变量到控件
}

选择目标


选择目标:
一样,我们从send发包函数下断点,一层一层向上跟。006B3EAC | 57                       | push edi                                | 对象id 后4个字节
006B3EAD | 53                       | push ebx                                | 对象id 前4个字节
006B3EAE | E8 5D2B6000              | call xajh.CB6A10                        | 选中目标call4我们发现我们前面分析的有问题+178确实是id,但是不完整,它真正的Id是有8个字节。push 0x01000000
push 0x2CB
call 0x00CB6A10
add esp,0x8然后测试,是没有问题的。注意这是一个C标准的调用约定,是外屏障,我们调用完记得堆栈平衡。

自动攻击分析

自动攻击分析:
同样的分析方法,网络游戏我们都从send入手。
004D1788 | 8B4424 24                | mov eax,dword ptr ss:[esp+24]           |
004D178C | 8B4E 08                  | mov ecx,dword ptr ds:[esi+8]            |
004D178F | 6A 01                    | push 1                                  |
004D1791 | 6A 00                    | push 0                                  |
004D1793 | 6A 01                    | push 1                                  |
004D1795 | 6A 00                    | push 0                                  |
004D1797 | 6A 00                    | push 0                                  |
004D1799 | 6A 00                    | push 0                                  |
004D179B | 6A 00                    | push 0                                  |
004D179D | 8D5424 2C                | lea edx,dword ptr ss:[esp+2C]           |
004D17A1 | 52                       | push edx                                |
004D17A2 | 8B56 18                  | mov edx,dword ptr ds:[esi+18]           |
004D17A5 | 50                       | push eax                                |
004D17A6 | 52                       | push edx                                |
004D17A7 | C74424 3C 01000000       | mov dword ptr ss:[esp+3C],1             |
004D17AF | C64424 40 FF             | mov byte ptr ss:[esp+40],FF             |
004D17B4 | C64424 41 00             | mov byte ptr ss:[esp+41],0              |
004D17B9 | C64424 42 01             | mov byte ptr ss:[esp+42],1              |
004D17BE | C64424 43 01             | mov byte ptr ss:[esp+43],1              |
004D17C3 | C64424 44 01             | mov byte ptr ss:[esp+44],1              |
004D17C8 | C64424 45 00             | mov byte ptr ss:[esp+45],0              |
004D17CD | C64424 46 00             | mov byte ptr ss:[esp+46],0              |
004D17D2 | 894C24 38                | mov dword ptr ss:[esp+38],ecx           |
004D17D6 | 8B8D 581F0000            | mov ecx,dword ptr ss:[ebp+1F58]         |
004D17DC | 6A 01                    | push 1                                  |
004D17DE | E8 DD981F00              | call xajh.6CB0C0                        | 技能攻击11~按下技能1
00000001
0000948E
00000001
0019EE58
00000000
00000000
00000000
00000000
00000001
00000000
00000001  0019EE58中的东西【固定的】:
00000002
00000001
010100FF
00000001  测试发现OK。就是要先选中怪物,然后就可以用下面的技能自动打怪了。
push 1
push 0
push 1
push 0
push 0
push 0
push 0
push 0x063F0000
push 1
push 0x948E   // 技能的id 0000948F 挨着的每个技能相差1,0x948E  是第一个技能的开始位置
push 1
mov ecx,0x2B0E2010
call 0x006CB0C0ecx,来源ebp+1F58ebp来源eaxeax通过这个call可以拿到
call 0x00456780

技能栏技能分析

技能栏技能分析:
分析技能栏,可以参考我们背包。我们要找到背包,是不是从背包中的物品入手,然后从物品一路往上推找到背包。
技能栏也是一样的,我们把每一个技能看作是一个物品,而技能栏是一个小个一点的背包。不过不像背包物品那么方便,可以拆分那些,所以我们想想技能还有什么属性。比如技能名称,或者调换技能在技能栏位置...
不过我们前面在分析技能call参数的适合,我们找到了技能的id。这也是我们的线索。我们可以通过技能id入手。004D178F | 6A 01                    | push 1                                  |
004D1791 | 6A 00                    | push 0                                  |
004D1793 | 6A 01                    | push 1                                  |
004D1795 | 6A 00                    | push 0                                  |
004D1797 | 6A 00                    | push 0                                  |
004D1799 | 6A 00                    | push 0                                  |
004D179B | 6A 00                    | push 0                                  |
004D179D | 8D5424 2C                | lea edx,dword ptr ss:[esp+2C]           |
004D17A1 | 52                       | push edx                                |
004D17A2 | 8B56 18                  | mov edx,dword ptr ds:[esi+18]           |
004D17A5 | 50                       | push eax                                |
004D17A6 | 52                       | push edx                                |
004D17A7 | C74424 3C 01000000       | mov dword ptr ss:[esp+3C],1             |
004D17AF | C64424 40 FF             | mov byte ptr ss:[esp+40],FF             |
004D17B4 | C64424 41 00             | mov byte ptr ss:[esp+41],0              |
004D17B9 | C64424 42 01             | mov byte ptr ss:[esp+42],1              |
004D17BE | C64424 43 01             | mov byte ptr ss:[esp+43],1              |
004D17C3 | C64424 44 01             | mov byte ptr ss:[esp+44],1              |
004D17C8 | C64424 45 00             | mov byte ptr ss:[esp+45],0              |
004D17CD | C64424 46 00             | mov byte ptr ss:[esp+46],0              |
004D17D2 | 894C24 38                | mov dword ptr ss:[esp+38],ecx           |
004D17D6 | 8B8D 581F0000            | mov ecx,dword ptr ss:[ebp+1F58]         |
004D17DC | 6A 01                    | push 1                                  |
004D17DE | E8 DD981F00              | call xajh.6CB0C0                        | 技能攻击11~第一个技能0000948E,然后我们发现是第二个参数,来源于edx。
那么我们就要追踪edx的来源。
然后发现edx来源自esi+18拿到,猜测esi可能就是技能栏的结构体之类的地址。然后我们追踪esi。004D17A2 | 8B56 18                  | mov edx,dword ptr ds:[esi+18]           |  esi:6C528CD0发现esi来源ecx,ecx来源eax
006BC964 | 8BC8                     | mov ecx,eax                             | eax: 6C528CD0发现eax是通过这个call拿到的
006BC941 | 6A 00                       | push 0                                  |
006BC943 | 56                           | push esi                                | 普通攻击0,Q是1...第一个技能是2,第二个技能是3....
006BC944 | 8BC8                         | mov ecx,eax                             |  ecx:6D2E66A0
006BC946 | E8 7567E1FF              | call xajh.4D30C0                        | 然后发现eax来源自另外一个call,然后这个call的ecx来源[eax+C]
006BC939 | 8B48 0C                  | mov ecx,dword ptr ds:[eax+C]            |
006BC93C | E8 0FAAE0FF              | call xajh.4C7350                        |寻找eax的来源。 发现刚好上面又有一个call
006BC934 | E8 679ED9FF              | call xajh.4567A0                        |然后我们分析这个call你内部 的确通过这个call返回的eax。
004567A0 | A1 507F5901              | mov eax,dword ptr ds:[1597F50]          |
004567A5 | 85C0                     | test eax,eax                            |
004567A7 | 74 0E                    | je xajh.4567B7                          |
004567A9 | 8B40 30                  | mov eax,dword ptr ds:[eax+30]           |
004567AC | 85C0                     | test eax,eax                            |
004567AE | 74 07                    | je xajh.4567B7                          |
004567B0 | 8B80 90000000            | mov eax,dword ptr ds:[eax+90]           |
004567B6 | C3                       | ret                                     |
004567B7 | 33C0                     | xor eax,eax                             |
004567B9 | C3                       | ret                                     |

技能列表分析

技能列表分析:
006BC941 | 6A 00                       | push 0                                  |
006BC943 | 56                           | push esi                                | 普通攻击0,Q是1...第一个技能是2,第二个技能是3....
006BC944 | 8BC8                         | mov ecx,eax                             |  ecx:6D2E66A0
006BC946 | E8 7567E1FF              | call xajh.4D30C0                        |
从前面这里出发,这里我们知道是用来拿到esi的,然后esi + 0x18就是技能的id
所以这个call就是拿到技能的call。然后停下来之后,我们进去这个call看一下,然后我们发现哪个call里面有一个数组的遍历。应该就是在遍历我们的技能。
然后我们在内存下一个硬件写入断点,看是谁往技能栏数组的起始位置写入的。
43BCD1B8  67A7CF88  &"@\tM"
43BCD1BC  67A7D000  &"@\tM"
43BCD1C0  67A7D0C8  &"@\tM"
43BCD1C4  67A7CD80  &"@\tM"
43BCD1C8  67A7CDA8  &"@\tM"发现停在了这一行,但是让我们比较疑惑的是,为什么我们移动一个技能,他要把我们全部的技能都清0,然后再重新填写。
004D24EE | 892CB0                   | mov dword ptr ds:[eax+esi*4],ebp        |追踪ebp的来源。
004D24BD | 8B6C24 14                | mov ebp,dword ptr ss:[esp+14]           | esp:0019EEE4
然后看到esp我们又猜测是局部遍历或者是函数参数。
然后发现应该是上一个函数传递过来的参数。但是我们追踪出来发现,却是固定填充0,但是我们刚刚其实也发现,第一遍是把数组全部填充为0.
后面才是填充真正的地址。也就是说,我们刚刚哪个函数不止一个地方在调用。我们要找的是真正填充的调用。004D2F61 | 8B4C24 2C                | mov ecx,dword ptr ss:[esp+2C]           |
004D2F65 | 51                       | push ecx                                |
004D2F66 | 6A 00                    | push 0                                  |
004D2F68 | 56                       | push esi                                | esi:&"@\tM"
004D2F69 | 53                       | push ebx                                |
004D2F6A | 8BCD                     | mov ecx,ebp                             |
004D2F6C | E8 2FF5FFFF              | call xajh.4D24A0                        |然后发现确实,这个时候我们再找第二个参数就是esi,esi就是它真实要填充的技能的地址。
004D2F11 | E8 9C08C900              | call <JMP.&??2@YAPAXI@Z>                | new 一个对象
004D2F16 | 83C4 04                  | add esp,4                               |
004D2F19 | 894424 28                | mov dword ptr ss:[esp+28],eax           |
004D2F1D | 897424 18                | mov dword ptr ss:[esp+18],esi           |
004D2F21 | 3BC6                     | cmp eax,esi                             |
004D2F23 | 74 09                    | je xajh.4D2F2E                          |
004D2F25 | 8BC8                     | mov ecx,eax                             | ecx=对象首地址=this指针
004D2F27 | E8 64D9FFFF              | call xajh.4D0890                        | 构造函数然后我们发现这条路行不通,我们跟踪地址的来源已经找到头了。
但是我们的构造函数里面肯定会对技能id进行赋值。那么我们就看看技能id是怎么来的。
004D2F3A | 57                             | push edi                                | 技能id来源
004D2F3B | 8BCE                          | mov ecx,esi                             |
004D2F3D | E8 7EDBFFFF              | call xajh.4D0AC0                        |
调用了这个call之后,技能id就有值了。然后发现来源edi。004D2EBD | 8B7C24 24                | mov edi,dword ptr ss:[esp+24]           |
然后看到esp,我们又猜测是局部变量或者函数参数,发现是函数参数的第二个参数。
然后我们发现不止一个地方调用了当前函数,我们第一次找到的函数,发现每次传递的技能id都是同一个,那么肯定不是它。不然我们怎么遍历的。
然后我们发现了第二个调用的函数就下面哪个。004CFD54 | 6A 01                    | push 1                                  |
004CFD56 | 6A 00                    | push 0                                  |
004CFD58 | 50                          | push eax                                |
004CFD59 | 8D46 01                 | lea eax,dword ptr ds:[esi+1]            |
004CFD5C | 50                          | push eax                                |
004CFD5D | 8BCF                     | mov ecx,edi                             |
004CFD5F | E8 2C310000          | call xajh.4D2E90                        |004CFD4D | 8B04B2                   | mov eax,dword ptr ds:[edx+esi*4]        | edx:6031A940
看到这种形式一般就是数组遍历了。edx就是数组的基地址了。
5F317430  0000948D
5F317434  0000948E
5F317438  0000948F
5F31743C  00009490
5F317440  00000000
然后发现确实,这就是在遍历我们的技能。那么edx就是数组的首地址。
但是我们测试发现这个数组里面的内容,随着我们移动绝技的位置会发生变化。
也就是这个地址不是我们固定的已经学的技能的数组。那么就是说这个游戏,外面技能栏一个数组,里面技能栏一个数组,已经学的技能又是一个数组。
如果我们现在追踪edx的来源,那么我们追踪的是里面绝技哪个数组的最终来源。不太合适。
我们就是想找已经学到的技能的数组。不过我们知道绝技中是来源下面哪个已经学的数组,那么我们可以给这个数组下一个硬件写入断点。
看看是谁往里面写的,从而追踪我们已经学到的技能。005835D9 | 891E                     | mov dword ptr ds:[esi],ebx              |
005835DB | 8956 04                  | mov dword ptr ds:[esi+4],edx            |
005835DE | 5B                       | pop ebx                                 |
005835DF | 8B46 04                  | mov eax,dword ptr ds:[esi+4]            |
005835E2 | 85C0                     | test eax,eax                            |
005835E4 | 74 08                    | je xajh.5835EE                          |
005835E6 | 8B4C24 0C                | mov ecx,dword ptr ss:[esp+C]            | ecx来源[esp+C]
005835EA | 8B11                     | mov edx,dword ptr ds:[ecx]              |   edx 来源[ecx]
005835EC | 8910                     | mov dword ptr ds:[eax],edx              | 然后发现停在了这一行,也就是edx就是技能id
005835EE | FF46 0C                  | inc dword ptr ds:[esi+C]                |
005835F1 | 8346 04 04             | add dword ptr ds:[esi+4],4              |
005835F5 | 5E                           | pop esi                                 |
005835F6 | 59                           | pop ecx                                 |
005835F7 | C2 0400                  | ret 4                                   |然后发现是上一个函数的第一个参数。 来源edx
004CFF19 | 52                              | push edx                                | 第一个参数
004CFF1A | 8BCE                          | mov ecx,esi                             |
004CFF1C | E8 3F360B00              | call xajh.583560                        |004CFF16 | 8D1481                     | lea edx,dword ptr ds:[ecx+eax*4]        |  然后我们发现来源ecx+eax*4,那么ecx也是一个数组的首地址,然后通过观察我们也发现存放的就是技能的id。
但是我们发现这也是一个临时的数组。
我们发现写这个代码的开发人员是一个奇葩人员。一个技能数组开辟了好几个数组互相拷贝,还一直清空和new。看不懂哩这代码写的。那么我们就要找这个临时的数组从哪里来的。
004CFF10 | 8B4D 12                  | mov ecx,dword ptr ss:[ebp+12]           | ebp:0019EF28
然后发现是一个局部变量。
然后发现这个地址是不变的,那么我们就可以头部下一个断点,这里下一个断点,然后下一个硬件写入断点,就知道是哪里给他赋值的了。然后我们猜测错了,它不是一个局部变量,因为没有地方往这里面写入。
然后我们发现在头部断下来的时候就已经有值了。004D0273 | 52                             | push edx                                |
004D0274 | 8BCE                         | mov ecx,esi                             |
004D0276 | E8 15FBFFFF              | call xajh.4CFD90                        |
然后发现ebp+12的位置,在这个函数被调用之前就已经有值了。
然后所以ebp+12其实还是一个参数,只是传递的应该是一个结构体的地址。004D0248 | E8 C35C8000              | call xajh.CD5F10                        |
然后发现调用了这个call以后,edx的值就被填充了。不过感觉这样追踪还是很麻烦。我们还是换一种方式把。
就是不从技能call中的技能id入手了。我们前面就是从这里被绕进去了。我们这里从技能名称入手。【换方式】
通过CE搜索到已经学的技能栏的名字的地址:5A81F2BC
然后老规矩通过硬件断点访问断点来进行追踪。00F0CA2B | 8B0D 4CA25701    | mov ecx,dword ptr ds:[157A24C]          |
00F0CA31 | 53                          | push ebx                                |
00F0CA32 | 8BD8                     | mov ebx,eax                             |
00F0CA34 | 894C24 08             | mov dword ptr ss:[esp+8],ecx            |
00F0CA38 | 0FB708                  | movzx ecx,word ptr ds:[eax]             | eax:5A81F2BC然后发现断下来了,来源ds:[eax]00F0CA00 | 8B4424 2C                | mov eax,dword ptr ss:[esp+2C]           | esp:0019ED14然后看见esp怀疑是局部变量或者函数参数。
00F0CC5B | 52                            | push edx                                |
00F0CC5C | 6A 00                        | push 0                                  |
00F0CC5E | 50                               | push eax                                |
00F0CC5F | 51                               | push ecx                                |
00F0CC60 | 56                            | push esi                                |
00F0CC61 | C74424 2C 00000000       | mov dword ptr ss:[esp+2C],0             |
00F0CC69 | E8 72FDFFFF               | call xajh.F0C9E0                        |然后发现是上一个函数传递的第二个参数,那么就是来源ecx
00F0CC57 | 8B4C24 24                | mov ecx,dword ptr ss:[esp+24]           |
看见esp+24又猜测是局部变量或者函数参数。00F3D13F | 57                       | push edi                                |
00F3D140 | 51                       | push ecx                                |
00F3D141 | 8D5424 28                | lea edx,dword ptr ss:[esp+28]           |
00F3D145 | 52                             | push edx                                |
00F3D146 | E8 E5FAFCFF              | call xajh.F0CC30                        |那么又是来源ecx。
00F3D12D | 8B4C24 1C                | mov ecx,dword ptr ss:[esp+1C]           | 然后猜测是局部变量或者函数参数
008D62D3 | 56                       | push esi                                |
008D62D4 | FFD0                     | call eax                                |然后追踪esi
008D62C8 | 8B76 24                  | mov esi,dword ptr ds:[esi+24]           |  esi:52E11770
然后我们发现esi是不变的地址,而esi+24存放的是技能的名称。很可能esi就是技能对象。008D61E6 | 52                             | push edx                                |
008D61E7 | E8 2448BFFF              | call xajh.4CAA10                        |
008D61EC | 8BF0                           | mov esi,eax                             |
发现esi来源eax,然后上面有个call,这个call接收技能id,就是说通过技能id返回技能对象。那么我们现在又是要追踪技能id的来源。
008D61DC | 8B4C24 20           | mov ecx,dword ptr ss:[esp+20]           | esp:0019EDB4
008D61E0 | 8B11                     | mov edx,dword ptr ds:[ecx]              |   ecx:549BB27C
然后看见esp+20怀疑是局部变量或者函数参数。
然后应该是局部变量。008D61CF | 897424 20                | mov dword ptr ss:[esp+20],esi           |
然后发现来源esi。008D61B7 | 81C6 64010000            | add esi,164                             |
然后我们发现esi这个地址保存的内容是点技能的id数组,那么我们就要抓着武学id走了,我们应该通过不同的面板遍历不同面板的技能、
esi + 164 = esi:0000948D然后esi来源这个call
008D614A | 8B0D 507F5901            | mov ecx,dword ptr ds:[1597F50]          |
008D6150 | 8BF8                            | mov edi,eax                             |
008D6152 | E8 59BDB5FF              | call xajh.431EB0                        |
008D6157 | 8D4C24 2C                 | lea ecx,dword ptr ss:[esp+2C]           |
008D615B | 51                              | push ecx                                | 出参 应该返回0x61才表示正常执行
008D615C | 6A 00                         | push 0                                  | 固定0
008D615E | 57                               | push edi                                | 武学id
008D615F | 8BC8                           | mov ecx,eax                             |
008D6161 | E8 1A383C00              | call 0x00C99980                       |也就是说给他一个武学id给我们一个武学对象。
edi来源eax,然后我们发现一个call。008D6145 | E8 96246000              | call xajh.ED85E0                        |调用了这个函数会返回一个武学id出来。
通过分析函数内部,发现武学id来源:
00ED863D | 8BB6 68010000            | mov esi,dword ptr ds:[esi+168]          | 00ED85F6 | 8BF1                     | mov esi,ecx                             |
然后发现来源外层的ecx。那么我们就是要追踪外层的ecx。也就是说ecx+168就是武学id
追踪ecx+168是谁写入的。武学结构id写写入断点。00ED86A0 | 8B4C24 14                | mov ecx,dword ptr ss:[esp+14]           |
00ED86A4 | 898E 68010000            | mov dword ptr ds:[esi+168],ecx          |来源上一个函数的第一个参数esp+14是。
008D6816 | 8B8C24 90000000          | mov ecx,dword ptr ss:[esp+90]           |
008D681D | 51                       | push ecx                                |
008D681E | 8BCF                     | mov ecx,edi                             |
008D6820 | E8 4B1E6000              | call xajh.ED8670                        |即来源ecx,然后ecx来源esp+90.
然后esp继续猜测是参数还是局部遍历,发现是上一个函数的第一个参数008C765A | 57                       | push edi                                |
008C765B | 8BCB                     | mov ecx,ebx                             |
008C765D | E8 0EEF0000              | call xajh.8D6570                        |即来源edi。
008C7610 | 56                       | push esi                                |
008C7611 | 57                       | push edi                                |
008C7612 | 6A 01                    | push 1                                  |
008C7614 | 8BF1                     | mov esi,ecx                             |
008C7616 | E8 B54C6100              | call xajh.EDC2D0                        |
edi来源eax,然后发现来源一个call。5
这个函数就是返回一个武学id出来。然后通过分析内部,其实真正的武学id是通过这个上面哪个call内部的一个call产生的。
00EDC391 | 8BCE                          | mov ecx,esi                             |
00EDC393 | E8 A8170600              | call xajh.F3DB40                        |
这个call内部就一行代码【00F3DB40 | 8B81 9C010000            | mov eax,dword ptr ds:[ecx+19C]          |】
所以我们主要分析ecx的来源,ecx+19C就可以拿到武学id了。然后我们最终ecx+19C是哪里写进来的。
00F3DB50 | 8B4424 04                | mov eax,dword ptr ss:[esp+4]            |
00F3DB54 | 8981 9C010000            | mov dword ptr ds:[ecx+19C],eax          |然后是上一个函数的第一个参数
008C7B5F | 8B04AB                   | mov eax,dword ptr ds:[ebx+ebp*4]        |
008C7B62 | 50                       | push eax                                |
008C7B63 | 8BCE                     | mov ecx,esi                             |
008C7B65 | E8 E65F6700              | call xajh.F3DB50                        |然后发现ebx像是一个数组的基址。然后我们发现循环了四次,刚好对应上了我们的四个武学。所以ebx存放的是武学的数组。
那么我们现在追踪ebx的来源。008C793B | 8B5C24 78                | mov ebx,dword ptr ss:[esp+78]           | 然后头尾夹住,发现是一个局部变量。
0019D898  30301EB8    数组的起始地址
0019D89C  30301EBC    数组的结束地址
0019D8A0  00000005    总共的容量
0019D8A4  00000001    当前元素个数它好像自己实现了一个类似vector一样的数组。这个数组是动态申请出来的,所以我们应该关注数组里面的元素是怎么来的。008C7864 | 52                              | push edx                                |   要填充进去的武学id
008C7865 | 8D4C24 7C                | lea ecx,dword ptr ss:[esp+7C]           |  动态数组的起始地址
008C7869 | E8 F2BCCBFF              | call xajh.583560                        |
通过这个call,每次会往数组里面写一个元素。然后我们分析这个call,看看数组里面的元素怎么来的。那么我们要追踪的是edx来源的。看看武学id的来源。008C7860 | 8D5424 30                | lea edx,dword ptr ss:[esp+30]           |
然后发现是局部变量的地址。008C7848 | 8DB8 20060000            | lea edi,dword ptr ds:[eax+620]          | eax:1082E010
008C784E | C74424 34 05000000    | mov dword ptr ss:[esp+34],5             | 循环次数 = 5
008C7856 | 8B07                              | mov eax,dword ptr ds:[edi]              |
008C7858 | 894424 30                     | mov dword ptr ss:[esp+30],eax           |也就是说edx=eax=edi=eax+620
即eax+620里面保存着武学id。然后发现这其实是循环。eax+620就是武学id的起始位置,然后每次循环edi会移动4.
我们现在观察出来的代码,就是从武学id的数组中每次取出来一个武学id,然后填充到刚刚发现的哪个动态数组里面。通过这个call可以拿到eax
008C782C | 8B80 700A0000            | mov eax,dword ptr ds:[eax+A70]          | eax+A70:"1\n"
008C7832 | 8B0D 507F5901            | mov ecx,dword ptr ds:[1597F50]          |
008C7838 | 8B89 60020000            | mov ecx,dword ptr ds:[ecx+260]          |
008C783E | 50                             | push eax                                |
008C783F | E8 4C60EBFF              | call 0x0077D890                        |那么我们就找eax和ecx的来源,然后通过这个call我们就可以拿到eax,然后eax+620就是我们的武学id数组
而ecx这里已经是基址了,所以我们只要找到eax的来源。然后我们发现eax是通过上面的call拿到的。。那么两个参数我们都有了,不就可以直接找到武学id数组的来源了。008C7827 | E8 54EFB8FF              | call 0x00456780                        |调用这个call拿到,eax,eax+A70就是我们要的参数,然后ecx通过基址可以拿到然后+260就是我们要的另一个参数。
然后就可以调用上面哪个call,上面哪个call+620就是我们的武学id数组了。总结:
当我们去寻找某个数据来源的时候,如果发现这个数据是源于某个堆内存,那么我们观察这个堆内存是否是不变的地址。
1.如果是不变的地址
那么我们去找这个不变的地址的来源,看看通过哪个基址加偏移可以定位到这个地址。
2.如果是变化的地址
同样要先寻找这个变化的地址来源,我们肯定能找到一个new出来这个结构的地方。
然后去看这个地址哪里赋的值,也就是继续去找这个值的来源。// 遍历人物技能
void CMainDlg::OnBnClickedButtonNpc2()
{/*call 0x00456780       这个call拿到eax mov eax,dword ptr ds:[eax+A70]         mov ecx,dword ptr ds:[1597F50]         mov ecx,dword ptr ds:[ecx+260]         push eax                         call 0x0077D890                这个call 拿到的eax + 620 = 武学数组的起始位置然后我们通过武学id去拿到武学对象mov ecx,dword ptr ds:[1597F50]          |call 0x00431EB0                        |push ecx                                | 出参 应该返回0x61才表示正常执行push 0                                  | 固定0push edi                                | 武学idmov ecx,eax                             |call 0x00C99980                         |武学对象+164 = 技能id数组push edx                                |call 0x004CAA10                        |mov esi,eax                             |通过技能id拿到技能对象技能对象+0x24就是技能名称*/DWORD mpObj = 0;DWORD mpArray = 0;__asm {mov eax, 0x00456780;call eax; // 拿到需要的eax偏移mov eax, dword ptr ds : [eax + 0xA70];mov ecx, 0x1597F50;mov ecx, [ecx]; // 倒一手mov ecx, dword ptr ds : [ecx + 0x260];push eax;mov eax, 0x0077D890;call eax; // 这样就拿到了eax,再+620 = 武学id数组的起始位置mov mpObj, eax;};mpArray = mpObj + 0x620; // 武学id数组的起始位置// 固定长度是5CString str;for (int i = 0; i < 5; i++) {if ((mpArray + i * 4) == 0) {continue; }DWORD wxId = *(DWORD*)(mpArray + i * 4); // 取地址里面的内容也就是武学idif (wxId == 0) {continue;}str.Format(L"武学id为:0x%X\r\n", wxId);this->m_skillsContent.Append(str);DWORD flag = 0; // 出参DWORD wxBigObj = 0; // 武学对象__asm {mov ecx, 0x1597F50;mov ecx, [ecx]; // 倒一手mov eax, 0x00431EB0;call eax;lea ecx, flag;push ecx; // 出参push 0; // 固定0push wxId; // 武学idmov ecx, eax;  mov edx, 0x00C99980;call edx; // 这样就拿到了武学对象mov wxBigObj, eax; // 保存武学对象};if (flag != 0x61) {continue; // 不是0x61就有问题}DWORD skillsArrary = wxBigObj + 0x164;for (int j = 0; j < 0x20; j++) {DWORD skillId = *(DWORD*)(skillsArrary + j * 8);if (skillId == 0) {continue;}str.Format(L"\t技能id为:0x%X\r\n", skillId);this->m_skillsContent.Append(str);}}UpdateData(FALSE); // 变量到控件
}

对象阵营分析

对象阵营分析:
我们从鼠标右键点击怪物会自动攻击,然后点击NPC没有反应这里入手。006BF8CB | 8B4E 40                  | mov ecx,dword ptr ds:[esi+40]           |
006BF8CE | 8B5424 34                | mov edx,dword ptr ss:[esp+34]           |
006BF8D2 | 51                       | push ecx                                |
006BF8D3 | 52                       | push edx                                |
006BF8D4 | 8B5424 1C                | mov edx,dword ptr ss:[esp+1C]           |
006BF8D8 | 53                       | push ebx                                |
006BF8D9 | 8D4C24 24                | lea ecx,dword ptr ss:[esp+24]           |
006BF8DD | 51                       | push ecx                                |
006BF8DE | 52                       | push edx                                |
006BF8DF | 50                       | push eax                                |
006BF8E0 | 8BCF                     | mov ecx,edi                             |
006BF8E2 | E8 19E1FFFF              | call xajh.6BDA00                        | 右键攻击15我们从这个call出发,因为点击怪物和NPC都会断下来。而再里面一个call,Npc不会断下来,就是说这个函数内部有判断是npc还是怪物。006BDCDA | FF2495 24E46B00          | jmp dword ptr ds:[edx*4+6BE424]         | 通过edi,这里跳走
NPC和怪物的edx值不一样
NPC的edx的值是0,即箭头是0,书本形式的是1,怪物的edx的值是2,即剑是2然后我们要分析edx的来源。
006BDCD3 | 0FB696 34E46B00          | movzx edx,byte ptr ds:[esi+6BE434]      |
怪物esi = 9,不能对话人物esi = 0,能对话人物 = 1..2啥的
我们主要分析9.006BDC34 | 8B5424 48                | mov edx,dword ptr ss:[esp+48]           |
006BDC38 | 8D4424 30                | lea eax,dword ptr ss:[esp+30]           |
006BDC3C | 50                       | push eax                                |
006BDC3D | 8B4424 48                | mov eax,dword ptr ss:[esp+48]           |
006BDC41 | 8D8C24 80000000          | lea ecx,dword ptr ss:[esp+80]           |
006BDC48 | 51                       | push ecx                                |
006BDC49 | 55                       | push ebp                                |
006BDC4A | 52                       | push edx                                |
006BDC4B | 50                       | push eax                                |
006BDC4C | 8BCF                     | mov ecx,edi                             |
006BDC4E | E8 2D68FFFF              | call xajh.6B4480                        |
006BDC53 | 8BF0                     | mov esi,eax                             |来源这个call,参数有5个,
$ ==>     000000EF    对象的id中四个字节 低位
$+4         01000000    对象的id中四个字节 高位【两个组合起来才是真正的ID】
$+8         5F529BE8    对象的地址
$+C         0019EF2C    局部变量1  通过观察函数内部,这个变量是一个输出参数
$+10       0019EEE0    局部变量2  通过观察,这个是一个固定的值,是0那么5个参数我们都有了,然后调用这个参数我们就可以拿到eax,即edi,即当前是怪物还是友方。ecx来源:
006962A6 | 8B8E 541F0000            | mov ecx,dword ptr ds:[esi+1F54]         |   esi:54EE2B18 = ecx0058CEEC | 8B88 8C000000            | mov ecx,dword ptr ds:[eax+8C]           |   eax:0F670D300058CEE9 | 8B45 04                  | mov eax,dword ptr ss:[ebp+4]            |  ebp:59492B30004541AC | 8B8E 94000000            | mov ecx,dword ptr ds:[esi+94]           | esi:0F670D300043581B | 8B4E 30                  | mov ecx,dword ptr ds:[esi+30]           |  esi:015995E0004623C5 | A3 88BE5901              | mov dword ptr ds:[159BE88],eax          |
004623CA | A3 8CBE5901              | mov dword ptr ds:[159BE8C],eax          |
004623CF | A3 90BE5901              | mov dword ptr ds:[159BE90],eax          |
004623D4 | A3 94BE5901              | mov dword ptr ds:[159BE94],eax          |
004623D9 | A3 98BE5901              | mov dword ptr ds:[159BE98],eax          |
004623DE | A3 9CBE5901              | mov dword ptr ds:[159BE9C],eax          |
004623E3 | A3 A0BE5901              | mov dword ptr ds:[159BEA0],eax          |
004623E8 | A3 A4BE5901              | mov dword ptr ds:[159BEA4],eax          |
004623ED | B9 E0955901              | mov ecx, 0x15995E0                    |
004623F2 | E8 0930FDFF              |  call 0x00435400                        |
004623F7 | 84C0                           |  test al,al                              |[[[[0x15995E0+30] +94]+4] +8C]+1F54
// 遍历周围对象
void CMainDlg::OnBnClickedButtonNpc()
{DWORD objectBeginAddr = 0;DWORD arrLen = 0;__asm {pushad;pushf;mov eax, 0x00456760;call eax;mov eax, [eax + 0x74];// 这样拿到了数组那个结构的地址 然后 +1C 首地址 +20结束 24大小lea ebx, objectBeginAddr; // 拿到变量地址mov edx, [eax + 0x1C]; // +0x1C的地址里面的内容保存的就是首地址mov dword ptr [ebx], edx; // 把首地址给变量lea ebx, arrLen; // 拿到变量地址mov edx, [eax + 0x24]; // +0x24的地址里面的内容保存的就是长度mov dword ptr [ebx], edx; // 把长度给变量popf;popad;};// 遍历CString str;for (int i = 0; i < arrLen; i++) {DWORD npcStruct = *(DWORD*)(objectBeginAddr + i * 4); // 取出来了数组第i个元素的内容if (npcStruct == 0) {continue;}DWORD npcRealStruct = *(DWORD*)(npcStruct + 0x4); // NPC结构体指针 首地址DWORD npcId = *(DWORD*)(npcRealStruct + 0x178);DWORD npcIdH = *(DWORD*)(npcRealStruct + 0x17C);DWORD virtualFunTable = ((DWORD*)npcRealStruct)[0]; // 获取虚函数表DWORD getNameFunc = *(DWORD*)(virtualFunTable + 0x88); // 这里就是拿到获取名称的虚函数LPWSTR npcName = L"";__asm {pushad;pushf;mov ecx, npcRealStruct; // 传递this指针mov eax, getNameFunc;call eax; // 获取名字mov npcName, eax; // 然后把名字地址保存popf;popad;}DWORD getObjectTypeArg1 = 0; // 固定0DWORD getObjectTypeArg2 = 0; // 输出参数DWORD objectType = 0;// [[[[0x15995E0+30] +94]+4] +8C]+1F54DWORD***** objBaseAddr = (DWORD*****)0x1599610;DWORD* objArr = (DWORD*)objBaseAddr[0][0x94/4][0x4/4][0x8C/4];// 最后+1F54这个地址里面保存的才是ecxDWORD realObjArr = *(DWORD*)((DWORD)objArr + 0x1F54);__asm {pushad;pushf;lea eax, getObjectTypeArg1;push eax; // 固定0lea eax, getObjectTypeArg2;push eax; // 输出参数push npcRealStruct; // 对象地址push npcIdH; // 高位push npcId; // 低位mov ecx, realObjArr;mov eax, 0x006B4480;call eax;mov objectType, eax;popf;popad;};str.Format(L"npcId为:%d\tnpc名字:%ls\t阵营:%ls\r\n", npcId, npcName,(objectType == 9 ? L"怪物":L"友军"));this->m_Npcs.Append(str);}UpdateData(FALSE); // 变量到控件
}

怪物血量分析


怪物血量分析:
5AE8E3A400AEC7BE | 8BC24 18                | mov ebp,dword ptr ss:[esp+18]           |然后发现来源上一层的第一个参数
00A3DB6C | DC05 D8744201            | fadd st(0),qword ptr ds:[14274D8]       |
00A3DB72 | E8 095E7200              | call xajh.1163980                       |
00A3DB77 | 50                                 | push eax                                |  eax:0000003B
00A3DB78 | 8D8E 90020000            | lea ecx,dword ptr ds:[esi+290]          |
00A3DB7E | E8 0DEC0A00                | call xajh.AEC790                        |00A3DB58 | D88B 4E050000            | fmul st(0),dword ptr ds:[ebx+54E]       | 总血量剩余百分多少  ebx:53409040
当前怪物的血值百分比,1满血,小于1就是百分比。0就是死了。
那么我们现在要追踪ebx。然后我们通过观察内存,发现ebx好像就是我们之前查找的周围对象,+178是怪物的id。
也就是说,ebx就是对象的首地址。对象的首地址+54E就是它的血值百分比。

技能CD分析

技能CD分析:
从我们之前找到的技能call入手,如果没在冷却状态,那么技能肯定可以直接释放,在冷却状态技能肯定不能正常释放。
也就是说存在分支。0051EFBD | 52                       | push edx                                |
0051EFBE | 894C24 60                | mov dword ptr ss:[esp+60],ecx           |
0051EFC2 | 8B8D 581F0000            | mov ecx,dword ptr ss:[ebp+1F58]         |
0051EFC8 | 50                       | push eax                                |
0051EFC9 | E8 F2291B00              | call xajh.6D19C0                        | 右键攻击8,技能攻击7说明关键的点在这里,因为经过测试无论是否在冷却状态最后一个调用的call就是这个,处于冷却状态就不会再调用下一层的攻击call。
也就是说这个call里面一定存在一个判断是否处于冷却。006D219D | 85ED                            | test ebp,ebp                            |
006D219F | 0F85 9C000000            | jne xajh.6D2241                         | 冷却调用不同的地方然后我们找到了这个关键的跳转,正常放技能不会执行这个跳转,会去执行技能攻击6的call。
而处于冷却的技能会执行这个跳转,直接跳过技能攻击6的call。然后我们测试,发现能使用的时候ebp的值是0,处于冷却的时候ebp的值是8然后ebp来源eax,追踪eax,发现一个call。
006D20D0 | 0FB643 0D                | movzx eax,byte ptr ds:[ebx+D]           |
006D20D4 | 8B4C24 40                | mov ecx,dword ptr ss:[esp+40]           |
006D20D8 | 8B9424 9C000000          | mov edx,dword ptr ss:[esp+9C]           |
006D20DF | 6A 00                    | push 0                                  |
006D20E1 | 50                       | push eax                                |
006D20E2 | 0FB643 0C                | movzx eax,byte ptr ds:[ebx+C]           |
006D20E6 | 51                       | push ecx                                |
006D20E7 | 52                       | push edx                                |
006D20E8 | 0FB653 09                | movzx edx,byte ptr ds:[ebx+9]           |
006D20EC | 6A 01                    | push 1                                  |
006D20EE | 50                       | push eax                                |
006D20EF | 6A 00                    | push 0                                  |
006D20F1 | 8D4C24 74                | lea ecx,dword ptr ss:[esp+74]           |
006D20F5 | 51                       | push ecx                                |
006D20F6 | 52                       | push edx                                |
006D20F7 | 8D8424 90000000          | lea eax,dword ptr ss:[esp+90]           |
006D20FE | 50                       | push eax                                |
006D20FF | 8D4C24 3C                | lea ecx,dword ptr ss:[esp+3C]           |
006D2103 | 51                       | push ecx                                |
006D2104 | 8D5424 78                | lea edx,dword ptr ss:[esp+78]           |
006D2108 | 52                       | push edx                                |
006D2109 | 8BC2                     | mov eax,edx                             |
006D210B | 8B9424 A4000000          | mov edx,dword ptr ss:[esp+A4]           |
006D2112 | 50                       | push eax                                |
006D2113 | 8B8424 B0000000          | mov eax,dword ptr ss:[esp+B0]           |
006D211A | 6A 00                    | push 0                                  |
006D211C | 8D4C24 70                | lea ecx,dword ptr ss:[esp+70]           |
006D2120 | 51                       | push ecx                                |
006D2121 | 6A 01                    | push 1                                  |
006D2123 | 52                       | push edx                                |
006D2124 | 50                       | push eax                                |
006D2125 | 57                       | push edi                                |
006D2126 | 8BCE                          | mov ecx,esi                             |
006D2128 | E8 A3CEFFFF              | call xajh.6CEFD0                        |但是我们发现很离谱的地方,这个函数需要19个参数。。
$-4C      7F6C4208  技能对象首地址+0x34
$-48      00000002   固定2
$-44      00000000   固定0
$-40      00000001  固定1
$-3C      0019E840  传递了怪物id
$-38      00000000  固定0
$-34      0019E854  固定dword ptr ds:[12F0158]
$-30      0019E854  固定dword ptr ds:[12F0158]
$-2C      0019E81C  输出参数
$-28      0019E874  发现我们填啥都可以正常执行,那就直接0
$-24      00000000  固定0
$-20      0019E860  12个字节全部为0
$-1C      00000000  固定0
$-18      00000001  固定1
$-14      00000001   固定1
$-10      00000000  固定0
$-C        0000948E   技能id
$-8        00000000    固定0
$-4        00000000    固定0edi来源,第19个参数:
006D1C2C | 8BF8                     | mov edi,eax                             | edi来源eax来源这个函数,这个函数需要一个参数:
006D1C1A | 8B4C24 48                 | mov ecx,dword ptr ss:[esp+48]           |
006D1C1E | 51                               | push ecx                                | ecx:00000A7D
006D1C1F | 8B4C24 18                 | mov ecx,dword ptr ss:[esp+18]           |
006D1C23 | E8 086FDFFF              | call xajh.4C8B30                        |这个call会断两次,不过只有第一次才会执行我们的技能冷却call,所以我们要分析第一次的时候。然后头尾下断点,硬件写入,看哪里给这个局部变量赋值的。006D1A3C | 894424 48                | mov dword ptr ss:[esp+48],eax           | eax = ecx然后继续追踪eax
006D1A30 | 8B47 08                  | mov eax,dword ptr ds:[edi+8]            |然后发现edi+8在函数头部就已经有值了,追踪上一个函数
0051EFB3 | 894C24 54                | mov dword ptr ss:[esp+54],ecx           | 技能id
0051EFB7 | 8B0F                         | mov ecx,dword ptr ds:[edi]              |
0051EFB9 | 8D5424 54                | lea edx,dword ptr ss:[esp+54]           |
0051EFBD | 52                             | push edx                                |
0051EFBE | 894C24 60                | mov dword ptr ss:[esp+60],ecx           | 赋值edi+8
发现在这一行赋值的。然后追踪edi。
0051EED4 | 037E 34                  | add edi,dword ptr ds:[esi+34]           |发现来源技能对象的首地址+34。那么我们就分析完了。所以哪个函数需要的参数就是技能对象首地址+34.不过第一个参数这个call还需要一个ecx,所以我们还要找ecx的来源。
006D1C1F | 8B4C24 18                | mov ecx,dword ptr ss:[esp+18]           |然后esp+18我们看看是局部变量还是参数,猜测是局部变量,所以首位下断点,然后硬件写入。
006D1A91 | 896C24 14                | mov dword ptr ss:[esp+14],ebp           | ebp = ecx发现来源ebp。006D1A88 | 8B68 24                  | mov ebp,dword ptr ds:[eax+24]           |然后eax发现来源这个call,不需要任何参数
006D1A7C | E8 1F4DD8FF              | call 0x004567A0                       | 那么我们就分析完了。总结第一个参数:-调用 call 0x004567A0,拿到eax,eax + 24里面的内容就是ecx-另一个参数就是技能对象首地址+34-然后就可以调用 call 0x004C8B30
这样就拿到了第一个参数。分析第7个参数来源:
006D19F8 | D95424 4C                | fst dword ptr ss:[esp+4C],st(0)         | edx来源
006D19C0 | D905 58012F01        | fld st(0),dword ptr ds:[12F0158]        |第一个参数:-调用 call 0x004567A0,拿到eax,eax + 24里面的内容就是ecx-另一个参数就是技能对象首地址+34-然后就可以调用 call 0x004C8B30
这样就拿到了第一个参数。分析ecx的来源:
0051EFC2 | 8B8D 581F0000            | mov ecx,dword ptr ss:[ebp+1F58]         | ebp:4CCCA018发现ecx来源esi,esi又来源ecx,然后ecx来源ebp+1F58。ebp又来源eax,eax来源这个call0051EEEE | E8 8D78F3FF              |   call 0x00456780                       |
也就是说通过这个call拿到eax,然后+1F58里面的内容就是ecx。

自动打怪逻辑实现

HANDLE autoThread = 0;
// 自动打怪
void CMainDlg::OnBnClickedCheckAutoAttch()
{UpdateData(TRUE); // 控件到变量if (this->m_AutoAttack.GetCheck()) {// 自动打怪autoThread = ::CreateThread(NULL, 0, AutoAttachMonster, NULL, 0, NULL);}else { // 如果取消勾线了 并且我们创建了线程if (autoThread != 0) {// 退出指定线程  退出自动打怪的线程TerminateThread(autoThread, 0);CloseHandle(autoThread);}}
}#pragma once
#include <Windows.h>
#include "CAttackMonster.h"extern CAttackMonster g_attackMonster;
DWORD WINAPI AutoAttachMonster(LPVOID args);#include "autoAttachThread.h"// 封装了自动打怪的操作 以及多线程的处理
CAttackMonster g_attackMonster;DWORD WINAPI AutoAttachMonster(LPVOID args) {while (1) {DWORD monsterID = g_attackMonster.GetCurrentMonsterId();if (monsterID != 0) { // 如果要攻击的怪物id不为0 那么就证明需要攻击// 打怪逻辑}Sleep(300); // 休息一下}return 0;
}#pragma once
#include <Windows.h>
class CAttackMonster
{
public:CAttackMonster();~CAttackMonster();// 获取当前要攻击的怪物idDWORD GetCurrentMonsterId();// 设置要攻击的怪物idBOOL SetCurrentMonsterId(DWORD monsterID);// 判断当前怪物是否死亡BOOL monsterIsDead();
private:DWORD g_attMonsterID; // 要攻击的怪物idCRITICAL_SECTION m_cs; // 临界区
};#include "pch.h"
#include "CAttackMonster.h"CAttackMonster::CAttackMonster()
{this->g_attMonsterID = 0;InitializeCriticalSection(&m_cs); // 初始化临界区
}CAttackMonster::~CAttackMonster()
{DeleteCriticalSection(&m_cs); // 销毁临界区
}DWORD CAttackMonster::GetCurrentMonsterId()
{// 对全局变量操作要加锁DWORD monsterID = 0;EnterCriticalSection(&m_cs);monsterID = this->g_attMonsterID;LeaveCriticalSection(&m_cs);return monsterID;
}BOOL CAttackMonster::SetCurrentMonsterId(DWORD monsterID)
{// 对全局变量操作要加锁EnterCriticalSection(&m_cs);this->g_attMonsterID = monsterID;LeaveCriticalSection(&m_cs);return TRUE;
}BOOL CAttackMonster::monsterIsDead()
{return TRUE;
}

自动打怪-挂接游戏主线程

自动打怪逻辑:1.遍历周围对象2.判断是否是怪物3.寻找最近的怪物4.选中怪物5.使用技能攻击以上步骤里面大量调用了游戏的功能call,如果在我们自己的dll里去调用游戏call会存在一个隐患。
我们的线程会跟游戏线程访问修改同一个内存数据,导致游戏崩溃。
【就是多线程环境,如果我们的线程在对数据进行修改的时候,游戏主线程也在修改,那么就有问题了】如何解决?
如果能让游戏线程去自己调用那么就避免了这个问题了,我们如何控制游戏去执行我们想要执行的功能call。方案1:使用定时器
方案2:使用消息钩子
方案3:设置窗口属性LONG_PTR g_preGameProc;LONG_PTR g_preGameProc;// 和之前写的区别在于,前面我们在执行的时候,游戏主线程也在执行
// 而通过窗口回调这样,我们在执行的时候相当于游戏主线程在执行,我们的逻辑执行完了它才执行
LRESULT CALLBACK GameProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam
) {switch (uMsg){case 0x400: // 吃药UseBageGood(); // 测试一下框架好用不好用SetEvent((HANDLE)lParam); // 执行完毕就可以设置为有信号状态了,那么下一个动作才能执行break;}// 我们的东西处理完了,剩下的交给游戏本来的窗口线程去进行处理return CallWindowProc((WNDPROC)g_preGameProc,hwnd,uMsg,wParam,lParam);
}// 挂接主线程
void CMainDlg::OnBnClickedCheckMsg()
{// 窗口句柄// 0046882A | 8B15 78955901            | mov edx,dword ptr ds:[1599578]          |UpdateData(TRUE); // 控件到变量DWORD hWndId = 0;__asm {mov edx, 0x1599578; // 倒一手mov edx, [edx]; // 取出里面保存的内容mov hWndId, edx;};if (this->m_setMsg.GetCheck()) {// 挂接主线程 通过设置窗口回调函数g_preGameProc = ::SetWindowLongPtrW((HWND)hWndId,GWLP_WNDPROC,(LONG_PTR)GameProc);}else {::SetWindowLongPtrW((HWND)hWndId, GWLP_WNDPROC, (LONG_PTR)g_preGameProc);}
}// 测试功能框架
void CMainDlg::OnBnClickedButtonEathp()
{DWORD hWndId = 0;__asm {mov edx, 0x1599578; // 倒一手mov edx, [edx]; // 取出里面保存的内容mov hWndId, edx;};// 消息id 要用WM_USER 开始HANDLE hEvent = CreateEventW(NULL,FALSE,FALSE,L"starHook");// Post投递过去以后就不理会了。就是不知道什么时候执行// 但是我们可能这个功能还没执行完,然后你马上又去执行下一个功能// 所以我们希望等待这个功能执行完毕再去执行下一个 所以用事件::PostMessage((HWND)hWndId,0x400,NULL,(LPARAM)hEvent);WaitForSingleObject(hEvent, 0x3000);CloseHandle(hEvent);
}那么后面我们就直接通过这个框架去实现我们的功能
不要直接点击按钮然后直接汇编代码,这样是存在问题的,因为我们和游戏主线程同时在执行。
而用这套框架,游戏主线程会帮我么执行。

自动打怪-选取最近目标

我们要选取最近的目标,肯定要有怪物的坐标和人物的坐标。
[[[[[15995E0+30]+8]+1*4+3C0] +C8]+0x10]+2A0+1C
不过我们发现我们前面分析的人物的血量是错误的,这个和人物没什么关系,只是显示的血量。不过这个显示的血量和真正的人物血量肯定是有关系的,肯定是从真正的人物血量来的。
那么我们就可以从这个角度出发。
我们设置一个硬件写入断点。00AEC253 | 897B 1C                  | mov dword ptr ds:[ebx+1C],edi           |来源
00A22BDF | 50                                 | push eax                                |
00A22BE0 | 8D8E A0020000            | lea ecx,dword ptr ds:[esi+2A0]          |
00A22BE6 | E8 A59B0C00              | call xajh.AEC790                        |发现来源eax,eax里面上面有一个call,发现执行完这个call,拿到的值就是血量,观察这个call
01163980 | 833D C8F08104 00         | cmp dword ptr ds:[481F0C8],0            |
01163987 | 74 2D                    | je xajh.11639B6                         |
01163989 | 55                            | push ebp                                |
0116398A | 8BEC                       | mov ebp,esp                             |
0116398C | 83EC 08                  | sub esp,8                               |
0116398F | 83E4 F8                   | and esp,FFFFFFF8                        |
01163992 | DD1C24                  | fstp qword ptr ss:[esp],st(0)           |
01163995 | F2:0F2C0424              | cvttsd2si eax,qword ptr ss:[esp]        |
0116399A | C9                              | leave                                   |
0116399B | C3                              | ret                                     |
发现血值来源st(0)然后st(0)来源于外层
00A22BD5 | D84424 2C                | fadd st(0),dword ptr ss:[esp+2C]        |然后esp+2C,发现是一个局部遍历,头尾断点,硬件写入。
00A22ABC | D95C24 24                | fstp dword ptr ss:[esp+24],st(0)        |00A22AB2 | D985 9F0A0000            | fld st(0),dword ptr ss:[ebp+A9F]        | ebp:0EF12810
然后发现ebp跟怪物的结构很像,那么ebp肯定就是人物的基址,然后A9F就是偏移值,人物基址+A9F拿到的就是血量地址。然后发现ebp来源eax,eax来源call。
00456780 | A1 507F5901              | mov eax,dword ptr ds:[1597F50]          |
00456785 | 85C0                     | test eax,eax                            |
00456787 | 74 0E                    | je xajh.456797                          |
00456789 | 8B40 30                  | mov eax,dword ptr ds:[eax+30]           |
0045678C | 85C0                     | test eax,eax                            |
0045678E | 74 07                    | je xajh.456797                          |
00456790 | 8B80 8C000000            | mov eax,dword ptr ds:[eax+8C]           |
00456796 | C3                       | ret                                     |
00456797 | 33C0                       | xor eax,eax                             |
00456799 | C3                          | ret                                     |即通过这个call就可以拿到人物基址了
00A22850 | E8 EBDFE1FF              | call xajh.840840                        |这个call不需要函数。

选取最近目标-2

// 选取最近的怪物
BOOL choseMinSpaceMonster()
{DWORD roleBaseAddr = 0;__asm {mov eax, 0x00840840;call eax;mov roleBaseAddr, eax; // 保存人物基址};// 通过偏移取出人物的X和Y坐标float roleX = *(float*)(roleBaseAddr + 0x3C);float roleY = *(float*)(roleBaseAddr + 0x44);DWORD objectBeginAddr = 0;DWORD arrLen = 0;__asm {pushad;pushf;mov eax, 0x00456760;call eax;mov eax, [eax + 0x74];// 这样拿到了数组那个结构的地址 然后 +1C 首地址 +20结束 24大小lea ebx, objectBeginAddr; // 拿到变量地址mov edx, [eax + 0x1C]; // +0x1C的地址里面的内容保存的就是首地址mov dword ptr[ebx], edx; // 把首地址给变量lea ebx, arrLen; // 拿到变量地址mov edx, [eax + 0x24]; // +0x24的地址里面的内容保存的就是长度mov dword ptr[ebx], edx; // 把长度给变量popf;popad;};// 遍历CString str;float minSpace = 10000.00; // 初始距离最远DWORD minSpaceIdL; // 最近的NpcDWORD minSpaceIdH; // 最近的Npcfor (int i = 0; i < arrLen; i++) {DWORD npcStruct = *(DWORD*)(objectBeginAddr + i * 4); // 取出来了数组第i个元素的内容if (npcStruct == 0) {continue;}DWORD npcRealStruct = *(DWORD*)(npcStruct + 0x4); // NPC结构体指针 首地址DWORD npcId = *(DWORD*)(npcRealStruct + 0x178);DWORD npcIdH = *(DWORD*)(npcRealStruct + 0x17C);// 取出怪物的X和怪物的Yfloat monsterX = *(float*)(npcRealStruct + 0x3C);float monsterY = *(float*)(npcRealStruct + 0x44);// 比对NPC和玩家的坐标//  = 根号下x^2 + y^2 三角函数float roleSpace = sqrt((monsterX - roleX)* (monsterX - roleX) + (monsterY - roleY) * (monsterY - roleY));if (roleSpace < minSpace){minSpace = roleSpace;minSpaceIdL = npcId;minSpaceIdH = npcIdH;}}// 循环结束我们就拿到了最近的NPC了choseObject(minSpaceIdL, minSpaceIdH); // 选取最近的NPCreturn TRUE;
}//  选择指定对象
BOOL choseObject(DWORD objIdL, DWORD objIdH)
{__asm {push objIdH;push objIdL;mov eax, 0x00CB6A10;call eax;add esp, 0x8;};return TRUE;
}

自动打怪-筛选出怪物

// 选取最近的怪物
BOOL choseMinSpaceMonster()
{DWORD roleBaseAddr = 0;__asm {mov eax, 0x00840840;call eax;mov roleBaseAddr, eax; // 保存人物基址};// 通过偏移取出人物的X和Y坐标float roleX = *(float*)(roleBaseAddr + 0x3C);float roleY = *(float*)(roleBaseAddr + 0x44);DWORD objectBeginAddr = 0;DWORD arrLen = 0;DWORD64 minSpaceID64 = 0;__asm {pushad;pushf;mov eax, 0x00456760;call eax;mov eax, [eax + 0x74];// 这样拿到了数组那个结构的地址 然后 +1C 首地址 +20结束 24大小lea ebx, objectBeginAddr; // 拿到变量地址mov edx, [eax + 0x1C]; // +0x1C的地址里面的内容保存的就是首地址mov dword ptr[ebx], edx; // 把首地址给变量lea ebx, arrLen; // 拿到变量地址mov edx, [eax + 0x24]; // +0x24的地址里面的内容保存的就是长度mov dword ptr[ebx], edx; // 把长度给变量popf;popad;};// 遍历CString str;float minSpace = 10000.00; // 初始距离最远DWORD minSpaceIdL; // 最近的NpcDWORD minSpaceIdH; // 最近的Npcfor (int i = 0; i < arrLen; i++) {DWORD npcStruct = *(DWORD*)(objectBeginAddr + i * 4); // 取出来了数组第i个元素的内容if (npcStruct == 0) {continue;}DWORD npcRealStruct = *(DWORD*)(npcStruct + 0x4); // NPC结构体指针 首地址DWORD npcId = *(DWORD*)(npcRealStruct + 0x178);DWORD npcIdH = *(DWORD*)(npcRealStruct + 0x17C);DWORD npcId64 = *(DWORD*)(npcRealStruct + 0x178); // 直接取8个字节// 取出怪物的X和怪物的Yfloat monsterX = *(float*)(npcRealStruct + 0x3C);float monsterY = *(float*)(npcRealStruct + 0x44);// 比对NPC和玩家的坐标//  = 根号下x^2 + y^2 三角函数float roleSpace = sqrt((monsterX - roleX)* (monsterX - roleX) + (monsterY - roleY) * (monsterY - roleY));if (roleSpace < minSpace){// 通过当前的NPCID 和 NPC的首地址 判断这是不是怪物DWORD objType = queryObjectType(npcId64, npcRealStruct);if (objType != 9) {continue; // 如果不是怪物 我们不去替换最近的目标 我们只选中怪物}minSpace = roleSpace;minSpaceIdL = npcId;minSpaceIdH = npcIdH;minSpaceID64 = npcId64; }}// 循环结束我们就拿到了最近的NPC了choseObject(minSpaceIdL, minSpaceIdH); // 选取最近的NPCreturn TRUE;
}//  选择指定对象
BOOL choseObject(DWORD objIdL, DWORD objIdH)
{__asm {push objIdH;push objIdL;mov eax, 0x00CB6A10;call eax;add esp, 0x8;};return TRUE;
}// 根据id判断对象类型
DWORD queryObjectType(DWORD64 objID,DWORD npcRealStruct)
{DWORD getObjectTypeArg1 = 0; // 固定0DWORD getObjectTypeArg2 = 0; // 输出参数DWORD objectType = 0;// [[[[0x15995E0+30] +94]+4] +8C]+1F54DWORD***** objBaseAddr = (DWORD*****)0x1599610;DWORD* objArr = (DWORD*)objBaseAddr[0][0x94 / 4][0x4 / 4][0x8C / 4];// 最后+1F54这个地址里面保存的才是ecxDWORD realObjArr = *(DWORD*)((DWORD)objArr + 0x1F54);char* idstr = (char*)&objID;DWORD npcIdH = 0;DWORD npcId = 0;memcpy(&npcId, idstr, 4);idstr += 4;memcpy(&npcIdH, idstr, 4);__asm {pushad;pushf;lea eax, getObjectTypeArg1;push eax; // 固定0lea eax, getObjectTypeArg2;push eax; // 输出参数push npcRealStruct; // 对象地址push npcIdH; // 高位push npcId; // 低位mov ecx, realObjArr;mov eax, 0x006B4480;call eax;mov objectType, eax;popf;popad;};return objectType;
}

自动打怪-使用技能自动攻击

//  选择指定对象
//  选择指定对象
BOOL choseObject(DWORD64 objID)
{char* idstr = (char*)&objID;DWORD npcIdH = 0;DWORD npcId = 0;memcpy(&npcId, idstr, 4);idstr += 4;memcpy(&npcIdH, idstr, 4);__asm {push npcIdH;push npcId;mov eax, 0x00CB6A10;call eax;add esp, 0x8;};// 选中完设置要攻击它g_attackMonster.SetCurrentMonsterId(objID);return TRUE;
}// 使用技能
BOOL userSkill(DWORD skillID)
{/*0019EE58中的东西【固定的】:00000002  00000001  010100FF  00000001  但是这些数据为空也可以使用*/char* arg1 = new char[12]{};__asm {mov eax,0x00456780;call eax;mov ecx, [eax + 0x1F58]; // 就拿到ecx了push 1push 0push 1push 0push 0push 0push 0push arg1push 1push 0x948E   // 技能的id 0000948F 挨着的每个技能相差1,0x948E  是第一个技能的开始位置push 1mov eax, 0x006CB0C0call eax};delete[] arg1;return 0;
}#include "pch.h"
#include "autoAttachThread.h"
#include "gamecall.h"// 封装了自动打怪的操作 以及多线程的处理
CAttackMonster g_attackMonster;DWORD WINAPI AutoAttachMonster(LPVOID args) {while (1) {DWORD monsterID = g_attackMonster.GetCurrentMonsterId();if (monsterID != 0) { // 如果要攻击的怪物id不为0 那么就证明需要攻击// 打怪逻辑userSkill(0);}Sleep(300); // 休息一下}return 0;
}接下来我们就要实现自动寻路,还有技能冷却判断那些,还有技能数组,现在就只能释放固定一个技能。
还有判断怪物是否死亡等等。即逻辑应该是:-如果怪物id为空,我们就选择最近的怪物-然后释放技能,如果当前技能是冷却的,那么就换一个技能-如果怪物死亡了,那么我们就设置怪物id为空

自动寻路

// 坐标结构
typedef struct _COORDINATES {float x;float z;float y;
}COORDINATES,*PCOORDINATES;// 自动寻路代码实现
BOOL autoFindWay(DWORD mapId, COORDINATES coordinates)
{/*fld st(0),dword ptr ds:[12F6B3C]        | 固定的地址push 0                                  |push 1                                  |push ecx                                |fstp dword ptr ss:[esp],st(0)           | ecx会被st0覆盖 【覆盖为:3F4CCCCD】push 0                                  |lea ecx,dword ptr ss:[esp+24]           | 局部变量坐标push ecx                                | 真正的坐标mov ecx,dword ptr ds:[eax+2B4]          |push edx                                | 固定值48,跟地图相关push 0                                  |push 0                                  |push 0                                  |push 0                                  |call xajh.6A3DE0                        | 是你了 自动寻路call*/__asm {push 0;push 1;mov eax, 0x12F6B3C;mov eax, [eax]; // 倒一手push eax;push 0;lea eax, coordinates;push eax; // 坐标信息push 0x48; // 地图id 这里暂时写死push 0;push 0;push 0;push 0;mov eax, 0x00456780;call eax;  // 拿到eaxmov ecx, [eax + 0x2B4]; // 拿到真正的eaxmov eax, 0x006A3DE0;call eax; // 自动寻路};return TRUE;
}对代码数据如何管理?不然游戏一更新我们就要改很多地方。
我们通过宏来管理,到时候我们就只需要把宏进行修改就好了,其他代码地方不动#pragma once
// 自动寻路ecx
#define GAME_FINDWAY_ECX_CALL 0x00456780
#define GAME_FINDWAY_ECX_OFFSET1 0x2B4
#define GAME_FINDWAY_CALL 0x006A3DE0
#define GAME_FINDWAY_ARG1 0x12F6B3C// 自动寻路代码实现
BOOL autoFindWay(DWORD mapId, COORDINATES coordinates)
{/*fld st(0),dword ptr ds:[12F6B3C]        | 固定的地址push 0                                  |push 1                                  |push ecx                                |fstp dword ptr ss:[esp],st(0)           | ecx会被st0覆盖 【覆盖为:3F4CCCCD】push 0                                  |lea ecx,dword ptr ss:[esp+24]           | 局部变量坐标push ecx                                | 真正的坐标mov ecx,dword ptr ds:[eax+2B4]          |push edx                                | 固定值48,跟地图相关push 0                                  |push 0                                  |push 0                                  |push 0                                  |call xajh.6A3DE0                        | 是你了 自动寻路call*/__asm {push 0;push 1;mov eax, GAME_FINDWAY_ARG1;mov eax, [eax]; // 倒一手push eax;push 0;lea eax, coordinates;push eax; // 坐标信息push mapId; // 地图id push 0;push 0;push 0;push 0;mov eax, GAME_FINDWAY_ECX_CALL;call eax;  // 拿到eaxmov ecx, [eax + GAME_FINDWAY_ECX_OFFSET1]; // 拿到真正的eaxmov eax, GAME_FINDWAY_CALL;call eax; // 自动寻路};return TRUE;
}

掉落物品分析

掉落物品分析:
思路:可以通过名称搜索,也可以通过鼠标的小手状态或者通过拾取物品这样肯定会调用功能call。然后进行分析。通过send下断点,然后找到一个只会断下来一次的物品call。
00698CB3 | 56                           | push esi                                | 物品的id
00698CB4 | 8BCF                        | mov ecx,edi                             |
00698CB6 | E8 05EDFFFF              | call xajh.6979C0                        | 拾取物品~5那么我们要寻找esi的来源
00698C91 | 8B73 08                  | mov esi,dword ptr ds:[ebx+8]            | ebx:470A2330然后我们看ebx+8里面这个物品id的来源。0060DDC7 | 8978 08                  | mov dword ptr ds:[eax+8],edi            |  edi:66E92C28
发现是edi往ebx+8里面写入物品id的。那么我们就要追踪edi的来源。0060DDB1 | 8B7C24 08                | mov edi,dword ptr ss:[esp+8]            |esp+8是上一个函数的第一个参数006B81CD | 50                       | push eax                                |
006B81CE | E8 DD5BF5FF              | call xajh.60DDB0                        |来源eax。发现eax来源这个call
006B8029 | E8 84B7AA00              | call <JMP.&??2@YAPAXI@Z>                |
但是通过观察这个call是申请一块内存空间,这个时候还没有写入数据的。我们看看哪里写入的数据。
006B81C5 | 8918                     | mov dword ptr ds:[eax],ebx              | 物品id来源
006B81C7 | 8950 04                  | mov dword ptr ds:[eax+4],edx            |
然后发现执行了这行,地址里面就保存着物品id了。
edx保存着高位,ebx保存着低位。然后我们现在就找ebx或者edx的来源,找哪个都无所谓,他们都是一样的,都是物品Id。006B7D99 | 8B9E 78010000            | mov ebx,dword ptr ds:[esi+178]          |  esi:6719AF18那么esi肯定就是物品的一个基址,因为人物id也是+178,很像。006B7D78 | 8BB424 C4000000          | mov esi,dword ptr ss:[esp+C4]           |
然后看见esp可能是局部变量或者参数了。但是这个时候我们已经到了函数头部,然后通过观察是上一个函数的第四个参数。006B8764 | 8B4424 14                | mov eax,dword ptr ss:[esp+14]           |
006B8768 | 8B34B8                   | mov esi,dword ptr ds:[eax+edi*4]        |
006B876B | 8B5424 78                | mov edx,dword ptr ss:[esp+78]           |
006B876F | 8D4C24 28                | lea ecx,dword ptr ss:[esp+28]           |
006B8773 | 51                       | push ecx                                |
006B8774 | 55                       | push ebp                                |
006B8775 | 33C0                     | xor eax,eax                             |
006B8777 | 56                       | push esi                                |
006B8778 | 52                       | push edx                                |
006B8779 | 894424 34                | mov dword ptr ss:[esp+34],eax           |
006B877D | 50                       | push eax                                |
006B877E | 894424 3C                | mov dword ptr ss:[esp+3C],eax           |
006B8782 | 894424 40                | mov dword ptr ss:[esp+40],eax           |
006B8786 | 894424 44                | mov dword ptr ss:[esp+44],eax           |
006B878A | 8D4424 24                | lea eax,dword ptr ss:[esp+24]           |
006B878E | 50                       | push eax                                |
006B878F | 8BCB                     | mov ecx,ebx                             |
006B8791 | 897424 3C                | mov dword ptr ss:[esp+3C],esi           |
006B8795 | C74424 28 03000000       | mov dword ptr ss:[esp+28],3             |
006B879D | E8 CEF5FFFF              | call xajh.6B7D70                        |那么就是来源esi。
006B8764 | 8B4424 14                | mov eax,dword ptr ss:[esp+14]           |
006B8768 | 8B34B8                   | mov esi,dword ptr ds:[eax+edi*4]        |然后我们发现了一个数组遍历的形式。eax很可能就是物品数组的首地址。然后edi*4取出来每一个物品对象。然后+178是物品id。
0019E9EC  72462328
0019E9F0  72462334  "l_id"
0019E9F4  00000005
0019E9F8  00000003
观察内存,很像vector,数组的首地址,数组的结束地址,最大容量,当前的元素个数。006B870B | 51                       | push ecx                                |
006B870C | 6A 01                    | push 1                                  |
006B870E | 51                       | push ecx                                |
006B870F | 8BCF                     | mov ecx,edi                             |
006B8711 | D91C24                   | fstp dword ptr ss:[esp],st(0)           |
006B8714 | E8 37FEECFF              | call xajh.588550                        |发现调用完这个call,局部变量中就填充了数组的首地址,结束地址数量那些,并且数组中的每一个元素也都存在了。
那么接下来我们就要分析这个call了。005887E2 | 50                            | push eax                                |
005887E3 | 8BCB                          | mov ecx,ebx                             |
005887E5 | E8 76ADFFFF              | call xajh.583560                        | 调用完这个call数组就填充完毕了
然后发现这个call内部的这个call调用完毕,数组的地址那些就填充了,然后发现会多次调用,每次调用会往数组里面写入一个值。
那么就是说这个类似vector的push操作。然后我们发现eax这个局部变量的地址中保存的就是要push进入的物品的地址。
那么eax肯定是从真正的数组中遍历取出来的一个物品。
那么我们就要看eax是从哪里来的。这样就可以找到真正的数组了。005887DE | 8D4424 14                | lea eax,dword ptr ss:[esp+14]           | 物品的来源
来源esp+14然后通过观察内存,应该是一个局部变量。然后这里是一个循环,我们走走,看看是哪里改变的。0058863F | 8B30                         | mov esi,dword ptr ds:[eax]              |
00588641 | 897424 14                | mov dword ptr ss:[esp+14],esi           |然后找eax的来源我们要。005887DE | 8D4424 14                | lea eax,dword ptr ss:[esp+14]           | 物品的来源
005887E2 | 50                       | push eax                                |
005887E3 | 8BCB                     | mov ecx,ebx                             |
005887E5 | E8 76ADFFFF              | call xajh.583560                        | 调用完这个call数组就填充完毕了
005887EA | 8D4C24 24                | lea ecx,dword ptr ss:[esp+24]           |
005887EE | E8 7D530000              | call xajh.58DB70                        | 该百年esp+2C
005887F3 | 8B6C24 2C                | mov ebp,dword ptr ss:[esp+2C]           | ebp改变1
005887F7 | 8B7C24 28                | mov edi,dword ptr ss:[esp+28]           |
005887FB | 8B4424 24                | mov eax,dword ptr ss:[esp+24]           |
005887FF | 8B7424 18                | mov esi,dword ptr ss:[esp+18]           |然后追踪到一个函数里面。。我们惊奇发现 这个函数我们分析过
0058DB70 | 8BC1                     | mov eax,ecx                             | 结构体的首地址,一共12字节
0058DB72 | 8378 04 00               | cmp dword ptr ds:[eax+4],0              | 判断是否进行遍历,eax+4 存放的是对象集合的首地址
0058DB76 | 74 47                    | je xajh.58DBBF                          |
0058DB78 | 57                       | push edi                                | 保存寄存器环境
0058DB79 | 8DA424 00000000          | lea esp,dword ptr ss:[esp]              | push了,把esp地址给esp地址,不变
0058DB80 | 8B48 08                  | mov ecx,dword ptr ds:[eax+8]            |
0058DB83 | 85C9                     | test ecx,ecx                            | 判断结构体第三个属性是否为0
0058DB85 | 75 23                    | jne xajh.58DBAA                         |
0058DB87 | 8340 04 04               | add dword ptr ds:[eax+4],4              | 取集合的下一个地址
0058DB8B | 8B08                     | mov ecx,dword ptr ds:[eax]              |
0058DB8D | 8B79 14                  | mov edi,dword ptr ds:[ecx+14]           | 数组的长度
0058DB90 | 8B50 04                  | mov edx,dword ptr ds:[eax+4]            | eax+4是当前正在遍历的对象集合地址
0058DB93 | 83C1 08                  | add ecx,8                               |
0058DB96 | 8B09                     | mov ecx,dword ptr ds:[ecx]              |
0058DB98 | 8D0CB9                   | lea ecx,dword ptr ds:[ecx+edi*4]        | 这个地方很明显看出来,这是数组的结束地址
0058DB9B | 3BD1                     | cmp edx,ecx                             | 是否遍历结束了
0058DB9D | 74 18                    | je xajh.58DBB7                          |
0058DB9F | 8B12                     | mov edx,dword ptr ds:[edx]              |
0058DBA1 | 8950 08                  | mov dword ptr ds:[eax+8],edx            |
0058DBA4 | 85D2                     | test edx,edx                            |
0058DBA6 | 75 16                    | jne xajh.58DBBE                         |
0058DBA8 | EB 05                    | jmp xajh.58DBAF                         |
0058DBAA | 8B11                     | mov edx,dword ptr ds:[ecx]              | 取出来对象集合的x元素
0058DBAC | 8950 08                  | mov dword ptr ds:[eax+8],edx            | [eax+8]==[esp+20]
0058DBAF | 8378 08 00               | cmp dword ptr ds:[eax+8],0              | 判断集合的xx元素是否为0这个函数就是在遍历我们的大数组,然后取出来每一个对象push,push,push.....
然后这个函数只接收了一个参数ecx,然后我们发现遍历也是从ecx中进行的遍历。也就是ecx就是我们的物品的大数组结构。

然后其实我们前面分析周围对象这个大数组的结构的时候分析错了。
因为这样其实很麻烦。
它其实是通过ID然后进行哈希运算,然后再根据哈希运算出来的下标。将对象地址存在长度0x301的数组里面。然后中间哪个小方块,+0的位置如果有值是存储类似的一个小方块,是用来解决哈希冲突的。所以我们之前遍历也有问题,我们应该在遍历每一个元素的时候的时候,要先判断+0的位置有没有值,如果有值的话就表示它有冲突。
这个位置还要下一个元素,那么我们就要跟遍历链表一样来进行遍历它。追踪ecx大数组来源:
005885E5 | 894424 24                | mov dword ptr ss:[esp+24],eax           |005885B3 | 8D77 14                  | lea esi,dword ptr ds:[edi+14]           |00588560 | 8BF9                     | mov edi,ecx                             |006B89BD | 8B48 78                  | mov ecx,dword ptr ds:[eax+78]           |006B89B2 | E8 A9DDD9FF              | call xajh.456760                        |
然后发现eax来源这个call,而且这个call不需要任何参数。就是通过这个call就可以拿到eax,eax+78我们就可以拿到大数组。大数组+1C就是遍历的首地址。【+14 再+8也一样】// 遍历地上物品
void CMainDlg::OnBnClickedButtonFloor()
{DWORD goodAddr = 0;DWORD goodArrLen = 0;__asm {mov eax,0x00456760;call eax; // +0x78地址内容是大数组首地址 mov eax, [eax + 0x78];lea edi, [eax + 0x14];mov eax, [edi + 0x8];mov goodAddr, eax; // 起始地址mov eax, [edi + 0x14];mov goodArrLen, eax; // 长度};CString str;// 就可以遍历我们的数组了 for (DWORD i = 0; i < goodArrLen; i++) {// 取出大数组中每一个元素,这个元素地址指向的内容才是我们要的DWORD linkDataAddr = *(DWORD*)(goodAddr + i * 4);if (linkDataAddr == 0) {continue;}DWORD linkData = 0;do {// +0的位置表示是否有哈希冲突 即下一个链表节点linkData = *(DWORD*)(linkDataAddr + 0);DWORD goodsDataAddr = *(DWORD*)(linkDataAddr + 4); // 真正的物品结构的地址DWORD goodsId = *(DWORD*)(goodsDataAddr + 0x178);DWORD goodsIdH = *(DWORD*)(goodsDataAddr + 0x17C);DWORD virtualFunTable = ((DWORD*)goodsDataAddr)[0]; // 获取虚函数表DWORD getNameFunc = *(DWORD*)(virtualFunTable + 0x88); // 这里就是拿到获取名称的虚函数LPWSTR goodsName = L"";__asm {pushad;pushf;mov ecx, goodsDataAddr; // 传递this指针mov eax, getNameFunc;call eax; // 获取名字mov goodsName, eax; // 然后把名字地址保存popf;popad;}if (linkData != 0) {linkDataAddr = *(DWORD*)(linkDataAddr + 0);}str.Format(L"物品ID为:%x\t物品名称为:%ls\r\n", goodsId,goodsName);this->m_floor.Append(str);}while(linkData != 0);}UpdateData(FALSE); // 变量到控件
}

任务列表分析

任务列表分析:
入手点:可以搜索任务名称,也可以搜索任务数据。然后我们通过CE工具搜索人物名称,找到了名称的地址:3D3C6DA8然后我们就可以用xdbg下一个硬件访问断点,看看谁那里访问了这个地址的数据。005D98AB | 8B35 4CA25701            | mov esi,dword ptr ds:[157A24C]          |
005D98B1 | 897424 04                | mov dword ptr ss:[esp+4],esi            | [esp+4]:L"<%s>%s"
005D98B5 | 83C1 08                  | add ecx,8                               |
005D98B8 | 51                       | push ecx                                |
005D98B9 | 50                       | push eax                                |
005D98BA | 8D4424 0C                | lea eax,dword ptr ss:[esp+C]            |
005D98BE | 68 E8023101              | push xajh.13102E8                       | 13102E8:L"<%s>%s"
005D98C3 | 50                       | push eax                                |
005D98C4 | 895424 20                | mov dword ptr ss:[esp+20],edx           |
005D98C8 | E8 D3E69800              | call xajh.F67FA0                        |eax是任务尖括号里面的文本,ecx是后面的文本。我们追踪其中一个的来源就好。而且两个其实都来源ecx。005D98B5 | 83C1 08                  | add ecx,8                               |005D989B | 8B81 CC0A0000            | mov eax,dword ptr ds:[ecx+ACC]          |偏移不同罢了。+8是后面那个,+ACC地址里面的内容是尖括号的。005DD04B | 8BCE                     | mov ecx,esi                             |
ecx来源上层esi。005DCE47 | 8B5D 0C                  | mov ebx,dword ptr ss:[ebp+C]            |
005DCE4A | 53                       | push ebx                                |  任务ID
005DCE4B | 8BCE                     | mov ecx,esi                             |
005DCE4D | E8 DEFE6800              | call xajh.C6CD30                        |  通过任务ID获取任务结构
005DCE52 | 8BF0                     | mov esi,eax                             |然后发现来源eax,eax来源这个call,这个call接受一个参数ebx。
然后通过内存观察,这个calll返回的是任务的结构。但是我们想要找的是任务的数组,所以我们没有必要分析这个call。我们的重点应该是ebx,ebx是在不停的变化的,那么肯定有一个循环,在遍历所有的任务id,然后通过每一个任务id取出每一个任务的信息。我们就是要找找被遍历的这个数组。005DCE47 | 8B5D 0C                  | mov ebx,dword ptr ss:[ebp+C]            |然后发现ebx来源ebp+c,那么也就是说可能是局部变量或者参数,然后通过观察,发现应该是上一个函数的第二个参数。005E86F0 | 8B8424 40030000          | mov eax,dword ptr ss:[esp+340]          |
005E86F7 | 8B4F 10                  | mov ecx,dword ptr ds:[edi+10]           |
005E86FA | 8B17                     | mov edx,dword ptr ds:[edi]              |
005E86FC | 50                       | push eax                                |
005E86FD | 6A 01                    | push 1                                  |
005E86FF | 41                       | inc ecx                                 |
005E8700 | 51                       | push ecx                                |
005E8701 | 52                       | push edx                                |
005E8702 | 8D4424 5C                | lea eax,dword ptr ss:[esp+5C]           |
005E8706 | 50                       | push eax                                |
005E8707 | C68424 44030000 02       | mov byte ptr ss:[esp+344],2             |
005E870F | E8 BC46FFFF              | call xajh.5DCDD0                        |第二个参数那么就是来源edx。005E86FA | 8B17                     | mov edx,dword ptr ds:[edi]              | edi:6BEF53AC然后edi来源:
005E860D | 8BBC24 34030000          | mov edi,dword ptr ss:[esp+334]          |看见esp猜测是局部变量或者函数参数,然后发现是上一个函数的第二个参数。00AC3263 | 884424 1C                | mov byte ptr ss:[esp+1C],al             |
00AC3267 | 8B5424 1C                | mov edx,dword ptr ss:[esp+1C]           |
00AC326B | 52                       | push edx                                |
00AC326C | 8D8424 B8000000          | lea eax,dword ptr ss:[esp+B8]           |
00AC3273 | 56                       | push esi                                |
00AC3274 | 50                       | push eax                                |
00AC3275 | E8 7653B2FF              | call xajh.5E85F0                        |那么就是来源esi。然后发现esi来源eax,eax来源这个call,这个call有一个参数。
00AC3215 | 8938                     | mov dword ptr ds:[eax],edi              |
00AC3217 | 8D4C24 10                | lea ecx,dword ptr ss:[esp+10]           |
00AC321B | 51                       | push ecx                                |
00AC321C | B9 B8025B02              | mov ecx,xajh.25B02B8                    |
00AC3221 | E8 9A4EB2FF              | call xajh.5E80C0                        |就是通过这个call,返回eax,eax里面保存的就是任务id。但是我们发现这个call接受的这个参数ecx也是一个任务的id。
那么我们也没必要分析这个call继续寻找ecx的来源。00AC3217 | 8D4C24 10                | lea ecx,dword ptr ss:[esp+10]           |
然后通过观察内存,这个应该是一个局部变量。00AC3111 | 897C24 18                | mov dword ptr ss:[esp+18],edi           |
然后发现来源edi,edi是任务的id。追踪edi的来源。00AC30EF | 8B3C88                   | mov edi,dword ptr ds:[eax+ecx*4]        |
发现edi来源这个。那么eax肯定就是任务id的数组的了。
通过这个数组每次取出来一个任务id。而有了任务id我们就可以通过前面找到的call,拿到任务的结构,那么就可以拿到任务的信息,名字那些了。00AC30E7 | 8B4424 30                | mov eax,dword ptr ss:[esp+30]           |
通过观察esp+0x30应该是一个局部变量。
0019EC68  6BD55048
0019EC6C  6BD55050
0019EC70  00000005
0019EC74  00000002
而且很熟悉,这个结构我们前面分析过多次,数组的首地址,数组的结束地址,总的容量,当前使用容量。00AC30A3 | 83C4 04                  | add esp,4                               |
00AC30A6 | 84C0                     | test al,al                              |
00AC30A8 | 74 16                    | je xajh.AC30C0                          |
00AC30AA | 8B4424 18                | mov eax,dword ptr ss:[esp+18]           |
00AC30AE | 8D4C24 18                | lea ecx,dword ptr ss:[esp+18]           |
00AC30B2 | 51                       | push ecx                                |
00AC30B3 | 8D4C24 34                | lea ecx,dword ptr ss:[esp+34]           |
00AC30B7 | 894424 1C                | mov dword ptr ss:[esp+1C],eax           |
00AC30BB | E8 A004ACFF              | call xajh.583560                        | 这个call应该就是push
然后我们发现每次执行完这个call,当前使用容量就增加1,所以我们猜测这个函数和我们之前分析的类似是一个push函数。这个函数接受一个参数是ecx。ecx是任务的id,那么我们就要找ecx的来源,发现来源esp+18.
然后观察内存,发现是一个局部变量。00AC309A | 894424 1C                | mov dword ptr ss:[esp+1C],eax           |
然后发现来源eax,那么我们追踪eax的来源。00AC3090 | 8B15 081E5D02            | mov edx,dword ptr ds:[25D1E08]          |
00AC3096 | 8B04B2                   | mov eax,dword ptr ds:[edx+esi*4]        |那么就找到头了,eax是来源[edx+esi*4],那么edx就是技能id的数组了,然后我们追踪edx发现直接找到了基址。
那么我们就可以自己遍历这个技能id了,然后通过上面的函数拿到技能的信息。但是我们还少了一点,我们还要知道这个技能id数组的长度。
esi是索引,所以它肯定有判断esi跟某个值比较。00AC30C1 | 3BF5                     | cmp esi,ebp                             |
那么我们就找ebp的来源。00AC3063 | 8B2D 141E5D02            | mov ebp,dword ptr ds:[25D1E14]          |也一下找到了基址,那么有了数组,有了长度,遍历自然也就不是事了。// 遍历任务列表
void CMainDlg::OnBnClickedButtonWorks()
{/*mov eax,dword ptr ds:[1597F50]         |mov esi,dword ptr ds:[eax+248]         |mov ebx,dword ptr ss:[ebp+C]            |push ebx                                |  任务IDmov ecx,esi                             |call 0x00C6CD30                         |  通过任务ID获取任务结构*/DWORD taskListAddr = *(DWORD*)0x25D1E08;DWORD taskListLen = *(DWORD*)0x25D1E14;LPCWSTR taskName = L"";for (int i = 0; i < taskListLen; i++) {DWORD taskID = *(DWORD*)(taskListAddr + i * 4);__asm {mov eax, 0x1597F50;mov eax, [eax]; // 倒一手mov esi, [eax + 0x248];mov ecx, esi; push taskID;mov eax, 0x00C6CD30;call eax; // 这样就拿到了任务结构// +8是后面那个文本,+ACC地址里面的内容是尖括号的。add eax, 0x8;mov taskName, eax;};this->m_works.Append(taskName);this->m_works.Append(L"\r\n");}UpdateData(FALSE); // 变量到控件
}

可接任务列表分析

可接任务列表分析:
一样我们通过搜索任务名称来下断点追踪。
44970A10找到了,那么现在就通过xdbg下一个硬件访问断点追踪。然后我们发现可接任务和已接任务调用的是同一个函数。
005D98AB | 8B35 4CA25701            | mov esi,dword ptr ds:[157A24C]          |
005D98B1 | 897424 04                | mov dword ptr ss:[esp+4],esi            |
005D98B5 | 83C1 08                  | add ecx,8                               |
005D98B8 | 51                       | push ecx                                | 任务
005D98B9 | 50                       | push eax                                |
005D98BA | 8D4424 0C                | lea eax,dword ptr ss:[esp+C]            |
005D98BE | 68 E8023101              | push xajh.13102E8                       | 13102E8:L"<%s>%s"
005D98C3 | 50                       | push eax                                |
005D98C4 | 895424 20                | mov dword ptr ss:[esp+20],edx           |
005D98C8 | E8 D3E69800              | call xajh.F67FA0                        |向上追踪
00BF1449 | 52                       | push edx                                | edx是任务结构00BF1440 | 8B14B1                   | mov edx,dword ptr ds:[ecx+esi*4]        |
然后发现edx来源这个结构,通过观察内存,ecx就是保存着所有任务结构的数组。00BF13E0 | 8B4C24 44                | mov ecx,dword ptr ss:[esp+44]           |
通过观察是局部变量。00BF13D7 | E8 04BD0700              | call xajh.C6D0E0                        |
然后发现这个call调用结束以后,esp+44的位置就保存了任务结构数组的地址。然后发现是内部还有一个call,是内部这个call执行完毕,任务结构数组的地址就写入进去了。
00C6D0F6 | E8 F5F8FFFF              | call xajh.C6C9F0                        |
不过调用完这个call,虽然把地址写了进去,不过地址指向的位置还没有内容。所以这两个call,应该是开辟一个新的数组,然后把任务结构从真正的数组拷贝过来。那么我们就要看它是从哪里来的。我们下一个硬件写入断点。
007A760D | 8910                     | mov dword ptr ds:[eax],edx              |然后发现来源edx。追踪edx。
007A7607 | 8B5424 10                | mov edx,dword ptr ss:[esp+10]           |
007A760B | 8B12                        | mov edx,dword ptr ds:[edx]              |看见esp,观察内存,然后发现是上一个函数的第一个参数。00C6D1EA | 8D4424 20                | lea eax,dword ptr ss:[esp+20]           |
00C6D1EE | 50                              | push eax                                |
00C6D1EF | E8 ECA3B3FF              | call xajh.7A75E0                        |然后发现eax的确是我们的任务结构。也就是填充进入临时任务结构数组的具体任务。那么我们要看eax从哪里来的。
发现从esp+20来的,猜测局部变量或者函数参数。
然后这里是一个循环,所以不太可能是函数参数,我们走一遍。00C6D138 | 897424 20                | mov dword ptr ss:[esp+20],esi           |发现来源esi。
00C6D135 | 8B73 0C                  | mov esi,dword ptr ds:[ebx+C]            |然后发现ebx来源ecx
00C6D101 | 8B19                     | mov ebx,dword ptr ds:[ecx]              |
不过ecx是在循环外面的,而且ecx是不变的,我们继续向下看循环里面00C6D1B8 | 8B1B                     | mov ebx,dword ptr ds:[ebx]              |
然后发现ebx又来源ebx,可能是一个链表结构。0的便宜是下一个元素。那么我们现在要找的就是ecx的来源。
00C6D0FB | 8B4E 1C                  | mov ecx,dword ptr ds:[esi+1C]           |然后追踪esi。
00C6D0E7 | 8BF1                     | mov esi,ecx                             |00BF13CD | 8B4C24 24                | mov ecx,dword ptr ss:[esp+24]           |然后通过ce搜索,ecx。。发现直接拿到了基址
01599828,拿到地址,取ecx+1c里面的内容,再取+C里面的内容,就是我们的任务。不过我们现在拿到的全部任务的列表,我们还需要筛选出来可接的任务列表。00BF13CB | 33F6                 | xor esi,esi                            |
00BF13CD | 8B4C24 24            | mov ecx,dword ptr ss:[esp+24]          | ecx全部任务的链表结构
00BF13D1 | 8D5424 38            | lea edx,dword ptr ss:[esp+38]          |
00BF13D5 | 52                   | push edx                               |
00BF13D6 | 53                   | push ebx                               |
00BF13D7 | E8 04BD0700          | call xajh.C6D0E0                       | <<拿到申请出来的可接任务数组那么我们现在要分析edx和ebx。00BF12FF | 8B58 30              | mov ebx,dword ptr ds:[eax+30]          |ebx来源eax+30地址中的内容。
00BF12FA | E8 A15486FF          | call 0x004567A0                       |然后发现来源这个call,这个call不需要任何参数。
所以通过这个call,拿到eax,然后再+30取内容就拿到了ebx。然后看edx,是一个局部变量,应该是一个结构体变量,然后是一个输出参数。
那么我们只要知道他大小就好了。通过硬件访问我们就知道它大小了。4 * 6 = 24
所以这个结构体大小是24.那么我们就可以通过这个call。拿到我们的列表了。我们现在通过另一种方式来分析我们的可接任务列表,不通过字符串。
我们从正向的角度出发。如果是我们自己写可接任务列表,也是先遍历所有的任务,然后根据人物的等级来判断是否可接。那么就是说他肯定会访问我们的人物等级,那么我们在我们的人物等级下一个硬件访问断点。看看哪里访问了它。
当然这里也有一个问题,人物等级肯定有很多地方在访问。那么我们就要想办法筛选出来那些跟我们的任务有关的。通过人物基址 + CE工具,搜索等级。然后找到之后下硬件访问断点。67D59A8400C517BF | C3                   | ret                                    |
00C517C0 | 8B41 08              | mov eax,dword ptr ds:[ecx+8]           |
00C517C3 | 8B80 6C0A0000        | mov eax,dword ptr ds:[eax+A6C]         |发现这里的eax就是可接任务列表中的等级。但是我们发现这里有多个地方调用。我们下断点看看哪里访问了,对每一个访问的地方我们都去修改代码,一个一个尝试。
看那个对我们的可接任务列表有影响。00AC829F | E8 FCFC4900          | call xajh.F67FA0                       | 遍历可接任务列表发现edi就是每一个可接任务的结构。那么我们就是去看看edi的来源。00AC8254 | 8B7C24 20            | mov edi,dword ptr ss:[esp+20]          |是一个局部变量。
00AC8107 | 894424 20            | mov dword ptr ss:[esp+20],eax          |然后eax来源这个call,这个call有一个参数。是任务id。
00AC80FD | 56                         | push esi                               |
00AC80FE | 897424 68            | mov dword ptr ss:[esp+68],esi          |
00AC8102 | E8 294C1A00          | call xajh.C6CD30                       | 通过任务id拿到任务对象。
通过这个call就可以拿到具体的一个任务。即这个call就是通过任务id拿到任务对象。
那么我们现在就要追id的来源,因为每次id都是不一样的。那么肯定是有一个遍历,然后每次传递不同的id过来。00AC80F6 | 8B34B9               | mov esi,dword ptr ds:[ecx+edi*4]       |
果然
那么ecx就是技能id数组。00AC80F2 | 8B4C24 44            | mov ecx,dword ptr ss:[esp+44]          |
看见esp+44猜测是局部变量或者函数参数。
然后发现是局部变量,那么继续追踪。00AC8076 | 51                   | push ecx                               |
00AC8077 | 8D4C24 48            | lea ecx,dword ptr ss:[esp+48]          |
00AC807B | E8 E0B4ABFF          | call xajh.583560                       |
然后发现来源这个call,这个call调用完毕以后,esp+44就存放了所有id数组的起始地址。
并且这个call会多次调用,每次调用就多写入一个id到esp+44指向的临时数组里面。所以这个call应该是一个push操作。果然,这个call接收一个参数,是任务的id。
所以这个call就是把每次拿到的任务id,push到这个临时数组里面。那么肯定有一个遍历真正的数组中的每一个任务id,然后取出来每一个任务id放到这个临时的任务数组中,那么我们就要追踪任务id来源。00AC806E | 8D4C24 14            | lea ecx,dword ptr ss:[esp+14]          |然后发现是局部变量。
00AC8072 | 894424 14            | mov dword ptr ss:[esp+14],eax          |
来源eax。00AC8068 | 8B14B0               | mov edx,dword ptr ds:[eax+esi*4]       |
00AC806B | 8B42 04              | mov eax,dword ptr ds:[edx+4]           |
eax来源edx + 4,然后发现edx来源eax+esi*4.
那么eax肯定是一个数组,通过内存观察,eax就保存了所有任务对象的地址,是所有任务对象地址的数组,edx就是其中一个具体的元素。
+4的话就是每一个元素的id,即任务id。那么我们现在就是要追踪eax。00AC8058 | 8B8424 AC000000      | mov eax,dword ptr ss:[esp+AC]          |00AC7F20 | 8B4C24 2C            | mov ecx,dword ptr ss:[esp+2C]          | 所有任务数组的链表
00AC7F24 | 8D9424 A0000000      | lea edx,dword ptr ss:[esp+A0]          |
00AC7F2B | 52                   | push edx                               |
00AC7F2C | 53                   | push ebx                               | ebx:&"?"
00AC7F2D | C68424 D8000000 05   | mov byte ptr ss:[esp+D8],5             |
00AC7F35 | E8 A6511A00          | call xajh.C6D0E0                       | 任务结构数组来源
调用完这个call。esp+AC这个地址里面就保存了任务结构数组的起始地址,结束地址和长度了,并且任务结构数组中的东西都已经被填充了。然后这个call和我们之前通过名称分析的哪个call是一样的。也就是说我们通过出发点不一样找到了同样的地方。// 遍历可接任务列表
void CMainDlg::OnBnClickedButtonWorks2()
{// 遍历可接任务列表DWORD taskListAddr[6]{0}; // 输出参数 可接任务数组结构__asm {mov eax, 0x004567A0;call eax; mov  ebx, [eax + 0x30]; // 拿到ebx参数lea edx, taskListAddr; // 拿到edx参数mov ecx, 0x01599828; // 可接任务链表基址的地址mov ecx, [ecx];  // 可接任务链表基址push edx;push ebx;mov eax,0x00C6D0E0;call eax; // 这样就拿到的临时的可接任务数组结构 下标3起始地址 4结束地址 5长度};DWORD taskLisstBeginAddr = taskListAddr[3];DWORD taskLisstEndAddr = taskListAddr[4];// 以0表示结束地址LPWSTR taskName = L"";while (taskLisstBeginAddr < taskLisstEndAddr) {DWORD taskStructAddr = *(DWORD*)(taskLisstBeginAddr); // 取出当前元素// 这个结构 + 8是名称的地址taskName = (LPWSTR)(taskStructAddr + 0x8);this->m_canWorks.Append(taskName);this->m_canWorks.Append(L"\r\n");taskLisstBeginAddr += 4; // 移动到下一个可接任务地址结构}// 释放我们申请出来的临时数组taskLisstBeginAddr = taskListAddr[3];free((void*)taskLisstBeginAddr);UpdateData(FALSE); // 变量到控件
}

接受任务分析

接受任务分析:
要先点击npc进行对话,对话完以后才能接任务。
我们从send函数入手。分析对话:
00CB642C | 52                   | push edx                               |
00CB642D | 50                   | push eax                               |
00CB642E | E8 4D340000          | call xajh.CB9880                       | 对话5
00CB6433 | 83C4 08              | add esp,8                              |接受的两个参数是NPC ID的高位和低位。push 0x01000000
push 0x2C6
call 0x00CB9880
add esp,8  发现没问题,那么对话我们就解决了。下面就是接受任务。
同样我们在send函数下断点跟踪。00ABD12E | 8B4C24 50            | mov ecx,dword ptr ss:[esp+50]          |
00ABD132 | 8B5424 48            | mov edx,dword ptr ss:[esp+48]          |
00ABD136 | 8B45 4C              | mov eax,dword ptr ss:[ebp+4C]          |
00ABD139 | 51                   | push ecx                               |
00ABD13A | 8B4D 44              | mov ecx,dword ptr ss:[ebp+44]          |
00ABD13D | 53                   | push ebx                               |
00ABD13E | 52                   | push edx                               |
00ABD13F | 8B5424 50            | mov edx,dword ptr ss:[esp+50]          |
00ABD143 | 50                   | push eax                               |
00ABD144 | A1 507F5901          | mov eax,dword ptr ds:[1597F50]         |
00ABD149 | 51                   | push ecx                               |
00ABD14A | 8B48 38              | mov ecx,dword ptr ds:[eax+38]          |
00ABD14D | 52                   | push edx                               | 任务id
00ABD14E | E8 BD152300          | call xajh.CEE710                       | 接受3$ ==>    0019EA6C  000025B9
$+4      0019EA70  00000000
$+8      0019EA74  00000000
$+C      0019EA78  00000000
$+10     0019EA7C  FFFFFFFF
$+14     0019EA80  00000000   ecx:354854B8使用代码注入器看看好不好使先
push 0
push 0xFFFFFFFF
push 0
push 0
push 0
push 0x25B9
mov ecx,0x354854B8
call 0x00CEE710发现没毛病,所以虽然参数很多,但是我们只需要关注任务id,然后再关注一个ecx就好了。然后任务id。我们前面找到了可接任务列表,里面有id。那么我们遍历可接任务列表就可以接任务了。现在只需要追踪ecx的来源。然后我们发现上一层的函数,更方便,只需要4个参数,而且ecx基址那些直接存在了。00ABDAD4 | 50                          | push eax                               |  00000000
00ABDAD5 | 51                          | push ecx                               |  FFFFFFFFF
00ABDAD6 | 8B0D C4155D02   | mov ecx,dword ptr ds:[25D15C4]         |
00ABDADC | 51                         | push ecx                               | ecx来源上面基址
00ABDADD | 52                         | push edx                               | 任务id
00ABDADE | E8 EDF3FFFF          | call xajh.ABCED0                       | 接受4但是我们发现有问题,虽然是来源基址,不过基址里面存放的东西会发生变化。
点哪个NPC,就会往这个地址里面写入基址。那么有没有办法不进行对话,就能直接接嘛?我们下一个硬件写入断点。00ABBA39 | A3 C4155D02          | mov dword ptr ds:[25D15C4],eax         |找eax的来源。00ABBA06 | 8B80 EC060000        | mov eax,dword ptr ds:[eax+6EC]         |
00ABBA0C | 8B0D 507F5901        | mov ecx,dword ptr ds:[1597F50]         | ecx来源
00ABBA12 | 8D5424 0C            | lea edx,dword ptr ss:[esp+C]           |
00ABBA16 | 52                   | push edx                               |   任务ID
00ABBA17 | 6A 00                | push 0                                 |  固定0
00ABBA19 | 50                   | push eax                               |
00ABBA1A | E8 916497FF          | call xajh.431EB0                       |  接收一个ecx返回eax。
00ABBA1F | 8BC8                 | mov ecx,eax                            |
00ABBA21 | E8 5ADF1D00          | call xajh.C99980                       | eax来源那么我们就要分析这个call的参数。00ABBA06 | 8B80 EC060000        | mov eax,dword ptr ds:[eax+6EC]         |一直往上追踪
00ABB9AA | 8DB1 78FDFFFF        | lea esi,dword ptr ds:[ecx-288]         |但是我们感觉好像有点问题,因为我们点击不同的npc,这个esi的值都是一样的,那么我们追踪esi没有什么意义。00ABB9C7 | 8B88 58050000        | mov ecx,dword ptr ds:[eax+558]         |
然后我们发现这行,点击不同NPC,eax+558里面保存的是不同的NPCID。所以真正的猫腻在这里。007E2000 | 8B4424 04            | mov eax,dword ptr ss:[esp+4]           |
007E2004 | 50                   | push eax                               |
007E200B | E8 E02B2F00          | call xajh.AD4BF0                       |发现是eax写入的,那么我们要找eax的来源。00AAD181 | 56                   | push esi                               |
来源上一个函数的esi。00AACE9C | 8BB424 940A0000      | mov esi,dword ptr ss:[esp+A94]         |看见esp猜测局部变量或者函数参数,发现是函数参数。007DFFDF | 8B7424 14            | mov esi,dword ptr ss:[esp+14]          |
看见esp猜测局部变量或者函数参数,发现是函数参数。006BB4D9 | 8B96 58090000        | mov edx,dword ptr ds:[esi+958]         |然后发现这个esi是NPC的首地址。那么我们就找到头了。

自动更新基址

原理就是搜索游戏内存,通过特征码进行匹配,然后返回匹配到的地址,#include <Windows.h>
#include <iostream>
#include <string>
#include <sstream>
#include <vector>using namespace std;/*@function 字符串切割函数@param [in] codestr 要切割的字符串@param [out] vc 返回切割完毕的字符串数组@param [int] flag 切割标志
*/
void split(string codestr, vector<string>& vc, const char flag = ',') {istringstream is(codestr);string temp;while (getline(is, temp, flag)) {vc.push_back(temp);}
}/*@function 暴力匹配@param code 我们刚开始的特征码@param readStr 转换后的特征码@param cmpLen 长度@return 是否转换成功
*/
BOOL cmpStrCode(CHAR* code, CHAR* readStr, INT cmpLen) {for (int i = 0; i < cmpLen; i++) {// 模糊匹配if (code[i] == '?') continue;// 精确匹配if (code[i] != readStr[i]) {return FALSE;}}return TRUE;
}/*@function BYTE数组转CHAR数组@param [in] byteCode 要转换的BYTE数组@param [in] strCode 转换后的CHAR数组@param [in] codeLen 要转换的BYTE数组长度@return 是否转换成功
*/
BOOL byteToChar(BYTE* byteCode, CHAR* strCode, DWORD codeLen) {for (int i = 0; i < codeLen; i++) {// 1个字节=两个16进制位,所以转为CHAR 要两个字节wsprintfA(&strCode[i * 2], "%02X", byteCode[i]);}return TRUE;
}
/*@function 扫描特征码@param [in] hProcess 游戏进程句柄@param [in] beginAddr 起始扫描地址@param [in] endAddr 结束扫描地址@param [in] code 特征码@param [in] codeLen 特征码长度@param [out] retAddr 返回搜索到的特征码地址@return 是否搜索成功
*/
BOOL sacnGameCode(HANDLE hProcess,DWORD beginAddr,DWORD endAddr,CHAR* code,DWORD codeLen,DWORD& retAddr)
{BYTE* readCode = new BYTE[0x1000];for (DWORD readAddr = beginAddr;readAddr <= beginAddr - 0x1000; readAddr += (0x1000 - codeLen)) {memset(readCode, 0, 0x1000);CHAR strCode[0x2001]{ 0 };BOOL retRead = ReadProcessMemory(hProcess, (LPVOID)readAddr, readCode, 0x1000, NULL);if (!retRead) {continue; // 直接进行下一次}// byte数组byteToChar(readCode, strCode, 0x1001);// 暴力比较for (int i = 0; i < 0x2001 - codeLen; i++) {// strCode作为每次比较的开始// 那么就是有0x2001-codeLen次比较 然后每次比较又codeLen次BOOL retCmp = cmpStrCode(code, &strCode[i], codeLen);if (retCmp) {printf("匹配到了!\n");printf("起始地址为:%X\n", readAddr + i / 2);retAddr = readAddr + i / 2;return TRUE;}}}return FALSE;
}/*@function 更新基址@param [in] hProcess 进程句柄@param [in] vc 要处理的数据@param [in] fp 文件指针@return 是否处理成功
*/
BOOL updateDataBase(HANDLE hProcess,vector<string>& codeArr,FILE *fp) {string codeStr = codeArr[0];string offsetStr = codeArr[1];string opstrStr = codeArr[2];DWORD readData = 0;DWORD addr = 0;CHAR* str = new CHAR[10];BOOL ret = sacnGameCode(hProcess, 0X400000, 0X800000, (CHAR*)codeStr.c_str(), strlen(codeStr.c_str()), addr);if(!ret) {return FALSE;}if (opstrStr == "+") {addr += std::stoi(offsetStr);}else if (opstrStr == "-") {addr -= std::stoi(offsetStr);}ret = ReadProcessMemory(hProcess, (LPVOID)addr,&readData, 4,NULL);if (!ret) {return FALSE;}_itoa_s(readData, str, 10, 16);fputs(str, fp);fputs("\n",fp);delete[] str;return TRUE;
}/*@function 读取特征码文件@param [in] hProcess 进程句柄@return 是否读取成功
*/
BOOL readCodeFile(HANDLE hProcess) {FILE* fp;FILE* inputStream;CHAR codeBuff[0x100]{0};errno_t errNum = fopen_s(&fp, "code.txt", "r");errno_t errNum2 = fopen_s(&inputStream, "dataBase.txt", "w");if (errNum != 0) {return FALSE;}if (errNum2 != 0) {return FALSE;}// 读取文件string codString;vector<string> codeArr;while (!feof(fp)) {memset(codeBuff,0, 0x100);codeArr.clear();codString.clear();fgets(codeBuff, 0x100, fp);codString.append(codeBuff);// 拆分字符串 特征码,偏移,加减split(codString, codeArr, ',');updateDataBase(hProcess,codeArr, inputStream);}fclose(inputStream);fclose(fp);return TRUE;
}int main() {HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 8916);readCodeFile(hProcess);system("pause");return 0;
}

笑傲江湖-逆向分析学习【仅供学习】相关推荐

  1. 人力资源学python有意义吗-python爬虫抖音 个人资料 仅供学习参考 切勿用于商业...

    本文仅供学习参考 切勿用于商业 本次爬取使用fiddler+模拟器(下载抖音APP)+pycharm 1. 下载最新版本的fiddler(自行百度下载),以及相关配置 1.1.依次点击,菜单栏-Too ...

  2. 爬取了京东商城上的部分手机评论数据,仅供学习使用

    京东的手机评论数据爬虫,仅供学习使用 说明 爬取了京东商城上的部分手机评论数据.由于项目的数据量要求不大,仅仅采用了比较简单的方式来进行数据的爬取,过程分为两个部分: 根据不同的手机品牌选择了第一页的 ...

  3. 每日简单小妙招:使用python实现控制摄像头拍照并将其发送某某邮箱(仅供学习)

    仅供学习,望注意隐私 文章目录 1.功能展示 2.代码展示 3.详细步骤 Ⅰ.安装opencv Ⅱ.QQ邮箱设置 1.功能展示 这里我使用自己的电脑进行控制拍照,将其发送到自己的邮箱:图片经过base ...

  4. kalilinux生成安卓木马(仅供学习使用)

    kalilinux生成安卓木马(仅供学习使用) 一.前期准备工作 1.1虚拟机安装好kalilinux 链接:https://pan.baidu.com/s/10rcLYOGYKQb0pETqJLbD ...

  5. 基于易语言的键盘监听器(仅供学习)

    基于易语言的键盘监听器(仅供学习) 软件原理 梳理 输入内容检测部分 发送部分 结束部分 准备工作 邮箱准备 支持库准备 模块准备 窗口准备 代码部分 程序集 启动窗口创建完毕 子程序1 编辑框1内容 ...

  6. 理解ConstraintLayout性能上的好处(转载,仅供学习)

    本文转载自:https://www.jianshu.com/p/fae1d533597b,仅供学习 (译)理解ConstraintLayout性能上的好处 本文介绍了ConstraintLayout对 ...

  7. 最新版WinRAR5.61去广告代码教程分享(仅供学习交流)

    最新版WinRAR5.61去广告代码教程分享(仅供学习交流) 第一步:到WinRAR官网www.rarlab.com下载自己需要的版本,选择Chinese Simplified 64bit 安装即可. ...

  8. 前程无忧爬虫,仅供学习使用

    前程无忧爬虫–仅供学习使用 前程无忧职位链接:https://search.51job.com/list/090200,000000,0000,00,9,99,%25E5%25A4%25A7%25E6 ...

  9. python爬虫爬取漫画(仅供学习)

    项目名: crawl_chuanwu 爬取链接:https://www.manhuadui.com/manhua/chuanwu/ 声明:本项目无任何盈利目的,仅供学习使用,也不会对网站运行造成负担. ...

  10. 解决:AWVS(Acunetix)激活频繁失效(仅供学习)

    注: 1.本文仅供学习,只提供思路,一切行为皆与本人无关,本人不负任何责任 2.本文仅为本人一个原创思路,搬运请注明本人 推荐在本人博客Yolel's Blog阅读 网上措施:重新将激活补丁wa_da ...

最新文章

  1. java servlet post_Java中Servlet Post和Get乱码
  2. Python学习教程实用技法:通过公共键对字典列表排序—itemgetter
  3. 设置span的宽度,让span象button那样显示
  4. Qt for Python使用Qt中的Properties
  5. 免费字典api ,查询汉字完整信息
  6. easyui label显示不全_easyui 元素遍历问题
  7. eof在c语言中表示什么_日语中的鍵为什么既能表示“钥匙”也能表示“锁”?...
  8. [Linux]gocron定时任务平台的部署
  9. PXE-cobbler 无人值守装机------续
  10. gps 捕获 matlab,基于FFT的GPS信号快速捕获方法
  11. U盘插入电脑后,提示需要格式化U盘如何解决?
  12. Mac下最好用的离线词典-欧陆词典破解版
  13. IP信息解析和地理定位,以及免费GeoLite2-City.mmdb的使用教程
  14. 完美mix-in(混入)模式———js对象想怎么玩就怎么玩
  15. 齿轮 matlab,齿轮传动的MATLAB软件建模及轻量化设计.pdf
  16. 计算机主板功能是什么,电脑主板的作用是什么_电脑主板作用详细介绍 - 系统家园...
  17. 拖动条控件 seekbar 设置
  18. 腾讯云CentOS7运行基于SSM的个人博客----第三节:使用Dokcer安装JDK、Tomcat环境
  19. 轨迹规划 trajectory planning
  20. STK12已出,STK 12 新特性介绍

热门文章

  1. JHOK-ZBG1,JHOK-ZBG2漏电继电器
  2. SendWS-Whatsapp全球通讯拓客工具
  3. vue2.0下axios实现跨域踩的坑
  4. pytorch加载模型时出现.....ckpt_100.pth is a zip archive (did you mean to use torch.jit.load()?)
  5. 计算机关机打不开,电脑强行关机打不开怎么办
  6. CAN总线介绍(一 ~ 三)---CAN基础知识
  7. Java容器概述及扩容
  8. Halcon-依据点关系计算物体三维位姿
  9. 实验十一、多级放大电路的参数设置
  10. Xee for Mac 特别版(图像查看器)