Skip to content
/ rxjh Public
  • Notifications
  • Fork 16
  • Star 20

热血江湖外挂

20 stars 16 forks Branches Tags Activity
Star
Notifications

JiaJinRong12138/rxjh

Branches Tags

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits

GameData

GameData

 
 

InjectDLL

InjectDLL

 
 

MFC_DLL

MFC_DLL

 
 

lib

lib

 
 

.gitattributes

.gitattributes

 
 

.gitignore

.gitignore

 
 

MFC_DLL.sln

MFC_DLL.sln

 
 

README.md

README.md

 
 

Repository files navigation

rxjh

热血江湖外挂

外挂编写(一)

学习案例:热血江湖

使用环境: Visual Studio 2019

学习使用的工具: 逆向工具集

注:更新日志:

  • 1.0:最新版人物信息基址:0x02C166D8;最新版背包存放基址:0x02E3B3E4
  • 2.0:最新版物品使用call 为0x00838480
  • 3.0:
    • 人物基址:0x02C176D8‬;
    • 背包存放基址:0x02E3C3E4;
    • 游戏主窗口基址:0x01195F88;
    • 使用物品的CALL:0x008384F0;
    • 人物动作基址:0x02E3CD58;
    • 动作使用的CALL:0x007139E0;
    • 所有对象数组:0x2E64A28;
    • 怪物基址:0x427FBA0
    • 角色对象基址:0x427FBA0

人物属性数据分析

注:

  • 一般游戏开发的时候,相关的数据都是放到一个结构或者是一个类中,那么这些数据的内存地址相距的比较近;
  • 一般内存地址使用CE工具逆向出来后表示为Client.exe+278A75C,表示软件地址加上偏移量为其基址

HP值和MP值

  • 生命值PH: Client.exe+278A75C = 02B8A758
  • 内功值MH: Client.exe+278A75C = 02B8A75C

金币值

  • 进行商品的买卖实现金币值的变动
  • 基址:Client.exe+278A7BC = 02B8A7BC

其他人物属性分析

  • OD软件的使用
    • 使用dd 02B8A758 查找到人物属性基址块
  • 人物属性值以及对应的内存地址
  • 基址 02B8A6D8
    • +0:人物名字
    • +80:生命值(红/HP)
    • +84:内功值(蓝/MP)
    • +88:愤怒值
    • +8C:最大生命值
    • +90:最大内功值
    • +94:最大值愤怒值
    • +98:当前经验值
    • +A0:升级到下一级要的经验值
    • 势力
    • +36:名声
    • +34:一字节空间表示等级
    • +35:一字节空间表示 几转
    • +AC:历练
    • 制造
    • 熟练度
    • 灵兽持有
    • 精力
    • +C8:攻击
    • 武器命中
    • +CC:防御
    • 武器防御
    • +D0:命中
    • 对人战斗
    • +D4:回避
    • 对怪攻击
    • 武功回避
    • 对怪防御
    • +B0:心
    • +B4:气
    • +B8:体
    • +BC:魂
    • +E4:金币值
  • 气功值分析
    • 气功点数:基址 + F0 = 02b8a7c8
    • 第num个气功的点数:(一个字节)02B8A6D8+0f0+4*num
    • 可能是第num个气功的ID(没有就为0):02B8A6D8+0f0+4*num+2

通过注入游戏进程读取游戏内数据

  • 注入DLL

    • 创建MFC DLL

      创建MFC_DLL

  • 在添加窗口后,需要进行配置,才能在动态链接库注入后显示窗口

    • 为窗口添加Class

    • 修改MFC_DLL.cpp的代码

      • // MFC_DLL.cpp: 定义 DLL 的初始化例程。
        //
        
        #include "pch.h"
        #include "framework.h"
        #include "MFC_DLL.h"
        
        // 包含含有主窗口的class1的头文件
        #include "CMainDialogWnd.h"
        
        
        #ifdef _DEBUG
        #define new DEBUG_NEW
        #endif
        
        
        BEGIN_MESSAGE_MAP(CMFCDLLApp, CWinApp)
        END_MESSAGE_MAP()
        
        
        // CMFCDLLApp 构造
        
        CMFCDLLApp::CMFCDLLApp()
        {
        	// TODO:  在此处添加构造代码,
        	// 将所有重要的初始化放置在 InitInstance 中
        }
        
        
        // 唯一的 CMFCDLLApp 对象
        
        CMFCDLLApp theApp;
        
        
        // CMFCDLLApp 初始化
        
        // 定义全局的窗口变量
        CMainDialogWnd* PMainDialog;
        
        BOOL CMFCDLLApp::InitInstance()
        {
        	CWinApp::InitInstance();
        
        	// 添加显示窗口的代码
        	// 创建对象,划分空间
        	PMainDialog = new CMainDialogWnd;
        	//DoModal 是以阻塞的方式来运行
        	PMainDialog->DoModal();
        	// 释放空间
        	delete PMainDialog;
        	return TRUE;
        }
        
    • 使用 注入工具讲编译生成的DLL 注入到游戏进程中

      • 注:DoModal() 函数是以阻塞的方式去执行的,所以会造成线程阻塞

      • 解决方式:将DoModal() 放到新的线程去执行

      • 实现窗口关闭后自动释放DLL

    使用代码实现动态连接库的注入

    • 使用到的windows API
      • HWND FindWindow(lpClassName, lpWindowNAme)
        • 通过类名指针或窗口名指针获取窗口句柄
      • DWORD GetWindowThreadProcessId(hwnd(窗口句柄), lpdwProcessId)
        • 获取窗口线程句柄的ID(lpdwProcessId)
      • HANDLE WINAPI OpenProcess(dwDesiredAccess(访问权限), bInheritHandle, dwProcessId)
        • 开启并创建一个本地进程
      • LPVOID WINAPI VirtualAllocEx(hProcess, lpAddress, dwSize, flAllocationType, flProtect)
        • 分配内存空间
      • BOOL WINAPI WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten)
        • 向内存中写入数据
      • HANDLE WINAPI CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId)
        • 为进程创建一个线程
      • DWORD WINAPI WaitForSingleObject(hHandle, dwMilliseconds)
        • 等待单个对象执行后再进行操作
      • BOOL WINAPI CloseHandle(hObject)
        • 关闭句柄
      • BOOL WINAPI VirtualFreeEx(hProcess, lpAddress, dwSize, dwFreeType)
        • 释放内存空间
      • BOOL WINAPI CloseHandle(hObject)
        • 关闭句柄
