当前位置:   article > 正文

通过开发深入解外挂原理【01】_c++ 开发内存外挂

c++ 开发内存外挂

课程简介

本教程面向任何学习过适合游戏黑客的编程语言(例如 C/C++、C#…)的人。
你不需要精通这门语言,但也没必要在这件事上思考太多,还不如去看一下基础,或者再去思考一下课程,或者先阅读另一个教程来巩固你的知识。
我会多次用一个或多个支持材料的链接在一个句子中涵盖重要原则,如果您不理解我的内容,需要点击链接并阅读建议的材料,否则您将不能达到最优的学习效果。
并且会用一些外挂例子来讲解,虽然不提倡外挂,但不得不说这确实是很能激发兴趣的一种学习例子。

本节会重点讲解两个Windows 函数,允许我们读取和写入另一个程序的内存,不言自明,名为ReadProcessMemoryWriteProcessMemory(缩写为 RPM 和 WPM)。

使用 RPM 函数可以告诉你的操作系统(OS):
“我想在此进程的内存地址 0x12345678 处读取 X 字节的数据,并将结果放在我的进程内存中的地址 0x87654321 处”。
也就是读取内存,例如外挂中的方框透视等,就是通过读取内存,从而绘制方框实现的。
同理WPM为改写内存,将目标的数值改为我们想要的,例如外挂中,将玩家的生命值保持在最大值,给予无限弹药等等……

编写我们的虚拟目标程序

这个练习有两个目标:
首先,为我们提供一个我们将尝试破解的虚拟极简程序,它将模拟我们的目标游戏/程序。
其次,是了解开发的过渡:
如果您在第一次练习中遇到困难,则应考虑您没有达到本教程的水平,相反您应该阅读更多您的编程语言的课程或先获得更多的经验。
如果你在缺乏经验的情况下坚持并试图强迫自己的方式,你很可能会感到沮丧并过早地放弃你的黑客冒险。

说明

您将编写一个简单的控制台程序,所有内容都将进入 main(),它执行以下操作:

  • 声明一个名为 varInt 的变量,其类型为等于 123456 的整数
  • 声明一个名为 varString 的变量,其类型为字符串,文本为“DefaultString "(仅限 C++,如果用 C 编写则忽略)
  • 声明一个名为 arrChar 的 char 数组,大小为 128,文本为“Long char array right there ->”(您可以将大小放在声明的常量中)
  • 声明指向指向 varInt 的名为 ptr2int 的整数
  • 声明一个指向名为 ptr2ptr 的指向 ptr2int 的指针的指针
  • 声明一个指向名为 ptr2ptr2 的 int 指针的指针,指向 ptr2ptr

完成此操作后,您将创建一个无限循环,您将在其中:

  • 打印到控制台“进程 ID:”,后跟使用 Windows 的程序的进程 ID API 函数GetCurrentProcessId()(因此,不要忘记 #include <Windows.h> !!)
  • 打印到控制台“varInt (0x[ADDRESS OR THE VARIABLE]) = [VALUE OF THE VARIABLE]”(记住你可以通过在变量前面加上“&”来获取变量的地址)
  • varString 和 varChar 也是如此
  • 我们的 3 个指针也是如此,例如打印“ptr2int (0x[ADDRESS OF THE POINTER]) = 0x[ADDRESS POINTED] )
  • 打印到控制台“按 ENTER 再次打印”。
  • 暂停一些东西,您可以使用 getchar() 或 system(“pause > nul”)
  • 打印到控制台一行破折号(“-”),然后让它循环回到开头这是预期的输出本练习结束(按回车键一次后):

打印控制台进程 ID 或内存地址时可能会遇到问题,因为进程 ID 应以十进制打印,内存地址以十六进制打印。
除此之外,由于我有点强迫症,我喜欢我的内存地址是大写的。
在 C++ 中,您可以这样做以十进制输出:
代码:

cout << "十进制:" << dec << 255 << endl ;    
  • 1

这将输出“十进制:255”。
就像这样在十六进制大写(我添加一个前缀“0x”让读者知道下面是十六进制):
代码:

cout << "十六进制:0x" << hex <<大写<< 255 << endl ;    
  • 1

这将输出“In hexadecimal: 0xFF”
…不要忘记“using namespace std;” 在您的#includes 和#declares 之后,否则您可以添加前缀“std::”(例如“ std::cout << “In hexadecimal: 0x” << std::hex << std::uppercase << 255 << std::endl; ")

解决方案(C++)

不要直接阅读解决方案,你应该能成功编写这样一个简单的程序。
如果有困难,不要犹豫,重新开始。
在您决定揭示解决方案之前,编写一些令人满意的代码或至少花 45-60 分钟尝试。
如果你失败了,不得不查看解决方案,请仔细阅读,然后自己重​​新编码。

#include <Windows.h>
#include <iostream>
#include <string>
#define CHAR_ARRAY_SIZE 128

using namespace std;

int main() {
	int varInt(123456);
	string varString("DefaultString");
	char arrChar[CHAR_ARRAY_SIZE] = "Long char array right there ->";
	int* ptr2int(&varInt);
	int** ptr2ptr(&ptr2int);
	int*** ptr2ptr2(&ptr2ptr);

	do {
		cout << "Process ID: " << dec << GetCurrentProcessId() << endl;
		cout << endl;
		cout << "varInt       (0x" << hex << uppercase << (uintptr_t)&varInt << ") = " << dec << varInt << endl;
		cout << "varInt       (0x" << hex << uppercase << (uintptr_t)&varInt << ") = " << dec << varInt << endl;
		cout << "varString    (0x" << hex << uppercase << (uintptr_t)&varString << ") = " << varString << endl;
		cout << "arrChar[" << dec << CHAR_ARRAY_SIZE << "] (0x" << hex << uppercase << (uintptr_t)&arrChar << ") = " << arrChar << endl;
		cout << endl;
		cout << "ptr2int      (0x" << hex << uppercase << (uintptr_t)&ptr2int << ") = 0x" << ptr2int << endl;
		cout << "ptr2ptr      (0x" << hex << uppercase << (uintptr_t)&ptr2ptr << ") = 0x" << ptr2ptr << endl;
		cout << "ptr2ptr2     (0x" << hex << uppercase << (uintptr_t)&ptr2ptr2 << ") = 0x" << ptr2ptr2 << endl;
		cout << endl;
		cout << "Press ENTER to print again." << endl;
		getchar();
		cout << endl << "---------------------------------------------------" << endl << endl;
	} while (true);

	return EXIT_SUCCESS;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

注意:不要被变量地址前的“(uintptr_t)”吓到,这是可选的,只允许输出它们的地址,开头没有不必要的零,它只是让它更干净,我有点强迫症看,程序中的其他变量会用于后面的使用

代码输出如图
在这里插入图片描述
现在我们有了这个虚拟程序,我们已经准备好开始正确地破解了。
我们现在要从另一个程序中读取这个虚拟程序内存中这些变量的值并覆盖它们,就像你将要进入你的游戏黑客一样。

读取另一个进程的内存

在第二个练习中,我们将使用函数ReadProcessMemory从我们的虚拟程序的内存中读取一些变量。
首先,阅读MSDN 上的 ReadProcessMemory链接 文档页面,以了解此函数的工作原理以及我们应该将其作为参数提供什么。
仔细阅读整个页面,在继续阅读下一段之前尝试真正想象它是如何工作的。

BOOL WINAPI ReadProcessMemory (
  _In_   HANDLE hProcess ,
  _In_   LPCVOID lpBaseAddress ,
  _Out_ LPVOID lpBuffer ,
  _In_   SIZE_T nSize ,
  _Out_ SIZE_T   * lpNumberOfBytesRead
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

对于不熟悉 Windows API 的人来说 ,第一个参数 In HANDLE hProcess 会很难理解。
操作系统 Windows 的众多角色之一是管理各种资源、访问和权限。
在我们的例子中,我们需要请求访问我们的目标程序,并拥有足够的权限来读写它的内存。
这就是“HANDLE”类型的第一个参数,它本质上是操作系统给出的授权。
为了避免混淆,只需将所有 HANDLE 类型的值视为操作系统给出的授权号。
为了获得这个必需的 授权号,我们将使用 Windows API 函数OpenProcess链接**。

代码原型

HANDLE WINAPI OpenProcess(
  _In_ DWORD dwDesiredAccess,
  _In_ BOOL  bInheritHandle,
  _In_ DWORD dwProcessId
);
  • 1
  • 2
  • 3
  • 4
  • 5

第一个参数_In_ DWORD dwDesiredAccess
这是我们请求的权限权限解释链接
您可以在文档中看到许多权限,从 PROCESS_ALL_ACCESS(进程对象的所有可能访问权限)到 PROCESS_QUERY_LIMITED_INFORMATION(检索有关进程的某些信息所必需)以及许多其他权限,快速浏览一下所有这些权限,如果你不明白其中一些允许你做什么,没关系,你今天不会完全理解。
为简单起见,我们将使用 PROCESS_ALL_ACCESS 请求目标进程的所有可能权限。
好的做法是将请求的权限限制在有用的最低限度,但我们稍后会处理这个问题。

第二个参数_In_ BOOL bInheritHandle
这个定义是子进程可以继承的句柄(请求的授权)。
我们并不真正关心这一点,简而言之,我们的程序将请求一个句柄,如果我们将其设置为 TRUE,我们程序的所有子进程也将拥有此授权。
由于我们不需要它,我们将其设置为 FALSE

第三个参数_In_ DWORD dwProcessId
这是我们想要访问的进程的唯一进程标识号,通常缩写为 Process ID 甚至 PID。
我们在虚拟程序中使用 GetCurrentProcessId() 将其打印到控制台。

否则即使没有它,我们也可以使用这个好的旧任务管理器或任何管理软件(如 Process Hacker 或 Process Explorer)找到它:

然后我们会给它我们的虚拟程序的 PID。

如果你阅读了足够多的 OpenProcess 文档,你会读到:

最初由MSDN发表 如果函数失败,则返回值为 NULL。要获取扩展错误信息,请调用 GetLastError。

然后,我们可以在代码中添加一个简单的检查,确保一切顺利。
所以我们的代码应该是这样的:

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, [OUR DUMMY PROGRAM PID]);
if (hProcess == NULL) { // Failed to get a handle
	cout << "OpenProcess failed. GetLastError = " << dec << GetLastError() << endl;
	system("pause");
	return EXIT_FAILURE;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们现在有了 ReadProcessMemory 的第一个参数,继续看
第二个参数“_In_LPCVOID lpBaseAddress”。
LPCVOID 只是一个 void* const(LP 代表 Long Pointer,C 代表 Constant,VOID 代表…是的, void)
标准库 C/C++文档相当清楚:“指向要从中读取的指定进程中的基地址的指针。”

下一个参数“_Out_LPVOID lpBuffer”。

最初由MSDN发表
指向从指定进程的地址空间接收内容的缓冲区的指针。

“缓冲区”仅表示内存中的位置。
作为、练习的第一个任务,我们将只读取第一个整数类型变量,数字 123456。
因此,我们将声明一个初始化为 0 的整数类型变量,名为“intRead”(int intRead = 0 ; ) 在我们的 hack 程序中,它将读取虚拟程序的内存,我们将提供该变量的地址 (&intRead) 作为缓冲区。
因此,如果 ReadProcessMemory 成功,我们的变量 intRead 将等于我们试图在目标进程中读取的变量的任何值。

倒数第二个参数“_In_SIZE_T nSize”。
这只是要读取的大小。
在我们的第一个练习中,我们正在读取一个整数。
如果您还记得您的课程,整数通常大小为 4 个字节,因此我们可以给函数“4”,但我们更愿意给它 sizeof(int),它会自动给它一个 int 的大小。
如果我们想读取一些文本,例如一个字符数组,我们会给它数组的大小,或者我们想要读取的文本的大小。

最后一个参数“*_Out_SIZE_T lpNumberOfBytesRead”允许我们在 SIZE_T 类型的变量中检索读取的字节数,函数参数非常清楚地表明了这一点。
因此,我们可以声明一个名为 bytesRead 的变量 SIZE_T,初始化为 0 ( SIZE_T bytesRead = 0; ),并为 ReadProcessMemory 提供其地址 (&bytesRead)。
这对于错误检查很有用,但是为了简单起见,因为这个参数是可选的(如文档中所示),我们只需将其设置为 NULL。
ReadProcessMemory 返回一个 BOOL (TRUE/FALSE),让我们知道一切是否顺利,我们可以使用它来检查操作是否成功。

我们现在拥有读取虚拟程序内存所需的一切。
如果你是初学者,这似乎需要学习很多东西,在这个练习之后,应该会清楚得多。

编写一个控制台程序,它将:

  • 声明一个名为“intRead”的整数类型变量,初始化为 0
  • 获取我们正在运行的虚拟程序的句柄(使用 OpenProcess 并使用我在课程中提供的检查代码检查它是否成功,如果出现错误,请使用错误代码调查原因)
  • 读取变量“varInt”的值" 在我们的虚拟程序中并将结果存储在我们的变量 “intRead” 中(使用 ReadProcessMemory 和您获得的句柄,我们的虚拟程序控制台中显示的内存地址,不要忘记添加前缀 (LPCVOID) 键入
    -强制转换它以便函数不会打扰你,将结果存储在我们的变量“intRead”中,只需通过 &intRead 给出其内存地址,将其用作缓冲区,使用 sizeof(int) 作为大小,并让最后一个可选参数为空)
  • 将我们的变量“intRead”的新值打印到控制台( cout << "intRead = " << dec << intRead << endl; )
  • 向控制台打印一条消息“按 ENTER 退出”。并使用 getchar() 或 system(“pause > nul”) 暂停执行;

预期的输出结果应该是
intRead = 123456

如果你的输出结果不对,看完故障排除后重新编写,一定要自己尝试编写后在看代码

故障排除

如果 OpenProcess 失败:
确保您的 2 个进程以相同的权限运行,如果虚拟程序以管理员身份运行,而不是另一个,它可能会失败。
如果您直接从 Visual Studio(或您的 IDE)运行虚拟程序,请尝试通过双击磁盘上的二进制文件来正常运行它。

如果 intRead 仍然等于 0:
首先确保 OpenProcess 成功。
其次,测试ReadProcessMemory的返回,它应该返回TRUE,否则,如果返回FALSE,你可以使用GetLastError()来获取错误码以获取更多关于错误的信息。

如果 intRead 不等于 0 但也不等于预期值:
您很可能读取了错误的内存地址或请求了对错误进程 ID 的权限。
检查PID和内存地址。
还要确保在两个程序中都声明了一个 int。
尝试为相同的架构编译和运行它们(x86 或 x64,即使它应该跨架构工作)。

解决方案 (C++)

在这个解决方案中,我提示用户输入 PID 和内存地址以读取而不是硬编码值,这是改进程序的一个想法

#include <Windows.h>
#include <iostream>
#include <string>
using namespace std;

int main() {
	DWORD pid = 0; // The process ID of our target process
	cout << "输入目标进程的 PID: ";
	cin >> dec >> pid; // Prompting user for PID

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hProcess == NULL) { // Failed to get a handle
		cout << "OpenProcess 失败. GetLastError = " << dec << GetLastError() << endl;
		system("pause");
		return EXIT_FAILURE;
	}

	// Prompting user for memory address to read
	uintptr_t memoryAddress = 0x0;
	cout << "输入希望读取的目标指针 (in hexadecimal): 0x";
	cin >> hex >> memoryAddress;
	cout << "读取中 0x" << hex << uppercase << memoryAddress << " ..." << endl;

	// Reading the integer from other process
	int intRead = 0;
	BOOL rpmReturn = ReadProcessMemory(hProcess, (LPCVOID)memoryAddress, &intRead, sizeof(int), NULL);
	if (rpmReturn == FALSE) {
		cout << "ReadProcessMemory 失败. GetLastError = " << dec << GetLastError() << endl;
		system("pause");
		return EXIT_FAILURE;
	}

	cout << "intRead = " << dec << intRead << endl;

	cout << "Press ENTER to quit." << endl;
	system("pause > nul");

	return EXIT_SUCCESS;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

阅读指针、跟随指针链、阅读文本等

读取指针

这是你在程序中肯定需要的东西,强烈建议做一下这个。

在我们的虚拟程序中读取指针“ptr2int”的值,然后您将读取该指针指向的地址处的
int,这将是我们等于 123456 的整数 处理指针时要记住的重要事项: x86指针长度为 4 字节(32 位),x64 指针长度为 8
字节(64 位),因此根据目标程序的体系结构调整参数“nSize”或 ReadProcessMemory。

跟随指针链
在黑客社区的大多数反转主题中,您会发现游戏中有价值信息的偏移量,例如指向您的健康的指针链。
这将按以下格式编写: BASE_ADDRESS ] + OFFSET1 ] + OFFSET2
在该语法中,“]”表示取消引用(读取的值)。
这意味着您应该取消引用(读取值)位于 BASE_ADDRESS 的指针,然后将 OFFSET1 的值添加到此地址,取消引用(读取)那里的任何值,然后添加到此地址 OFFSET2 并读取该地址到获得健康的价值。
在我们的dummy程序中,没有偏移,指针ptr2ptr2直接指向ptr2ptr所在的位置,ptr2ptr直接指向ptr2int,ptr2int指向varInt
因此我们的链是:
[ADDRESS OF ptr2ptr2] ] + 0 ] + 0 ] + 0
您可以创建一个函数,该函数接受输入一个链表(一个向量),其中包含要遵循的基地址和偏移量(使用向量<uintptr_t>)
这将遵循这个指针链。

