频道分类

D7开发卡布西游辅助(小页游)的体验

作者:夜行的路人 来源:原创 日期:2020/2/16 17:05:00 人气: 标签:

 

本人高一学生,技术不是很高。曾经在初中的时候接触了作为竞赛语言的Pascal语言,从而认识了Delphi。

在中考结束后的寒假,我突发奇想。可不可以把用本人用易语言模块堆砌而成的辅助改写成Delphi代码呢?当时本人使用的是D7,目前已经改用Xe了,辅助虽然是半成品,但是希望能给需要的人一点思绪。

CSDN怕被大佬们笑话,也就畏畏缩缩的分享在了QQ空间。恰巧今日和站长在群里聊天,同时也明白了如何在站上上传文章。

 





这个游戏是卡布西游,我从小学玩到现在(偶尔看看)的小游戏(类似洛克王国)了。我为什么会萌生做辅助的想法呢?在接触卡布西游之前从2010年起我接触了6年的洛克王国。当时的我十分单纯,傻乎乎花了六年升了一只100级(洛克王国以前升级真的不简单,而且我又佛系)。看着其他同学用着辅助一个星期六七只100级我甚至觉得自己十分高洁。第一次接触外挂还是在小学有一个同学请我去黑网吧玩游戏,当时我接触了造梦西游外挂,一招秒杀的感觉让我萌生了思考——它是怎么做到的?

 

卡布西游现在几乎属于不怎么更新的游戏,用WPE工具可以截取发送封包说明该游戏大部分功能应该是封包没有加密的,于是在各种机遇下,我开始了辅助开发的研究......

 

图片中bate应该是beta,别介意。除此之外我个人也是很喜欢卡布西游这个游戏的,在之前我在小学和初中都写过它的相关攻略,其中2.0(初二创作)攻略成为了卡布西游贴吧的精品帖,欢迎大家来看看。地址:https://tieba.baidu.com/p/5270328867

最早我是打算使用Delphi开发辅助的,但是屡遭挫折

  Set8087CW($133F);  //Delphi自带BUG,不加此句进游戏就FLASH模块崩溃

  wb1.Navigate('http://news.4399.com/login/kbxy2.html?pass=1&v=0.2920092660933733');//Delphi不能在属性页设置url,要在代码中输入

  wb1.Silent := True;//使用浏览器控件打开网站时会跳出很多错误信息,此句让错误信息不显示
这三条是刚接触Delphi不久的时候碰到的问题。尤其是Set8087CW($133F),要是当初不是一个大哥告诉我要加上这一句我几乎都要放弃Delphi开发辅助了。错误大致是这样的:



具体这段代码的功能好像是关闭FPU(在使用一些浮点操作时,微软默认设计会抛出一系列的异常.这些异常按理说不应该阻止程序运行,所以在VB等其他语言的浏览器控件是不会报错的,但是Delphi中默认把报错信息抛出那程序就要闪退了),这句的作用就是直接禁止浮点数报错(浮点数就是小数),那么也不存在Delphi把报错信息抛出的情况。

当时其实也没什么思路,但是偶然间在bbs.125.la——易语言精易论坛上看到了某前辈的卡布辅助半成品,他最核心的功能——获取套接字(套接字是IP和端口的绑定,用于收发封包)是损坏掉了的,但是又是在机缘巧合之下我在论坛看到一个大佬说加他QQxxx,他在回答一个人问题。没想到他就是卡布西游Kid辅助的作者(卡布西游能算辅助的也就魔鬼辅助和Kid辅助了),我是用小号加的,刚开始气氛很尴尬,但是我把论坛下的半成品发给他,他很热心地帮我修好了获取套接字的功能,还附赠了一个Kid辅助老版源码(他本来想靠卖卡密赚钱的,没想到卡布西游基本都不怎么更新了。老版源码也是坏的,基本没怎么看)。后来我问他一些问题他让我自己想,再后来我小号密码忘了也就断了联系了。接下来我利用修好的半成品开发了路人卡布西游辅助,目前更新到第三版就没怎么更新了,我添油加醋改得自以为算还能用了——原本的半成品除了买药和获取人物4399号、卡布号外基本没有什么功能。

 

