逆向工程与汇编语言之软件逆向分析(四)

完成“植物大战僵尸”的逆向复现报告

①针对“阳光值”

(1)采用 3 种以上的方法扫描至少 3 关的“阳光值”的内存地址,并能修改;

第一种根据准确值来搜索

图3.1

图3.2

图3.3

找到阳光的内存地址1BC18098

第二种根据减少量来判断

图3.4

图3.5

第三种方法使用数值减少了

图3. 6

图3.7

同样也成功了

(2)找到阳光值的内存基址,简述如何分析和找出基址的过程

首先阳光是内存地址1BC18098

图3.8

图3.9

拾取一个阳光多了三条指令

图3.10

图3.11

图3.12

图3.13

图3.14

图3.15

图3.16

图3.17

图3.18

图3. 19

一直变化是第一个

图3.20

第二个不是

图3.21

第三个也不是

图3.22

图3.23

图3.24

基地址找见了

通过分析只有006A9F38是阳光的基地址

图3.25

图3.26

图3.27

锁定之后重启游戏阳光值不变

综上所述,阳光的基地址是006A9F38。

(3)画出阳光值的内存访问示意图或工作原理

图3.28

(4)分析阳光值功能实现的设计算法

一、阳光值的生成与初始设置

初始化:

在游戏开始时,会为玩家设定一个初始阳光值。这个初始值通常是一个固定的常量,比如在经典版本的《植物大战僵尸》中,初始阳光值可能设定为 50 或其他合适的数值。这一步的算法实现较为简单,就是将一个特定的数值赋给代表阳光值的变量,例如:sunshine_value = 50。

阳光生成器相关:

游戏中存在阳光生成器(如向日葵、双子向日葵等植物),它们会按照一定的时间间隔产生阳光。这里涉及到一个定时器算法。

对于每一株阳光生成器植物,会设置一个独立的定时器。当种植下一株向日葵后,游戏会启动一个定时器,该定时器以固定的时间间隔(例如每隔数秒)触发一次阳光产生事件。假设向日葵产生阳光的时间间隔为 T_sunflower 秒,算法伪代码可能如下:

sunflower_timer = 0 # 向日葵定时器初始化为0 while game_running: if sunflower_planted: # 如果向日葵已种植 sunflower_timer += game_time_step # 按照游戏时间步长增加定时器的值 if sunflower_timer >= T_sunflower: # 当定时器达到产生阳光的时间间隔 generate_sunshine(sunflower_sunshine_amount) # 调用生成阳光的函数,传入向日葵产生的阳光量 sunflower_timer = 0 # 重置定时器

其中 generate_sunshine 函数用于实际增加玩家的阳光值,sunflower_sunshine_amount 是向日葵每次产生的阳光数量(如向日葵可能每次产生 25 阳光)。

二、阳光值的增减操作

阳光增加:

除了阳光生成器植物产生阳光外,游戏中可能还存在其他获取阳光的方式,比如某些关卡奖励、击败僵尸掉落等。

当发生阳光增加事件时(无论是哪种方式),算法就是简单地将增加的阳光量累加到当前阳光值变量上。例如,如果玩家通过关卡奖励获得了 100 阳光,伪代码如下:

current_sunshine_value += 100

阳光减少:

玩家种植植物需要消耗阳光。不同的植物有不同的阳光消耗成本。当玩家点击种植一种植物时,游戏会首先检查玩家当前的阳光值是否足够支付该植物的阳光消耗。

假设玩家要种植一株豌豆射手,其阳光消耗为 100 阳光,算法伪代码如下if current_sunshine_value >= pea_shooter_sunshine_cost: # 检查阳光值是否足够种植豌豆射手

current_sunshine_value -= pea_shooter_sunshine_cost # 扣除种植豌豆射手所需的阳光

plant_pea_shooter() # 调用种植豌豆射手的函数

else:

show_not_enough_sunshine_message() # 如果阳光不足,显示提示信息:

三、阳光值的显示与更新

显示逻辑:

游戏界面上需要实时显示当前的阳光值,以便玩家了解自己可支配的阳光资源。这涉及到将阳光值变量转换为可视化的文本或图形元素在屏幕上显示出来。

一般会有一个专门的函数负责绘制游戏界面中的阳光值显示部分,它会获取当前阳光值变量的值,并根据游戏的界面设计风格将其渲染成合适的文本(如数字形式)或图形(如阳光图标及对应的数量)。例如,在使用某种图形库绘制游戏界面时,伪代码可能如下:

def draw_sunshine_display():

sunshine_text = str(current_sunshine_value) # 将阳光值转换为字符串

draw_text(sunshine_text, x_position, y_position, font_size, color) # 使用指定的位置、字体、颜色等绘制文本

更新频率:

阳光值可能会在游戏过程中频繁变化(如阳光不断产生、种植植物消耗等),所以需要定期更新其在游戏界面上的显示。这通常与游戏的主循环更新频率相关。

在游戏的主循环中,每次循环迭代都会检查阳光值是否有变化(通过比较当前阳光值与上一次循环记录的阳光值),如果有变化,则调用上述的 draw_sunshine_display 函数来更新显示。伪代码如下:

last_sunshine_value = current_sunshine_value

while game_running:

# 游戏主循环中的其他操作...

if current_sunshine_value!= last_sunshine_value: # 检查阳光值是否变化

draw_sunshine_display() # 更新阳光值显示

last_sunshine_value = current_sunshine_value

四、阳光值的上限与下限设置

上限设置:

为了避免阳光值无限制增长导致游戏平衡性问题,游戏会设定一个阳光值上限。当玩家的阳光值达到上限后,即使再有阳光产生事件,阳光值也不会再增加。

假设阳光值上限为 SUNSHINE_UPPER_LIMIT,算法伪代码如下:

if current_sunshine_value >= SUNSHINE_UPPER_LIMIT:

current_sunshine_value = SUNSHINE_UPPER_LIMIT

下限设置:

虽然阳光值一般不会出现负数情况(因为在没有阳光时玩家无法种植植物),但为了程序的严谨性,可能也会设定一个下限,通常就是 0。当玩家试图种植植物但阳光值不足时,扣除操作会确保阳光值不低于下限。例如:

if current_sunshine_value < 0:

current_sunshine_value = 0

(5)分析阳光值的相关汇编语言代码的工作过程

在游戏程序的初始化阶段,会为阳光值这个变量分配特定的内存地址。这个地址是在程序的数据段(通常用于存储全局变量等静态数据)中确定变量访问方式:

当游戏需要读取或修改阳光值时,就会通过相应的汇编指令来访问这个内存地址。

二、阳光值增加的汇编实现

阳光生成器相关增加:

以向日葵产生阳光为例,当向日葵的定时器触发产生阳光事件时,会执行一系列汇编指令来增加阳光值

这里先将内存中的阳光值读取到 eax 寄存器,然后通过 add 指令在寄存器内增加 25 阳光,最后再将更新后的阳光值写回原来的内存地址,完成了阳光值的增加操作。

其他阳光获取方式增加:

比如玩家通过击败僵尸掉落阳光等情况

三、阳光值减少的汇编实现

种植植物消耗阳光:

当玩家点击种植一种植物,如豌豆射手(假设其阳光消耗为 100 阳光)时,需要检查阳光值是否足够并进行相应的扣除操作

这段代码首先读取阳光值到 eax 寄存器,然后通过 cmp 指令与豌豆射手的阳光消耗进行比较。如果阳光值不足(jl 指令判断小于情况),就跳转到阳光不足的处理代码段;如果阳光值足够,就通过 sub 指令扣除 100 阳光,并将更新后的阳光值写回内存,之后再继续执行种植豌豆射手的其他相关代码。

四、阳光值显示相关的汇编操作

获取阳光值用于显示:

为了在游戏界面上显示阳光值,需要先获取当前的阳光值

传递给显示函数:

游戏中应该有专门的显示函数来处理在屏幕上呈现阳光值的工作。获取到阳光值到 eax 寄存器后,可能会通过函数调用指令将 eax 寄存器的值(即阳光值)传递给显示函数

这里先将阳光值压入栈作为函数的参数,然后通过 call 指令调用显示函数来在屏幕上显示阳光值的具体数值。

五、阳光值上限与下限处理的汇编代码

上限处理:

当阳光值达到上限(假设上限为 SUNSHINE_UPPER_LIMIT)时,需要确保阳光值不再增加

这段代码首先读取当前阳光值到 eax 寄存器,然后通过 cmp 指令与上限值进行比较。如果阳光值大于等于上限值(jge 指令判断),就跳转到上限处理代码段,在那里将阳光值设置为上限值并写回内存,防止阳光值超过上限。

下限处理:

虽然阳光值一般不会出现负数情况,但为了程序的严谨性,可能也会处理下限情况(假设下限为 0)。

同样是先读取当前阳光值到 eax 寄存器,然后通过 cmp 指令与下限值进行比较。如果阳光值小于下限值(jl 因指令判断),就跳转到下限处理代码段,在那里将阳光值设置为下限值并写回内存,保证阳光值不低于下限。

通过以上对阳光值相关汇编语言代码的分析,可以看出汇编代码是如何通过一系列指令来实现阳光值在《植物大战僵尸》游戏中的生成、增减、显示以及上下限处理等功能的。实际游戏中的汇编代码可能会更加复杂,并且会根据具体的游戏逻辑和实现细节有所不同,但基本的原理和流程是类似的。

(6)画出逆向分析的思维导图,并将以说明其逆向原理。

图3.29