#### 代码
#include <iostream>
#include <windows.h>
#define GameClassName "D3D Window"
#define DllPath "D:\\c_work\\MFC_DLL\\Debug\\MFC_DLL.dll"

using namespace std;


void InjectDll() {

	DWORD pid = 0;
	HANDLE hProcess = NULL;
	LPDWORD lpAddr = NULL; // 获取远程分配成功的地址
	DWORD size = NULL;
	HANDLE threadHandle = NULL;

	// 获取游戏窗口句柄
	HWND GameH = FindWindow((LPCTSTR)GameClassName, NULL);
	if (GameH != 0) {
		//句柄获取成功
		// 获取进程PID

		GetWindowThreadProcessId(GameH, &pid);
		if (pid != 0) {
			// PID 获取成功
			// 获取进程句柄
			// 开启所以权限打开进程

			hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE ,pid);
			if (hProcess != NULL) {
				// 打开进程成功
				// 分配内存空间,写入动态链接库的全路径名
				//D:\\c_work\\MFC_DLL\\Debug\\MFC_DLL.dll

				lpAddr = (LPDWORD)VirtualAllocEx(hProcess, NULL, 256, MEM_COMMIT, PAGE_READWRITE);
				if (lpAddr != NULL) {
					// 地址分配成功, 写入DLL 的全路径

					WriteProcessMemory(hProcess, lpAddr, DllPath, strlen(DllPath) + 1, &size);
					if (size >= strlen(DllPath)) {
						threadHandle = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddr, NULL, NULL);
						// 等待注入DLL 的线程执行完再执行下一步(等待的进程句柄, 等到多少毫秒)
						WaitForSingleObject(threadHandle, 0xFFFFFFFF);
						// 关闭线程
						CloseHandle(threadHandle);
						// 释放进程
						VirtualFreeEx(hProcess, lpAddr, 256, MEM_DECOMMIT);
						// 关闭句柄
						CloseHandle(hProcess);
						
						
						// 清除内存数据

					}
					else {
						cout << "写入DLL 失败" << endl;
					}
				}
				else {
					cout << "地址分配失败" << endl;
				}

			}
			else {
				cout << "打开进程失败" << endl;
			}
		}
		else {
			cout << "获取PID 失败" << endl;
		}
	}
	else {
		cout << "获取窗口句柄失败" << endl;
	}

}

int main()
{
    
	// 添加注入DLL 代码
	cout << "开始注入DLL" << endl;
	InjectDll();
	cout << "注入DLL结束" << endl;

}

  • 步骤
    1. 获取窗口对应的进程的PID
    2. 根据PID 获取进程
    3. 获取游戏进程的PID
    4. 根据PID 获取进程
    5. 在目标进程分配内存空间,方便写入DLL 全路径
    6. 将DLL 全路径写入到目标进程
    7. 远程注入DLL
    8. 等待目标进程执行完成
    9. 释放进程空间
    10. 关闭线程句柄

整合游戏内数据

  • 整数
    • QWORD 类型变量 nq前缀 //8字节 无符号整数 不能表示负数
    • DWORD 类型变量 nd前缀 //4字节 无符号整数 不能表示负数
    • WORD 类型变量 nw前缀 //2字节 无符号整数 不能表示负数
    • BYTE 类型变量 nb前缀 //1字节 无符号整数 不能表示负数
    • int 带符号类型 ni前缀 //4字节 带符号整数 可表示正负数
    • _int64 带符号整型 ni64 //8字节 带符号整数 不能表示负数
    • UINT 类型变量 ui前缀 // 无符号整数 一般是4字节
    • //浮点数
    • float 单精度浮点数 fl前缀
    • double 双精度浮点数 fd前缀
  • 字符串
    • char*和char [] sz前缀 //PCHAR szp
  • CString str前缀
  • 结构名 T开头全大写
  • 类名 C开头单词首字大写