之前说了论坛下的半成品辅助内部结构本身就十分混乱,在扩充的过程中我甚至不知道游戏套接字 = 套接字是什么意思,后来了解APIHOOK之后才知道那是把Send函数第一个参数s也就是套接字获取到一个全局变量中,我在之后会详细阐述我的见解。
        我逐渐发现易语言代码尽管是全中文的,但是很多地方阅读起来不是很方便。我没有忘记已经被遗弃了数月的Delphi制作辅助计划,当时我的想法很单纯,把易语言辅助核心功能写成DLLDelphi调用就可以了。实际上结果会令你大失所望。
        1.易语言写的DLL体积特别大,比exe还要大
        2.易语言编写的DLL易语言自己调用很正常,其他语言就会出现非常多的问题,莫名其妙报错。因为易语言有一个数据类型是字节集,我在查阅Delphi编写易语言支持库官方文献的时候发现其实字节集类型等效于字节数组。

API的电子书是群里下的,英语不好,尽管只收录了八百多个常用APIWindows下应该有一两千个API),平时查查还是很方便的(MSDN都是英文)。在图中我们可以看到套接字句柄,那么我们该怎么解决这个问题呢?我偶然间得到一款枚举套接字软件,这可帮了我的大忙,首先我在易语言中编写了最简发封包程序。但是移植到Delphi的过程中出现了一些问题(一般情况下不停枚举套接字,到最后剩下的唯一一个客户端.套接字就是我们要的,对于卡布西游来说120xx端口的就肯定是了)。

从中大家发现了什么? 为什么指向缓冲区的指针易语言要用复制字符串的函数?因为易语言没有指针类型,但是在计算机二进制语言中都一样的,所以就用整数型替代了指针型。复制自己到自己,返回自己的字符串指针。但是显然在Delphi中是不用那么麻烦的,Delphi是有指针类型的,功能与C++相当。经过我的无数次折腾,我终于将发包函数实现了,大家不妨看一看(缓冲区也就是Buffer是内存中一段连续的地址,数组也是内存中一段连续的地址,所以数组也算缓冲区,我当时是想了好一会的)。

procedure TForm1.btn1Click(Sender: TObject);

begin

  SendData(StrToInt(edt2.text),'44530000021c120001000000');

end;

 

procedure TForm1.SendData(Socket:WinSock2.Tsocket;HexData:PChar);

var

  Pbuf:PByte;                   //动态缓冲区指针

  buf:array of Byte;            //动态缓冲区

  WantToSendData:PChar;         //字符指针,等同于String,我喜欢用PChar

  record1,record2:Integer;      //循环和数组成员赋值变量

begin

   WantToSendData := HexData;

   SetLength(buf,Length(WantToSendData) div 2); //缓冲区长度需等于数据长度,否则会进入假死

   record1 := 1;

   record2 := 0;

   while record1 <= Length(WantToSendData) do

   begin

     buf[record2] := StrToInt('$' + WantToSendData[record1 - 1] + WantToSendData[record1]);

     record1 := record1 + 2;

     record2 := record2 + 1;

   end;

   Pbuf:=@buf[0];

   //简化后相当于Send(Socket,Pbyte(@b[0])^,Length(buf),0);

   Send(Socket,buf[0],Length(buf),0);  //SocketTCP套接字句柄,句柄都为整数型 ,pbuf^=buf[0]

end;
我来给你们解释一下,就是把字符串的每一个字符都单独转换成一个整数(字节型BYTE),然后把每个转换好以后的整数放到字节数组的一个单元,但是在内存中单元与单元间是在一起的。 44530000021c120001000000放入字节数组后在内存中依然是44530000021c120001000000。但是如果44530000021c120001000000是字符串那在内存中一一对应的是他们的ASCII

我真正的了解APIHOOK是在学习Delphi钩子过程中学习到的。在此之前易语言模块中我甚至一度以为Send函数什么的要高级,不能用一般手段HOOK(易语言模块很多单独分了APIHOOK和安装网络函数HOOK,而且我易语言工程中的模块还是不开源的)。

type

  HookAPI = function(hWnd:hwnd;lpText,lpcaption:PChar;uType:Cardinal):Integer;stdcall;
//Delphi默认出入栈模式和C++写的WindowsAPI函数模式不一样,要加入关键字stdcall手动指定Delphi的函数参数出入栈模式

  TJmpCode = packed record

  JmpCode: BYTE;

  Address: HookAPI;

  MovEAX: Array [0..2] of BYTE;

  end;
 var

  Form1: TForm1;

  OldFuncAdd,NewFuncAdd:Pointer;

  OldMsgBox:HookAPI;

  JmpCode: TJmpCode;

  OldProc: array [0..1] of TJmpCode;

  TmpJmp: TJmpCode;

  ProcessHandle: THandle;
 function MyMessageBox(hWnd:hwnd;lpText,lpcaption:PChar;uType:Cardinal):Integer;stdcall;  //回调函数