中心主题:植物大战僵尸阳光值逆向分析

一级分支:准备工作

工具选取:列出如 IDA Pro(反汇编工具,用于将游戏可执行文件的二进制代码转换为汇编代码,方便查看程序逻辑和指令)、OllyDbg(动态调试工具,可实时跟踪程序运行过程,查看寄存器、内存数据变化等)、Cheat Engine(内存修改工具,辅助查找阳光值在内存中的地址和相关数据)等工具。

游戏样本:确定要分析的《植物大战僵尸》游戏版本,获取对应的可执行文件或安装包,确保其完整性和可操作性。

一级分支:阳光值数据定位

静态分析:

文件结构查看:使用十六进制编辑器或专门的文件分析工具,查看游戏可执行文件的文件头、节区信息等,初步了解文件布局,判断可能存储数据的区域。

代码搜索:在反汇编代码中,查找与阳光值初始化、显示、更新等相关的字符串常量或函数调用,例如搜索 “sunshine”“add_sunshine” 等关键词,定位到相关代码段,进而推测阳光值变量的位置或其相关数据结构的引用。

动态调试:

运行游戏并监控:通过调试器启动游戏,设置断点在疑似阳光值相关操作的代码处,如游戏开始时的初始化函数、阳光产生函数、种植植物消耗阳光的函数等。

内存变化追踪:当游戏运行到断点处,观察寄存器和内存数据的变化。特别是在阳光值发生改变时(如初始值设定、植物生产阳光后),记录内存地址和数据变化情况,通过多次试验和对比,确定阳光值在内存中的准确存储地址和数据格式(如整数类型、字节数等)。

一级分支:阳光值变化机制剖析

增加逻辑:

阳光产生器代码分析:深入研究向日葵、双子向日葵等阳光产生植物对应的代码逻辑。包括定时器的设置和触发机制,如何在定时器到期时执行阳光值增加的指令,增加的数值是如何确定和计算的,以及相关数据的存储和更新方式。

其他获取途径代码:分析如僵尸掉落阳光、完成特定任务奖励阳光等情况对应的代码流程。查找触发这些事件的条件判断代码,以及阳光值增加的具体实现代码,了解其与阳光产生器增加阳光逻辑的异同。

减少逻辑:

种植消耗代码解析:针对种植不同植物时消耗阳光的操作,查看代码中如何检查阳光值是否足够,若足够则进行扣除操作的代码实现。包括阳光值与植物阳光消耗值的比较指令,以及扣除阳光值并更新内存数据的代码序列,还可能涉及到对阳光值下限的判断和处理(如防止阳光值扣为负数)。

一级分支:阳光值显示机制探究

显示函数定位:在反汇编代码或调试过程中,通过查找与图形绘制、文本显示相关的函数调用,确定负责在游戏界面上显示阳光值的函数。可以从阳光值变化后引发的函数调用链入手,逐步回溯找到源头的显示函数。

数据传递与渲染:分析从阳光值存储地址到显示函数之间的数据传递过程。查看是通过寄存器传递阳光值数据,还是通过栈传递,以及显示函数内部如何将接收到的数据转换为可视化的文本或图形元素在屏幕上进行渲染,包括字体设置、颜色选择、显示位置确定等相关代码逻辑。

一级分支:阳光值限制机制分析

上限限制代码:在代码中查找对阳光值进行上限判断的部分,通常会有比较指令将阳光值与设定的上限值进行对比,若超过上限则阻止阳光值继续增加或进行特殊处理的逻辑代码,了解其具体的实现方式和相关数据的存储与引用。

下限限制代码:类似地,确定防止阳光值低于下限(通常为 0)的代码实现。包括下限判断的条件语句,以及当阳光值低于下限时的修正操作代码,如将阳光值强制设置为 0 或进行相关提示操作的代码逻辑。

一级分支:逆向成果总结与应用

机制总结:将上述对阳光值的数据定位、变化机制、显示机制和限制机制的分析结果进行梳理和整合,形成完整的阳光值在《植物大战僵尸》游戏中的逆向分析报告,详细描述其在内存中的存储结构、各种操作的代码实现细节和逻辑流程。

应用场景拓展:基于逆向分析成果,探讨其可能的应用。例如开发游戏修改器,通过修改阳光值在内存中的数据实现无限阳光功能;或者进行游戏漏洞研究,分析阳光值相关代码中可能存在的安全漏洞,如数据溢出、非法内存访问等问题,为游戏的安全防护和优化提供参考;还可以用于开发游戏辅助工具,如自动收集阳光的脚本等,但需注意在合法合规的范围内使用这些逆向分析成果。

通过这个思维导图,可以全面、系统地对《植物大战僵尸》阳光值进行逆向分析,从多个角度深入探究其内部机制,并思考如何将逆向分析结果应用到实际场景中

(7)观察“阳关”的其它属性,并采用上述(1)-(5)的方法分析之。

(8)分析该软件所用内存,并画图说明其数据分布。

以下是一个简单的、以文字形式描述的《植物大战僵尸》游戏内存布局图示例,你可以根据实际情况进一步细化和完善成具体的图形:

内存区域

主要用途

示例数据分布

代码段

存储可执行指令

植物种植函数、阳光值处理函数等指令代码

数据段

存储全局变量和静态数据

植物属性(阳光消耗、产生间隔等)、僵尸属性、阳光值变量等

动态分配内存用于存储动态数据结构

种植植物的状态信息、新出现僵尸的属性等

存储函数调用相关信息

函数参数、局部变量、返回地址等

通过这样的内存分析和布局描述,可以更清楚地了解《植物大战僵尸》游戏在运行时如何利用不同的内存区域来实现各种功能,以及各类数据在内存中的大致分布情况。不同版本的游戏可能会在具体细节上有所不同,但基本的内存利用原理是相似的

(9)实现自动收集阳光,并详细说明原理。

图3.30

图3.31

图3. 32

图3.33

图3.34

图3.35

图3.36

图3.37

图3. 38

图3.39

至此就实现了无条件拣去阳光

图3.40

②针对“植物

(1)采用 3 种以上的方法扫描至少 3 关的“植物”的内存地址

第一个未知值扫描法
第二个采用0 1 法

图3.41

向日葵的内存地址10ECF838

第三个扫描增加的数值

图3.42


(2)找到该植物的内存基址,分析该植物的全部属性,并画出其数据结构;

分析全部属性

基本属性:

阳光消耗:向日葵种植时需要消耗一定量的阳光,如前面提到的 50 阳光,这是一个固定属性,决定了玩家种植向日葵的成本。

产生阳光间隔:向日葵会按照一定的时间间隔产生阳光,假设为 24 秒,这个属性影响着玩家获取阳光的频率。

植物类型:属于生产阳光类植物,与攻击类植物(如豌豆射手)等有明显区分,用于游戏内部逻辑对不同类型植物的分类处理。

状态属性:

是否存活:在游戏中,向日葵可能会被僵尸攻击导致死亡,所以有一个表示是否存活的属性,初始值为 True(存活),当受到足够伤害后变为 False(死亡)。

当前阳光生产状态:用于表示向日葵当前是否正在产生阳光,可能是一个布尔值,例如 True 表示正在产生阳光,False 表示处于休息或等待生产的间隔期。

位置属性:

在游戏场景中的坐标:向日葵种植在游戏场景中的特定位置,通过坐标(x, y)来表示,这个属性对于植物与僵尸之间的交互、以及游戏画面的显示等都非常重要。

图3.43

(3)画出该植物的内存访问示意图或工作原理

内存区域

访问目的

访问方式

数据段(可能存储静态属性)

获取向日葵的基本属性,如阳光消耗、产生阳光间隔等

通过基址偏移来读取对应的值。例如,基址为 0x12345678,阳光消耗属性可能偏移 0x10 字节,通过 mov 指令从 [0x12345678 + 0x10] 读取阳光消耗值

堆(可能存储动态状态信息)

获取向日葵的状态属性,如是否存活、当前阳光生产状态等,以及更新这些状态属性

当种植向日葵时,在堆上分配一块内存区域,通过基址找到这块区域,然后通过偏移量读取或写入状态属性值。比如,要判断向日葵是否存活,通过 mov 指令从 [堆分配基址 + 0x20] 读取 isAlive 属性值;要更新当前阳光生产状态,通过 mov 指令将新值写入 [堆分配基址 + 0x24]

栈(用于函数调用相关)

在涉及向日葵的函数调用过程中,存储函数参数、局部变量等

(4)分析该植物功能实现的设计算法;

一、种植功能

阳光值检查算法:

当玩家点击种植向日葵时,游戏首先会获取玩家当前的阳光值(假设通过读取内存中阳光值变量的地址)。

然后将获取到的阳光值与向日葵的阳光消耗值(如 50)进行比较。如果当前阳光值大于等于向日葵的阳光消耗值,那么允许种植;否则,显示阳光不足的提示信息,阻止种植。

位置确定算法:

游戏会获取玩家点击的种植位置坐标(假设通过鼠标点击事件获取)。

检查该位置是否符合种植条件,比如是否在可种植区域内(排除已种植植物的位置、游戏场景边界等不可种植区域)。

内存分配与初始化算法:

如果阳光值足够且位置合适,游戏会在堆上为向日葵分配一块内存区域来存储其状态信息(如前面提到的数据结构)。

初始化这块内存区域中的各个属性值,比如将 isAlive 设置为 True,isProducingSunshine 设置为 False(初始时还未开始生产阳光),并设置向日葵的坐标位置为玩家点击的种植位置坐标。

