当前位置:   article > 正文

C# 外挂入门 配套 实例+视频 教程

c#外挂教程
C# 外挂入门 配套 实例+视频 教程
2010年06月25日
  // 交流 QQ 406050799 (加我也没用我会的就这点)
  /*
  做外挂我也是现学的。可以说写的这个教程是现学现卖,希望对用C#的外挂爱好者能有点帮助。
  * 有错误希望大家指出来我怕耽人子弟
  第一课:先说下本教程用C#做该挂的原理!(也算不上什么c#了 就几个 winApi)
  1、 通过一些汇编调试工具(OllyICE、Cheat Engine)得到指定应用程序(游戏)的一些内存数据 包括{某些值在内存中存放的位置 比如人物的血 和
  应用程序(游戏)所作动作的函数的位置 比如按f1是攻击那么 按下f1必然会调用一个攻击函数这里叫做call}
  上面的工具得到的是反汇编就是把机器码搞成汇编 所以你需要懂一点汇编
  2、 通过我们写的程序去访问\调用 我们所找到的地址。比如单机游戏 人物的血值 你通过工具找到它在内存的地址 那么你想让血为1万 你就通过你写的程序修改这个内存地址的值为10000 就可以了
  具体怎么做呢? 首先c#是做不到地 但是c#有个很好的靠山 就是win api 我们通过c#调用winApi来对内存操作
  第二课:一些汇编调试工具(OllyICE、Cheat Engine)的用法
  在这里我也就不说明了 我会提供视频教程下载地址
  1、OllyICE 视频: (由于压缩包有点大 老是上传不了,过几天我会搞定的)
  2、ce 视频: (由于压缩包有点大 老是上传不了,过几天我会搞定的)
  3、C# 实例+调试使用小程序: http://www.ghoffice.com/bbs/down.php?tid=79095&aid=47126&name=%CD%E2%B9%D2%C1%B7%CF%B0.rar
  第三课:通过第二课我们可以得到应用程序(游戏)中我们想要的内存地址了
  接下来是我这段列子的 步骤
  1、申明WinApi函数 为什么要申明WinApi函数? 答:上面说了 做外挂需要用的一些操作内存的函数 而c#没有 所以要用到win api
  2、看应用程序(游戏)是否启动 -为什么要判断应用程序(游戏)是否启动? 答:如果没有启动应用程序(游戏) 你写的程序(外挂)操作的对象都没有了,你的代码就没意义了。。
  3、得到应用程序(游戏)的进程句柄 -为什么要得到进程句柄 ? 答:应为在对内存地址操作时 必须知道要你操作的是哪个进程范围内的内存地址
  4、读取内存地址内的值 -比如读出游戏人物(角色的血和名字等...)
  5、修改内存中的值
  6、调用应用程序(游戏)内的函数(call) 主要是通过winapi向制定的进程中注入机器码
  下面请看代码(为了方便我就把代码写在一个页面了)
  */
  using System;
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.Data;
  using System.Drawing;
  using System.Linq;
  using System.Text;
  using System.Windows.Forms;
  using System.Threading;//多线程命名空间
  using System.Runtime.InteropServices;//申明 WinApi 关键词DllImport的命名空间
  namespace 外挂练习
  {
  public partial class Form1 : Form
  {
  #region 申明WinApi函数
  //下面先看下WinApi是如何引用的
  /*
  -------------------------------------------------------看下面
  ///
  /// 函数功能
  ///
  /// 参数
  /// 参数
  /// 参数
  /// 参数
  /// 参数
  /// 返回值
  ------------------------------------------------------上面们明白吧是对某个函数的说明吧
  ------------------------------------------------------看下面
  [DllImport("kernel32.dll"), EntryPoint = "ReadProcessMemory")] 这里是用于指明 要应用的api所在的dll文件和api(函数名)
  public static extern int ReadProcessMemory( 这里是申明函数
  IntPtr hProcess, 定义参数1
  int lpBaseAddress, 定义参数2
  int[] lpBuffer, 定义参数3
  int nSize, 定义参数4
  int lpNumberOfBytesWritten 定义参数5 关于参数信息你可以去看msdn
  );
  -----------------------------------------------------上面就引用了一个装在kernel32.dll文件下名为 ReadProcessMemory 的 api(函数)
  * 下面关于api的说明自己去看msdn
  */
  #region 申明WinApi函数
  [DllImport("kernel32.dll", EntryPoint = "OpenProcess")]
  public static extern IntPtr OpenProcess(
  Int32 dwDesiredAccess,
  Int32 bInheritHandle,
  int dwProcessId
  );
  ///
  /// 读写内存
  ///
  /// 进程,如果你是做外挂的话,它代表你要挂的那个游戏。
  /// 你要读取的内存地址
  /// 从上面那个参数地址里读出来的东西(调用这个函数的就是为了它) 不管这个参数是什么类型,它应该是一个数组,否则读不出东西来 }
  /// /长度,上一个参数,类型是int,那个长度应该用
  /// 用0就行了,想知道它是干嘛的,自己去MSND吧
  ///
  [DllImport("kernel32.dll")]
  public static extern int ReadProcessMemory(
  IntPtr hProcess,
  int lpBaseAddress,
  int[] lpBuffer,
  int nSize,
  int lpNumberOfBytesWritten
  );
  [DllImport("kernel32.dll")]
  public static extern int ReadProcessMemory(
  IntPtr hProcess,
  int lpBaseAddress,
  byte[] lpBuffer,
  int nSize,
  int lpNumberOfBytesWritten
  );
  [DllImport("Kernel32.dll")]
  public static extern Int32 VirtualAllocEx(
  IntPtr hProcess,
  Int32 lpAddress,
  int dwSize,
  Int32 flAllocationType,
  Int32 flProtect
  );
  [DllImport("kernel32.dll")]
  public static extern IntPtr WriteProcessMemory(
  IntPtr hProcess,
  Int32 lpBaseAddress,
  [In, Out] byte[] buffer,
  int size,
  Int32 lpNumberOfBytesWritten
  );
  [DllImport("kernel32.dll")]
  public static extern IntPtr WriteProcessMemory(
  IntPtr hProcess,
  Int32 lpBaseAddress,
  int buffer,
  int size,
  Int32 lpNumberOfBytesWritten
  );
  ///
  ///
  ///
  /// 目标进程的句柄
  /// 指向线程的安全描述结构体的指针,一般设置为NULL,表示使用默认的安全级别
  /// 线程堆栈大小,一般设置为0,表示使用默认的大小,一般为1M
  /// 线程函数的地址
  /// 线程参数
  /// 线程的创建方式 CREATE_SUSPENDED 线程以挂起方式创建
  /// 输出参数,记录创建的远程线程的ID
  ///
  [DllImport("kernel32", EntryPoint = "CreateRemoteThread")]
  public static extern int CreateRemoteThread(
  IntPtr hProcess,
  Int32 lpThreadAttributes,
  Int32 dwStackSize,
  Int32 lpStartAddress,
  Int32 lpParameter,
  Int32 dwCreationFlags,
  ref int lpThreadId
  );
  [DllImport("kernel32")]
  public static extern int WaitForSingleObject(
  int hProcess,
  UInt32 lpThreadAttributes
  );
  [DllImport("Kernel32.dll")]
  public static extern System.Int32 VirtualFreeEx(
  IntPtr hProcess,
  Int32 lpAddress,
  int dwSize,
  int flAllocationType
  );
  [DllImport("kernel32.dll", EntryPoint = "CloseHandle")]
  public static extern int CloseHandle(
  int hObject
  );
  #endregion
  #endregion
  #region 成员
  ///
  /// 申明一个进程 {和外挂没有关系 只是我这里用了循环去读 应用程序(游戏)内的信息 所以的单独用个进程来做这个事 不然会造成 (死循环程序死掉) }
  ///
  Thread thred;
  ///
  /// 申明一个全局变量来保存 得到的应用程序(游戏)的进程id
  ///
  int ProcessID;
  ///
  /// 申明一个全局变量来保存 得到的应用程序(游戏)的进程句柄
  ///
  IntPtr process;
  #endregion
  public Form1()
  {
  InitializeComponent();
  }
  private void Form1_Load(object sender, EventArgs e)
  {
  Control.CheckForIllegalCrossThreadCalls = false;//取消线程保护 应为我用了多线程 我在子线程里面修改主线程的东西 在c# 默认是不可以的 所以我这里 是= false(不保护)
  #region 判读应用程序(游戏)是否启动
  System.Diagnostics.Process[] GamesProcess = System.Diagnostics.Process.GetProcessesByName("F8_CALL_04"); //得到名字 为“F8_CALL_04” 的所有进程
  //System.Diagnostics.Process是.NET里的进程类,GetProcessesByName静态方法是通过进程的名字获得进程数组。这行语句执行之后,所有游戏进程就放在了GamesProcess里面。
  if (GamesProcess.Length 这个函数是不停得读内存地址里面的值
  thred.Start();//启动线程
  getstrname();//读取字符串
  }
  #region 读取应用程序(游戏)内存地址内的值
  //读int
  private void WriteMemory()
  {
  int[] xx = new int[1];//声明变量存放存放 api函数从内存中得到的值(x坐标) 为什么要转为int[] 应为Api WriteProcessMemory 的第3个参数要用数组
  int[] yy = new int[1];//声明变量存放存放 api函数从内存中得到的值(y坐标) 为什么要转为int[] 应为Api WriteProcessMemory 的第3个参数要用数组
  while (true) //循环
  {
  ReadProcessMemory(process, 0x462D04, xx, 4, 0);//读取process进程内462D04内存地址中的值 放在xx中 462D04是个16进制 在C#里我们在前面加0x表示 最后2个参数是 读取长度4 表示是int 0不知道是什么东西自己去查一下嘛
  //ReadProcessMemory是个api函数他的作用就是读取内存中的值
  x.Text = xx[0].ToString();//把得到的值让空间x显示出来
  ReadProcessMemory(process, 0x462d08, yy, 4, 0);//读取process进程内462d08内存地址中的值 放在yy中 462d08是个16进制 在C#里我们在前面加0x表示 最后2个参数是 读取长度4 表示是int 0不知道是什么东西自己去查一下嘛
  //ReadProcessMemory是个api函数他的作用就是读取内存中的值
  y.Text = yy[0].ToString();//把得到的值让空间y显示出来
  /*
  在这里这大家需要注意的是地址与指针的偏移 上面我的的地址是写的固定的其实是不对的 正确的用法是 基地址加上偏移量 !看下面
  来写一段读取诛仙人物HP的代码。首先,我们需要知道人物HP的地址,(废话:如何知道这个地址,用CE还是IE,你自己搞定吧。)我是用IE在这里http://www.ghoffice.com/bbs/read.php?tid-35908-fpage-2.html找到的,它这里是这样写的:
  人物基址:[[&H12F830]+&H28]=base
  生命:[base+&H254]
  (注:&H表示16进制,在C#里我们用0x表示)
  一对[]表示读一次地址。也就是说123表示值123,而[123]就表示从地址123读出来的值。几对[],就要用几次ReadProcessMemory,我们来写下代码:
  int[] Base=new int[1];
  int[] hp=new int[1];
  ReadProcessMemory(process, 0x12F830, Base;, 4, 0);//相当于Base=[&H12F830]
  ReadProcessMemory(process, Base+0x28, Base;, 4, 0);//相当于Base=[Base+&H28]
  //读出了人物基址base
  ReadProcessMemory(process, Base+0x254, hp;, 4, 0);//相当于hp=[base+&H254]
  //读出了hp
  怎么样,很简单吧。
  我们读HP只用了3行ReadProcessMemory。有的时候,读某个值可能需要很多对[],就要写N行ReadProcessMemory,这样写起来就很麻烦,看起来也很晕。下面我们来写个函数,让读内存的过程看起来和[]表示法差不多。
  //为了看起来好看,函数的名字最好短些,所以我们用r,表示read
  public static int r(int add)
  {
  int[] r=new int[1];
  try
  {
  ReadProcessMemory(process, add, r, 4, 0); }
  return r[0];
  }
  catch (Exception ex)
  {
  return -1;
  }
  }
  这个函数很简单,不用我多说了吧。
  有了这个函数,上面的读取HP的代码,我们就可以写成这样了:
  int Base;
  int hp;
  Base=r(r(0x12F830)+0x28);
  //读出了人物基址base );
  hp=r(base+&H254);
  //读出了hp
  看起来清晰多了吧。
  */
  }
  }
  //读文本
  private void getstrname()
  {
  byte[] nn = new byte[256];//声明变量存放存放函数的到的值(文本)
  ReadProcessMemory(process, 0xACAA44, nn, 256, 0);//读取process进程内ACAA44内存地址中的值 放在nn中 ACAA44是个16进制 在C#里我们在前面加0x表示
  string temp = System.Text.Encoding.Default.GetString(nn); //读出的byte[]要按某种方式编码为字符串 这里用的是 Default
  names.Text = temp.Split('\0')[0].ToString();//截取第一段字符串 给name控件显示
  }
  #endregion
  #region 修改内存中的值
  private void button2_Click(object sender, EventArgs e)
  {
  byte[] nn = System.Text.Encoding.Default.GetBytes(name.Text);//这里是把字符串转为byte[] 为什么要转为byte[] 在前面也说了Api WriteProcessMemory 的第3个参数必须是数组
  WriteProcessMemory(process, 0xACAA44, nn, nn.Length, 0);//用Api WriteProcessMemory 修改process进程中0xACAA44内存中的值 为nn变量里面的值
  getstrname();//修改后重新获取
  }
  #endregion
  #region 调用应用程序(游戏)内的函数(call)主要是通过winapi向制定的进程中注入机器码
  //按钮事件
  private void button1_Click(object sender, EventArgs e)
  {
  #region 得到机器码1
  /*
  下面是我通过OllyICE找到的汇编代码 简单分析下
  0045AA16 |. E8 555DFCFF call F8_CALL_.00420770
  0045AA1B |. 5B pop ebx
  0045AA1C \. C3 retn
  0045AA1D 8D40 00 lea eax, dword ptr ds:[eax] 这里有个参数
  0045AA20 /. 55 push ebp
  0045AA21 |. 8BEC mov ebp, esp
  0045AA23 |. 8B15 042D4600 mov edx, dword ptr ds:[462D04] ; ds:[00462D04]=00000068
  0045AA29 |. 8915 142D4600 mov dword ptr ds:[462D14], edx ; edx=00000068
  0045AA2F |. 8B15 082D4600 mov edx, dword ptr ds:[462D08] ; ds:[00462D08]=00000041
  0045AA35 |. 8915 182D4600 mov dword ptr ds:[462D18], edx ; edx=00000041
  0045AA3B |. 8B4D 08 mov ecx, dword ptr ss:[ebp+8] ; 这里有个参数(y)
  0045AA3E |. 8B55 0C mov edx, dword ptr ss:[ebp+C] ; 这里有个参数(x)
  0045AA41 |. E8 B6000000 call F8_CALL_.0045AAFC 这里是个 call(函数)
  0045AA46 |. 5D pop ebp
  0045AA47 \. C2 0C00 retn 0C
  -----------------------------------------------------------------
  0045AAF7 . C3 retn
  0045AAF8 . 00002041 dd float 10.00000
  0045AAFC /$ 53 push ebx
  0045AAFD |. 8BD8 mov ebx, eax 这里把eax给了ebx----------看下面
  0045AAFF |. 8915 1C2D4600 mov dword ptr ds:[462D1C], edx 这里是个参数 |
  0045AB05 |. 890D 202D4600 mov dword ptr ds:[462D20], ecx 这里是个参数 ↓
  0045AB0B |. 8BC3 mov eax, ebx 这里----------- 又把ebx 放在了eax里
  0045AB0D |. E8 D6FDFFFF call F8_CALL_.0045A8E8
  0045AB12 |. B2 01 mov dl, 1
  0045AB14 |. 8B83 68030000 mov eax, dword ptr ds:[ebx+368]
  0045AB1A |. E8 C511FDFF call F8_CALL_.0042BCE4
  --------------
  * 通过上面分析可以得到函数0045AAFC有3个有用参数(eax、ecx、edx)还原成汇编如下
  pushad
  mov ecx, 100
  mov edx, 100
  mov eax, 00A77170
  call 0045AAFC
  popad
  ret
  然后再把这段汇编代码放在这个工具 (汇编转机器码(new))中 转换 这个工具有些是用方法会在下载的工具包内有说明
  转换后会得到类似60bdfcaa4500b964000000ba64000000bb7071a700b87071a700ffd561c3的字符串,大家可以用下面的getBytes2方法把它转换为byte[]
  如下代码
  */
  #endregion
  byte[] bytes = getBytes2("60bdfcaa4500b964000000ba64000000b87071a700ffd561c3"); //bytes就是我们要的 所谓的机器码
  #region 使用得到机器码
  Int32 addr = VirtualAllocEx(process, 0, bytes.Length, 0x1000, 0x40);//一,申请空间
  WriteProcessMemory(process, addr, bytes, bytes.Length, 0);//二,把代码写进去
  int hThread = CreateRemoteThread(process, 0, 0, addr, 0, 0, ref ProcessID);//三,执行写入的代码
  WaitForSingleObject(hThread, 0xFFFFFFFF);//等待线程结束
  VirtualFreeEx(process, addr, 0, 0x8000); //四,释放申请的空间
  CloseHandle(hThread);//五,关闭线程句饼
  #endregion
  }
  private void button3_Click(object sender, EventArgs e)
  {
  #region 得到带参数的机器码
  /*
  关于参数:
  你可以把参数放到汇编里,转成机器码。然后再换个参数,在转机器码。
  观察机器码的区别,有区别的地方就是放参数的地方。
  观察的时候注意,机器码里的数字是两位为单位,倒序排列的,
  比如转之前,汇编代码里有12345678,转后的机器码里就一定能找到78563412.
  然后你就可以在自己的程序里,以字符串连接方式,处理带参数机器码。
  如:把下面的汇编放入工具
  pushad
  mov ebp,0045AAFC
  mov ecx, 100
  mov edx, 100
  mov ebx, 00A77170
  mov eax, 00A77170
  call ebp
  popad
  ret
  得到的字符串是60bdfcaa4500b964000000ba64000000bb7071a700b87071a700ffd561c3 然后我把第一个100换成123把第二个100换成456 再放入工具
  得到的字符串是60bdfcaa4500b97b000000ba7b000000bb7071a700b87071a700ffd561c3
  仔细观察会发现第一个64000000变成了7b000000
  第二个64000000变成了c8010000
  这就说明这个2个地方是放参数的
  问题又来了参数位置得到了 那么参数要以什么格式的字符串格式加上去呢?
  先将用户输入的值转为16进制 然后通过getByte函数处理16进制就得到了我们想要的字符串了
  看下面代码
  */
  string xx = string.Empty;//定义变量存放用户输入值 的16进制
  string yy = string.Empty;//定义变量存放用户输入值 的16进制
  try
  {
  xx = Convert.ToString(int.Parse(x2.Text), 16);//将用户输入的值装换为16进制
  yy = Convert.ToString(int.Parse(y2.Text), 16);//将用户输入的值装换为16进制
  }
  catch
  {
  MessageBox.Show("请输入正确坐标!");
  return;
  }
  xx = getByte(xx);//把16进制装换为我们需要的字符串
  yy = getByte(yy);//把16进制装换为我们需要的字符串
  #endregion
  byte[] bytes = getBytes2("60bdfcaa4500b9" + yy + "ba" + xx + "b87071a700ffd561c3");//我们要写入的代码(这里我调用的是F8_CALL_04 中 方块移动的函数传了2个参数 x和y)
  #region 使用得到机器码
  Int32 addr = VirtualAllocEx(process, 0, bytes.Length, 0x1000, 0x40);//一,申请空间
  WriteProcessMemory(process, addr, bytes, bytes.Length, 0);//二,把代码写进去
  int hThread = CreateRemoteThread(process, 0, 0, addr, 0, 0, ref ProcessID);//三,执行写入的代码
  WaitForSingleObject(hThread, 0xFFFFFFFF);//等待线程结束
  VirtualFreeEx(process, addr, 0, 0x8000); //四,释放申请的空间
  CloseHandle(hThread);//五,关闭线程句饼
  #endregion
  }
  #endregion
  #region 内部方法
  //移位 补位函数
  public static string getByte(string HEX)
  {
  string hex = string.Empty;
  HEX = "0" + HEX;
  for (int i = 2; HEX.Length >= i; i += 2)
  {
  if (HEX.Length%2==0)
  {
  hex += HEX.Substring(HEX.Length - i,2);
  }
  else
  {
  hex += HEX.Substring(HEX.Length - i,1);
  }
  }
  while (hex.Length < 8)
  { hex += "0"; }
  return hex;
  }
  public static byte[] getBytes2(string HEX)
  {
  byte[] bytes = new byte[HEX.Length / 2];
  for (int i = 0; i < bytes.Length; i++)
  {
  bytes = Convert.ToByte(Int32.Parse(HEX.Substring(i * 2 , 2),
  System.Globalization.NumberStyles.AllowHexSpecifier));
  }
  return bytes;
  }
  //关闭清空
  private void Form1_FormClosed(object sender, FormClosedEventArgs e)
  {
  if (thred != null)
  thred.Abort();
  }
  #endregion
  }
  }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/934350
推荐阅读
相关标签
  

闽ICP备14008679号