var

  //tmp1:PChar;

  dwSize: cardinal;  //cardinal可以视为不能为负数的integer(整数型),也就是DWORD双字型

begin

  //tmp1 := lpText;

  //ShowMessage(tmp1);

  WriteProcessMemory(ProcessHandle, NewFuncAdd, @OldProc[0], 8, dwSize);

  Result := OldMsgBox(hwnd,'hook内容','hook标题',uType);

  JmpCode.Address := @MyMessageBox;

  WriteProcessMemory(ProcessHandle, NewFuncAdd, @JmpCode, 8, dwSize);

end;

 

procedure TForm1.btn1Click(Sender: TObject);

begin

  MessageBoxA(0,PChar('内容'),'标题',MB_OK);

end;

 

procedure TForm1.HookAPI;

var

  DLLModule: THandle;

  dwSize: cardinal;

begin 

  ProcessHandle := GetCurrentProcess();  //获取当前进程伪句柄

  DLLModule := LoadLibrary('User32.dll');

  NewFuncAdd := GetProcAddress(DLLModule, 'MessageBoxA');

  JmpCode.JmpCode := $B8;

  JmpCode.MovEAX[0] := $FF;

  JmpCode.MovEAX[1] := $E0;

  JmpCode.MovEAX[2] := 0;  

  ReadProcessMemory(ProcessHandle, NewFuncAdd, @OldProc[0], 8, dwSize);  //保存原函数地址,以便于恢复

  JmpCode.Address := @MyMessageBox;

  WriteProcessMemory(ProcessHandle, NewFuncAdd, @JmpCode, 8, dwSize);

  OldMsgBox := NewFuncAdd;

end;

 

procedure TForm1.UnHookAPI;

var 

  dwSize: Cardinal;

begin

  WriteProcessMemory(ProcessHandle, NewFuncAdd, @OldProc[0], 8, dwSize);  //将改写的代码恢复

end;

 

procedure TForm1.btn3Click(Sender: TObject);

begin

  UnHookAPI();

end;

 

procedure TForm1.btn2Click(Sender: TObject);

begin

  HookAPI();

end;

其实我最不理解的就是为什么数组的内容是B8 FF E0 00,后来经过研究我才发现原因