整合游戏数据步骤

  1. 新建解决方案GameData

  2. 创建头文件BaseGame.h和 StructGame.h


    • BaseGame.h
    #pragma once
    
    // 游戏人物的基址
    #define BaseRole 0x02B8A6D8
    

    • StructGame.h
    #pragma once
    #include <windows.h>
    typedef unsigned __int64 QWORD;
    // 游戏结构以及偏移量的管理
    
    typedef struct TROLE_PROPERTY {
    
    	// +0:人物名字
    	char* szRoleName;
    	// + 80:生命值(红 / HP)
    	DWORD ndRoleHP;
    	// + 84:内功值(蓝 / MP)
    	DWORD ndRoleMP;
    	// + 88:愤怒值
    	DWORD ndRoleAnger;
    	// + 8C:最大生命值
    	DWORD ndRoleMaxHP;
    	// + 90:最大内功值
    	DWORD ndRoleMaxMP;
    	// + 94:最大值愤怒值
    	QWORD nqRoleMaxAnger;
    	// + 98:当前经验值
    	QWORD nqRoleExprienceNow;
    	// + A0:升级到下一级要的经验值
    	DWORD ndRoleExperienceNext;
    	// + 36:名声
    	char* szReputation;
    	// + 34:一字节空间表示等级
    	BYTE nbClassValue;
    	// + 35:一字节空间表示 几转
    	BYTE nbJZ;
    	// + AC:历练
    	DWORD ndExprience;
    	// + C8:攻击
    	DWORD ndAttack;
    	// + CC:防御
    	DWORD ndDefense;
    	// + D4:回避
    	DWORD ndAvoid;
    	// + B0:心
    	DWORD ndHeart;
    	// + B4:气
    	DWORD ndGas;
    	// + B8:体
    	DWORD ndBody;
    	// + BC:魂
    	DWORD ndSoul;
    	// + E4:金币值
    	QWORD nqMoney;
    	// 气功
    	DWORD ndQg[32];
    
    	TROLE_PROPERTY* GetData();
    
    	char* GetRoleName();
    
    }_TROLE_PROPERTY;
    
  3. 创建源文件StructGame.cpp


    • StructGame.cpp
    #include "StructGame.h"
    #include "BaseGame.h"
    TROLE_PROPERTY* TROLE_PROPERTY::GetData()
    {
    
    	// 添加异常处理
    	try {
    		// +0:人物名字
    		szRoleName = (char*)BaseRole;
    		// + 80:生命值(红 / HP)
    		ndRoleHP = (DWORD)(BaseRole + 0x80);
    		// + 84:内功值(蓝 / MP)
    		ndRoleMP = (DWORD)(BaseRole + 0x84);
    		// + 88:愤怒值
    		ndRoleAnger = (DWORD)(BaseRole + 0x88);
    		// + 8C:最大生命值
    		ndRoleMaxHP = (DWORD)(BaseRole + 0x8c);
    		// + 90:最大内功值
    		ndRoleMaxMP = (DWORD)(BaseRole + 0x90);
    		// + 94:最大值愤怒值
    		nqRoleMaxAnger = (QWORD)(BaseRole + 0x94);
    		// + 98:当前经验值
    		nqRoleExprienceNow = (QWORD)(BaseRole + 0x98);
    		// + A0:升级到下一级要的经验值
    		ndRoleExperienceNext = (DWORD)(BaseRole + 0xA0);
    		// + 36:名声
    		szReputation = (char*)(BaseRole + 0x36);
    		// + 34:一字节空间表示等级
    		nbClassValue = *(BYTE*)(BaseRole + 0x34);
    		// + 35:一字节空间表示 几转
    		nbJZ = *(BYTE*)(BaseRole + 0x35);
    		// + AC:历练
    		ndExprience = (DWORD)(BaseRole + 0xac);
    		// + C8:攻击
    		ndAttack = (DWORD)(BaseRole + 0xc8);
    		// + CC:防御
    		ndDefense = (DWORD)(BaseRole + 0xcc);
    		// + D4:回避
    		ndAvoid = (DWORD)(BaseRole + 0xd4);
    		// + B0:心
    		ndHeart = (DWORD)(BaseRole + 0xb0);
    		// + B4:气
    		ndGas = (DWORD)(BaseRole + 0xb4);
    		// + B8:体
    		ndBody = (DWORD)(BaseRole + 0x8c);
    		// + BC:魂
    		ndSoul = (DWORD)(BaseRole + 0xbc);
    		// + E4:金币值
    		nqMoney = (QWORD)(BaseRole + 0xe4);
    
    		for (int i = 0; i < 32; i++) {
    			ndQg[i] = *(BYTE*)(BaseRole + 0xf0 + 4 * (i + 1));
    		}
    	}
    	catch (...) {
    		// 处理所有的异常
    		OutputDebugStringA("读取人物数据异常\r\n");
    	}
    	
    
    
    	return this;
    }
    
    // 获取角色的名称、
    char* TROLE_PROPERTY::GetRoleName() {
    	return GetData()->szRoleName;
    }
    

VS2019 导入静态链接库(lib)、配置输出路径的方式

  • 配置链接库路径

    • 配置 附加包含目录

    • 配置 添加库目录

  • 配置编译输出路径

    • 修改输出目录

游戏中的物品使用

  • 在游戏中,对应的物品都会有一个结构/类,包含了物品的一些信息
  • 使用物品实际上调用了应该CALL
以金疮药为例
  • 寻CALL 的过程

    • 使用CE工具找到对象地址指针

    • 去查看访问改指针的地址

    • 使用OD 工具对这些地址进行动态调试

    • 远程注入代码(使用金疮药)

    • push 1
      push 1
      push 0
      mov ecx, 21DF06D0
      call 00838470
      
背包数据的分析
  • 背包在游戏中一般会写成应该结构体/类来存放物品对象
  • 物品对象在背包中使用数组的形式存在
  • 汇编中数组的访问方式一般是 数组基址 + 4 * i(' i '为数组下标)
  • 查找背包数组基址:
    • 找到背包的物品格
    • 反复讲里面的物品拿出/放入
    • 使用CE工具进程分析
  • 结果
    • 存放背包基址的内存空间:0x02DAF3E4
    • 第num 个格子的数据获取
      • *背包基址+num*4+0x43C
      • 注:0x43 是偏移量
      • 物品对象指针 + 0x64 = 物品名字
      • 物品对象指针 + 0xf9 = 对物品的描述
      • 物品对象指针 + 0xC4C = 物品剩余数量

封装背包数据

封装背包结构体

// 物品结构
typedef struct TBACKPACK_GOODS {

	// *物品对象指针 + 0x64 = 物品名字
	char* szGoodsName;
	// * 物品对象指针 + 0xf9 = 对物品的描述
	char* szGoodsIntro;
	// * 物品对象指针 + 0xC4C = 物品剩余数量
	DWORD ndGoodsNum;

} _TBACKPACK_GOODS;

// 背包结构
typedef struct TGOODSLIST_PROPERTY {
	_TBACKPACK_GOODS mtGoodsList[nGoodsNum];

	// 对数据的初始化
	TGOODSLIST_PROPERTY* getData();
}_TGOODSLIST_PROPERTY;

实现初始化方法(getDate())

TGOODSLIST_PROPERTY* TGOODSLIST_PROPERTY::getData()
{

	// 通过获取背包基址对每样物品进行分析

// *物品对象指针 + 0x64 = 物品名字
#define GOODSNAME 0x64
// * 物品对象指针 + 0xf9 = 对物品的描述
#define GOODSINTRO 0xf9
// * 物品对象指针 + 0xC4C = 物品剩余数量
#define GOODSNUM 0xc4c

	// 背包公式: ndBaseAddr + num*4 + 0x43c
	try {
		// 读取背包基址
		DWORD ndBase = *(DWORD*)(BaseBackpack);
		// 第一个物品的地址
		DWORD ndFirstGoodsBase = ndBase + 4 * 0 + 0x43c;
		// 第一个物品的对象
		DWORD ndObj = NULL;
		for (int i = 0; i < nGoodsNum; i++) {
			ndObj = *(DWORD*)(ndFirstGoodsBase + 4 * i); // 取出第i个对象的地址
			if (ndObj == NULL) {
				// 如果读取数据为0===> 背包这一格没有物品
				this->mtGoodsList[i].ndGoodsNum = 0;
				continue;
			}
			// 读取物品的名字
			this->mtGoodsList[i].szGoodsName = (char*)(ndObj + GOODSNAME);
			// 读取物品的介绍
			this->mtGoodsList[i].szGoodsIntro = (char*)(ndObj + GOODSINTRO);
			// 读取物品的剩余数量
			this->mtGoodsList[i].ndGoodsNum = *(DWORD*)(ndObj + GOODSNUM);

		}
	}
	catch (...) {
		// 处理所有异常
		OutputDebugStringA("读取背包数据异常\r\n");
		MessageBox(NULL, "读取背包数据异常(StructGame)", "Error", MB_OK);
	}

	return this;
}