OpenProcess 的弊端
我们使用 PROCESS_ALL_ACCESS 作为第一个参数为我们的句柄请求了所有权限,但我们不需要那么多。
由于我们只使用 ReadProcessMemory,请阅读该函数的文档并在我们的 OpenProcess 调用中请求最低要求的权限。

OpenProcess 弊端
对于 OpenProcess,我们请求系统提供给我们的 HANDLE,但是如果您阅读文档,您会发现我们应该在不再需要它时正确销毁它,以避免我们所谓的“资源泄漏”。
再次阅读文档,了解这是如何完成的并将其包含在您的代码中。

从 varString 和 arrChar 读取文本
您应该能够从 varString 和 arrChar 读取文本。
为此,您应该调整作为 ReadProcessMemory 参数给出的大小,并且还有一个足够大的缓冲区来包含读取的文本。
直接的解决方案是使用足够大小的数组,否则 C++ 对象字符串允许您使用方法保留内存,您可以尝试实现该方法。

一些错误!
尝试读取目标中的随机地址或地址 0 并查看 ReadProcessMemory 返回的内容和错误代码,在 MSDN 错误代码列表中查找它,看看您是否能够理解为什么它不起作用。
与 OpenProcess 相同,如果您尝试打开一个不存在的进程 ID,会发生什么?您从 GetLastError() 得到什么错误代码?你能理解吗?
如果您获得的句柄权限不足会怎样?假设您仅使用 PROCESS_QUERY_LIMITED_INFORMATION 调用 OpenProcess,然后您尝试将此句柄与 ReadProcessMemory 一起使用,会发生什么?错误代码说明了什么?

