一、分析角色 HP/MP 地址

我们的目标是这个,热血江湖。我们要找出基本信息中,所有数据的地址。

郁金香商业辅助教程 2016 笔记 1~5_搜索

我们要用到一款工具,CE。打开之后点击左上角打开进程,会弹出进程列表,我们需要选择游戏的进程。

郁金香商业辅助教程 2016 笔记 1~5_数据_02

我们可以点击下面的“窗口列表”,然后从打开的窗口中搜索,这样可能比较好找。

郁金香商业辅助教程 2016 笔记 1~5_句柄_03

由于 HP 数值变动较快,我们把扫描类型改为“两者之间的数值”,在上面把数值设置为 352 和 370(视具体情况而定),然后点“首次扫描”。

郁金香商业辅助教程 2016 笔记 1~5_数据_04

左边就会出现结果。因为结果太多,我们不知道是哪个,需要进一步筛选。

郁金香商业辅助教程 2016 笔记 1~5_句柄_05

我们把扫描类型改为“增加的数值”,点击“再次扫描”。这个很重要,因为再次扫描是在上一次的结果中搜索,可以缩小范围。游戏中一些值是不变的,可以过滤一些。

郁金香商业辅助教程 2016 笔记 1~5_数据_06

我们双击唯一的结果,它会在底部的列表中出现,双击描述可以修改名称。双击地址会弹出这样一个框。

郁金香商业辅助教程 2016 笔记 1~5_句柄_07

我们可以看到,地址采用模块名称(基址)加偏移来描述。这是因为一些模块是共享库,加载时会改变基址。因为我们这是一个 EXE,不需要这个名称也可以。

接下来我们尝试寻找 MP(蓝的那个)的地址。我们不需要重新搜索。我们假设程序以int保存这些数值,而int在 windows x86/x64 上是四个字节。我们还假设这些东西是挨着存储的。所以我们将那个地址加四。

郁金香商业辅助教程 2016 笔记 1~5_句柄_08

我们看到右边的 251 正好的游戏的 MP,说明我们找对了。

二、分析角色金钱基址

我们查看金币数量,是 173061:

郁金香商业辅助教程 2016 笔记 1~5_搜索_09

这个数值不太可能有重的,所以我们直接搜索:

郁金香商业辅助教程 2016 笔记 1~5_数据_10

我们选上面那个,因为它和我们上节课的地址在一个段里面。

我们找到了金钱的地址。但这样一个一个找太麻烦了,有没有可能一次找到全部呢?

首先记下基本信息:

郁金香商业辅助教程 2016 笔记 1~5_句柄_11

我们打开 OD 并用它附加游戏。

郁金香商业辅助教程 2016 笔记 1~5_搜索_12

然后执行dd 2f86170,在左下角的窗口中,我们可以看到这个地址附近的数据。

郁金香商业辅助教程 2016 笔记 1~5_搜索_13

我们双击第一行第一列,将第一列转换为偏移形式:

郁金香商业辅助教程 2016 笔记 1~5_句柄_14

我们看到第二列是十六进制形式,需要将其转换为十进制。

地址

数值

属性

(0x2f86170)+0x0

381

HP

0x4

252

MP

0x8

390

愤怒值

0xc

381

最大 HP

0x10

252

最大 MP

0x14

1000

最大愤怒值

0x18 QWORD

12185

经验值

0x20 QWORD

24782

下一级所需的经验值

0x28

10


0x2c

27138

历练

0x30

51


0x34

54


0x38

31


0x3c

91


0x40

0


0x44

0


我们可以根据数值猜出绝大部分。但是 HP 之前还有个昵称,这里没有,可能我们需要向前找找。

我们点击右键,点击“文本->ASCII(32 字符)”:

郁金香商业辅助教程 2016 笔记 1~5_数据_15

-0x80的地方找到了角色名称。我们切换为十六进制视图,然后把这个地方作为新的基址:

郁金香商业辅助教程 2016 笔记 1~5_句柄_16

我们得到了一些新的东西:

地址

数值

属性

(0x2f860f0)+0x0 STR

-

角色名称

0x30

11


0x34 BYTE

0x17

等级

0x35 BYTE

0x1

几转

0x36 STR

-

名声

并且之前那些地址需要加上0x80,这里就不再写一遍了。

我们回到 CE,可以点击右边的“手动加入地址”,保存它们。

郁金香商业辅助教程 2016 笔记 1~5_数据_17

三、分析角色气功加点

这次我们要分析角色的气功点数:

郁金香商业辅助教程 2016 笔记 1~5_搜索_18

我们首先寻找第一个,因为其它气功很可能在第一个后面。

并且,我们不知道这个属性用几个字节来表示。但如果多于一个字节,那么14应该在它的最低字节。也就是说,无论怎么表示,我们都可以搜索一个字节14

(实际上气功点数最大为 20,剩余点数最大为 100,不超出一个字节的最大值。就算它多于一个字节,高字节也用不上。)

郁金香商业辅助教程 2016 笔记 1~5_搜索_19

搜索结果太多了,我们让它变化一下,给它加一点变成 15,然后再搜。

郁金香商业辅助教程 2016 笔记 1~5_数据_20

最上面的两个以0x02f开头,和上一节的其它数据在同一个段里面。那么到底哪个是呢?

我们用 OD 附加进程(其它很多软件都可以),查看具体的内存布局。首先是第一个0x02f861e4

郁金香商业辅助教程 2016 笔记 1~5_搜索_21

0x02f861e4是第一个气功点数,每隔0x4就有一个气功点数。0x02f861e0是剩余点数。所有点数都是一字节。

然后是第二个0x02f888a0

郁金香商业辅助教程 2016 笔记 1~5_搜索_22

