说起来为什么一个图片会扯出来这么多内容,本来我是不打算将这些基础到不能再基础的概念的,但是想到如果连这些都不懂,那连图中的那第一行注释为“触发”代码都看不懂,所以便还是讲了些,大概明白点也好继续看下去。
继续要讲的是基址和偏移的本质,我是非常有想找到我N年前录得视屏,就不用再把这里废话一遍了,但是我找不到了。
基址是啥?他就是个地址,可以转换成指针来进行存取操作,那为啥不叫地址呢,因为它通常和偏移一同使用,偏移是基于此地址的,所以叫“基”址。
而且它是每次运行都不变的,因为他是个全局变量或者static变量(单例模式),在游戏开发中多为后者,这个static变量是指针,指向某个类或结构的唯一实例。
偏移是啥?他就上面最后一例代码*(int*)(i+4)中的4。
程序设计语言,不论是面向过程还是面向对象,都具备定义和操作数据结构的能力,上面的S就是最简单的数据结构,现在来把他和它的成员变一下名字:
struct soldier {
int hp;
int mp;
};
感觉是不是微妙了一些呢,这不就是游戏中常见的概念吗?一个“战士”拥有最基本的“血量”和“蓝量”两个属性,这就是一个结构,那之前对于这个结构的操作,都可以理解为WG对游戏内数值的修改行为。
一般情况下我们会通过逆向来找出“基址”,当然也可以使用别人找到共享出来的,假设我们通过调试器找到了某个游戏中角色的基址为0x1234,并且通过观察内存数据发现了偏移0处为hp,偏移4处为mp,那么我们对其hp和mp的修改就可以这样操作(注意我们是没有结构体的定义的,我们只知道基址和偏移):
*(int*)(*(int*)0x1234 + 0) = 999999;
*(int*)(*(int*)0x1234 + 4) = 999999;
是不是很像图片里的代码了,只是比图里的少了几层而已。但是跟之前的例子对比,又有些不太一样,如果按之前的例子写,那应该是 *(int*)(0x1234 + 4) = 999999 啊,为什么会多了一次*(int*)呢?这并不是写错了,而是因为我上文说的单例模式,也就是基址存在的本质原因。
何为单例模式,它是一种设计模式,作用是限制一个结构只能定义一个对象,考虑游戏中角色的概念,通常角色只有一个,就是你所操作的那一个,所以角色是非常适合使用单例模式的结构。
对于常规的结构比如前文的S,我们可以:
S s1;
S s2;
这是完全没有问题的,他们是类型相同的两个不同对象。
而对于单例结构,只允许定义(实例化)一个对象,它定义对象的方式是通过定义一个静态的实例指针,并保存在结构中,暴露接口给外部访问,而其构造函数设为私有,因此外部不可以访问其构造函数,也就无法定义一个实例(超纲,不懂啥是构造函数的自行搜索或者跳过这块)。
而所谓基址,其实就是这个结构中保存的指针的地址,这个static指针和全局对象拥有相同的特性,即他是硬编码在二进制文件中的,体现在汇编中就是一个立即数,是由程序本身的加载的基地址加上其在.data节中的rva所得到的(听不懂没关系,就知道他是不变的就行了)。
因此0x1234其实是这个指针的地址,指针保存的值才是我们角色的地址,所以要先对这个指针的地址进行一次解引用取值操作,得到角色的地址,就可以代入之前的例子了。
搞清楚了基址和偏移的本质,再来看看多级偏移,上个例子我们只有一个偏移量,要么是4要么是0,那是因为这个角色的结构太简单了,而一个好玩的游戏,人物的属性必然不会只有血和蓝,起码他还得有技能吧。
那么看这样一个结构:
struct skill {
char name[10];
int damage;
};
这是一个简单的技能结构,他有两个成员,一个是技能的名字,一个是技能的伤害。
我们来定义一个技能
skill taunt;
taunt.name = "嘲讽"; //这里不能这么用,对字符串的操作不可以直接赋值,暂且不讲,就当成可以这么用
taunt.damage = 800;
以上定义了一个名为嘲讽的技能,它对敌人造成800点伤害,但是这个技能暂时不属于任何人。
我们再改一下角色的定义:
struct soldier {
int hp;
int mp;
skill* skl;
};
然后我们做这样一个操作:
soldier s;
s.hp = 1000;
s.mp = 400;
s.skl = &taunt;
这样我们通过给角色结构的成员中加入了一个技能的指针,使我们的战士得到了一个嘲讽的技能,为什么用指针而不直接在结构中定义技能呢,这是游戏设计的问题,这里不详细讨论。
总之无论是指针还是直接定义,都有读写它的办法,多级偏移正是由于使用指针的设计方式而出现的。
接下来写代码来修改我们角色嘲讽技能的攻击力(你可以先不往下看,自己尝试写一下):
*(int*)(*(int*)(*(int*)0x1234 + 8) + 10) = 999999;
emmm...这样看起来和图片里的更像了,来看一步步分析。
*(int*)0x1234 按上文所述是拿到人物的地址,+8是跳过hp和mp成员计算出嘲讽技能指针的地址,因为计算地址要int型,所以算完后还要将其转为指针,再对其解引用,*(int*)(*(int*)0x1234 + 8) 即是嘲讽技能的地址。
此时我们拿到的是另一个结构的地址,如果你真理解了上面的内容,那你应当知道+10是何意:为的是跳过技能名字的数据,即10个char,每个char是一个字节,因此+10(此处依然不考虑对齐,有好奇心的同学自行搜索)。
跳过10个字节后 *(int*)(*(int*)0x1234 + 8) + 10 便是damage成员的地址了,damage是int型数据所以将其转换为int*进行访问,*(int*)(*(int*)(*(int*)0x1234 + 8) + 10) = 999999 自然便是修改其为999999。
我不会再写例子还原图片中的5级偏移,因为原理和2级偏移是一样的,你可以想象一下,有一个地牢、地图里有个人、人身上有个袋子、袋子里有很多小袋子、某个小袋子是装战利品的、战利品有自己的属性(名称、价值、描述、重量、是否可出售等等),那如果你要修改这个物品的价值,那你起码需要几次偏移呢?
我再结合之前的某个东西提个问题并做出解答,建议你先自己做出来再看答案:如果技能结构的伤害成员是float类型,你该怎么修改他的值呢?答案在文章末尾。
继续要讲的是基址和偏移的本质,我是非常有想找到我N年前录得视屏,就不用再把这里废话一遍了,但是我找不到了。
基址是啥?他就是个地址,可以转换成指针来进行存取操作,那为啥不叫地址呢,因为它通常和偏移一同使用,偏移是基于此地址的,所以叫“基”址。
而且它是每次运行都不变的,因为他是个全局变量或者static变量(单例模式),在游戏开发中多为后者,这个static变量是指针,指向某个类或结构的唯一实例。
偏移是啥?他就上面最后一例代码*(int*)(i+4)中的4。
程序设计语言,不论是面向过程还是面向对象,都具备定义和操作数据结构的能力,上面的S就是最简单的数据结构,现在来把他和它的成员变一下名字:
struct soldier {
int hp;
int mp;
};
感觉是不是微妙了一些呢,这不就是游戏中常见的概念吗?一个“战士”拥有最基本的“血量”和“蓝量”两个属性,这就是一个结构,那之前对于这个结构的操作,都可以理解为WG对游戏内数值的修改行为。
一般情况下我们会通过逆向来找出“基址”,当然也可以使用别人找到共享出来的,假设我们通过调试器找到了某个游戏中角色的基址为0x1234,并且通过观察内存数据发现了偏移0处为hp,偏移4处为mp,那么我们对其hp和mp的修改就可以这样操作(注意我们是没有结构体的定义的,我们只知道基址和偏移):
*(int*)(*(int*)0x1234 + 0) = 999999;
*(int*)(*(int*)0x1234 + 4) = 999999;
是不是很像图片里的代码了,只是比图里的少了几层而已。但是跟之前的例子对比,又有些不太一样,如果按之前的例子写,那应该是 *(int*)(0x1234 + 4) = 999999 啊,为什么会多了一次*(int*)呢?这并不是写错了,而是因为我上文说的单例模式,也就是基址存在的本质原因。
何为单例模式,它是一种设计模式,作用是限制一个结构只能定义一个对象,考虑游戏中角色的概念,通常角色只有一个,就是你所操作的那一个,所以角色是非常适合使用单例模式的结构。
对于常规的结构比如前文的S,我们可以:
S s1;
S s2;
这是完全没有问题的,他们是类型相同的两个不同对象。
而对于单例结构,只允许定义(实例化)一个对象,它定义对象的方式是通过定义一个静态的实例指针,并保存在结构中,暴露接口给外部访问,而其构造函数设为私有,因此外部不可以访问其构造函数,也就无法定义一个实例(超纲,不懂啥是构造函数的自行搜索或者跳过这块)。
而所谓基址,其实就是这个结构中保存的指针的地址,这个static指针和全局对象拥有相同的特性,即他是硬编码在二进制文件中的,体现在汇编中就是一个立即数,是由程序本身的加载的基地址加上其在.data节中的rva所得到的(听不懂没关系,就知道他是不变的就行了)。
因此0x1234其实是这个指针的地址,指针保存的值才是我们角色的地址,所以要先对这个指针的地址进行一次解引用取值操作,得到角色的地址,就可以代入之前的例子了。
搞清楚了基址和偏移的本质,再来看看多级偏移,上个例子我们只有一个偏移量,要么是4要么是0,那是因为这个角色的结构太简单了,而一个好玩的游戏,人物的属性必然不会只有血和蓝,起码他还得有技能吧。
那么看这样一个结构:
struct skill {
char name[10];
int damage;
};
这是一个简单的技能结构,他有两个成员,一个是技能的名字,一个是技能的伤害。
我们来定义一个技能
skill taunt;
taunt.name = "嘲讽"; //这里不能这么用,对字符串的操作不可以直接赋值,暂且不讲,就当成可以这么用
taunt.damage = 800;
以上定义了一个名为嘲讽的技能,它对敌人造成800点伤害,但是这个技能暂时不属于任何人。
我们再改一下角色的定义:
struct soldier {
int hp;
int mp;
skill* skl;
};
然后我们做这样一个操作:
soldier s;
s.hp = 1000;
s.mp = 400;
s.skl = &taunt;
这样我们通过给角色结构的成员中加入了一个技能的指针,使我们的战士得到了一个嘲讽的技能,为什么用指针而不直接在结构中定义技能呢,这是游戏设计的问题,这里不详细讨论。
总之无论是指针还是直接定义,都有读写它的办法,多级偏移正是由于使用指针的设计方式而出现的。
接下来写代码来修改我们角色嘲讽技能的攻击力(你可以先不往下看,自己尝试写一下):
*(int*)(*(int*)(*(int*)0x1234 + 8) + 10) = 999999;
emmm...这样看起来和图片里的更像了,来看一步步分析。
*(int*)0x1234 按上文所述是拿到人物的地址,+8是跳过hp和mp成员计算出嘲讽技能指针的地址,因为计算地址要int型,所以算完后还要将其转为指针,再对其解引用,*(int*)(*(int*)0x1234 + 8) 即是嘲讽技能的地址。
此时我们拿到的是另一个结构的地址,如果你真理解了上面的内容,那你应当知道+10是何意:为的是跳过技能名字的数据,即10个char,每个char是一个字节,因此+10(此处依然不考虑对齐,有好奇心的同学自行搜索)。
跳过10个字节后 *(int*)(*(int*)0x1234 + 8) + 10 便是damage成员的地址了,damage是int型数据所以将其转换为int*进行访问,*(int*)(*(int*)(*(int*)0x1234 + 8) + 10) = 999999 自然便是修改其为999999。
我不会再写例子还原图片中的5级偏移,因为原理和2级偏移是一样的,你可以想象一下,有一个地牢、地图里有个人、人身上有个袋子、袋子里有很多小袋子、某个小袋子是装战利品的、战利品有自己的属性(名称、价值、描述、重量、是否可出售等等),那如果你要修改这个物品的价值,那你起码需要几次偏移呢?
我再结合之前的某个东西提个问题并做出解答,建议你先自己做出来再看答案:如果技能结构的伤害成员是float类型,你该怎么修改他的值呢?答案在文章末尾。