二、阳光生产功能

定时器设置算法:

当向日葵成功种植后,游戏会为其设置一个定时器,用于控制阳光生产的间隔时间。

定时器的初始值设置为向日葵的产生阳光间隔(如 24 秒),并开始倒计时。

阳光生产触发算法:

随着游戏时间的推进,定时器的值不断减少。当定时器的值减到 0 时,触发阳光生产事件。

在阳光生产事件中,首先检查向日葵是否存活(通过读取堆上存储的 isAlive 属性)。如果存活,执行以下操作:

将玩家的阳光值增加向日葵每次产生的阳光量(假设为 25 阳光),通过读取和更新内存中阳光值变量的地址来实现。

将向日葵的当前阳光生产状态 isProducingSunshine 设置为 True,表示正在产生阳光。

重新设置定时器的值为向日葵的产生阳光间隔,准备下一次阳光生产。

阳光生产结束算法:

当阳光生产过程持续一段时间(可能是一个短暂的动画展示时间)后,将向日葵的当前阳光生产状态 isProducingSunshine 设置为 False,表示阳光生产结束,进入下一个生产间隔等待期。

三、被攻击功能

伤害计算算法:

当僵尸攻击向日葵时,游戏会根据僵尸的攻击力(假设通过读取僵尸相关属性获取)计算向日葵所受的伤害值。

将计算出的伤害值从向日葵的当前生命值(假设初始生命值为 100,存储在堆上向日葵的内存区域内)中扣除。

存活判断算法:

在扣除伤害值后,检查向日葵的剩余生命值是否大于 0。如果大于 0,向日葵继续存活,更新其生命值属性值;如果小于等于 0,将向日葵的 isAlive 属性设置为 False,表示向日葵已死亡,同时进行一些相关的清理工作,比如从游戏场景中移除向日葵的显示等。

(5)分析该植物的相关汇编语言代码的工作过程

一、种植向日葵时阳光值检查的汇编代码分析

1.读取阳光值:

2.比较阳光值与向日葵阳光消耗值:

3.根据比较结果跳转:

二、种植向日葵时位置确定的汇编代码分析

获取种植位置坐标:

假设通过鼠标点击事件获取种植位置坐标,并将其存储在特定的寄存器或内存地址中。例如,x 坐标存储在 ebx 寄存器,y 坐标存储在 ecx 寄存器。具体的获取方式可能因游戏的输入处理代码而异,但大致思路是通过相关的输入设备驱动程序和游戏代码的交互来实现。

检查种植位置是否合适:

可能会有一系列的汇编指令来检查种植位置是否在可种植区域内。比如,会有代码来比较 x 坐标和 y 坐标与游戏场景边界值以及已种植植物位置坐标的关系。假设游戏场景边界的 x 最大值为 0xFFFF,y 最大值为 0xFFFF,已种植植物位置坐标存储在一个数组中,

cmp ebx, 0xFFFF

jg out_of_bounds_x_label

cmp ecx, 0xFFFF

jg out_of_bounds_y_label

; 检查是否与已种植植物位置冲突,这里假设通过循环遍历已种植植物位置数组来检查

mov esi, 0

plant_position_loop:

cmp [planted_plants_array + esi * 4], ebx

jne not_conflict_x

cmp [planted_plants_array + esi * 4 + 2], ecx

jne not_conflict_y

jmp conflict_label

not_conflict_x:

jne plant_position_loop

not_conflict_y:

jne plant_position_loop

; 如果通过以上检查,位置合适,继续执行后续代码

上述代码首先检查 x 坐标和 y 坐标是否超出游戏场景边界,如果超出则跳转到相应的越界处理标签。然后通过循环遍历已种植植物位置数组,检查种植位置是否与已种植植物位置冲突,如果冲突则跳转到冲突处理标签;如果位置合适,则继续执行种植向日葵的后续代码。

三、种植向日葵时内存分配与初始化的汇编代码分析

1.内存分配指令:

在确定阳光值足够且位置合适后,游戏需要在堆上为向日葵分配一块内存区域来存储其状态信息。不同的操作系统和游戏开发环境可能使用不同的内存分配函数,假设这里使用的是类似于 malloc 的函数

2.初始化内存区域中的属性值

(6)画出逆向分析的思维导图,并将以说明其逆向原理。

一、逆向分析的总体目标

通过对游戏可执行文件及运行时的相关数据进行深入剖析,以了解向日葵在游戏中的各种属性、功能实现机制以及其在内存中的存储和访问方式等,从而揭示游戏内部关于向日葵这一元素的运作原理。

二、数据定位原理

静态分析数据定位

文件结构洞察:游戏可执行文件具有特定的文件结构,包含不同的节区(如代码段、数据段等)。通过十六进制编辑器或专门的文件分析工具查看文件头、节区信息等,可以初步判断可能存储向日葵相关数据的区域。例如,数据段通常会存放一些全局变量和静态数据,可能包含向日葵的初始属性值(如阳光消耗值等),所以先从这里入手寻找相关线索。

代码搜索线索:在反汇编后的代码中,搜索与向日葵相关的特定字符串常量(如 “sunflower”“sunCost” 等)或函数调用(如种植向日葵的函数、向日葵产生阳光的函数等)。这些关键词能帮助定位到与向日葵属性、功能相关的代码段,进而推测向日葵数据结构的引用位置或内存基址。因为在程序代码中,对向日葵进行操作的函数通常会涉及到对其相关数据的访问和处理。

动态调试数据定位

运行时监控:利用调试器(如 OllyDbg 等)启动游戏并运行,设置断点在疑似向日葵相关操作的代码处(比如种植向日葵、向日葵产生阳光、向日葵被攻击等时刻对应的代码位置)。当游戏运行到这些断点时,程序会暂停执行,此时可以观察寄存器和内存数据的变化。

状态变化追踪:特别关注向日葵在不同状态变化时(如种植后、产生阳光前后、被攻击前后等)内存地址和数据的改变情况。通过多次试验和对比不同状态下的数据,能够确定向日葵在内存中的准确存储地址以及数据格式(如整数类型、字节数等)。例如,当向日葵种植后,其相关状态属性(如是否存活、当前阳光生产状态等)会在内存中有相应的存储位置,通过动态调试可以捕捉到这些位置的变化及数据特征。

三、属性分析原理

基本属性分析

阳光消耗属性:通过定位到与向日葵种植相关的代码(如种植函数),查看其中对玩家阳光值进行检查和比较的部分。在这部分代码中,会将玩家当前阳光值与向日葵的阳光消耗值(如固定的 50 阳光)进行比较,由此可以确定阳光消耗值在代码中的存储方式以及如何参与运算(如比较指令等),进而分析出这一基本属性的实现机制。

产生阳光间隔属性:在分析向日葵产生阳光的相关代码时,重点关注定时器设置和触发阳光生产事件的部分。定时器的初始值设置通常与向日葵的产生阳光间隔(如 24 秒)相关,通过研究定时器相关的代码逻辑(如定时器的启动、倒计时、触发条件等),可以了解产生阳光间隔这一属性是如何在代码中体现并影响阳光生产功能的。

植物类型属性:游戏内部为了区分不同类型的植物(如向日葵与豌豆射手等),会有相应的标识机制。通过查看代码中对植物类型进行判断和处理的部分(如在游戏逻辑中根据植物类型执行不同操作的代码段),可以找出用于标识向日葵植物类型的变量或数据结构中的特定字段,从而分析出植物类型属性的实现方式。

状态属性分析

是否存活属性:在向日葵被攻击相关的代码中,观察伤害计算和存活判断的流程。当僵尸攻击向日葵时,会根据僵尸的攻击力计算向日葵所受的伤害值,并从向日葵的当前生命值(假设初始生命值为 100)中扣除。然后通过检查扣除伤害值后的剩余生命值是否大于 0 来判断向日葵是否存活。通过分析这一系列的代码操作(如伤害计算的指令、存活判断的条件语句等),可以深入了解是否存活这一状态属性的实现原理。

当前阳光生产状态属性:在向日葵产生阳光的全过程代码中,查看在阳光生产过程中如何设置、更新这一状态属性。例如,当定时器触发阳光生产事件时,会先检查向日葵是否存活,若存活则会将当前阳光生产状态设置为 True,表示正在产生阳光;当阳光生产结束后,又会将这一状态设置为 False。通过分析这些代码中的赋值、判断等操作,可以明晰当前阳光生产状态属性的运作机制。

位置属性分析:

在种植向日葵以及游戏过程中涉及向日葵与其他元素(如僵尸、其他植物等)交互的代码中,观察游戏如何获取、存储和使用向日葵在游戏场景中的坐标位置。种植时,会获取玩家点击的种植位置坐标,并检查其是否符合种植条件;在游戏过程中,根据向日葵的坐标位置进行相关的画面显示、与其他元素的交互等操作。通过分析这些涉及坐标位置处理的代码(如获取坐标的指令、检查种植条件的代码、根据坐标进行交互的代码等),可以掌握位置属性的实现方式。

四、功能实现分析原理

种植功能分析

阳光值检查:在种植向日葵的函数代码中,首先会读取玩家当前的阳光值(通过访问内存中阳光值变量的地址),然后将其与向日葵的阳光消耗值进行比较。通过分析读取阳光值的指令(如 mov 指令将阳光值从内存读取到寄存器)、比较指令(如 cmp 指令将读取到的阳光值与阳光消耗值进行比较)以及根据比较结果进行跳转的指令(如 jl 指令在阳光值小于阳光消耗值时跳转到阳光不足处理代码段),可以了解阳光值检查这一操作在汇编语言中的实现原理。

