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

一、分析角色 HP/MP 地址

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

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

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

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

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

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

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

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

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

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

二、分析角色金钱基址

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

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

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

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

首先记下基本信息:

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

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

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

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

地址数值属性
(0x2f86170)+0x0381HP
0x4252MP
0x8390愤怒值
0xc381最大 HP
0x10252最大 MP
0x141000最大愤怒值
0x18 QWORD12185经验值
0x20 QWORD24782下一级所需的经验值
0x2810
0x2c27138历练
0x3051
0x3454
0x3831
0x3c91
0x400
0x440

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

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

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

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

地址数值属性
(0x2f860f0)+0x0 STR-角色名称
0x3011
0x34 BYTE0x17等级
0x35 BYTE0x1几转
0x36 STR-名声

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

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

三、分析角色气功加点

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

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

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

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

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

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

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

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

然后是第二个0x02f888a0

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

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

这是第一个地址0x02f861e0

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

然后是第二个地址0x02f888a0

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

下面我们总结一下信息:

地址数值属性
(0x2f860f0)+0xf0 BYTE2气功剩余点数
0xf4 BYTE15第一个气功点数
0xf0+4*i BYTE-i个气功点数

四、注入 DLL

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

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

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

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

然后打开“源文件->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注入工具,点击左上角的按钮:

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

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

五、手动编写注入代码

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

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

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

句柄是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 就被注入,我们也就能看到熟悉的窗口了。

posted @ 2018-02-20 10:48  绝不原创的飞龙  阅读(20)  评论(0编辑  收藏  举报  来源