调试调用

void CMainDialogWnd::OnBnClickedButton1()
{
	TROLE_PROPERTY role;
	TROLE_PROPERTY* r = role.GetData();
	TGOODSLIST_PROPERTY goods;

	TRACE("GameDebug:我的调试信息\r\n");
	TRACE("GameDebug: 人物名=%s\r\n", r->GetRoleName());
	TRACE("GameDebug: 人物等级=%d\r\n", r->nbClassValue);
	TRACE("GameDebug: 人物名声=%s\r\n", r->szReputation);
	TRACE("GameDebug: 人物血量HP=%d//%d\r\n", r->ndRoleHP, r->ndRoleMaxHP);
	TRACE("GameDebug: 人物内功MP=%d//%d\r\n", r->ndRoleMP, r->ndRoleMaxMP);
	TRACE("GameDebug: 人物愤怒值=%d\r\n", r->ndRoleAnger);
	TRACE("GameDebug: 人物金币=%d\r\n", r->nqMoney);
	TGOODSLIST_PROPERTY* g = goods.getData();
	try {
		for (int i = 0; i < nGoodsNum; i++) {
			if (g->mtGoodsList[i].ndGoodsNum == 0) {
				continue;
			}
			TRACE("GameDebug: 人物第%d格数据:%s\r%s\r%d\r\n", i,
				g->mtGoodsList[i].szGoodsName,
				g->mtGoodsList[i].szGoodsIntro,
				g->mtGoodsList[i].ndGoodsNum
			);
		}
	}
	catch (...) {
		MessageBox(TEXT("读取背包数据异常(Dialog)"), TEXT("Error"), MB_OK);
	}


	// 进行数据修改
}

文件结构

背包物品的使用

之前Call 的分析

push 背包物品下标
push 1
push 0
mov ecx, 背包基址
call 00838470

封装函数

UseGoods(char* szGoodsName)
{
	// 若存在则使用它
	return 1;
}

背包物品使用代码

  • 定义基址
// 添加背包物品使用CALL 的地址 ===> 通过背包物品下标进行物品的使用
#define BaseCall_UseGoodsForIndex 0x00838470
  • 定义结构
// 背包结构
typedef struct TGOODSLIST_PROPERTY {
	// 背包列表
	_TBACKPACK_GOODS mtGoodsList[nGoodsNum];

	// 对数据的初始化
	TGOODSLIST_PROPERTY* getData();

	// 使用背包物品
	int UseGoodsForIndex(DWORD ndIndex);

	// 通过名字查询下标,存在返回下标,不存在返回FALSE
	int GetGoodsIndexByName(char* szGoodsName);

	// 根据物品的名字进行使用
	int UseGoodsForName(char* szGoodsName);
}_TGOODSLIST_PROPERTY;
  • 实现方法
// 通过物品下标使用物品
int TGOODSLIST_PROPERTY::UseGoodsForIndex(DWORD ndIndex) {

	try {
		// 使用内联汇编
		__asm {
			mov eax, ndIndex
			push eax
			push 1
			push 0
			// 读取背包地址
			mov ecx, [BaseBackpack]
			mov eax, BaseCall_UseGoodsForIndex
			call eax
		}
	}
	catch (...) {
		OutputDebugStringA("物品使用异常");
	}

	return TRUE;

}

int TGOODSLIST_PROPERTY::UseGoodsForName(char* szGoodsName)
{
	// 查找物品的下标
	DWORD ndIndex = this->GetGoodsIndexByName(szGoodsName);
	if (ndIndex != -1) {
		this->UseGoodsForIndex(ndIndex);
		return TRUE;
	}
	return FALSE;
}

int TGOODSLIST_PROPERTY::GetGoodsIndexByName(char* szGoodsName) {
	// 遍历整个背包,看是否存在该物品
	TGOODSLIST_PROPERTY* g = this->getData();// 初始化背包结构
	for (int i = 0; i < nGoodsNum; i++) {
		// 比较字符串,判断该物品是否存在
		if (strcmp(szGoodsName, g->mtGoodsList[i].szGoodsName) == 0) {
			return i;
		}
	}

	return -1;
}

  • 调用方法,实现物品的使用
if (g->UseGoodsForName("回城符(泫勃派)")) {
		TRACE("GameDebug: 使用 回城符(泫勃派) 成功");
	}

  • 文件结构

编写自定义的DbgPrintMine方法用于打印格式化调式信息

// DbgPrintMine.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <windows.h>
using namespace std;

// 定义变参函数
void DbgPrintMine(char* pszFormat, ...) {
#ifdef _DEBUG
	// 如果在DEBUG 版本下才执行以下代码
	// 定义list
	va_list argList;
	// 初始化list
	va_start(argList, pszFormat);
	// 定义字符串缓冲区
	char szBufFormat[0x1000];

	// 定义调试前缀
	char szBufFormat_Game[0x1008] = "Game:";


	// 获取参数 va_arg(list, paramType)
	/*int i = va_arg(argList, int);
	int j = va_arg(argList, int);
	char* szK = va_arg(argList, char*);*/


	vsprintf_s(szBufFormat, pszFormat, argList);
	strcat_s(szBufFormat_Game, szBufFormat);
	OutputDebugStringA(szBufFormat_Game);


	// 清除list
	va_end(argList);
#endif

}


int main()
{
	DbgPrintMine((char*)"%d, %d, %s\n", 1, 2, "贾谨荣");
	system("pause");
}



注:多线程访问数据造成异常的原因以及解决方式