位置确定:种植向日葵时,需要获取玩家点击的种植位置坐标(通过相关的输入设备驱动程序和游戏代码的交互来实现),并检查其是否符合种植条件(如是否在可种植区域内、是否与已种植植物位置冲突等)。通过分析获取种植位置坐标的代码(如通过特定寄存器或内存地址存储坐标值)、检查种植位置是否合适的代码(如通过比较坐标值与游戏场景边界值、已种植植物位置坐标等来判断),可以明白位置确定这一功能在汇编语言中的实现方式。

内存分配与初始化:当阳光值足够且位置合适时,游戏会在堆上为向日葵分配一块内存区域来存储其状态信息。通过分析调用内存分配函数(如类似 malloc 的函数,其汇编代码涉及将向日葵数据结构大小压入栈中,然后调用函数并将返回的内存地址存储在特定寄存器或内存地址中)的代码,以及初始化内存区域中各个属性值的代码(如通过 mov 指令将初始值写入相应的偏移量位置来设置属性值),可以揭示内存分配与初始化功能在汇编语言中的实现原理。

阳光生产功能分析

定时器设置:在向日葵成功种植后,游戏会为其设置一个定时器来控制阳光生产的间隔时间。通过分析设置定时器初始值(如将定时器的值设置为向日葵的产生阳光间隔时间,通过 mov 指令将间隔时间值存储到定时器相关的寄存器或内存地址中)、启动倒计时(如通过相关的指令启动定时器的倒计时功能)的代码,可了解定时器设置这一功能在汇编语言中的实现原理。

阳光生产触发:当定时器的值减到 0 时,触发阳光生产事件。在这一过程中,首先会检查向日葵是否存活(通过读取堆上存储的向日葵是否存活属性值),若存活则会进行一系列操作,包括增加玩家阳光值(通过读取和更新内存中阳光值变量的地址)、将向日葵的当前阳光生产状态设置为 True(通过 mov 指令将值写入相应的偏移量位置)、重新设置定时器的值为向日葵的产生阳光间隔时间(通过 mov 指令将间隔时间值存储到定时器相关的寄存器或内存地址中)。通过分析这些操作对应的汇编语言代码,可以掌握阳光生产触发这一功能在汇编语言中的实现原理。

阳光生产结束:当阳光生产过程持续一段时间(如一个短暂的动画展示时间)后,会将向日葵的当前阳光生产状态设置为 False(通过 mov 指令将值写入相应的偏移量位置),表示阳光生产结束,进入下一个生产间隔等待期。通过分析这一操作对应的汇编语言代码,可以明晰阳光生产结束这一功能在汇编语言中的实现原理。

被攻击功能分析

伤害计算:当僵尸攻击向日葵时,会根据僵尸的攻击力计算向日葵所受的伤害值。通过分析根据僵尸攻击力计算伤害值的代码(如通过乘法或加法等运算指令,结合僵尸攻击力和可能存在的伤害系数等来计算伤害值),可以了解伤害计算这一功能在汇编语言中的实现原理。

存活判断:在扣除伤害值后,检查向日葵的剩余生命值是否大于 0。通过分析扣除伤害值后检查剩余生命值的代码(如通过 cmp 指令将剩余生命值与 0 进行比较,再根据比较结果进行跳转等操作),可以明白存活判断这一功能在汇编语言中的实现原理。

通过对向日葵在《植物大战僵尸》游戏中的这些方面进行逆向分析,能够深入了解其在游戏内部的运作原理,包括属性设定、功能实现以及内存中的存储和访问方式等,为进一步的游戏研究、修改或相关开发提供有力的依据。

图3.44

(7)分析第 1-3 关的所有植物的全部基址,并用思维导图分析全部卡槽的基址分布,

画出其数据结构;

图3.45

所有的植物的内存地址都是0075B870

利用卡槽中植物的冷却状态进行查找

第一个卡槽的位置

图3. 46

第二个卡槽

图3.47

以此类推

最后一个卡槽的位置

图3.48

图3.49

(8)画出第 1-3 关全部植物的属性的数据结构图生成,并将以说明

图3.50

图3.51

图3.52

图3. 53

二、说明

1. 整体结构

整个数据结构以 PlantsData 结构体作为根结构体,它包含了第 1 - 3 关常见的几种植物(向日葵、豌豆射手、樱桃炸弹、土豆雷)各自的数据结构作为其成员。这种组织方式方便在游戏程序中对不同植物的属性进行统一管理和操作。

2. 向日葵属性说明

sunCost:表示种植向日葵所需要消耗的阳光数量,这是一个固定值,决定了玩家种植向日葵的资源成本。例如,在游戏中向日葵可能需要消耗 50 阳光才能种植。

productionInterval:用于设定向日葵产生阳光的间隔时间,以秒为单位。比如设定为 24 秒,意味着每隔 24 秒向日葵会产生一次阳光,影响着玩家获取阳光资源的频率。

isAlive:是一个布尔值,用于表示向日葵在游戏场景中的存活状态。初始值为 true,表示向日葵是存活的,当受到僵尸攻击且生命值降为 0 时,该值会变为 false,此时向日葵在游戏画面中会消失等相应处理。

isProducingSunshine:同样是布尔值,用来表明向日葵当前是否正在产生阳光。初始值为 false,当满足产生阳光的条件(如定时器到达生产间隔时间且向日葵存活)时,该值会变为 true,在阳光生产过程中保持为 true,生产结束后又变回 false。

xPosition 和 yPosition:这两个属性用于记录向日葵在游戏场景中的坐标位置,通过坐标值可以确定向日葵在游戏画面中的具体种植位置,方便进行画面显示以及与其他元素(如僵尸)的交互等操作。

3. 豌豆射手属性说明

sunCost:与向日葵类似,这里表示种植豌豆射手所需消耗的阳光数量,例如为 100 阳光,玩家需要拥有足够的阳光值才能种植豌豆射手。

attackPower:定义了豌豆射手每次发射豌豆对僵尸造成的伤害值,比如设定为 20 伤害,表明每次豌豆击中僵尸会扣除僵尸 20 点生命值。

attackRange:指定了豌豆射手的攻击范围,这里假设为前方整行,即豌豆射手可以攻击到在其正前方同一行的僵尸,确定了其攻击的有效区域。

attackSpeed:表示豌豆射手发射豌豆的速度,以每多少秒发射一次豌豆来衡量,如每 1.5 秒发射一次豌豆,影响着豌豆射手对僵尸的攻击频率。

isAlive:布尔值,用于判断豌豆射手在游戏中的存活状态,初始为 true,当受到僵尸攻击导致生命值耗尽时变为 false,之后游戏会对其进行相应处理(如从画面中移除等)。

xPosition 和 yPosition:记录豌豆射手在游戏场景中的坐标位置,以便在游戏画面中准确显示豌豆射手的位置,并实现与周围元素的交互。

4. 樱桃炸弹属性说明

sunCost:表示种植樱桃炸弹需要消耗的阳光数量,这里假设为 150 阳光,相对较高的阳光消耗体现了其较强的攻击能力。

explosionRadius:用于定义樱桃炸弹爆炸时的影响范围,比如以自身为中心周围九宫格范围,在此范围内的僵尸都会受到爆炸的伤害。

isPlanted:布尔值,初始为 false,当玩家成功种植樱桃炸弹后该值变为 true,用于标记樱桃炸弹是否已经被种植在游戏场景中。

isExploded:也是布尔值,初始为 false,当樱桃炸弹触发爆炸后该值变为 true,用于表示樱桃炸弹是否已经完成爆炸操作,之后可能会进行一些清理工作(如从画面中移除等)。

xPosition 和 yPosition:同样是记录樱桃炸弹在游戏场景中的坐标位置,以便准确进行画面显示和与周围元素的交互。

5. 土豆雷属性说明

sunCost:表示种植土豆雷所需消耗的阳光数量,如 25 阳光,相对较低的阳光消耗适合在游戏前期或资源紧张时使用。

isPlanted:布尔值,初始为 false,当玩家放置土豆雷后该值变为 true,用于表明土豆雷是否已经被种植在游戏场景中。

isTriggered:同样是布尔值,初始为 false,当满足一定条件(如僵尸靠近到一定距离)时该值变为 true,表示土豆雷已经触发,即将进行爆炸操作。

countdown:用于设定土豆雷种植后到可爆炸的等待时间,单位为秒,比如设定为 15 秒,意味着放置土豆雷后需要等待 15 秒才会触发爆炸(如果在这期间没有被僵尸触发提前爆炸的话)。

xPosition 和 yPosition:记录土豆雷在游戏场景中的坐标位置,以便在游戏画面中准确显示土豆雷的位置并实现与周围元素的交互。

通过这样的数据结构图,可以清晰地看到第 1 - 3 关常见植物的各种属性及其组织方式,这些属性在游戏的运行过程中会不断被读取、更新和处理,以实现植物的各种功能和与其他游戏元素的交互。不同版本的游戏可能会在具体属性值和部分属性设置上有所不同,但基本的属性分类和作用原理是相似的。

③针对“金币”

(1)扫描金币的内存地址

图3.54

(2)找到该金币的内存基址,分析其的全部属性,并画出其数据结构;

一、寻找金币内存基址

使用 Cheat Engine 等工具,在游戏中金币数量发生变化时(如收集一个金币、花费金币购买道具等)进行多次搜索。例如,初始金币数为 100,先搜索 100,然后收集一个金币后变为 125,再搜索 125,如此反复,逐渐缩小搜索范围,最终确定金币的内存基址。假设经过搜索,金币内存基址为 0xABCDEF。

