C#模拟键盘鼠标之二:错误跳转以及内存查看

  上一次我们利用windows API以及xml配置来模拟键盘鼠标,但是并没有对模拟中出现的逻辑错误或者异常进行处理(例如:模拟开启之后,当前程序的部分窗体或者业务逻辑上出现错误,这时我们不只要提示相应的错误,而且必须要让模拟程序将当前正常退出,以保证数据安全。),而且在窗体内部类似Label的控件也没有办法通过抓取窗体句柄或者遍历子窗体句柄来获取相应的信息,因此在一段时间的研究和整改之后,对于一些配置节点增加了ErrorTo属性以及新增Error节点,在出现逻辑错误或者异常的时候,直接跳转到对应的节点运行对应的正常退出步骤。

  我设定是可将ErrorTo的属性设置在所有节点之上,但是如果子节点不存在的话,则会使用父节点的设置。配置更改如下:

1 <Program Name="kp" Value="\BIN\kp.exe" ErrorTo="No2">SOFTWARE\航天信息\防伪开票\路径</Program>

  关于以上的变更,我们需要对AbstractStep进行修改,将默认的Init初始化方法做一些改变,代码变化如下:

 1 ///<summary>
2 /// 初始化
3 ///</summary>
4 ///<param name="step">上一步</param>
5 ///<param name="node">节点</param>
6 public virtual void Init(AbstractStep step, XElement node)
7 {
8 //原始代码省略
9 //新增代码
10 this.ErrorTo = this.GetAttribute("ErrorTo");
11 if (string.IsNullOrEmpty(this.ErrorTo))
12 {
13 this.ErrorTo = this.ParentStep.ErrorTo;
14 }
15 }

  至于新增的Error节点,则主要的作用是做一个正常退出的步骤配置,因为我们可能开启多个窗体,因此需要依次进行关闭,或者对于某些数据进行存储撤消等等。除了主要节点以外,其他的子步骤还是原来的那些配置,大致如下:

 1 <Error Name="No1">
2 <Form Class="TInvQueryForm" Caption="选择发票号码查询">
3 <KeyBoard>{ESC}</KeyBoard>
4 <KeyBoard>{ESC}</KeyBoard>
5 <KeyBoard>{ESC}</KeyBoard>
6 <Form Class="TMainForm" Caption="增值税防伪税控系统防伪开票子系统">
7 <ClickTo>-5,5</ClickTo>
8 </Form>
9 <Form Class="TFaceForm" Caption="增值税防伪税控系统开票子系统">
10 <ClickTo>504,330</ClickTo>
11 </Form>
12 </Form>
13 </Error>

  新增节点的话,则需要对解析XML配置做一些判断,改动如下:

  

 1 ///<summary>
2 /// 获取子节点步骤
3 ///</summary>
4 ///<param name="parentStep">父节点步骤对象</param>
5 ///<param name="childList">子节点列表</param>
6 ///<param name="nowList">子节点步骤列表</param>
7 void GetChildStep(AbstractStep parentStep, IEnumerable<XElement> childList, IList<AbstractStep> nowList)
8 {
9 foreach (var node in childList)
10 {
11 if (node.Name.LocalName == "Error")
12 {
13 var type = node.Attribute("Name").Value;
14 this.dicError[type] = new List<AbstractStep>();
15 this.GetChildStep(parentStep, node.Elements(), this.dicError[type]);
16 }
17 else
18 {
19 //原始代码省略
20 }
21 }
22 }

  基本的配置和解析就差不多了,剩下的就是关于内部运行时需要做的一些修改了。因为我们需要对逻辑错误和异常进程处理,异常的话还是比较简单的,只需要在Timer的Tick事件内,使用Try...Catch进行捕捉就可以了,逻辑错误的话,因为涉及到的方面比较广泛,例如:抓取不到对应的窗体句柄、抓取的句柄值在比对数据中不存在、句柄不可用等等问题,由于我们前期已经设置了一个枚举用于判断步骤的状态---EnumStepState,因此我们需要增加2个枚举值,例如:LogicError、Exception,然后再参照可能会出现的错误情况,因此需要修改的步骤派生类为:FromStep、ChildStep、IfStep、EachStep以及ProgramStep。

  FormStep:判断超过等待时间后,不存在窗体时,出现逻辑错误。

  ChildStep:与FormStep类似,只是范围是在窗体的子窗体中寻找。

  IfStep:这个就比较复杂了,因为If会引发另一个模拟装置。因此我们要将If的模拟装置的正常结束设定为原模拟装置的启动,并且将原本的模拟装置的配置设定与If的模拟装置上。

  EachStep:类似IfStep,只是循环步骤中的返回值,需要多一个对于逻辑错误的状态返回。

  ProgramStep:对于需要启动的程序进行判断,如果存在,则逻辑错误。

  到此,我们对于错误的修改也就差不多了,接下来,我们要对类似Label的取值,使用抓取内存的方式来使用。因为只是单独的测试,因此还未加入到原本的功能里面。这时我们需要用到另外一个windows api----kernel32.dll,主要的api如下:

 1 ///<summary>