造成异常的原因:

  • 游戏主线程与外挂线程同时访问共享数据区域,造成程序异常
  • 让两个线程依次使用共享数据或者将注入线程到主线程

模拟游戏主线程和辅助线程同时执行

  • 代码
// 定义变参函数
void DbgPrintMine(char* pszFormat, ...) {
#ifdef _DEBUG
	// 如果在DEBUG 版本下才执行以下代码
	// 定义list
	va_list argList;
	// 初始化list
	va_start(argList, pszFormat);
	// 定义字符串缓冲区
	char szBufFormat[0x1000];

	// 定义调试前缀
	char szBufFormat_Game[0x1008] = "Game:";


	// 获取参数 va_arg(list, paramType)
	/*int i = va_arg(argList, int);
	int j = va_arg(argList, int);
	char* szK = va_arg(argList, char*);*/


	vsprintf_s(szBufFormat, pszFormat, argList);
	strcat_s(szBufFormat_Game, szBufFormat);
	OutputDebugStringA(szBufFormat_Game);


	// 清除list
	va_end(argList);
#endif

}

DWORD g_ndGameData[10] = { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
DWORD* g_pndGameData[10];

void UseGoods(char* szGoodsName) {

	for (int i = 0; i < 10; i++) {
		DbgPrintMine("%s, %d\r\n", szGoodsName, *g_pndGameData[i]);
		Sleep(1 * 1000);
	}
	return;
}

DWORD WINAPI GameMainThreadProc(LPVOID lpData) {
	while (TRUE) {
		// 初始化内存
		memset(g_pndGameData, NULL, sizeof(g_pndGameData));
		for (int i = 0; i < 10; i++) {
			g_pndGameData[i] = g_ndGameData + i; // &g_ndGameData[i]
			Sleep(1000);
		}
		// 物品使用的CALL
		UseGoods("游戏主线程");
	}
}

DWORD WINAPI GameMyThreadProc(LPVOID lpData) {
	while (TRUE) {
		// 初始化内存
		
		UseGoods("外挂线程:222");
		Sleep(1 * 1000);
	}
}

// 游戏主线程
void CDataExceptionTestDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	CreateThread(NULL, NULL, GameMainThreadProc, NULL, 0, NULL);
}

// 外挂线程
void CDataExceptionTestDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	CreateThread(NULL, NULL, GameMyThreadProc, NULL, 0, NULL);
}

  • 异常

解决方式

  • 将程序注入到主线程

  • 使用临界区

    • 1

    • 2、代码

      // 定义变参函数
      void DbgPrintMine(char* pszFormat, ...) {
      #ifdef _DEBUG
      	// 如果在DEBUG 版本下才执行以下代码
      	// 定义list
      	va_list argList;
      	// 初始化list
      	va_start(argList, pszFormat);
      	// 定义字符串缓冲区
      	char szBufFormat[0x1000];
      
      	// 定义调试前缀
      	char szBufFormat_Game[0x1008] = "Game:";
      
      
      	// 获取参数 va_arg(list, paramType)
      	/*int i = va_arg(argList, int);
      	int j = va_arg(argList, int);
      	char* szK = va_arg(argList, char*);*/
      
      
      	vsprintf_s(szBufFormat, pszFormat, argList);
      	strcat_s(szBufFormat_Game, szBufFormat);
      	OutputDebugStringA(szBufFormat_Game);
      
      
      	// 清除list
      	va_end(argList);
      #endif
      
      }
      
      // 定义临界区;
      CRITICAL_SECTION lpCriticalSection;
      
      DWORD g_ndGameData[10] = { 111, 222, 333, 444, 555, 666, 777, 888, 999, 000 };
      DWORD* g_pndGameData[10];
      
      void UseGoods(char* szGoodsName) {
      	// 进入临界区
      	EnterCriticalSection(&lpCriticalSection);
      
      	for (int i = 0; i < 10; i++) {
      		DbgPrintMine("%s, %d\r\n", szGoodsName, *g_pndGameData[i]);
      		Sleep(1 * 100);
      	}
      	// 离开临界区
      	LeaveCriticalSection(&lpCriticalSection);
      
      	return;
      }
      
      DWORD WINAPI GameMainThreadProc(LPVOID lpData) {
      	while (TRUE) {
      		// 进入临界区
      		EnterCriticalSection(&lpCriticalSection);
      
      		// 初始化内存
      		memset(g_pndGameData, NULL, sizeof(g_pndGameData));
      		for (int i = 0; i < 10; i++) {
      			g_pndGameData[i] = g_ndGameData + i; // &g_ndGameData[i]
      			Sleep(1000);
      		}
      		// 离开临界区
      		LeaveCriticalSection(&lpCriticalSection);
      
      		// 腾出有点时间片给外挂线程使用
      		Sleep(1 * 1000);
      		// 物品使用的CALL
      		UseGoods("游戏主线程");
      	}
      }
      
      DWORD WINAPI GameMyThreadProc(LPVOID lpData) {
      	while (TRUE) {
      		// 初始化内存
      		
      		UseGoods("外挂线程:222");
      		Sleep(1 * 1000);
      	}
      }
      
      // 游戏主线程
      void CDataExceptionTestDlg::OnBnClickedButton1()
      {
      	// TODO: 在此添加控件通知处理程序代码
      	CreateThread(NULL, NULL, GameMainThreadProc, NULL, 0, NULL);
      }
      
      // 外挂线程
      void CDataExceptionTestDlg::OnBnClickedButton2()
      {
      	// TODO: 在此添加控件通知处理程序代码
      	CreateThread(NULL, NULL, GameMyThreadProc, NULL, 0, NULL);
      }
      
      
      void CDataExceptionTestDlg::OnBnClickedOk()
      {
      	// TODO: 在此添加控件通知处理程序代码
      	CDialogEx::OnOK();
      }
      
      
      void CDataExceptionTestDlg::OnBnClickedButton3()
      {
      	// TODO: 在此添加控件通知处理程序代码
      	// 初始化临界区
      	InitializeCriticalSection(&lpCriticalSection);
      }
      
      

将代码注入游戏的主线程

  • 关键词

    SetWindowsHooksExa UnhookWindowsHookEx CWPSTRUCT
    

定义方法

#pragma once