二、金币属性分析

数量:表示玩家当前拥有的金币总数,这是金币最核心的属性,决定了玩家在游戏商店中购买道具、植物等的能力。

来源标识:记录金币的获取来源,如来自僵尸掉落、完成关卡奖励、植物生产(某些特殊植物可能生产金币)等,可用于游戏统计和分析。

状态:如是否处于可收集状态(在游戏场景中未被收集的金币处于可收集状态,收集后变为不可收集),是否被锁定(某些特殊游戏模式或条件下金币不能使用或被冻结)。

图3.55

图3. 56

(3)画出该金币的内存访问示意图或工作原理;

一、内存访问示意图

内存区域

访问目的

访问方式

数据段(可能存储静态属性)

获取金币初始属性,如初始数量等

通过基址偏移来读取对应的值。例如,要获取金币数量,可能是 mov eax,[0xABCDEF + 0x0]

堆(可能存储动态状态信息)

获取金币的动态状态属性,如是否可收集、是否被锁定等,以及更新这些状态属性

当金币状态变化时,通过基址找到对应的堆内存区域,如判断是否可收集,通过 mov eax,[堆分配基址 + 0x4] 读取 collectible 属性值;若要更新为不可收集状态,则通过 mov [堆分配基址 + 0x4], 0 将其设为 false

栈(用于函数调用相关)

在涉及金币操作的函数调用过程中,存储函数参数、局部变量等

例如,当调用收集金币的函数时,将收集到的金币数量等参数压入栈中,函数执行过程中可能会定义局部变量来临时处理金币相

(4)分析该金币功能实现的设计算法;

一、金币获取算法

当有金币产生事件(如僵尸掉落金币):

确定金币来源(根据掉落僵尸类型或游戏场景事件确定来源标识)。

将对应来源标识的金币数量增加一定值(如普通僵尸掉落 10 金币,则将 amount += 10)。

更新金币的可收集状态为 true(如果是新产生的金币)。

二、金币消耗算法

当玩家进行购买操作(如购买植物):

检查金币数量是否足够(比较 amount 与购买所需金币数)。

如果足够,扣除相应金币数量(amount -= 购买所需金币数),并执行购买相关逻辑(如在卡槽添加购买的植物等)。

如果不足,提示玩家金币不足信息。

三、金币状态更新算法

当金币被收集:

检查金币的可收集状态是否为 true。

如果是,将该金币数量累加到玩家总金币数中,然后将可收集状态设为 false,并从游戏场景中移除金币显示(可能涉及图形渲染相关操作)。

(5)分析该金币的相关汇编语言代码的工作过程

一、金币获取时汇编代码分析

假设僵尸掉落金币事件触发:

确定来源标识的汇编代码可能是根据游戏场景中僵尸的类型数据,通过一系列比较和跳转指令确定对应的来源标识值,并存入某个寄存器(如 ebx)

二、金币消耗时汇编代码分析

三、金币收集时汇编代码分析

(6)画出逆向分析的思维导图,并将以说明其逆向原理。

图3. 57

准备工作原理

工具准备:不同的工具在逆向分析中发挥不同作用。反汇编工具能将游戏的二进制代码转化为汇编语言代码,让我们能以更接近程序底层的视角去理解代码逻辑。调试器则可实时跟踪游戏运行过程,观察寄存器和内存数据的实时变化,这对于定位关键数据和代码执行流程至关重要。内存修改工具可帮助我们快速找到金币相关数据在内存中的大致位置,通过修改内存值并观察游戏中的相应变化来验证我们的分析结果。

游戏样本:确保获取完整且可正常运行的游戏版本,是因为逆向分析需要在真实的游戏运行环境下进行,这样才能准确捕捉到与金币相关的各种操作和数据变化情况。

金币数据定位原理

静态分析:通过查看游戏可执行文件的文件头和节区信息,我们可以对文件的整体布局有一个初步了解,推测出可能存储金币相关数据的区域。例如,数据段可能存储一些全局变量和静态数据,很可能包含金币的初始属性值等。然后在反汇编代码中搜索与金币相关的关键词,因为在程序代码中,涉及金币操作的函数通常会引用到金币的数据结构,所以通过定位这些相关代码段,我们可以进一步推测金币数据结构的引用位置或内存基址。

动态调试:在游戏运行时设置断点并监控,当游戏执行到与金币相关的操作时,程序会暂停,此时我们可以详细观察寄存器和内存数据的变化。特别是在金币状态发生改变时,这些变化能提供最直接的线索,帮助我们确定金币在内存中的准确存储地址和数据格式。通过多次试验,即在不同的金币操作场景下(如获取、消耗、收集等)进行观察和对比,我们可以更精准地锁定金币的数据存储位置。

金币属性分析原理

数量属性:金币数量是一个核心属性,其在内存中的存储方式决定了我们如何获取和更新它。通过分析汇编代码,我们可以找到读取和更新金币数量的指令,了解其数据类型(通常为整数)以及存储位置(可能通过基址偏移等方式)。在金币获取时,会通过加法指令将增加的数量累加到原有数量上;在消耗时,则通过减法指令扣除相应数量,并将更新后的结果写回内存地址。

来源标识属性:游戏需要明确金币的来源,以便进行不同的处理(如不同来源可能对应不同的数量增加规则等)。在代码中,会根据游戏中的事件(如僵尸掉落、关卡奖励等)通过一系列的判断和赋值操作来确定来源标识,并将其存储在特定的位置。分析这些代码,我们可以了解到来源标识是如何确定的,以及它在内存中的存储位置和数据表示形式。

状态属性:金币的可收集状态和锁定状态对于游戏的运行和玩家体验有着重要影响。在代码中,通过特定的赋值指令来设置和更新这些状态。例如,当金币产生时,会将可收集状态设为可收集;当收集完成后,会将其设为已收集。对于锁定状态,会根据游戏中的特定条件(如特殊游戏模式等)通过比较、跳转指令判断是否满足锁定条件,并进行相应的赋值操作来设置锁定状态。分析这些代码,我们可以深入了解状态属性的设置和更新机制,以及它们与游戏场景显示等方面的关系。

金币功能实现分析原理

获取功能:当金币有获取事件发生时,首先要判断金币的来源。这通过在汇编代码中运用比较、跳转等指令,根据游戏中的不同情况(如僵尸掉落、关卡奖励等)来确定。确定来源后,会通过加法指令增加金币的数量,并将更新后的数量写回内存地址。同时,还会更新可收集状态等相关属性,通过赋值指令将可收集状态设为可收集(新产生的金币)。通过分析这些代码,我们可以清楚地了解金币获取功能的实现机制。

消耗功能:在金币消耗操作(如购买植物、道具等)中,首先要检查金币数量是否足够。这通过比较指令将当前金币数量与消耗所需数量进行比较,并根据比较结果进行跳转操作(如数量不足则跳转到提示不足的代码段)。如果数量足够,则通过减法指令扣除相应金币数量,并将扣除后剩余的数量写回内存地址。通过分析这些代码,我们可以深入了解金币消耗功能的实现机制。

收集功能:在金币收集过程中,首先要检查可收集状态是否满足。这通过读取内存中的可收集状态值,并进行条件判断(如判断是否为可可收集状态)。如果满足可收集状态,则会通过赋值指令将金币数量累加到玩家总金币数,更新可收集状态为已收集,并可能通过调用图形渲染相关函数从游戏场景中移除金币显示。通过分析这些代码,我们可以清楚地了解金币收集功能的实现机制。

逆向成果总结与应用原理

机制总结:对金币的逆向分析涉及多个方面,通过梳理整合数据定位、属性分析、功能实现分析的结果,我们可以形成一个完整的金币在《植物大战僵尸》游戏中的逆向分析报告。这份报告详细描述了金币在内存中的存储结构、各种操作的代码实现细节和逻辑流程,使我们对金币在游戏中的运作机制有一个全面、深入的了解。

应用场景拓展:基于对金币的逆向分析成果,我们可以进一步探讨其可能的应用。例如,开发游戏修改器时,我们可以通过修改金币相关数据在内存中的值实现无限金币功能,这对于一些玩家可能希望在游戏中拥有更多资源的需求提供了满足途径。同时,通过分析金币相关代码中可能存在的安全漏洞,如数据溢出、非法内存访问等问题,我们可以为游戏的安全防护和优化提供参考,确保游戏的正常运行和玩家数据的安全。

(7)分析至少 3 关的金币的全部基址,并用思维导图分析全部金币的基址内存分布。

画出其数据结构;

图3.58

图3.59

(8)总结至少 3 光的全部植物的不同特性,并说明逆向分析的过程

一、植物特性总结

向日葵

特性:

生产资源型植物,是前期经济来源的关键。消耗 50 阳光即可种植,每隔一段时间(如 24 秒)产生 25 阳光,自身无攻击与防御能力,主要功能是为玩家积累阳光以种植其他更具攻击性或防御性的植物,在游戏布局中通常放置在较为安全的后排位置。

外观上有标志性的黄色大花盘,在游戏画面中较为醒目,其动画效果包括轻微的晃动以及阳光产生时的闪烁特效,以提示玩家阳光已产出。

豌豆射手

特性:

基础攻击型植物,消耗 100 阳光。攻击方式为直线发射豌豆,攻击速度约每 1.5 秒发射一颗豌豆,每颗豌豆对普通僵尸造成 20 点伤害,射程覆盖前方整行,是前期防御僵尸进攻的主力植物。

