看流星社区

 找回密码
 注册账号
查看: 5668|回复: 4

[Delphi] 【转】教大家写特征码扫描工具来自动寻找CALL地址

[复制链接]

该用户从未签到

发表于 2011-11-20 16:39:26 | 显示全部楼层 |阅读模式
本帖最后由 hapi 于 2011-11-25 12:02 编辑

&id=142701]原文在此处快一年没接触智辅了。现在回来学习,哎,但师傅事情忙,又没空理我了。

闲着没事来发篇文章给各位新手,高手可以直接跳过了。

今天要说的是怎样制作自己的特征码扫描工具。

众所周知,游戏更新一般那些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; //如果定义不正确就直接退出这个过程
止空指针的情况
    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  
            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

最后,在此谢谢师傅长期以来对我的帮助,这个方法也是师傅教我的。就在此借花献佛一下。

该用户从未签到

发表于 2011-11-20 20:54:39 | 显示全部楼层
好难懂啊 、、、、、、、、、、、、

该用户从未签到

发表于 2011-11-20 20:54:56 | 显示全部楼层
你当我师父好么

该用户从未签到

发表于 2011-11-20 20:55:09 | 显示全部楼层
回复 1# hapi


    你当我师父好么

该用户从未签到

 楼主| 发表于 2013-1-26 15:29:04 | 显示全部楼层
好文啊!!
点击按钮快速添加回复内容: 支持 高兴 激动 给力 加油 苦寻 生气 回帖 路过 感恩
您需要登录后才可以回帖 登录 | 注册账号

本版积分规则

小黑屋|手机版|Archiver|看流星社区 |网站地图

GMT+8, 2024-3-28 21:32

Powered by Kanliuxing X3.4

© 2010-2019 kanliuxing.com

快速回复 返回顶部 返回列表