// HookGameMainThread.h
#define MSG_USEGOODSFORNAME 1 //使用物品的消息种类

// 挂载主线程
DWORD HookMainThread();

// 卸载主线程
DWORD UnHookMainThread();

DWORD msgUseGoodsForName(char* szpName);

实现方法

// HookGameMainThread.cpp
#include "StructGame.h"
#include "HookGameMainThread.h"

HHOOK g_hhkGame;
const DWORD MyMsgCode = RegisterWindowMessageA("MyMsgCode");
// 回调函数
LRESULT CALLBACK GameWndProc(
	int nCode,
	WPARAM wParam,
	LPARAM lParam
) {
	CWPSTRUCT* lpArg = (CWPSTRUCT*)lParam;
	if (nCode == HC_ACTION) {
		if (lpArg->hwnd == GetGameWndHandle() && lpArg->message == MyMsgCode) {
			DbgPrintMine((char*)("消息传到	%s\r\n"), lpArg->lParam);
			switch (lpArg->wParam)
			{
			case MSG_USEGOODSFORNAME: {
				TGOODSLIST_PROPERTY goods;
				TGOODSLIST_PROPERTY* g = goods.getData();
				if (g->UseGoodsForName((char*)lpArg->lParam)) {
					DbgPrintMine((char*)("使用 %s 成功"), lpArg->lParam);
				}
			}; break;
			default:
				break;
			}
			return 1;
		}
	}
	return CallNextHookEx(g_hhkGame, nCode, wParam, lParam);
}
DWORD HookMainThread() {

	HWND hGame = GetGameWndHandle();
	DWORD ndThreadId = GetWindowThreadProcessId(hGame, NULL);
	if (ndThreadId != 0) {
		// 安装钩子
		g_hhkGame = SetWindowsHookEx(WH_CALLWNDPROC, GameWndProc, NULL, ndThreadId);
	}
	return 1;
}


DWORD UnHookMainThread() {
	UnhookWindowsHookEx(g_hhkGame);
	return 1;
}

DWORD msgUseGoodsForName(char* szpName) {
	// 传递消息(句柄、自定义的注册消息、自定义消息类别、消息内容(字符串))
	SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_USEGOODSFORNAME, (LPARAM)szpName);
	return 1;
}

调用方法

// 连接主线程
void CMainDialogWnd::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	HookMainThread();
}


void CMainDialogWnd::OnBnClickedButton3()
{
	// TODO: 在此添加控件通知处理程序代码
	UnHookMainThread();
}


void CMainDialogWnd::OnBnClickedButton4()
{
	// TODO: 在此添加控件通知处理程序代码
	msgUseGoodsForName("金创药(小)");
}

文件结构

运行效果

分析怪物列表

  • 分析思路:从怪物明显的属性入手:名字、血量

    1*4+427EBA0 //怪物列表基址(1-5)

    +8 种类/2E:怪物

    +354 显示血条

    +C 怪物选中参数

    +5f4 怪物血量

    +5f8 怪物等级

    +360 怪物名字

    +1060 怪物位置X

    +1068 怪物位置Y

    +3C0 怪物生命状态 0活/1死

    [0427EBA0] //角色对象指针

    +8 //角色分类31人物/2E

    +18 //角色名字

封装怪物对象属性

  • 定义基址

    // 怪物列表基址
    #define BaseMonseterList 0x427EBA4
    
  • 定义结构

    // 怪物结构
    typedef struct TMonseterObj {
    	//+5f4 怪物血量
    	DWORD ndHp;
    	//+ 5f8 怪物等级
    	DWORD ndLevel;
    	//+ 360 怪物名字
    	char* szMName;
    	//+ 1060 怪物位置X
    	float flX;
    	//+ 1068 怪物位置Y
    	float flY;
    	//+ 3C0 怪物生命状态 0活 / 1死
    	BOOL IsDead;
    }_TMonseterObj;
    
    // 怪物列表
    #define MONSETERNUM 20
    typedef struct TMonseterList {
    
    	_TMonseterObj tMonList[MONSETERNUM];
    
    	// 初始化
    	TMonseterList* getData();
    
    	// 打印信息
    	BOOL dbgPrintMsg();
    }_TMonseterList;
    
  • 实现结构方法

    TMonseterList* TMonseterList::getData()
    {
    	DWORD ndObj = NULL;
    	//memset(this, 0, sizeof(TMonseterList));
    	try
    	{
    		for (int i = 0; i < MONSETERNUM; i++) {
    			ndObj = *(DWORD*)(BaseMonseterList + 4 * i);
    			if (ndObj == 0) {
    				this->tMonList[i].ndLevel = 0;
    				continue;
    			}
    			// 怪物名字
    			this->tMonList[i].szMName = (char*)(ndObj + 0x360);
    			// 怪物血量
    			this->tMonList[i].ndHp = *(DWORD*)(ndObj + 0x5f4);
    			// 怪物等级
    			this->tMonList[i].ndLevel = *(DWORD*)(ndObj + 0x5f8);
    			// 怪物位置X
    			this->tMonList[i].flX = *(float*)(ndObj + 0x1060);
    			// 怪物位置Y
    			this->tMonList[i].flY = *(float*)(ndObj + 0x1068);
    			// 怪物生命状态
    			this->tMonList[i].IsDead = *(BOOL*)(ndObj + 0x3c0);
    		}
    	}
    	catch (...)
    	{
    		// 处理所有的异常
    		DbgPrintMine((char*)"读取怪物数据异常");
    	}
    	return this;
    }
    
    BOOL TMonseterList::dbgPrintMsg() {
    	for (int i = 0; i < MONSETERNUM; i++) {
    		if (tMonList[i].ndLevel == 0) {
    			continue;
    		}
    		DbgPrintMine((char*)("%s,等级:%d级;血量:%d;当前位置X:%f Y:%f;生命状态:%d"),
    			tMonList[i].szMName,
    			tMonList[i].ndLevel,
    			tMonList[i].ndHp,
    			tMonList[i].flX,
    			tMonList[i].flY,
    			tMonList[i].IsDead);
    	}
    	return TRUE;
    }
    
  • 在HOOK 内定义测试方法

    #define MSG_TEST 2 // 测试使用消息
    
    // 测试怪物
    DWORD msgTest(LPVOID lpData);
    
  • 实现

    // HookGameMainThread.cpp
    #include "StructGame.h"
    #include "HookGameMainThread.h"
    
    HHOOK g_hhkGame;
    const DWORD MyMsgCode = RegisterWindowMessageA("MyMsgCode");
    // 回调函数
    LRESULT CALLBACK GameWndProc(
    	int nCode,
    	WPARAM wParam,
    	LPARAM lParam
    ) {
    	CWPSTRUCT* lpArg = (CWPSTRUCT*)lParam;
    	if (nCode == HC_ACTION) {
    		if (lpArg->hwnd == GetGameWndHandle() && lpArg->message == MyMsgCode) {
    			DbgPrintMine((char*)("消息传到	%s\r\n"), lpArg->lParam);
    			switch (lpArg->wParam)
    			{
    			case MSG_USEGOODSFORNAME: {
    				TGOODSLIST_PROPERTY goods;
    				TGOODSLIST_PROPERTY* g = goods.getData();
    				if (g->UseGoodsForName((char*)lpArg->lParam)) {
    					DbgPrintMine((char*)("使用 %s 成功"), lpArg->lParam);
    				}
    			}; break;
    			
    ////////////////////////////////////////////////////////////////////////
    			case MSG_TEST: {
    				TMonseterList tMonList;
    				TMonseterList* ptMonList = tMonList.getData();
    				ptMonList->dbgPrintMsg();
    			}; break;
    			default:
    				break;
    			}
    			return 1;
    		}
    	}
    	return CallNextHookEx(g_hhkGame, nCode, wParam, lParam);
    }
    DWORD HookMainThread() {
    
    	HWND hGame = GetGameWndHandle();
    	DWORD ndThreadId = GetWindowThreadProcessId(hGame, NULL);
    	if (ndThreadId != 0) {
    		// 安装钩子
    		g_hhkGame = SetWindowsHookEx(WH_CALLWNDPROC, GameWndProc, NULL, ndThreadId);
    	}
    	return 1;
    }
    
    
    DWORD UnHookMainThread() {
    	UnhookWindowsHookEx(g_hhkGame);
    	return 1;
    }
    
    DWORD msgUseGoodsForName(char* szpName) {
    	// 传递消息(句柄、自定义的注册消息、自定义消息类别、消息内容(字符串))
    	SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_USEGOODSFORNAME, (LPARAM)szpName);
    	return 1;
    }
    
    ////////////////////////////////////////////////////////////////////////
    DWORD msgTest(LPVOID lpData)
    {
    	SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_TEST, (LPARAM)lpData);
    	return 0;
    }
    
    
  • 控件调用

    void CMainDialogWnd::OnBnClickedButton5()
    {
    	// TODO: 在此添加控件通知处理程序代码
    	msgTest(NULL);
    }
    
    
  • 目录结构

    img