造型上是一个绿色的炮台状植物,有明显的发射口,在攻击时会有豌豆发射的动画效果,以及发射动作产生的轻微后坐力动画表现,使游戏画面更具动感和真实感。

樱桃炸弹

特性:

范围杀伤型植物,阳光消耗较高,达 150 阳光。其具有强大的爆炸范围攻击能力,能瞬间对以自身为中心九宫格范围内的僵尸造成高额伤害(如对普通僵尸造成 1800 点伤害),可在僵尸大量聚集或面临强大僵尸突破防线时使用,起到清场或重创僵尸群的作用。

外观是一颗红色的大樱桃,放置在战场上后有明显的倒计时显示(如 3 秒倒计时),倒计时结束时触发爆炸特效,爆炸效果震撼且伴有强烈的音效,给玩家带来视觉和听觉上的冲击。

二、逆向分析过程

准备阶段

工具收集与熟悉:

准备反汇编工具(如 IDA Pro),用于将游戏的二进制可执行文件转换为汇编语言代码,以便深入研究程序逻辑。学习并掌握其基本操作,如如何加载游戏文件、如何查看代码段、数据段以及如何进行代码搜索等功能。

调试器(如 OllyDbg),用于在游戏运行时动态跟踪程序的执行过程,观察寄存器、内存数据的实时变化。了解如何设置断点、如何单步执行程序、如何查看寄存器和内存值等调试技巧。

内存修改工具(如 Cheat Engine),用于初步查找植物相关数据在内存中的地址。熟悉其搜索数据、修改数据以及设置内存地址热键等操作,以便快速定位目标数据。

植物数据定位

静态分析:

使用十六进制编辑器或反汇编工具查看游戏可执行文件的结构,分析文件头、节区信息等,推测可能存储植物数据的区域。例如,数据段可能存放植物的初始属性数据,如阳光消耗值、攻击威力等常量数据。

在反汇编代码中搜索与植物相关的字符串常量,如植物的名称(“Sunflower”“PeaShooter”“CherryBomb” 等)、与植物属性相关的关键词(如 “sunCost”“attackPower” 等),通过这些搜索结果定位到与植物属性设置、功能实现相关的代码段,进而推测植物数据结构的引用位置或内存基址。

动态调试:

启动调试器并运行游戏,在游戏中进行植物的种植、攻击、被攻击等操作,同时设置断点在疑似与这些操作相关的代码处。例如,在种植植物的函数入口处设置断点,当种植操作发生时,程序会暂停执行。

观察寄存器和内存数据的变化。在种植向日葵时,注意观察内存中哪些地址的数据发生了变化,可能与向日葵的属性设置有关。比如,某个内存地址的值从 0 变为 50,可能就是向日葵的阳光消耗值的存储地址。通过多次试验不同植物的操作,并对比内存数据变化,确定植物在内存中的准确存储地址和数据格式(如整数类型、字节数等)。

植物属性分析

基本属性剖析:

以向日葵为例,分析其阳光消耗属性时,在反汇编代码中查找种植向日葵函数中对阳光值进行检查和扣除的代码部分。可能会发现类似 cmp eax, 50(比较当前阳光值与向日葵阳光消耗值 50)的比较指令,以及后续根据比较结果进行跳转的指令,由此确定阳光消耗值在代码中的存储和使用方式。

对于豌豆射手的攻击属性,在其攻击相关的代码段中,查找计算伤害值的指令。可能会看到类似 imul eax, 20(将某个与攻击相关的寄存器值乘以豌豆射手的攻击威力 20)的乘法指令,从而确定攻击威力在代码中的体现。同时,查找控制攻击速度的代码,可能涉及定时器相关代码,如设置定时器的初始值为攻击间隔时间(如 1.5 秒对应的定时器计数值),并通过定时器中断来触发攻击操作。

针对樱桃炸弹的爆炸属性,在其爆炸触发的代码中,分析爆炸范围的确定方式。可能会有代码根据樱桃炸弹的中心坐标,计算九宫格范围内的坐标点,并对这些坐标点上的僵尸进行伤害计算。例如,通过循环遍历九宫格内的坐标,对每个坐标对应的僵尸生命值进行扣除操作,从而确定爆炸范围和伤害计算的代码实现。

状态属性探究:

对于植物的存活状态属性,在植物被攻击的代码段中,观察生命值的计算和存活判断逻辑。当僵尸攻击植物时,会有代码根据僵尸的攻击力计算植物所受伤害,并从植物的当前生命值中扣除。例如,可能会有类似 sub [plant_health_address], zombie_attack_power(从植物生命值地址处减去僵尸攻击威力)的减法指令。然后通过比较剩余生命值与 0 的大小来判断植物是否存活,如 cmp [plant_health_address], 0 和根据比较结果进行跳转的指令,以确定存活状态的更新和相关处理代码。

对于樱桃炸弹的倒计时状态属性,在其种植后到爆炸前的时间控制代码中,分析倒计时的实现机制。可能会有一个定时器在后台运行,每隔一定时间(如 1 秒)倒计时数值减 1,通过查看定时器相关代码以及对倒计时变量的更新操作(如 dec countdown_variable),了解倒计时状态在代码中的实现。

植物功能实现分析

种植功能:

分析种植函数代码,首先是阳光值检查部分,如前面所述,通过比较指令判断玩家当前阳光值是否足够种植植物。若足够,接着进行位置确定代码分析。在获取玩家点击种植位置坐标后,会检查该位置是否在可种植区域内,可能会有代码比较坐标值与游戏场景边界值以及已种植植物位置坐标,以确定是否冲突。例如,通过循环遍历已种植植物的坐标数组,与当前种植位置坐标进行比较,若发现冲突则禁止种植并提示玩家。

然后是内存分配与初始化代码。当位置合适时,会在堆上为种植的植物分配一块内存区域来存储其状态信息。在反汇编代码中,可能会看到类似调用内存分配函数(如 call malloc 并传入植物数据结构大小作为参数)的指令,然后对分配到的内存区域进行初始化操作,如设置植物的初始生命值、初始状态(如向日葵的初始未生产阳光状态)、坐标位置等属性值。

攻击功能(以豌豆射手为例):

攻击触发条件分析,在游戏运行过程中,当僵尸进入豌豆射手的攻击范围时,会触发攻击操作。通过分析代码中对僵尸位置和豌豆射手攻击范围的判断逻辑,可能会有代码比较僵尸的坐标与豌豆射手的坐标及攻击范围参数,如判断僵尸的 x 坐标是否在豌豆射手的 x 坐标加减攻击范围值内,且 y 坐标是否相同(因为豌豆射手是直线攻击),若满足条件则触发攻击。

攻击过程代码,包括豌豆的发射动画显示相关代码(可能涉及图形渲染函数调用,传入豌豆发射的起始坐标、方向等参数来绘制豌豆飞行的动画)以及伤害计算与施加代码。在伤害计算部分,如前面提到的根据豌豆射手的攻击威力计算对僵尸的伤害值,并将伤害值传递给僵尸的生命值扣除函数,实现对僵尸的攻击效果。

特殊功能(以樱桃炸弹为例):

樱桃炸弹的爆炸功能实现分析,在倒计时结束时,触发爆炸操作。首先是爆炸范围内僵尸伤害计算代码,通过循环遍历爆炸范围内的僵尸,如前面所述计算对每个僵尸的伤害值并扣除其生命值。然后是爆炸特效显示代码,可能涉及调用复杂的图形渲染函数来展示强大的爆炸效果,包括火焰、烟雾、冲击波等特效的绘制,同时伴有爆炸音效的播放代码(可能调用音频播放函数并传入爆炸音效文件路径或音频资源 ID),以增强游戏的沉浸感和视觉听觉效果。

通过以上对《植物大战僵尸》前三关常见植物的特性总结和逆向分析过程阐述,可以深入了解植物在游戏中的设计与实现细节,为进一步研究游戏机制、开发游戏修改器或辅助工具等提供有力的依据。不同版本的游戏可能在具体代码实现和数据结构上略有差异,但整体的逆向分析思路和方法是相似的。

④针对“僵尸”

(1)扫描第 1-3 关或其它 3 关的全部僵尸内存地址;

图3.60

图3.61

二、僵尸属性分析

基本属性

生命值:表示僵尸能够承受的伤害量,不同类型僵尸生命值不同。如普通僵尸生命值相对较低,铁桶僵尸因头戴铁桶防护,生命值较高。

移动速度:决定僵尸在游戏场景中行进的快慢,例如普通僵尸移动速度适中,而橄榄球僵尸移动速度较快,能更迅速地接近植物防线。

攻击力:指僵尸攻击植物时造成的伤害值,像普通僵尸每次攻击可能对植物造成 10 点伤害,橄榄球僵尸攻击力更强,可能达到 20 点伤害。

类型标识:用于区分不同种类的僵尸,如普通僵尸、铁桶僵尸、橄榄球僵尸等,游戏内部根据这个标识来执行不同的逻辑处理针对不同类型僵尸的行为。

状态属性

是否存活:初始为 True,当僵尸生命值降为 0 时变为 False,表示僵尸已死亡,此时其在游戏场景中的实体应被移除等相关处理。

是否被减速:在游戏中,有些植物(如寒冰射手)能对僵尸起到减速作用,该属性用于记录僵尸是否处于被减速状态,初始为 False,被减速时变为 True。