2 /// 关闭句柄资源
3 ///</summary>
4 ///<param name="processWnd">进程窗体句柄</param>
5 [DllImport("kernel32.dll")]
6 public static extern void CloseHandle(IntPtr processWnd);
7
8 ///<summary>
9 /// 打开进程句柄资源
10 ///</summary>
11 ///<param name="wndDesiredAccess">窗体需要访问权限值,最高权限为0x1F0FFF</param>
12 ///<param name="bInheritHandle">是否继承处理</param>
13 ///<param name="processId">进程ID</param>
14 ///<returns></returns>
15 [DllImportAttribute("kernel32.dll", EntryPoint = "OpenProcess")]
16 public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int processId);
17
18 ///<summary>
19 /// 将进程指定内存的值读入缓冲区
20 ///</summary>
21 ///<param name="processWnd">进程窗体句柄</param>
22 ///<param name="addressWnd">内存地址句柄</param>
23 ///<param name="bufferWnd">缓冲区地址句柄</param>
24 ///<param name="bufferSize">缓冲区大小</param>
25 ///<param name="bufferReadWnd">缓冲区读取的字节数句柄</param>
26 ///<returns></returns>
27 [DllImportAttribute("kernel32.dll", EntryPoint = "ReadProcessMemory")]
28 public static extern bool ReadProcessMemory(IntPtr processWnd, IntPtr addressWnd, IntPtr bufferWnd, int bufferSize, IntPtr bufferReadWnd);
29
30 ///<summary>
31 /// 将值写入进程指定的内存中
32 ///</summary>
33 ///<param name="processWnd">进程窗体句柄</param>
34 ///<param name="addressWnd">内存地址句柄</param>
35 ///<param name="values">值数组</param>
36 ///<param name="bufferSize">缓冲区大小</param>
37 ///<param name="bufferWriteWnd">缓冲区写入的字节数句柄</param>
38 ///<returns></returns>
39 [DllImportAttribute("kernel32.dll", EntryPoint = "WriteProcessMemory")]
40 public static extern bool WriteProcessMemory(IntPtr processWnd, IntPtr addressWnd, int[] values, int bufferSize, IntPtr bufferWriteWnd);

  另外是我们需要自己用到的方法,因为我个人比较喜欢使用扩展方法,因此扩展方法会比较多。代码如下:

 1 ///<summary>
2 /// 根据进程名获取进程
3 ///</summary>
4 ///<param name="name">进程名</param>
5 ///<returns></returns>
6 public static Process GetProcessByName(string name)
7 {
8 Process process = null;
9 try
10 {
11 Process[] processes = Process.GetProcessesByName(name);
12 process = processes[0];
13 }
14 catch (Exception)
15 {
16 throw new Exception(string.Format("不存在进程名为{0}。", name));
17 }
18 return process;
19 }
20
21 ///<summary>
22 /// 根据进程标题获取进程
23 ///</summary>
24 ///<param name="title">进程标题</param>
25 ///<returns></returns>
26 public static Process GetProcessByTitle(string title)
27 {
28 Process process = null;
29 try
30 {
31 Process[] processes = Process.GetProcesses();
32 foreach (var p in processes)
33 {
34 if (p.MainWindowTitle.IndexOf(title) != -1)
35 {
36 process = p;
37 }
38 }
39 }
40 catch (Exception)
41 {
42 throw new Exception(string.Format("不存在进程标题为{0}。", title));
43 }
44 return process;
45 }
46
47 --------------------------------------------------扩展方法----------------------------------------------
48 ///<summary>
49 /// 读取进程对应内存的值
50 ///</summary>
51 ///<param name="p">进程</param>
52 ///<param name="address">内存地址</param>
53 ///<returns></returns>
54 public static int ReadProcessMemoryValue(this Process p, int address)
55 {
56 //设置缓冲区,打开进程句柄资源,读取内存,关闭进程句柄资源
57 byte[] buffer = new byte[4];
58 IntPtr byteAddress = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0);
59 IntPtr processWnd = Memory.OpenProcess(0x1F0FFF, false, p.Id);
60 Memory.ReadProcessMemory(processWnd, (IntPtr)address, byteAddress, 4, IntPtr.Zero);
61 Memory.CloseHandle(processWnd);
62 return Marshal.ReadInt32(byteAddress);
63 }
64
65 ///<summary>
66 /// 将值写入进程指定内存
67 ///</summary>
68 ///<param name="baseAddress"></param>
69 ///<param name="processName"></param>
70 ///<param name="value"></param>
71 public static void WriteProcessMemoryValue(this Process p, int address, int value)
72 {
73 //打开进程句柄资源,将值写入指定内存地址,释放进程句柄资源
74 IntPtr processWnd = Memory.OpenProcess(0x1F0FFF, false, p.Id); //0x1F0FFF 最高权限
75 Memory.WriteProcessMemory(processWnd, (IntPtr)address, new int[] { value }, 4, IntPtr.Zero);
76 Memory.CloseHandle(processWnd);
77 }

  拥有了方法之后呢,我们还需要借助一个工具的帮忙----CheatEngine,它可以帮助我们筛选需要的值,并且找到内存地址。使用代码如下:

1 var p = Memory.GetProcessByTitle(this.txtProcessTitle.Text);
2 var address = 0x014C14DC;
3 var value = p.ReadProcessMemoryValue(address);

  因为时间的关系,学习的并不多,差不多就到这里了,谢谢各位。由于上一次忘了提供源码,非常抱歉,这次我有补上哦,写得差,多多包涵,呵呵。源码在此!  <------ 记得下载哦

原文地址:https://www.cnblogs.com/ahl5esoft/p/2259857.html