分析动作数组(攻击和打坐)

  • 思路:
    • 通过选中的对象逆向回溯出动作的数组
    • 通过动作对象访问逆向回溯到攻击CALL 附近
    • 封包断点bp WSASend
  1. 通过选中动作,利用CE 查找基址

  2. 使用OD 分析访问内存信息,得到基址

    0082D8F2    8D8CB7 3C040000 LEA ECX,DWORD PTR DS:[EDI+ESI*4+43C]
    
    

    动作公式:[02e3bd58]+43c+4*0

  3. 找动作的CALL

    • 使用CE 分析动作对象的调用访问

    • 得到一下信息

      // 攻击
      008530CE - 6A 01 - push 01
      008530D0 - E8 0B4AFBFF - call Client.exe+407AE0
      008530D5 - 8B 8C B7 3C040000  - mov ecx,[edi+esi*4+0000043C] <<
      008530DC - 85 C9  - test ecx,ecx
      008530DE - 74 62 - je Client.exe+453142
      
      008544B9 - 83 BF 34160000 35 - cmp dword ptr [edi+00001634],35
      008544C0 - 75 20 - jne Client.exe+4544E2
      008544C2 - 8B 84 B7 3C040000  - mov eax,[edi+esi*4+0000043C] <<
      008544C9 - 85 C0  - test eax,eax
      008544CB - 74 15 - je Client.exe+4544E2
      
      
      
    • 使用OD分析得动作CALL为

      mov edi, [02E3CD58]
      mov esi, 下标
      MOV EAX,DWORD PTR DS:[EDI+ESI*4+43C]
      mov ecx, [eax+0x54]
      push ecx
      CALL 007139E0
      
      
      

封装动作数组功能

  • 封装动作对象
  • 封装动作对象列表
  • 封装使用对象功能函数