最后
您还可以根据需要修改程序输出,提示用户输入 PID 和内存地址。
您还可以询问用户我们要阅读什么:数字或文本,如果用户想阅读文本,还可以询问要阅读的文本长度。

现在尝试用WriteProcessMemory 函数改写一下值

唯一的主要区别是它将缓冲区中的内容写入另一个进程的地址。

解决方案(C++)

#include <Windows.h>
#include <iostream>
#include <string>
using namespace std;

int main() {
	DWORD pid = 0; // The process ID of our target process
	cout << "PID: ";
	cin >> dec >> pid; // Prompting user for PID

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hProcess == NULL) { // Failed to get a handle
		cout << "OpenProcess 失败. GetLastError = " << dec << GetLastError() << endl;
		system("pause");
		return EXIT_FAILURE;
	}

	// Prompting user for memory address to overwrite
	uintptr_t memoryAddress = 0x0;
	cout << "输入希望改写的目标指针 (in hexadecimal): 0x";
	cin >> hex >> memoryAddress;

	// Prompting user for integer value to overwrite
	int intToWrite = 0;
	cout << "改写为 (in decimal): ";
	cin >> dec >> intToWrite;

	// Overwriting the integer in the other process
	BOOL wpmReturn = WriteProcessMemory(hProcess, (LPVOID)memoryAddress, &intToWrite, sizeof(int), NULL);
	if (wpmReturn == FALSE) {
		cout << "WriteProcessMemory 失败. GetLastError = " << dec << GetLastError() << endl;
		system("pause");
		return EXIT_FAILURE;
	}

	cout << "执行成功" << endl;

	cout << "Press ENTER to quit." << endl;
	system("pause > nul");

	return EXIT_SUCCESS;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

实现效果如图
在这里插入图片描述

完成这几个程序,基本就了解内存外挂的 最基本最重要的原理。

本章节结束!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/934300
推荐阅读
相关标签
  

闽ICP备14008679号