是否跳过植物:部分特殊僵尸(如跳跳僵尸)有跳过植物的能力,此属性用于标识僵尸当前是否处于跳过植物的状态,初始为 False,当满足跳过条件时变为 True。

图3.62

(3)画出全部僵尸的内存访问示意图或工作原理;

内存区域

访问目的

访问方式

数据段(可能存储静态属性)

获取僵尸的初始基本属性,如生命值、移动速度、攻击力等的初始值

通过基址偏移来读取对应的值。例如,要获取普通僵尸的生命值,若其基址为 0x12345678,可能是 mov eax,[0x12345678 + 0x0]

堆(可能存储动态状态信息)

获取僵尸的动态状态属性,如是否存活、是否被减速、是否跳过植物等,以及更新这些状态属性

当僵尸状态变化时,通过基址找到对应的堆内存区域,如判断普通僵尸是否存活,通过 mov eax,[堆分配基址 + 0x10] 读取 isAlive 属性值;若要更新为死亡状态,则通过 mov [堆分配基址 + 0x10], 0 将其设为 False

栈(用于函数调用相关)

在涉及僵尸操作的函数调用过程中,存储函数参数、局部变量等

例如,当调用僵尸攻击植物的函数时,将僵尸的攻击力等参数压入栈中,函数执行过程中可能会定义局部变量来临时处理僵尸相关信息,这些都在栈上进行存储和访问

(4)分析全部僵尸功能实现的设计算法;

图3.63

图3.64

(5)分析全部僵尸的相关汇编语言代码的工作过程;

一、移动功能相关汇编代码分析

二、攻击功能相关汇编代码分析

三、特殊功能(跳跳僵尸跳过植物)相关汇编代码分析

(6)画出逆向分析的思维导图,并将以说明其逆向原理。

图3.65

图3.66

图3.67

图3.68

图3.69

图3.70

(7)分析至少 3 关的全部僵尸的全部基址,并用思维导图分析全部僵尸的基址内存分

布。画出其数据结构;

图3.71

图3.72

图3.73

图3. 74

(8)总结至少不同级别的 3 种的僵尸的不同特性,并说明逆向分析的过程

图3.75

图3.76

图3.77

图3.78

(9)修改僵尸的属性,说明实现的过程,并画图说明

图3.79

图3.80

图3.81

10)大胆分析“僵尸”的一般属性,看看能否出现意外功能

图3.82

图3.83

图3.84

⑤针对关(级别)

(1)扫描第 1-3 关或其它不同关的数据内存地址;

图3.85

图3.86

图3. 87

图3.88

(2)找到全部关数的内存基址,分析其的全部属性,并画出其数据结构;

图3.89

图3.90

图3.91

图3.92

(3)画出全部关的内存访问示意图或工作原理;

一、内存访问示意图

内存区域

访问目的

访问方式

数据段(全局数据)

存储各关的一些通用基础属性,如初始阳光数量、关卡目标设定(如存活一定波数的僵尸、收集特定数量的物品等)等。这些属性在整个游戏过程中相对固定,但可能因关数不同而有不同的初始值。

通过固定的内存地址偏移或全局变量名来访问。例如,初始阳光数量可能存储在地址 0x1000,在不同关数启动时会被初始化为此关对应的初始值,访问方式如 mov eax, [0x1000]

代码段(程序逻辑)

包含各关执行的主要逻辑代码,如关卡初始化函数、每波僵尸生成函数、游戏结束判断函数等。这些函数根据关数的不同会执行不同的操作流程和逻辑判断。

通过函数调用的方式来执行相应代码段。例如,当进入第 3 关时,游戏会调用 level3_init 函数来进行关卡的初始化操作,在汇编语言中可能是 call level3_init

堆(动态分配数据)

用于存储各关运行过程中动态生成的数据,如本关生成的植物对象、僵尸对象、道具对象等的相关信息。每个对象在堆上会有各自的内存区域分配,其内存布局和数据结构会根据对象类型而定。

当创建一个新的对象(如种植一株植物)时,会调用内存分配函数(如 malloc)来在堆上分配一块合适大小的内存区域,然后通过指针来访问和操作这块内存区域中的数据。例如,种植一棵向日葵后,会在堆上分配一块内存区域给向日葵对象,假设其内存基址为 0x2000,可以通过 mov
eax, [0x2000]
来访问向日葵对象的某个属性(如阳光消耗值)。

栈(函数调用相关)

在各关的函数调用过程中,用于存储函数参数、局部变量、返回地址等信息。例如,当调用一个生成僵尸的函数时,会将生成僵尸的相关参数(如僵尸类型、生成位置等)压入栈中。

(4)分析全部关功能实现的设计算法;

图3. 93

图3.94

图3.95

(5)分析全部关的相关汇编语言代码的工作过程;

一、关卡初始化相关汇编代码分析

二、僵尸波次生成相关汇编代码分析

三、植物种植与管理相关汇编语言代码分析

(6)画出逆向分析的思维导图,并将以说明其逆向原理。

图3.96

图3.97

图3.98

图3.99

图3.100

图3.101

图3.102

图3.103

(7)修改关数的属性,说明实现的过程,并画图说明;

图3.104

图3.105

图3. 106

图3.107

(8)大胆分析“关数”的一般属性,看看能否出现意外功能。

图3.108

图3.109

图3.110

图3.111

图3.112

⑥针对第 3 关豌豆射手

(1)扫描冷却时间的基址

图3.113

(2)找出豌豆射手的冷却时间的数值和内存基址;

图3.114

(3)找出豌豆射手的子弹的类型的的内存基址,并修改子弹为至少三种类型;

(4)找出豌豆射手的子弹的速度的内存基址;

图3.115

(5)找出连续三个豌豆射手的子弹类型,并修改至少三种类型;

图3. 116

图3.117

图3.118

(6)分析相关汇编语言代码的含义和工作过程;画出内存访问示意图或工作原理

图3.119

图3.120

图3.121

图3.122

图3.123

内存区域

访问目的

访问方式

数据段(可能存储静态属性)

存储豌豆射手的初始状态信息(如是否准备好发射子弹的标志 peaShooter_ready_flag)、子弹类型的默认值(如普通豌豆类型用 0 表示)、不同类型子弹的伤害值默认值(如普通豌豆伤害值 20、冰冻豌豆伤害值 15、火焰豌豆伤害值 25、爆炸豌豆伤害值 30 等)等。

通过固定的内存地址偏移来读取或写入数据。例如,要读取豌豆射手是否准备好发射子弹的标志,可能是 mov eax, [peaShooter_ready_flag_address];要写入子弹类型的值,如设置为冰冻豌豆类型,可能是 mov eax, 1mov [bullet_type_address], eax

堆(可能存储动态对象信息)

当豌豆射手在游戏场景中被创建时,可能会在堆上分配一块内存区域来存储其动态信息,比如当前豌豆射手发射的子弹对象信息(包括子弹类型、伤害值等当前状态)。

通过指针来访问和操作堆上的内存区域。例如,如果 peaShooter_ptr 是指向豌豆射手对象的指针,那么要访问其发射的子弹类型信息,可能是 mov eax, [peaShooter_ptr + bullet_type_offset];要修改子弹类型,如改为火焰豌豆类型,可能是 mov eax, 2mov [peaShooter_ptr + bullet_type_offset], eax

栈(用于函数调用相关)

在涉及豌豆射手发射子弹的函数调用过程中,存储函数参数、局部变量等信息。例如,在调用 render_bullet 函数时,可能会将子弹的起始坐标、飞行方向等参数压入栈中。

在函数调用时自动进行栈操作。比如调用 render_bullet 函数,可能会有代码如 push bullet_start_x(推送子弹起始 x 坐标)、push bullet_start_y(推送子弹起始 y 坐标)、push bullet_direction(推送子弹飞行方向)等将参数压入栈,函数执行完后再通过相应的栈操作恢复之前的状态。

(7)画出逆向分析的思维导图,并将以说明其逆向原理;

图3. 124

图3.125

图3.126

图3.127

图3.128

图3.129

图3.130

(8)大胆分析子弹类型如何实现不断变化,看看能否出现意外功能。

图3.131

图3. 132

图3.133

图3.134

⑦针对第 2 关向日葵

(1)扫描冷却与否的状态基址、冷却时间的数值和基址

图3.135

(2)找出向日葵种植的位置坐标,并能直接修改位置;

图3.136

图3.137

图3.138

图3.139

(3)实现第 1 颗向日葵吐连续 吐阳光(连续吐泡泡);

图3.140

图3.141

(4)实现 3 颗向日葵吐连续吐阳光(连续吐泡泡);

图3.142

(5)找出第 1 颗向日葵被僵尸吃掉的时间基址;

图3.143

图3.144

(6)尝试如何让向日葵隐身,写出对应的内存地址;

图3.145

图3.146

(7)分析相关汇编语言代码的含义和工作过程;画出内存访问示意图或工作原理;

画出逆向分析的思维导图,并将以说明其逆向原理;

相关汇编语言代码含义及工作过程分析

图3.147

图3.148

内存区域

访问目的

访问方式

数据段(可能存储静态属性)

存储向日葵的初始状态信息(如初始可见性标志 sunflower_visible_flag、初始透明度值(假设正常可见时为 1)、初始生命值等)、不同状态下的属性默认值(如隐身状态下的透明度值假设为 0 等)等。

通过固定的内存地址偏移来读取或写入数据。例如,要读取向日葵是否可见的标志,可能是 mov eax, [sunflower_visible_flag_address];要写入向日葵隐身状态的透明度值,如设置为隐身状态,可能是 mov eax, 0mov [sunflower_transparency_address], eax