这个地址中没有剩余点数,而且都是紧密挨着的。

下面我们验证一下,将第二个气功的点数加一。

郁金香商业辅助教程 2016 笔记 1~5_数据_23

这是第一个地址0x02f861e0

郁金香商业辅助教程 2016 笔记 1~5_搜索_24

我们看到第二个气功的点数变成了 2。

然后是第二个地址0x02f888a0

郁金香商业辅助教程 2016 笔记 1~5_搜索_25

也变了,说明两个地址都有效。我们选择第一个,因为它和我们上一节的基址近一些。我们减一下,得到第一个地址的偏移是0xf0

下面我们总结一下信息:

地址

数值

属性

(0x2f860f0)+0xf0 BYTE

2

气功剩余点数

0xf4 BYTE

15

第一个气功点数

0xf0+4*i BYTE

-

i个气功点数

四、注入 DLL

一般来说,在同一个进程中读取数据比较方便。所以我们编写 DLL,将其注入同一个进程中。

打开 VS,新建项目,选择“MFC DLL”。创建项目完成后,我们的目录是这样:

郁金香商业辅助教程 2016 笔记 1~5_句柄_26

接下来我们创建窗口,点击资源视图(左下角),然后右键添加资源对话框(Dialog):

郁金香商业辅助教程 2016 笔记 1~5_句柄_27

然后我们新建类CMainDialogWnd,使用 MFC 创建类向导:

郁金香商业辅助教程 2016 笔记 1~5_句柄_28

然后打开“源文件->MainDialogWnd.h”,代码是这样。

class CMainDialogWnd: public CDialogEx
{
    DECLARE_DYNAMIC(CMainDialogWnd)

public:
    CMainDialogWnd(CWnd* pParent=NULL); //标准构造函数
    virtual ~CMainDialogWnd();

    //对话框数据
    enum { IDD = IDD_DIALOG1 }

protected:
    virtual void DoDataExchange(CDataExchange* pDX) //DDX/DDV 支持

    DECLARE_MESSAGE_MAP()
}

我们打开MFC_DLL.cpp,创建全局变量:

CMainDialogWnd *PMainDialog;

CMFC_DLLApp::InitInstance中添加:

PMainDialog = new CMainDialogWnd;
PMainDialog->DoModal();
delete PMainDialog;
// 释放 DLL,以便反复注入
FreeLibraryAndExitThread(theApp.m_hInstance, 1);

但这样有个问题,这个窗口是模态的。窗口显示的时候会卡住游戏。我们可以将其放到子线程中。把上面的代码移到一个函数中:

DWORD WINAPI ShowDialog(LPARAM lpData) 
{
    PMainDialog = new CMainDialogWnd;
    PMainDialog->DoModal();
    delete PMainDialog;
    // 释放 DLL,以便反复注入
    FreeLibraryAndExitThread(theApp.m_hInstance, 1);

    return TRUE;
}

CMFC_DLLApp::InitInstance中编写:

CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ShowDialog, NULL, NULL, NULL);

我们编译它,在debug目录下面得到MFC_DLL.dll。然后我们打开CodeInEx注入工具,点击左上角的按钮:

郁金香商业辅助教程 2016 笔记 1~5_句柄_29

我们首先在上面的列表中选择要注入的进程,然后点击下面的“注入DLL”按钮,会弹出一个选择框。我们在里面选择刚才的 DLL。

之后我们发现我们的窗口打开了,并且游戏还有反应。

五、手动编写注入代码

上一节中,我们使用工具来注入 DLL。这一节我们尝试自己编程来实现。

首先新建 Win32 控制台项目,在“源文件”目录下创建InjectDll.cpp(名字不重要)。

我们首先要获取窗体类名,之后要拿它获取窗口句柄。为什么这样,是因为窗体类名是永远不变的,句柄可能每次启动都要变。我们打开Spy++

郁金香商业辅助教程 2016 笔记 1~5_句柄_30

句柄是D3D Window。我们在代码开头定义一个常量:

#define GameClassName "D3D Window"

我们还需要定义 DLL 的路径:

#define DllFullPath "path\\to\\your\\dll"

之后我们编写函数InjectDll

bool InjectDll() 
{
    // 根据窗口类名获得句柄
    HWND hWnd = FindWindow(GameClassName, NULL);
    if(hWnd == NULL) 
        return false;

    DWORD pid = 0;
    // 根据窗口句柄获取 PID
    GetWindowThreadProcessId(hWnd, &pid);
    if(pid == 0)
        return false;

    // 根据 PID 获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if(hProcess == NULL)
        return false;

    // 在游戏内存中分配一片空间
    LPVOID address = VirtualAllocEx(hProcess, NULL, 256, MEM_COMMIT, PAGE_READWRITE);
    if(address == NULL)
        return false;

    // 写入 DLL 全路径名
    DWORD bytesWritten;
    WriteProcessMemory(hProcess, address, DllFullPath, strlen(DllFullPath) + 1, &bytesWritten);
    if(bytesWritten < strlen(DllFullPath))
        return false;

    // 在目标进程中启动线程
    // 加载动态链接库
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, address, NULL, NULL);

    // 等待
    WaitForSingleObject(hThread, 0xffffffff);

    // 回收
    CloseHandle(hThread);
    VirtualFreeEx(hProcess, address, 256, MEM_DECOMMIT);
    CloseHandle(hProcess);

    return true;
}

然后编写main

int main() 
{
    // 注入 DLL 代码
    printf("注入 DLL\n");
    if(!InjectDll())
        printf("注入 DLL 失败\n");

    // 让控制台停住
    getchar();

    return 0;
}

编译运行之后,DLL 就被注入,我们也就能看到熟悉的窗口了。