我们的项目主要做的是植物大战僵尸的外挂 需要有三个步骤:
1.外挂界面
2.事件处理(比如点击无CD 可以免除技能的冷却时间)
3.跨进程访问(植物大战僵尸外挂和植物大战僵尸属于两个不同的进程(程序) 外挂的功能中需要跨进程访问植物大战僵尸这款软件)
1.Windows的桌面开发
C++:MFC、Qt
C#:WinForm、WPF
这边我们选择使用MFC进行桌面开发 那么就需要安装好MFC相关的组件才行
2.图标
我们可以右击对话框 选择属性 选择属性中的边框 边框选择对话框外框 这样就能实现运行之后无法拖动边框的效果了
3.按钮
注意:按钮拖动到对话框以后 不要双击 否则后续的错误很难改进 你双击的目的可能是为了修改按钮的文字 我们可以先单击他 然后修改他的属性即可
我们按钮的实现效果是点击之后会执行某些操作 显然就是要将按钮和点击事件绑定在一起
我们首先可以对重置一下该按钮的id 使其更具有可读性(具体就是单击按钮 选择属性中的ID进行修改即可) 比如我将其改成了IDC_COURSE
修改了ID以后 我们需要修改一下Resource.h(重点在于清除之前的ID 保留现在的ID)
接着我们需要设置一下点击事件(函数) 头文件和源文件(Dlg文件)都需要设置 我们取名为OnBnClickedCourse
最后我们需要在BEGIN_MESSAGE_MAP函数中完成按钮和点击事件的绑定操作 需要两个参数 一个是按钮的ID 另一个是函数的地址值(函数名本身就是地址值 不需要&) 该绑定函数名为ON_BN_CLICKED
为了体现出我们点击按钮之后调用了按钮点击事件函数 我们可以在里面内置打印操作
但是以前的cout打印操作仅仅适用于命令提示符窗口 并不适用于MFC使用 但是MFC内置也有专门类似于printf的打印函数 该函数为TRACE 但是注意他得在debug调试模式下才可以看到打印信息
void CPVZCheaterDlg::OnBnClickedCourse() {
int age = 20;
TRACE("age is %d", age);
}
除了上述的TRACE函数以外 我们还可以借助AfxMessageBox函数达到打印信息的弹窗显示 并且我们还需要知道一个点 就是MFC中不能用""表示字符串 需要通过CString来表示 如果想要表示格式化字符串的话 则需要调用CString.Format函数
可以直接非调试状态下运行 结果可以看到弹窗上显示打印信息
void CPVZCheaterDlg::OnBnClickedCourse() {
CString str;
str.Format(CString("age is %d"), 20);
AfxMessageBox(str);
}
除了上述这两种方法可以体现我们调用了点击事件函数以外 还可以借助MessageBox函数实现该功能 和AfxMessageBox不同的是 只能在CWnd的子类中使用 显然我们的对话框类最终继承了CWnd 而且在功能上也比AfxMessageBox多(可以定义对话框标题、内容以及按钮、图标) 同样的 也可以直接在非调试状态下运行 即可看到最终效果
void CPVZCheaterDlg::OnBnClickedCourse() {
CString str;
str.Format(CString("age is %d"), 20);
MessageBox(str, CString("警告"), MB_YESNOCANCEL | MB_ICONWARNING);
}
我们其实可以通过宏定义优化一下上述的三种做法 提高一下代码的复用率 避免频繁使用下代码的增加
宏定义中 我们可以使用__VA_ARGS__来代替可变参数…
由于宏定义的替换只包含了log的右边部分 如果想要包含下面的部分 那么需要通过\完成包含操作
#define log(fmt, ...) \
CString str; \
str.Format(CString(fmt), __VA_ARGS__); \
AfxMessageBox(str);
void CPVZCheaterDlg::OnBnClickedCourse() {
int age = 10;
log("age is %d", age);
}
但是我们该函数的出现是为了实现打开链接的目的 我们借助全局函数ShellExecute 里面存在很多参数 但是我们不必一一设置 只需要设置第二、三个以及最后一个即可 第二个控制的是你的操作 比如打开链接的操作就是打开 即open 第三个控制的是打开的对象 打开链接的对象就是链接 最后一个控制的是是否能够正常显示
void CPVZCheaterDlg::OnBnClickedCourse() {
ShellExecute(NULL, CString("open"), CString("https://www.baidu.com"), NULL, NULL, SW_SHOWNORMAL);
}
还有就是声明处的OnBnClickedCourse函数最好添加一个afx_msg声明 该声明体现所修饰函数为事件处理函数
上述是将按钮和点击事件函数手动捆绑在了一起 我们当然也有自动绑定的相关操作了
具体操作就是 当我们设置好按钮以后 右击选择添加事件处理程序 然后如下图所示进行选项的选择
之后就会自动在头文件和源文件中都创建一个有关该按钮的事件处理函数 并且在MESSAGE_MAP中完成了按钮和点击事件函数的绑定操作(具体就是按钮id和点击事件函数的绑定)
在声明中 他会将我们添加的函数用public修饰 但是这个函数没必要提供外界访问 所以直接使用默认的protected即可
其实自动绑定不仅仅只有以上这一种做法 还有以下这种做法 就是直接对着组件进行双击操作 他就会自动为我们创建出相关函数以及按钮和函数的绑定工作(但是在此之前 我们还是要设置好按钮的id以及内容)
同样的 在声明中我们依然把他的public修饰改成protected修饰
4.单选框
在MFC中 单选框其实和按钮属于同一类CButton 只不过样子不一样罢了 因此在判断单选框的勾选状态时名称包含button也就不奇怪
1.单选框的状态读取
我们在制作单选框的过程中 不同的勾选状态肯定对应着不同的操作 所以说我们应该分类讨论
那么如何读取单选框的状态呢 可以利用IsDlgButtonChecked(该方法属于CWnd类 而对话框类继承自他 所以可以使用该方法)返回一个BOOL值判断是否勾选 接着通过if-else对不同的条件执行不同的操作
void CPVZCheaterDlg::OnBnClickedKill()
{
// TODO: 在此添加控件通知处理程序代码
BOOL checked = IsDlgButtonChecked(IDC_KILL);
if (checked) {
log("勾选");
}
else {
log("没勾选");
}
}
其实状态读取不止上述这一种方法 还有第二种方法 先获取指定id对应按钮的地址值(使用GetDlgItem方法) 返回值为CWnd* 但是由于等下要使用GetCheck方法(该方法属于CButton类 CButton继承自CWnd) 所以我们需要将CWnd*强制转换为CButton类才能正常使用GetCheck方法
void CPVZCheaterDlg::OnBnClickedKill()
{
// TODO: 在此添加控件通知处理程序代码
CButton* button = (CButton*)GetDlgItem(IDC_KILL);
BOOL checked = button->GetCheck();
if (checked) {
log("勾选");
}
else {
log("没勾选");
}
}
当然不止前两种方法可以获取按钮的状态 还有第三种方法 具体就是定义一个变量和对话框红的单选框绑定(在DoDateExchange中完成变量和组件的绑定操作 绑定函数为DDX_Control Control为控件的意思) 之后直接调用该对象的GetCheck方法就可以获取该单选框的状态了
void CPVZCheaterDlg::OnBnClickedKill()
{
// TODO: 在此添加控件通知处理程序代码
BOOL check = bnKill.GetCheck();
if (check) {
log("勾选");
}
else {
log("没有勾选");
}
}
而且变量和组件的绑定不仅仅可以手动进行 也可以自动进行 具体操作就是右击组件 选择添加变量 接着按照如下图示进行操作
然后就会自动在DoDateExchange中完成变量和组件的绑定
我们就可以通过变量获取按钮状态 根据不同状态执行不同操作
void CPVZCheaterDlg::OnBnClickedSun()
{
// TODO: 在此添加控件通知处理程序代码
// 通过绑定的变量获取按钮的勾选状态
BOOL check = m_bnSun.GetCheck();
if (check) {
log("勾选");
}
else {
log("没有勾选");
}
}
我们可以总结一下这三种方法:
1.直接通过按钮的id获取该按钮的勾选状态
2.根据按钮的id获取该按钮的指针 然后通过调用按钮指针的状态获取方法来获取勾选状态
3.自定义变量和按钮绑定 然后通过调用变量的状态获取方法来获取勾选状态(除了手动也可自动)
(MESSAGE_MAP方法中完成的是监听函数和组件的绑定 而DoDataExchange中则完成的是变量的组件的绑定)
5.软件破解
不同平台的可执行文件格式不同
Windows:PE格式
Linux:ELF格式
Mac(ios):Mach-O格式
并且不同平台的文件格式有着不同的内存布局(比如:代码段、数据段、栈空间、堆空间之间的先后顺序)
Windows平台软件破解所需知识:
1.文件格式:PE格式(这里不学 借助软件查看)
2.汇编语言:x86、x64汇编
3.工具:Ollydbg(我们借助od这个工具可以查看PE格式下内存中不同分区的位置) 将可执行文件拖拽进去 可显示对应的汇编/机器码
4.Windows API(因为你破解的是Windows平台的软件 所以肯定需要了解一些有关Windows平台的API才行)
以下案例中 我们利用od尝试破解一款序列号输入软件
我们将该软件拖拽到od中 查看他的汇编/机器码以及od自带的一些解析
在vs中打断点的快捷键为f5 而在od中打断点的快捷键则是f2 而且我们需要通过od来打开拖拽的文件 这样两者才会产生关联 保证od可以成功侵入拖拽的文件(双击打开的文件和od没有任何联系)
接着我们分析一下以下汇编/机器码/解析 可以发现解析中有一句lstrcmp 分明表示的是比较两字符串的意思 大胆猜测可能就是比较我们输入的字符串和正确的字符串 再看看下面有一个指令为jnz 意为jump not zero(如果lstrcmp的结果不为0的话 那么就执行跳转操作 而且lstrcmp是一个逐个比较字符的函数 一旦产生结果就停止比较 如果对应字符较小的话 那么返回负数 较大的话 返回正数 如果相等的话 则返回0) 显然当我们的输入和正确字符串不相等的时候 执行跳转操作 如果相等的话 则继续执行
我们可以在lstrcmp处打个断点 然后点击运行操作 打开程序后 输入序列号 然后再次点击运行操作 就可以看到正确序列号了
除了上述这种非暴力破解的方法之外 还可以尝试使用暴力破解的手段 这样 我们输入任何的序列号都可以正确
上述我们分析过 其实他是将两个字符串进行比较 然后对比较的结果进行是否为0的判断 非0跳转 0继续 我们只需要将非0跳转这个条件清空即可(不要直接删除 因为这样会破坏原来汇编的结构) 我们使用NOP指令(NOP占用一个字节 对应的机器码为90 在这里我们需要填充两个NOP) 具体的操作为右击选择需要执行删除操作的指令 选择二进制-用NOP填充
之后我们可以导入修改之后的文件 并且对其重命名 具体操作为右击文件 复制到可执行文件-所有修改-全部复制 然后右击修改后的文件 选择保存并且重命名
1.加壳和脱壳
这方面仅作了解 为了加大破解难度 有时候会在软件外面在套一层软件 使得破解指定软件的时候首先要先脱去软件外面的那一层软件
6.CE(无限阳光)
Cheat Engine这款软件可以帮助我们模拟无限阳光的操作
打开ce之后 由于阳光的初始值都是50 所以我们可以输入50 查找所有50的结果(new scan) 可以发现有800多条结果
之后我们可以继续收集阳光 第二次的阳光数值为75 我们可以在第一次查询结果的基础上继续查询75这个数值(next scan) 就可以找到阳关数值所在的那个内存了
其实无限阳光也是一样的 就是需要我们先找到阳光数值所在的内存 然后在对其进行修改操作 ce就类似于一个外挂的存在了
7.CE(秒杀僵尸)
首先我们需要用ce工具捕捉到修改僵尸生命值的代码 具体的操作就是随着游戏中僵尸生命的不断损耗来捕捉僵尸生命值修改的代码
捕捉到以后 我们将获取的代码拷贝到加载到ollydbg的游戏中 然后找到游戏代码中修改僵尸生命值代码的位置 接着可以查找上下文 发现其实最终赋值给僵尸生命值的为寄存器edi 而edi之前已经进行了sub自减操作 而自减的对象是一个内存 如果我们想要实现秒杀僵尸的效果的话 那么就可以直接将sub的自减对象改成edi即可 那么edi = edi - edi = 0了
但是值得注意的是 不同的僵尸有着不同的修改生命值的代码 我们在对不同僵尸生命值修改的代码进行获取的时候就可以发现差异
8.监控游戏
我们有这么一个需求 就是当我们开启游戏以后 部分功能才供点击 当关闭游戏之后 部分功能不可点击 这个需求应按以下操作才能实现:
我们的想法肯定是做到实时监控 看一下每时每刻游戏的状态是开启还是关闭 需要借助到死循环 但是死循环会使得当前线程阻塞 所以说我们需要将其放置在另外一个线程 防止主线程阻塞 无法继续执行之后的代码
在MFC中通过createThread创建一个线程 里面有一个有效参数 即线程执行代码的函数 我们将其声明为全局函数 并且在里面储存线程执行代码 具体执行的操作为:先获取游戏开启的标志(通过findWindow获取 类型为HWND 参数为窗口的类和标题 可以通过vs的spy++获取) 然后判断标志是否为空来执行相应的操作 如果标志为空 证明游戏没有开启 那么就需要使得部分按钮无法点击 反之则可以点击 而且该线程的创建时机放置在外挂打开之后
createThread有一个返回值HANDLE 为线程的句柄(句柄可以理解为线程的唯一标识) 我们可以声明一个成员变量用于保存这个值
值得注意的是 由于存放线程代码的函数是全局函数 所以无法访问成员变量m_bnKill和m_bnSun(成员变量需要依赖于实例存在 而全局函数不需要) 我们可以定义一个指向类的指针 然后通过类指针访问即可 但是由于这些成员变量默认是protected修饰 所以我们无法正常访问 可以通过将该函数声明为友元函数来实现访问protected成员的目的
除此之外 你可以对监视代码加上一个睡眠的效果 也就是说 每一次死循环代码的最后可以睡眠若干时间 然后在执行下一次的代码 以提高游戏的流畅度
这个功能还有一个bug就是我们关闭游戏以后除了不能点击部分按钮之外 还需要取消按钮勾选状态 这样才符合预期 可以通过SetCheck来实现
如此一来 监视游戏的效果就达到了
9.秒杀僵尸
针对不同的僵尸 生命值的修改有着不同的代码 所以说我们需要分别针对不同的僵尸进行不同的处理
我们先以普通僵尸为例
首先的话 那么要先对比一下生命值代码修改前后的汇编:
修改之前 汇编为2B7C2420 而修改之后 汇编则为2BFF9090 两者的差异在于后三个字节 如果我们等一下在编写外挂代码的时候 根据情况将(非)秒杀僵尸的代码储存到指定的内存中
既然要写入内存 就要用到写入内存的函数 这边封装了一份写入内存的函数 就不需要大家自行编写了(如果触发了点击事件 那么就需要将秒杀代码写入指定内存中 如果没有触发的话 那么就将原来的代码写入到指定内存中)
// 将某个值写入植物大战僵尸内存(后面的可变参数是地址链,要以-1结尾)
void WriteMemory(void* value, DWORD valueSize, ...) {
if (value == NULL || valueSize == 0 || g_process == NULL) return;
DWORD tempValue = 0;
va_list addresses;
va_start(addresses, valueSize);
DWORD offset = 0;
DWORD lastAddress = 0;
while ((offset = va_arg(addresses, DWORD)) != -1) {
lastAddress = tempValue + offset;
::ReadProcessMemory(g_process, (LPCVOID)lastAddress, &tempValue, sizeof(DWORD), NULL);
}
va_end(addresses);
::WriteProcessMemory(g_process, (LPVOID)lastAddress, value, valueSize, NULL);
}
void WriteMemory(void* value, DWORD valueSize, DWORD address) {
WriteMemory(value, valueSize, address, -1);
}
但是上述代码中WriteMemory()中用到了g_process 即进程的句柄 所以说要求我们实现一个进程句柄 我们可以将其声明为全局变量 并且放置在线程监视的代码中(这份代码中储存着根据游戏进程的开关执行的不同操作 而游戏进程的开关同时也决定着进程句柄的初始化值 如果游戏开启 那么就赋予一个初始值 如果关闭则置为空)
我们将初始化进程句柄的语句放置在游戏进程开启的语句中 并且在游戏进程关闭语句的最后将进程句柄置为空 这些操作除了符合常理之外 还可以解决开启之后继续执行开启代码的弊端 因为我们的预期是开启一次之后 就无需再重复开启
我们在初始化进程句柄的时候 用到的函数为OpenProcess 里面所需的参数为打开进程所需的操作 这边为访问进程 还有进程的id
而进程id是通过GetWindowThreadProcessId获取的 所需的参数为游戏窗口的句柄以及游戏进程id的地址值
然后我们注意一下WriteMemory的参数如何书写 从前往后依次为写入的数据、写入数据的大小以及写入内存的地址值 当我们触发点击事件 那么就将修改后的代码写入到指定地址值中 反之则将修改前的代码写入到指定地址值中 由于写入的是多个字节 所以我们可以定义一个数组来储存这若干个字节 然后传参的时候传入字节数组的地址值
上述是针对普通僵尸而言的操作 现在我们在来处理一下带铁帽子的僵尸 由于他们生命值修改的代码不同 所以我们得另行处理
首先我们还是通过ce找一下修改铁帽子僵尸生命值的代码的地址值 然后在od中按址寻找 可以查找到修改生命值的代码为MOV DWORD PTR SS:[EBP+D0],ECX 也就是说重点是观察ecx这个寄存器 可以发现上文中ecx的最终赋值语句为SUB ECX,EAX 我们就可以将其修改为sub ecx, ecx 就可以实现秒杀僵尸的效果了 我们在将修改前后的机器指令获取即可开始编写代码
修改前为00531044 2BC8 SUB ECX,EAX
修改后为00531044 2BC9 SUB ECX,ECX
然后还是按照不同的条件将不同的语句写入到内存中 写入内存函数需要游戏进程的句柄 而游戏进程句柄初始化通过OpenProcess实现 该函数所需参数为进程id以及打开进程的操作(访问)以及第二个参数(这里写FALSE 意思为子进程无需继承父进程的句柄) 而进程id则需要通过GetWindowThreadProcessId获取 该函数需要一个游戏窗口的句柄以及id的地址值 而id我们将其声明为全局变量 他的初始化操作我们放置于线程处理代码中游戏开启的语句中 这样既符合常理 也可以解决开启之后重复开启的弊端
值得注意的是 我们修改以后 可以发现铁帽子僵尸的铁帽子代表一个僵尸 相当于铁帽子僵尸等于两个普通僵尸 相当于我们执行的秒杀操作秒杀的对象是两个普通僵尸
然后有一个小细节 就是我们的全局变量不提供给外部文件访问 所以我们用static修饰他们即可
10.无限阳光地址分析
我们通过ce查找不同进程中阳光的地址值 可以发现 在不同的进程中 阳光的地址值是不一样的(我们所访问的进程是编译以后的exe文件)
这说明阳光是一个局部或者成员变量 不是一个全局变量(编译完成以后 全局变量地址值不会发生改变 除非重新编译 而局部或者成员不管何时都可能发生改变)
通过上述分析 我们可以来尝试一下不同的情况
以下案例是不同的两个进程中获取同一个全局变量的地址值(我没有重新编译 但是两次的全局变量我赋的是不同的值 很多人肯定有疑惑说为什么相同的地址值赋不同的值允许进行呢)
#include <iostream>
using namespace std;
int g_age = 30;
int main() {
cout << &g_age << endl;
getchar();
return 0;
}
#include <iostream>
using namespace std;
int g_age = 20;
int main() {
cout << &g_age << endl;
getchar();
return 0;
}
结果打印的地址值都一样 因为我们实际上访问的是虚拟内存 真正的物理内存是不允许访问的 虽然两个内存地址值一样 但是实际上他们是两块值相同、空间不同的内存
有点以下这种感觉
而且还有这样一个问题 就是植物大战僵尸或者外挂这些软件在不同的电脑中运行 都是可以正常运行的 但是这些软件中或多或少都涉及到直接访问地址值的代码 说明这些源代码在移至到不同电脑的过程中每句代码的地址值都是不变的 如果发生改变 那么其中的若干代码就会无法正常运行 所以在移植过程中 可执行文件的代码地址值不会发生改变
11.无限阳光
我们无限阳光功能的实现 具体的操作就是对将数据填充到阳光所在的内存位置处
但是我们通过ce这个工具也发现了 阳光所在内存的地址值是每一次运行都发生变化 说明他是局部变量或者成员变量 但是局部变量也不可能 因为阳光这个东西是整个游戏过程中都存在的 而局部变量会随着函数的执行完毕而销毁 所以说我们猜测他可能是堆空间中申请的对象中的成员变量 大致的结构如下所示:
也就是说 虽然每一次GameData对象和Sun对象的地址值都在发生改变 但是GameData指针作为全局变量 地址值不会发生改变 所以说 每一次的运行 我们都可以通过这个固定的GameData地址值来间接找到阳光值所在的内存 获取里面的阳光值 我们在编写代码的思路就是如此
我们可以借助ce这个工具先来模拟一下我们的这个思路
以下这个案例中 我们模拟了阳光的大致结构以及找到阳光所在内存的路径 我们通过ce去验证一下看看我们的路径经不经得起推敲
struct Sun {
int temp1;
int value;
};
struct GameData {
int temp1;
int temp2;
Sun* m_sun;
};
GameData* g_data;
int main() {
g_data = new GameData();
g_data->m_sun = new Sun();
g_data->m_sun->value = 20;
while (1) {
cout << g_data->m_sun->value << endl;
}
getchar();
return 0;
}
ce的操作如下图所示(类的结构决定了偏移量)
结果可以从ce的底部面板获取 可以发现 的确获取了符合预期的结果
我们也可以通过双击修改value值
可以证明 我们假设的结构是合理的
值得注意的是 虽然全局变量g_data的地址值是不变的 但是他是在编译完成的前提下成立的 如果重新进行编译了 那么就不尽而知了 而修改代码会导致vs重新发生编译行为 所以说我们得重新获取全局变量的地址值
接下来通过ce这个工具找寻一下阳光地址到固定基地址中的连续多个地址
先找到阳光地址
接着找到阳光地址偏移前的地址(右击选择是什么修改了该地址) 偏移后地址为12977FE8 偏移前的地址为12972A88 偏移量为5560
然后我们将该地址作为精确值进行查询(点击十六进制 并点击新的查询) 查找第一个开头非00的地址值 因为00开头的地址值几乎没有可能称为储存偏移前地址的指针地址
右击选择什么访问了该地址 查看该地址偏移前的地址为什么 可以看到偏移前的地址为02B19D90 偏移后的地址为02B1A4F8 偏移量为768
我们在将偏移前的地址当作精确值进行查询 第一个绿色地址即为基地址 即为006A9EC0
我们可以测试一下看看我们获取到的地址结构是否准确 点击add address manually 按一下步骤操作(006A9EC0->02B19D90、02B19D90 + 768 = 02B1A4F8、02B1A4F8->12972A88、12972A88 + 5560 = 12977FE8) 可以看到 结构确实符合我们的推理
现在我们就可以实现一下无限阳光这个功能了 由于该功能需要实时监测 所以要写在死循环中(而且无限阳光这个功能如果可以勾选的话 那么证明游戏进程是打开的 就可以将数据写入内存) WriteMemory需要一个地址链参数 即基地址到阳光地址之间的偏移量和头尾地址
DWORD monitorThreadFunc(LPVOID lpThreadParameter) {
// 首先通过findwindow获取游戏的开关状态
while (1) {
HWND windowHandle = FindWindow(CString("MainWindow"), CString("植物大战僵尸中文版"));
if (windowHandle == NULL) {
// 窗口句柄为空 证明游戏未开启 执行关闭相关的操作
// 使得部分单选框不可选
// 我们需要借助对象指针才可以访问对象中的成员变量 因为全局函数是不和实例挂钩的 但是由于我们索要访问的成员变量被protected修饰 所以说我们不能够直接访问 如果我们将该函数声明为类的友元函数的话 那么他就可以忽视修饰权限直接访问所在类的所有成员
g_dlg->m_bnKill.EnableWindow(FALSE);
g_dlg->m_bnSun.EnableWindow(FALSE);
g_dlg->m_bnKill.SetCheck(FALSE);
g_dlg->m_bnSun.SetCheck(FALSE);
g_processHandle = NULL;
}
else if(g_processHandle == NULL){
// 窗口句柄不为空 证明游戏开启 执行开启相关的操作
// 使得部分单选框可选
g_dlg->m_bnKill.EnableWindow(TRUE);
g_dlg->m_bnSun.EnableWindow(TRUE);
DWORD processId;
GetWindowThreadProcessId(windowHandle, &processId);
g_processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);;
}
// 由于无限阳光功能需要实时监控 所以说我们将其放置在死循环中即可
if (g_dlg->m_bnSun.GetCheck()) {
DWORD value = 9990;
WriteMemory(&value, sizeof(value), 0x006A9EC0, 0x768, 0x5560, -1);
}
}
return NULL;
}