堆(可能存储动态对象信息)

当向日葵在游戏场景中被创建时,可能会在堆上分配一块内存区域来存储其动态信息,比如当前向日葵的实际透明度值、当前生命值等当前状态信息。

通过指针来访问和操作堆上的内存区域。例如,如果 sunflower_ptr 是指向向日葵对象的指针,那么要访问其透明度信息,可能是 mov eax, [sunflower_ptr + transparency_offset];要修改透明度值,如改为隐身状态,可能是 mov eax, 0mov [sunflower_ptr + transparency_offset], eax

栈(用于函数调用相关)

在涉及向日葵隐身设置、状态更新等函数调用过程中,存储函数参数、局部变量等信息。例如,在调用 update_game_screen 函数时,可能会将向日葵的坐标、当前状态等参数压入栈中。

在函数调用时自动进行栈操作。比如调用 update_game_screen 函数,可能会有代码如 push sunflower_x(推送向日葵的 x 坐标)、push sunflower_y(推送向日葵的 y 坐标)、push sunflower_current_state(推送向日葵当前状态)等将参数压入栈,函数执行完后再通过相应的栈操作恢复之前的状态。

图3.149

(7)大胆分析向日葵的属性,实现僵尸吃不掉,并看看能否出现意外功能

图3.150

图3.151

参考与反思

一)技术层面

深入理解游戏机制

通过逆向分析汇编语言代码以及探究内存数据结构,我们得以深入游戏内部的运行逻辑。例如,了解到豌豆射手子弹类型的设置涉及到多个关键内存地址,分别对应着子弹类型标识、伤害值以及特殊效果的触发条件等。这不仅揭示了游戏开发者如何构建游戏的战斗系统,还让我们明白不同元素之间的相互关联和影响。这种深度的理解对于游戏开发爱好者或相关专业人士在学习游戏开发技术、设计游戏机制时有重要的参考意义,可以帮助他们避免一些常见的设计陷阱,同时借鉴成功的设计思路,提升自己的设计能力。

对于向日葵隐身功能的逆向分析,使我们清楚地看到游戏是如何处理植物的可见性和状态更新的。从内存地址中存储的可见性标志到透明度值的设置,再到与游戏画面更新函数的关联,这一系列的机制展示了游戏在图形渲染与对象状态管理方面的精细设计。这可以为游戏开发中的场景管理、特效处理以及对象隐藏与显示等功能的实现提供实际的代码示例和设计范例,启发开发人员思考如何在自己的项目中高效地实现类似功能,同时优化性能和资源利用。

工具使用技巧

在整个逆向分析过程中,我们熟练运用了多种工具,如反汇编工具(IDA Pro)、调试器(OllyDbg)和内存修改工具(Cheat Engine)。这些工具各自具有独特的功能,在不同的分析阶段发挥了关键作用。通过实际操作,我们掌握了如何使用 IDA Pro 快速定位关键代码段、分析函数结构和数据引用;学会了利用 OllyDbg 实时跟踪游戏进程,精准地设置断点并观察寄存器和内存数据的动态变化;以及掌握了 Cheat Engine 在搜索和定位特定数据内存地址方面的高效技巧。这些工具使用经验对于从事游戏逆向工程、软件安全分析或其他相关领域的人员来说是非常实用的技能,可以大大提高他们在处理类似任务时的工作效率和准确性。

(二)创新与玩法拓展层面

激发创意玩法

对游戏元素属性的大胆分析和尝试修改,如让豌豆射手子弹类型不断变化的各种设想,为游戏玩法创新提供了丰富的灵感来源。例如,基于时间间隔、目标属性、玩家操作或游戏场景因素来动态改变子弹类型的想法,可以被应用到其他射击类游戏或策略游戏中,创造出更加多样化和富有挑战性的战斗模式。这种创新玩法能够吸引更多玩家的参与,延长游戏的生命周期,同时也为游戏开发者在设计新游戏或更新现有游戏时提供了全新的思路和方向,鼓励他们突破传统玩法的限制,探索更多元化的游戏体验。

向日葵隐身功能的探索也为游戏玩法带来了新的想象空间。例如,在一些解谜类游戏中,可以借鉴这种隐身机制,设计隐藏道具或角色,玩家需要通过特定的条件或操作来触发隐身效果,从而获取隐藏信息或避开敌人的攻击,增加游戏的趣味性和神秘感。或者在多人竞技游戏中,隐身功能可以作为一种特殊技能或道具,为游戏的策略性和平衡性带来新的挑战和变化,玩家需要根据隐身角色的存在制定不同的战术和应对策略。

二、反思与教训

(一)法律与道德风险

侵权问题

在进行游戏逆向分析并尝试修改游戏功能的过程中,我们必须认识到这可能涉及到侵犯游戏开发者的知识产权。游戏的代码、图像、音频等内容都受到版权法的保护,未经授权的逆向分析和修改行为可能违反相关法律规定。虽然我们的目的可能是出于学习、研究或探索创新玩法,但这种行为仍然处于法律的灰色地带。在实际应用中,如果将逆向分析得到的成果用于商业目的或传播修改后的游戏版本,很可能会面临法律诉讼和侵权赔偿的风险。因此,在进行任何游戏逆向分析活动之前,必须充分了解并尊重相关法律法规,确保自己的行为合法合规。

游戏生态影响

过度的游戏修改行为可能对游戏的生态平衡和公平性产生负面影响。例如,如果玩家通过修改游戏获得了超强的能力或无限资源,这将破坏游戏原本设计的挑战性和平衡性,使得其他正常玩家的游戏体验受到损害。这种不公平的竞争环境可能导致玩家流失,影响游戏的在线社区氛围和长期运营。此外,修改后的游戏版本可能存在安全漏洞或不稳定因素,这不仅会对玩家的设备和个人信息安全造成威胁,还可能影响游戏服务器的正常运行,给游戏开发者带来额外的维护成本和声誉损失。因此,我们应该倡导健康、合法的游戏行为,尊重游戏开发者的劳动成果和其他玩家的游戏权益。

(二)技术局限性与复杂性

版本兼容性

游戏开发者经常会发布游戏的更新版本,对游戏内容、机制和代码进行修改和优化。这就导致我们在逆向分析过程中得到的结果可能只适用于特定的游戏版本,一旦游戏更新,之前找到的内存地址、汇编代码逻辑等可能会发生变化,使得我们的分析成果失效。例如,在对豌豆射手子弹类型的分析中,不同版本的游戏可能对子弹类型的存储方式、伤害计算算法或特殊效果触发条件进行了调整,这就需要我们重新进行逆向分析工作。这种版本兼容性问题增加了游戏逆向分析的难度和工作量,也提醒我们在进行相关研究时要密切关注游戏的更新动态,及时调整分析方法和策略。

代码混淆与加密

为了保护游戏的知识产权和防止逆向分析,许多游戏开发者会采用代码混淆和加密技术。这些技术会使游戏的二进制代码变得更加复杂和难以理解,增加了逆向分析的难度。例如,代码混淆可能会通过重命名变量、函数名、打乱代码结构等方式来隐藏代码的真实逻辑;加密技术则可能对关键代码段或数据进行加密处理,使得在不了解加密算法和解密密钥的情况下无法直接获取原始信息。在面对这种情况时,我们需要具备更深入的密码学知识和逆向分析技巧,如识别常见的代码混淆模式、使用动态调试工具绕过加密代码的执行等,但这仍然是一个极具挑战性的任务,需要投入大量的时间和精力进行研究和探索。

(三)分析方法的优化

数据追踪的准确性

在使用内存修改工具(如 Cheat Engine)搜索与游戏元素属性相关的内存地址时,我们发现由于游戏中数据的复杂性和动态变化性,很容易出现误判或搜索结果不准确的情况。例如,在寻找向日葵隐身相关的内存地址时,可能会因为游戏中其他因素导致的透明度值变化而干扰搜索结果,使得我们需要进行大量的筛选和验证工作。这提示我们在进行数据追踪时,需要更加仔细地设计搜索策略,结合游戏的运行逻辑和元素属性变化规律,同时利用调试器等工具进行辅助验证,提高数据追踪的准确性和效率。

代码逻辑理解的深度

尽管通过反汇编工具和调试器能够获取游戏的汇编语言代码,但理解这些代码的逻辑仍然是一个巨大的挑战。游戏代码通常具有高度的复杂性和嵌套性,涉及到多个模块和函数之间的相互调用和数据传递。在分析豌豆射手子弹类型的相关代码时,我们发现一些代码段的功能可能不仅仅局限于表面上的子弹发射和属性设置,还可能与游戏的其他系统(如资源管理、游戏状态更新等)存在深层次的关联。这就要求我们在分析代码逻辑时,不能仅仅满足于对单个函数或代码块的理解,而需要从整个游戏系统的角度出发,深入挖掘代码之间的内在联系和逻辑关系,构建完整的代码逻辑框架,以便更好地理解游戏的运行机制和实现功能。

综上所述,游戏逆向分析是一项具有挑战性但又充满价值的工作。通过对游戏内部机制的深入探索,我们可以获得宝贵的技术经验、激发创新玩法的灵感,但同时也必须清醒地认识到其中存在的法律风险、技术局限性和分析方法的不足。在未来的游戏研究和开发过程中,我们应该在合法合规的前提下,不断提升自己的技术能力和分析水平,以更加科学、高效的方式进行游戏逆向分析,为游戏行业的发展贡献积极的力量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值