CE找基址原理
抄的:https://blog.csdn.net/zhangfengz1995/article/details/79056552?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
一般来说,先要找到一个内存地址,比如说你通过不断增大减小金钱,用CE搜,搜出来了代表金钱的内存地址。这个地址我们称之为 目标地址 (我瞎起的名字)。 目标地址是会变化的,每次重开游戏,甚至换个角色,换个地图,都有可能变。
所以它的可用性不强。我们需要的是一个稳定的访问方式,这个东西就是基址。
让我们从写游戏的程序员角度来思考,程序里面需要一个地方放金钱。
假设这个游戏是C/C++写的。(一般来说都是这样的,就算不是,一般来说其他语言的底层细节和C是类似的)
最简单的情况:
比如说一个非常简单的小游戏,可能它的金钱直接就是个全局变量。因为这是一个C/C++写的游戏,全局变量的位置应该是固定的。当一个程序被加载后,其映像地址被确定,比如说现在的windows,可执行文件的加载地址一般是 (囧,忘了)。 然后该全局变量就在相对于这个映像地址的某个偏移处,这个偏移是固定的。这种情况,我们在CE搜到的直接就是不变的地址了。
所以它的可用性不强。我们需要的是一个稳定的访问方式,这个东西就是基址。
让我们从写游戏的程序员角度来思考,程序里面需要一个地方放金钱。
假设这个游戏是C/C++写的。(一般来说都是这样的,就算不是,一般来说其他语言的底层细节和C是类似的)
最简单的情况:
比如说一个非常简单的小游戏,可能它的金钱直接就是个全局变量。因为这是一个C/C++写的游戏,全局变量的位置应该是固定的。当一个程序被加载后,其映像地址被确定,比如说现在的windows,可执行文件的加载地址一般是 (囧,忘了)。 然后该全局变量就在相对于这个映像地址的某个偏移处,这个偏移是固定的。这种情况,我们在CE搜到的直接就是不变的地址了。
如果是另一种情况:
假设这个游戏有一堆全局变量,写游戏的程序员看着这一堆全局变量很发愁。他决定把代码重构一下。把所有的全局变量整合到一个结构体里面,比如说原来是 血,蓝,金钱 都直接裸体放在全局。现在搞了一个struct,把血,蓝,金钱,都弄到结构体里面,当成员。然后全局放一个这个struct的实例。这样的话,因为这个实例是全局的,它的地址不变,然后金钱在struct内的偏移不变,所以目标地址还是可以直接当基址用。
假设这个游戏有一堆全局变量,写游戏的程序员看着这一堆全局变量很发愁。他决定把代码重构一下。把所有的全局变量整合到一个结构体里面,比如说原来是 血,蓝,金钱 都直接裸体放在全局。现在搞了一个struct,把血,蓝,金钱,都弄到结构体里面,当成员。然后全局放一个这个struct的实例。这样的话,因为这个实例是全局的,它的地址不变,然后金钱在struct内的偏移不变,所以目标地址还是可以直接当基址用。
但是如果更进一步:
假如这个struct太大了,程序员决定把它动态分配,也就是说现在全局放了一个指针,指针指向了一片动态分配出来的内存,放着这个struct。现在我们可以知道,指针是全局的,它的地址不变,但是它的内容是变化的(因为是动态分配的),所以我们搜到的目标地址就是变化的了。但是如果我们每次都先找到指针(指针是全局的,它的地址固定),再找指针指向的位置,得到struct,然后金钱在相对于这个struct的固定偏移处,这样一个迂回的方式,就可以保证每次都取到正确金钱地址了。
假如这个struct太大了,程序员决定把它动态分配,也就是说现在全局放了一个指针,指针指向了一片动态分配出来的内存,放着这个struct。现在我们可以知道,指针是全局的,它的地址不变,但是它的内容是变化的(因为是动态分配的),所以我们搜到的目标地址就是变化的了。但是如果我们每次都先找到指针(指针是全局的,它的地址固定),再找指针指向的位置,得到struct,然后金钱在相对于这个struct的固定偏移处,这样一个迂回的方式,就可以保证每次都取到正确金钱地址了。
基本上其他的都是上面这种指针式的扩展了:
比如说某游戏采用了 一关一关的 数据结构。每关都会重新搞一块儿内存,但是金钱在这块内存的固定偏移处。这样的话,我们需要找到一个东西指向 关卡 ,然后再加一个固定的偏移。
再组合一下,可能金钱不在这个关卡结构的固定偏移处,可能关卡内部一个指针,指向一个角色,然后金钱在这个角色的固定偏移处。这就是两重指针。
当然也可能更复杂。但是总是可以找到一个方式来寻找到目标地址。
可以这样想,游戏本身肯定需要一个方式来访问金钱。这个方式就是 指针 和 固定地址、固定偏移 的组合。
并且这个方式是固定的。既然如此,我们就可以用同样的方式来访问金钱。
(一个相关的问题。以前想过,如果游戏本身加入了随机因素,那么访问方式就不是固定的了。会不会导致我们找不到基址。想了想应该不会。所谓随机,如何随机呢。举个例子,某游戏,在开启时产生一个随机数,然后在一个数组的该随机数偏移处存放金钱。这样我们可以找到存放这个随机数的基址,然后找到存放数组位置的基址,组合起来其实还是 指针和固定地址、固定偏移的组合。只不过这个固定地址(也就是这个随机数)的获得,也需要搞一遍基址。)
比如说某游戏采用了 一关一关的 数据结构。每关都会重新搞一块儿内存,但是金钱在这块内存的固定偏移处。这样的话,我们需要找到一个东西指向 关卡 ,然后再加一个固定的偏移。
再组合一下,可能金钱不在这个关卡结构的固定偏移处,可能关卡内部一个指针,指向一个角色,然后金钱在这个角色的固定偏移处。这就是两重指针。
当然也可能更复杂。但是总是可以找到一个方式来寻找到目标地址。
可以这样想,游戏本身肯定需要一个方式来访问金钱。这个方式就是 指针 和 固定地址、固定偏移 的组合。
并且这个方式是固定的。既然如此,我们就可以用同样的方式来访问金钱。
(一个相关的问题。以前想过,如果游戏本身加入了随机因素,那么访问方式就不是固定的了。会不会导致我们找不到基址。想了想应该不会。所谓随机,如何随机呢。举个例子,某游戏,在开启时产生一个随机数,然后在一个数组的该随机数偏移处存放金钱。这样我们可以找到存放这个随机数的基址,然后找到存放数组位置的基址,组合起来其实还是 指针和固定地址、固定偏移的组合。只不过这个固定地址(也就是这个随机数)的获得,也需要搞一遍基址。)
原理就是这样。
对照网上的教程,给出一些小时候不明白的地方的解释。
一般来说CE找到目标地址以后,教程里会用CE找,访问此地址的代码。然后找出来一堆。从里面以某种方式选中一项,查看其反汇编。举个例子,假如说金钱放在0x00000018, 然后我们找到的相关反汇编是这样的
对照网上的教程,给出一些小时候不明白的地方的解释。
一般来说CE找到目标地址以后,教程里会用CE找,访问此地址的代码。然后找出来一堆。从里面以某种方式选中一项,查看其反汇编。举个例子,假如说金钱放在0x00000018, 然后我们找到的相关反汇编是这样的
mov eax,[0x00001234] mov ebx, [eax+4] //这条执行完毕后ebx=0x00000010 add [ebx+8],9 //金钱增加9
这里我们就可以看到目标地址0x00000018是怎么来的。
首先访问 0x00001234,得到一个数值,把这个数值+4,当成一个地址,访问这个地址得到一个数值,这个数值是0x00000010。
后面我们发现,把它加8得到目标地址,把目标地址的内容加9,也就是金钱增加9.
可以这样想(这是一个猜测),0x00001234是一个全局指针的地址(注意,这个地址是固定的),这个指针指向关卡的数据结构,访问0x1234得到这个指针的内容,也就是关卡的首地址。关卡首地址+4处存放了一个指针。这个指针指向了一个角色的数据结构。访问这个指针得到其内容,也就是角色的首地址。再角色内部+8处存放着金钱。
这样我们就知道 基址是 [[0x00001234]+4]+8 ,这个基址的内容就是金钱,这个基址本身就是金钱的地址。
当然这里是我编的例子,一般来说反汇编出来不会这么紧凑的写着所有关键代码。很有可能我们只能看到
add [ebx+8], 9 //ebx=0x00000010
然后周围都是无关代码。
这时候教程做法是在CE里再搜 0x00000010。
为什么要这样呢,我们的目的是找 这个 0x00000010是怎么来的。
这时候我们会在CE里看到很多地方都存放有0x00000010,以某种方式找到其中一项,查看谁访问了它,再找反汇编。
这时候我们有可能看到
mov ebx, [eax+4] 这条执行完毕后ebx=0x00000010, eax=0x00004321
这就是0x00000010怎么来的。是访问 eax +4 得来的, eax=0x00004321,然后再看0x00004321怎么来的。
先搜,再看谁访问,我们又找到了
mov eax,[0x00001234] eax=0x00004321
这就是0x00004321怎么来的,是访问 0x00001234得来的。这个0x00001234哪来的?。。它就是个固定值
所以这样我们就找到了基址。
但是这样有一个很大的缺点。如果像我编的那个很紧凑的例子一样,我们一次性看到了所有相关代码,那自然好。可是如果不是这样(基本上都是这样)。我们就要看某些值是从哪来的。但是关键点就在于,我们是用CE搜的相关值,用CE查的相关代码,谁也不知道是不是驴头不对马嘴,有可能找到的不是同一套访问方式的中间代码和中间值。 这是很有几率的。不过一般来说大量值和中间代码都是类似的,比如说寄存器从eax换成了ecx,但是访问方式还是不变。这就是这个不严谨方式几乎总是能成功的一大部分原因。
(后记:多用了几次以后发现,OD查反汇编虽然理论上来说是最准的,但太费事了,万一某一条线 路上跟丢了简直崩盘,还会时不时用一个不知道哪里赋值的寄存器的值,真累。。有时候还是直接CE强搜比较方便。)
add [ebx+8], 9 //ebx=0x00000010
然后周围都是无关代码。
这时候教程做法是在CE里再搜 0x00000010。
为什么要这样呢,我们的目的是找 这个 0x00000010是怎么来的。
这时候我们会在CE里看到很多地方都存放有0x00000010,以某种方式找到其中一项,查看谁访问了它,再找反汇编。
这时候我们有可能看到
mov ebx, [eax+4] 这条执行完毕后ebx=0x00000010, eax=0x00004321
这就是0x00000010怎么来的。是访问 eax +4 得来的, eax=0x00004321,然后再看0x00004321怎么来的。
先搜,再看谁访问,我们又找到了
mov eax,[0x00001234] eax=0x00004321
这就是0x00004321怎么来的,是访问 0x00001234得来的。这个0x00001234哪来的?。。它就是个固定值
所以这样我们就找到了基址。
但是这样有一个很大的缺点。如果像我编的那个很紧凑的例子一样,我们一次性看到了所有相关代码,那自然好。可是如果不是这样(基本上都是这样)。我们就要看某些值是从哪来的。但是关键点就在于,我们是用CE搜的相关值,用CE查的相关代码,谁也不知道是不是驴头不对马嘴,有可能找到的不是同一套访问方式的中间代码和中间值。 这是很有几率的。不过一般来说大量值和中间代码都是类似的,比如说寄存器从eax换成了ecx,但是访问方式还是不变。这就是这个不严谨方式几乎总是能成功的一大部分原因。
(后记:多用了几次以后发现,OD查反汇编虽然理论上来说是最准的,但太费事了,万一某一条线 路上跟丢了简直崩盘,还会时不时用一个不知道哪里赋值的寄存器的值,真累。。有时候还是直接CE强搜比较方便。)
另外一个要提醒的是,有的复杂情况可能中间的+4 +8偏移不是固定值,而是 +ecx 这种变量。然后还要查这个ecx哪来的。
知道了上面这种方式的原理和缺点,就可以知道,上面这种方式完全没必要。最好还是找到目标地址后,看谁访问了,然后直接转到反汇编,看整个上下文,这样保证总是对的。比如说用CE查到目标地址,然后用OD开开,下访问断点,然后看访问的这个东西,如何访问的,就能直接找到一整套的访问方式。
然后就是看反汇编了。找到底如何访问的。
这些内容就没必要说了,看各人功力了
这些内容就没必要说了,看各人功力了
另外,之前我们找到的基址是[[0x00001234]+4]+8,我们的猜测是 0x00001234指向关卡,关卡+4是角色,角色+8是金钱
那我们就可以去试一试,看一看,说不定角色+12是血,+16是蓝呢?
说不定关卡+8是关卡名呢?
另外说一句,一般字符串是C字符串。也就是说是一个指针,指向一个char型数组,以0结尾,查看的时候要注意。
那我们就可以去试一试,看一看,说不定角色+12是血,+16是蓝呢?
说不定关卡+8是关卡名呢?
另外说一句,一般字符串是C字符串。也就是说是一个指针,指向一个char型数组,以0结尾,查看的时候要注意。