封装

  1. 封装基址

    // 人物动作使用的CALL 的基址
    #define BaseActionCall 0x00713970
    
  2. 封装结构

    // 动作对象的结构
    typedef struct TActionObj {
    
    	// 对象名字
    	char* szpName;
    	// 调用CALL 的参数
    	DWORD ndActionID;
    
    }_TActionObj;
    
    // 动作对象数组
    #define ActionNum 18
    typedef struct TCActionList {
    
    	// 定义动作数组
    	_TActionObj tList[ActionNum];
    
    	// 初始化
    	TCActionList* getData();
    
    	// 打印信息
    	BOOL TestActionMsg();
    
    	// 使用动作通过下标
    	BOOL UseActionByIndex(DWORD ndIndex);
    
    	// 使用动作通过名字
    	BOOL UseActionByName(char* szpName);
    
    }_TCActionList;
    
  3. 实现结构方法

    TCActionList* TCActionList::getData()
    {
    	//dc [[02e3bd58]+ 43c + 4 * 0] + 64
    	//+ 64 动作名字
    
    	//[[02e3bd58]+ 43c + 4 * 0] + 54
    	//+ 54 调用CALL参数
    
    	DWORD ndFirstObj = 0;
    	DWORD ndObj;
    	try {
    		ndFirstObj = (*(DWORD*)(BaseActionList))+0x43C;
    		for (int i = 0; i < ActionNum; i++) {
    			ndObj = *(DWORD*)(ndFirstObj + 4 * i);
    			if (ndObj == NULL) {
    				tList[i].ndActionID = 0;
    				continue;
    			}
    			tList[i].szpName = (char*)(ndObj + 0x64);
    			tList[i].ndActionID = *(DWORD*)(ndObj + 0x54);
    		}
    	}
    	catch (...) {
    		DbgPrintMine((char*)("内存读取异常"));
    	}
    
    	return this;
    }
    
    BOOL TCActionList::TestActionMsg()
    {
    	for (int i = 0; i < ActionNum; i++) {
    		if (tList[i].ndActionID == 0) {
    			continue;
    		}
    		DbgPrintMine((char*)("动作名:%s, 动作ID:%X"),
    			tList[i].szpName,
    			tList[i].ndActionID);
    
    	}
    	return TRUE;
    }
    
    
    
    DWORD getObjByIndex(char* szpName) {
    	TCActionList tList;
    	TCActionList* ptList = tList.getData();
    	for(int i = 0; i < ActionNum; i++) {
    		if (strcmp(szpName, ptList->tList[i].szpName) == 0) {
    			return i;
    		}
    	}
    	return -1;
    }
    
    BOOL UseAction(DWORD ndIndex) {
    	TCActionList tList;
    	TCActionList* ptList = tList.getData();
    	DWORD ndPrarm = ptList->tList[ndIndex].ndActionID;
    	try {
    		__asm {
    			mov ecx, ndPrarm
    			push ecx
    			mov eax, BaseActionCall
    			call eax
    		}
    	}
    	catch (...) {
    		DbgPrintMine((char*)("动作使用失败"));
    		return FALSE;
    	}
    	return TRUE;
    }
    
    BOOL TCActionList::UseActionByIndex(DWORD ndIndex)
    {
    	if (UseAction(ndIndex)) {
    		MessageBeep(0);
    		return TRUE;
    	}
    	return FALSE;
    }
    
    BOOL TCActionList::UseActionByName(char* szpName)
    {
    	DWORD ndIndex = getObjByIndex(szpName);
    	if (ndIndex != -1) {
    		if (UseAction(ndIndex)) {
    			MessageBeep(0);
    			return TRUE;
    		}
    	}
    	return FALSE;
    }
    
  4. 添加消息类型

    #define MSG_ACTIONTEST 3 //测试动作
    
  5. 在主线程内调用结构体方法

    case MSG_ACTIONTEST: {
    				TCActionList* ptLIst = tList.getData();
    				ptLIst->TestActionMsg();
    				//ptLIst->UseActionByIndex(1);
    				ptLIst->UseActionByName((char*)("攻击"));
    			}; break;
    
  6. 发送消息到主线程

    DWORD testActionMsg(LPVOID lpData) {
    	SendMessageA(GetGameWndHandle(), MyMsgCode, MSG_ACTIONTEST, (LPARAM)lpData);
    	return 0;
    }
    
  7. 绑定控件,执行方法

    void CMainDialogWnd::OnBnClickedButton6()
    {
    	// TODO: 在此添加控件通知处理程序代码
    	testActionMsg(NULL);
    }
    
  • 文件结构

    img

选怪功能实现

  • 实现怪物选中
  • 可能情况:
    • 选怪变量被赋值
    • 怪物是否被选中的属性

选怪功能相关地址

  • 玩家:

    [2E63A24] //存放的玩家对象的地址

    +3428 玩家是否被选中

  • 怪物:

    [2E63A24]+1A64

    选中怪物时,传入怪物的选中ID

    没选中怪物时,值为0xFFFF

选怪功能的封装

计算怪物与玩家的距离

更新ING

About

热血江湖外挂

Resources

Readme
Activity

Stars

20 stars

Watchers

5 watching

Forks

16 forks
Report repository

Releases

No releases published

Packages

No packages published

Languages

  • C++ 75.6%
  • C 24.4%

Footer

© 2024 GitHub, Inc.

两个鬼故事吃饺子店起名大全女总裁的贴身兵王小说昆明大师起名起聊天群的名字网站你懂我意思WWW正能量女婴起名大全集穿成耽美文炮灰女配奥普集成吊顶价格消杀公司起啥名字好五行缺土如何起名字电脑自动关机电池公司起什么名字好起名带土带金女孩名字制服诱惑2儿童游戏4399五行八字起名姓名测试机动天使苑起名大全女孩大明王朝1566剧情痞幼吃鸡是什么意思秦姓女儿起名高端大气的孙姓的男孩起名济南区号环保科技类公司起名大全片吧影院姚姓男宝起名字大全战神关键词工具小孩起名测试周易起名测名字打评分爱上特种兵电视剧全集(1-45)少年生前被连续抽血16次?多部门介入两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”淀粉肠小王子日销售额涨超10倍高中生被打伤下体休学 邯郸通报单亲妈妈陷入热恋 14岁儿子报警何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言张家界的山上“长”满了韩国人?男孩8年未见母亲被告知被遗忘中国拥有亿元资产的家庭达13.3万户19岁小伙救下5人后溺亡 多方发声315晚会后胖东来又人满为患了张立群任西安交通大学校长“重生之我在北大当嫡校长”男子被猫抓伤后确诊“猫抓病”测试车高速逃费 小米:已补缴周杰伦一审败诉网易网友洛杉矶偶遇贾玲今日春分倪萍分享减重40斤方法七年后宇文玥被薅头发捞上岸许家印被限制高消费萧美琴窜访捷克 外交部回应联合利华开始重组专访95后高颜值猪保姆胖东来员工每周单休无小长假男子被流浪猫绊倒 投喂者赔24万小米汽车超级工厂正式揭幕黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发当地回应沈阳致3死车祸车主疑毒驾恒大被罚41.75亿到底怎么缴妈妈回应孩子在校撞护栏坠楼外国人感慨凌晨的中国很安全杨倩无缘巴黎奥运校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变王树国卸任西安交大校长 师生送别手机成瘾是影响睡眠质量重要因素国产伟哥去年销售近13亿阿根廷将发行1万与2万面值的纸币兔狲“狲大娘”因病死亡遭遇山火的松茸之乡“开封王婆”爆火:促成四五十对奥巴马现身唐宁街 黑色着装引猜测考生莫言也上北大硕士复试名单了德国打算提及普京时仅用姓名天水麻辣烫把捣辣椒大爷累坏了

两个鬼故事 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化