快一年没接触外挂了。现在回来学习,哎,但师傅事情忙,又没空理我了。
闲着没事来发篇文章给各位新手,高手可以直接跳过了。
今天要说的是怎样制作自己的特征码扫描工具。
众所周知,游戏更新一般那些CALL(过程或函数)都是不改变的。
(除非有什么或需要变动什么功能才会去改吧,总之极少更改。)
那么在这种条件下,我们就来写一个根据特征码来找到内存中这函数的地址。
寻找特征码的方法有两种,一是根据看哪里调用了这个函数。二是直接找这个函数体。
下面我将以诛仙的打坐CALL为例。来写一下到底要怎么实现今天这个话题。
我使用的是Delphi。所以就用Delphi来讲解了。而且本次使用指针操作。
在此我也推荐各位如果条件允许尽量使用指针操作。既快又安全。不过要注意空指针的问题。
1. 找到CALL原形
目前诛仙(356版)打坐CALL的地址是$5FFF40。
那么我们直接打开OD。CTRL + G跳转到005FFF40这个地址。
下面是我复制的打坐CALL的原形给大家看看。
005FFF40 /$ 56 PUSH ESI
005FFF41 |. 6A 02 PUSH 2
005FFF43 |. E8 88601900 CALL ElementC.00795FD0
005FFF48 |. 8BF0 MOV ESI,EAX
005FFF4A |. 83C4 04 ADD ESP,4
005FFF4D |. 85F6 TEST ESI,ESI
005FFF4F |. 74 1E JE SHORT ElementC.005FFF6F
005FFF51 |. 66:C706 2E00 MOV WORD PTR DS:[ESI],2E
005FFF56 |. A1 EC83A100 MOV EAX,DWORD PTR DS:[A183EC]
005FFF5B |. 6A 02 PUSH 2 ; /Arg2 = 00000002
005FFF5D |. 56 PUSH ESI ; |Arg1
005FFF5E |. 8B48 20 MOV ECX,DWORD PTR DS:[EAX+20] ; |
005FFF61 |. E8 6A34FDFF CALL ElementC.005D33D0 ; \ElementC.005D33D0
005FFF66 |. 56 PUSH ESI
005FFF67 |. E8 74601900 CALL ElementC.00795FE0
005FFF6C |. 83C4 04 ADD ESP,4
005FFF6F |> 5E POP ESI
005FFF70 \. C3 RETN
那么,我们怎么根据CALL原形来写特征码搜索工具呢?
请大家注意上面的机器码部分(56 6a 02 ...这部分),这个就是我们要的了。
下面我把我的函数并加了详细注释的给大家看看。
function ScanAddr: DWORD;
const
StartAddr = $500000; //这里定义一个边界。只搜索这一部分的内存
StopAddr = $650000; //如果超过了,那肯定就不是我们需要的了。
var
Addr: DWORD; //保存我们函数执行的当前地址
begin
Result := 0; //没搜索到就返回0了
Addr := StartAddr; //从定义的StartAddr开始扫描
if StartAddr >= StopAddr then Exit; //如果定义不正确就直接退出这个过程
while Addr <= StopAddr do //循环读内存,每次增加一字节,看下面的Inc(Addr)
try //注意,这里为什么要用try 而不用begin end呢?就是为了防止空指针的情况
if (PDWORD(Addr)^ = $E8026A56) and
//大家返回上面我们的CALL原形,仔细看看这个什么意思。
//005FFF40 /$ 56 PUSH ESI
//005FFF41 |. 6A 02 PUSH 2
//005FFF43 |. E8 88601900 CALL ElementC.00795FD0
//或者我换个写法,大家可能比较容易看懂
//if (PByte(Addr)^ = $56) and
// (PWORD(Addr + $1)^ = $026A) and
// (PByte(Addr + $3)^ = $E8) and
//大家都看明白了吧?注意要去跟我上面说的机器码对比一下哦。
//呵呵。到此。我相信大家都明白这个思路了。直接跳过这个过程。
(PDWORD(Addr + $8)^ = $C483F08B) and
(PDWORD(Addr + $C)^ = $74F68504) and
(PDWORD(Addr + $11)^ = $2E06C766) and
(PByte(Addr + $16)^ = $A1) and
(PDWORD(Addr + $1B)^ = $8B56026A) and
(PWORD(Addr + $1F)^ = $2048) and
(PByte(Addr + $21)^ = $E8) and
(PWORD(Addr + $26)^ = $E856) and
(PDWORD(Addr + $2C)^ = $5E04C483) and
(PByte(Addr + $30)^ = $C3) then
begin
Result := Addr; //直到上面的条件都满足了。那结果也就出来了。
Break; //找出结果了就退出循环。
end
else //如果上面的条件有一个不满足。则增加一个地址。
Inc(Addr); //如第一次搜索的$500000不对那就跳到$500001。再不对就再跳。
except
Inc(Addr); //对面try 如果上面读入的过程出现异常则直接增加1
Continue; //继续下一次循环
end;
end;
加了注释比较花,大家可以复制到记事本把注释去掉看看。
如果大家用的ReadProcessMemory函数来读内存也没关系,方法还是一样的。
下面我就再写个过程给那些使用ReadProcessMemory读数据的新手。
//方便重用,自己先写个函数来返回内存值
function ReadMemory(dwAddress, nSize: DWORD): DWORD;
var
BytesRead: DWORD;
R1: Byte;
R2: WORD;
R4: DWORD;
begin
case nSize of
1 : begin
ReadProcessMemory(hProcess, Pointer(dwAddress), @R1, nSize, BytesRead);
Result := R1;
end;
2 : begin
ReadProcessMemory(hProcess, Pointer(dwAddress), @R2, nSize, BytesRead);
Result := R2;
end;
4 : begin
ReadProcessMemory(hProcess, Pointer(dwAddress), @R4, nSize, BytesRead);
Result := R4;
end;
end;
end;
//下面就是读取我原先的函数转换的。
function ScanAddr: DWORD;
const
StartAddr = $500000;
StopAddr = $650000;
var
Addr: DWORD;
begin
Result := 0;
Addr := StartAddr;
if StartAddr >= StopAddr then Exit;
while Addr <= StopAddr do
begin
if (ReadMemory(Addr, 4) = $E8026A56) and
(ReadMemory(Addr + $8, 4) = $C483F08B) and
(ReadMemory(Addr + $C, 4) = $74F68504) and
(ReadMemory(Addr + $11, 4) = $2E06C766) and
(ReadMemory(Addr + $16, 1) = $A1) and
(ReadMemory(Addr + $1B, 4) = $8B56026A) and
(ReadMemory(Addr + $1F, 2) = $2048) and
(ReadMemory(Addr + $21, 1) = $E8) and
(ReadMemory(Addr + $26, 2) = $E856) and
(ReadMemory(Addr + $2C, 4) = $5E04C483) and
(ReadMemory(Addr + $30, 1) = $C3) then
begin
Result := Addr;
Break;
end
else
Inc(Addr);
end;
end;
到此,这篇教程就写完了。我想各位都能理解了吧。上面的方法是使用函数体本身来写的,其实还是不推荐这样。
为什么呢?如果游戏公司稍微改变了代码那我们的搜索工具就也得改代码。
而且比如像寻路CALL就可以使用调用的方法来一次性找到3个地址(好象是3个吧?忘了)。
所有用到的地址。这个就当给各位新手留着做作业吧。o(∩_∩)o
最后,在此谢谢师傅长期以来对我的帮助,这个方法也是师傅教我的。就在此借花献佛一下。
阅读(5026) | 评论(0) | 转发(0) |