{---------------------------------------}

  {定义一个结构体,packed关键字内存对齐

  (若不加此关键字windows将按一次4个字节的方式申请内存

  sizeof(TJmpCode)=4*3=12

  若加上则sizeof(TJmpCode)=1+4+3=8,经测试得出

  无论函数参数与返回值情况,所占字节保持=4不变}

  {---------------------------------------}

TJmpCode = packed record

  JmpCode: BYTE;

  Address: HookAPI;

  MovEAX: Array [0..2] of BYTE;

  end;
可以看到结构体定义的时候的顺序是JmpCodeAddressMovEAX。那么B8 Address的意思实际上是MOV EAX,Address MessageBoxA函数的地址放入EAX寄存器中 ,然后FF E0 00实际上是JMP EAX00应该删掉也可以,那函数中的8应该改为7
我当时就纳闷了,为什么不可以直接JMP Address?还要放到EAX寄存器中?(当时困扰了我很久,网上普遍说E9JMP的二进制编码,经过查阅资料得知E9是短转移,而长转移需要寄存器做中转。所以要使用FF


type 

TSockProc = function (s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;//send recv参数一致
 

  TJmpCode = packed record   //声明结构体

  JmpCode: BYTE;

  Address: TSockProc;

  MovEAX: Array [0..2] of BYTE;

  end;
 

var

  Sock:Cardinal;
 OldSend, OldRecv: TSockProc; 

  JmpCode: TJmpCode;

  OldProc: array [0..1] of TJmpCode;

  TmpJmp: TJmpCode;

function MySend(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall; 

var 

  dwSize: cardinal;  

begin

  asm

    mov eax,[s]

    mov sock,eax

  end;  //sock := s; 

  WriteProcessMemory(ProcessHandle, AddSend, @OldProc[0], 8, dwSize);

  Result := OldSend(S, Buf, len, flags);

  JmpCode.Address := @MySend;

  WriteProcessMemory(ProcessHandle, AddSend, @JmpCode, 8, dwSize);

end;


sock := s; 这句话就是获取套接字的关键了。因为Send函数第一个参数就是TCP套接字






回忆一下之前发送封包的代码(具体往上翻)
Send(Socket,buf[0],Length(buf),0);
Send的第二个参数是数组buf的第一个单元的值(或许也是数组buf的内存地址)buf[0]而并非数组buf。可是我们的回调函数MySend的第二个参数是Buf啊,意思说这里的buf=buf[0];那怎么搞,因为我几次琢磨后写出来的字节数组转字符串函数参数只接受将buf转换成字符串而不接受buf[0]。那么就需要转换了,这里也是思索了很久。

procedure TForm1.btn1Click(Sender: TObject);

const

  a:array[0..11] of byte = ($44,$53,$00,$00,$02,$1c,$12,$00,$01,$00,$00,$00);

var

  b:array of Byte;

  dwsize:Cardinal;

begin

  SetLength(b,Length(a));

  ReadProcessMemory(GetCurrentProcess(),@a[0],@b[0],Length(a), dwsize);

  ShowMessage(ArrayToHexStr(b));

end;

 

function TForm1.ArrayToHexStr(arraydata:array of byte):pchar;

var

  record1,record2:integer;

  str:string;

begin

  record1:=0;

  record2:=0;

  setlength(str,length(arraydata)*2);

  while record1 <= length(arraydata) do

  begin

    str[record2] := IntToHex(arraydata[record1],2)[1];

    str[record2 + 1] := IntToHex(arraydata[record1],2)[2];

    record1 := record1 + 1;

    record2 := record2 + 2;

  end;

  result := pchar(str);

end;
以上是我写的测试程序的部分源码,之前我以为数组不能作为函数参数 。一度想过用一个全局变量来替换,实际试验过也是可以的。
先给大家讲讲ArrayToHexStr函数是怎么写出来的。一个string类型的数实际上就是Char数组,所以SetLenth这个设置数组长度的函数也可以设置字符串长度,为什么length(arraydata)*2呢,因为我们一个数组元素里面放了取值范围为0-FF的数据。所以转化为字符串的时候要乘以2,感兴趣可以翻翻看上面发送封包的函数将字符串中数据放入数组的时候是设置数组长度为字符串长度除以2的。IntToHex函数的第二个参数应该是保留的数位吧(所以0会保留00)。我之前想过使用
str[record2] := IntToHex(arraydata[record1],2)[1] + IntToHex(arraydata[record1],2)[2];照理来说两个字符串相加一个没问题,还能减少一行的代码量。实际上编译器会报错。


我代码中的dwsize是多余的,NULL应该也可以(DelphiNULL得用nil)。lpBuffer参数中指向一个缓冲区(数组)读入目标进程中待读取的内容。GetCurrentProcess()就是获取当前进程伪句柄的函数了(为什么是伪暂且不深究)。有没有觉得lpBaseAddress参数和Send函数第二个参数特别相似(不清楚往上翻)。也就是缓冲区的地址。那我代码的意思就是将a数组地址处Length(a)个数据放入到数组b地址处( 别忘SetLength(b,Length(a)); b长度设置为a的长度)。然后b = a肯定恒成立了。所以想到我们上面问题的解决方法了吗?

SetLength(b,len); //lenSend第三个参数,意为缓冲区数据长度,直接Length(buf)貌似是不可以的

ReadProcessMemory(GetCurrentProcess(),@buf,b[0],len, nil);

Memo1.Lines.Add(ArrayToHexStr(b)); //Memo相当于VB6ListBox
buf视为a[0],则在无法直接操作a只能操作a[0]的情况下成功将a的值复制到数组b,通过转换函数将b转换为字符串。间接实现了只知道a[0]a的内存地址)和长度的情况下读取a中的内容。(我相信肯定有更方便的方法,我比较愚钝只能想到这个笨办法)。

写在后面:基本就是从自己的空间上复制下来的,偏口语化和稚嫩化,大佬勿喷。本人QQ1393774265,欢迎大家和我交流。另外站上传照片不是很方便,挑了一点点上传。自我感觉写的不是很好,但是如果能帮到有需要的人,我也是很开心的。