HTTP请求报文的拦截与篡改--目录
本来写这个仅仅只是为了完成公司布置的任务--每周一篇心得,谁知道一写就写成了长篇大论,看着还没有完全写完,就已经达到40多页的篇幅,着实有点成就感。现把已经写好的部分整理一下,发到这里,文笔差,望见谅。
目前已经写完请求拦截部分(文章和代码都是),请求篡改部分还没开始写,已经写好的部分已经比较长了,所以大概要分为8个部分,会陆续发布出来
目录
前些日子同事在给同学做测试的时候,使用了一个叫IEC的工具,他是一个IE表单的拦截和篡改工具,可以拦截HTTP请求,并篡改其中的GET或者POST数据,然后再将篡改后的数据提交给服务器。这个功能有点意思,下面我们就来自己实现一个,同时分析分析一下他的实现原理。
实现这个功能的方法很多。可以使用WINPCAP,直接从网卡截包然后篡改,也可以HOOK住SEND函数,或者其它相关的网络API。至于IEC这个软件使用的技术我还不太清楚,不过从他只可以截IE的数据来看,应该是BHO或者异步可插入协议(Asynchronous Pluggable Protocols),具体就不研究了,说实话,VB P-CODE模式下编译的程序就是个虚拟机, 在汇编层面,分析VB虚拟机里执行的P-CODE代码,实在是太麻烦。
使用WINPCAP实现这个功能,有点牛刀杀鸡了,而使用HOOK技术,又不是太安全,基本上带主动防御,智能行为分析的杀软,HOOK用到的几个函数都是被严密监控的。所以在这里,以上两种技术我们都不采用 。
我们采用的是第三种技术--代理拦截技术,看到代理两个字,稍稍有点计算机知识的人,第一时间反应的词语应该都是代理服务器,是的,代理拦截技术,正是基于代理服务器技术实现的。
下面,我们先来看看代理服务器的基本知识 。
一般情况下的HTTP请求与响应,如下图所示。


加入代理服务器后,如下图所示


有没有看出来什么门道,是的,当你使用代理服务器后,所有的HTTP请求,都是先发到代理服务器,然后由代理服务器重新包装后,再发给目标(实际)服务器,响应也是如此,代理服务器先接收来自目标(实际)服务器的响应,然后包装后发给客户机。到这里,实现方式,是不是已经很明显了。
我们可以自己实现一个代理服务器,然后,将本机的代理服务器设置成指向我们自己实现的代理服务器,这样,当本机的浏览器发出去请求的时候,其实都是发送到了我们自己实现的代理服务器上,如此请求的拦截就算实现了,拦截实现后,下一步就是篡改了。刚才也讲了,发到代理服务器的的请求,是经过代理服务器重新包装后,再转发给目标(实际)服务器的,代理服务器是我们自己实现的,那么请求不就是随我们自己的意进行“包装”吗 :)这样篡改也就没有什么问题了 。
思路有了,下面就是来实现它了,在这里,我们采用我们熟悉的C#来实现,C#下有一个完整的WEB DEBUGGER(含代理服务器方式实现的HTTP请求拦截与篡改功能)的实现--Fiddler,Fiddler是一个免费软件,所以并没有对源码进行混淆和加密,反编译后的代码可读性也很好。我们后面的所有代码,主要就是参考他的代码来实现,当然他实现的功能非常强大,我们仅仅是演示一下HTTP请求与拦截技术的实现,所以没有必要完整的去模拟它,故而我们模拟的原则是,手动的一个类一个类一个方法一个方法的进行模拟,在模拟的过程中能简化的尽量简化,有些思路不太清楚,或者的确是重复造轮子的部分,就直接使用他的代码,以后如果有时间,再对其源码进行一次详细的分析与研究,这次有些部分我们就不再深究了。
首先,我们来实现一个代理服务器,完成HTTP请求的拦截
先来看一看实现后的样子,这样有助于理解代码 。
编译后的程序,我拷贝了一份放在了附录的根目录下的Build文件夹里
在build文件夹里找到
这个图标,双击启动
需要.net framework 4.0 。 没有的到微软官网下载
http://www.microsoft.com/zh-cn/download/details.aspx?id=17718
启动后如图所示

这时候,我们打开浏览器,随便CHROME,FIREFOX还是IE,然后输入 www.baidu.com 回车(Enter)
看一下我们的主界面,没有任何反应,这是正常的,因为我们还没有设置代理服务器。 :)
任何浏览器,都有设置代理服务器的地方,鉴于天朝 IE的使用率,这里讲IE里的操作步骤。
打开IE--工具--Internet选项--连接--局域网设置

勾上(跳过本地地址的代理服务器不要勾选),然后点高级

依照上图进行设置,HTTPS和FTP等不要设置(本代理服务器未对这两种协议进行处理),IP是127.0.0.1 表示代理服务器是在本机,监听端口设成8888,目前代码里写死了。
在IE里设置完后,我们会发现其它浏览器也自动开始使用代理服务器了,这是因为设置代理服务器是系统的功能,每个浏览器打开的都是同一个设置代理服务器的程序
看到这里,大家是不是发现了什么问题。我们不是以后每次要拦截的时候,都要手动设置一次代理服务器吧,-_-!!! ,当然,这是不可能的,后面我们将在篡改一节的开始,就实现一个自动设置代理服务器的功能,这样,当我们的代理服务器启动的时候,就自动将本机的代理服务器设置成自己,退出的时候,再恢复成原样,这样就不再需要向上面一样手动设置了。
OKAY , 一切就绪了,这时候,再次在浏览器里输入网址: http://www.baidu.com
看下主界面

看左边,会话已经被监听到了。我们点第一栏的编号为1的会话,界面如下:

看右边
先看上面的文本框,这里显示出了HTTP请求的头信息。
再看下面的文本框,这里也显示出了HTTP响应的头信息。
从这两张图里我们可以清楚看到,所有的请求和响应信息都已经被我们监听到了。
再看一下浏览器,网页也正常的打开了,速度也不是很慢,看来,我们的代理服务器算是正常的运转起来了 :) 。
附录(源码+程序)
HTTP代理实现请求报文的拦截与篡改
下面我们就来分析一下源代码,详细的代码可以看源码,在这里为了使实现的思路更加的清晰,我们只对部分关键的代码进行说明
Fiddler 的设计思想还是相当不错的,在程序启动的时候,new 一个代理(Proxy)类的实例,然后调用这个实例的Start方法,来启动代理服务,在Start方法里就是不停的异步监听本机的8888端口(还记得刚才设置代理服务器时设置的端口吗),如果监听到了,就从线程池里,取出来一个线程,并在这个线程里,构造一个Session对象。一个Session对象,代表客户端与服务器的一次会话,从开篇的两张图可以知道,在有代理服务器情况下的一次会话(Session)代表的是1.从客户端读请求,2.重新包装客户端的请求,转发至目标服务器. 3.从目标服务器读取响应信息 4.包装接收到的响应信息并返回给客户端。故而在Session类里,封装一个ClientChatter类型的名为Request的对象,用来实现和客户端的通讯,另外又封装了一个ServiceChatter类型的名为Response的对象,用来实现和目标服务器的通讯。 ClientChatter和ServiceChatter是对通讯的高层封装,而原始的数据流,则分别由ClientChatter里的ClientPipe(客户端管道)和ServiceChatter里的ServicePipe(服务端管道)来进行读取。ClientChatter 调用 ClientPipe的Receive方法来读取原始请求流,并对原始的请求流进行分析,将分析出来的HTTP头信息保存在 Headers变量里,并将其它相关信息,存放在相应的变量里。ServiceChatter的功能类似,只是通讯的对象由客户端变成目标服务器而已 。
我们的代码基本上沿袭Fiddler的 思想 。
下面来看具体的代码。
先从Program.cs开始
1 [STAThread] 2 static void Main() 3 { 4 Application.EnableVisualStyles(); 5 Application.Run(new FrmMain()); 6 }
如果不刻意的在工程属性里设置,这里就是整个程序的入口点。
从上面的代码可以看出来,默认启动了 FrmMain 窗体类
FrmMain.cs
顺藤摸瓜
下一步我们来看FrmMain窗体类的构造方法,界面部分略去,直接看最后两句。
1 proxy = new Proxy(); 2 proxy.Start(8888);
在这里,我们的核心类 Proxy 出现了,刚才已经讲过,当调用Start方法时,我们的代理服务就正式启动了。
Start方法,只有一个参数listenPort
internal bool Start(int listenPort)
就是我们的代理服务器要监听的端口,在这里我们写死成了8888 。 proxy.Start(8888) ;
Net/Proxy.cs
再到Proxy.Start方法体里看看有些什么。
1 internal bool Start(int listenPort) 2 { 3 try 4 { 5 this.acceptor = 6 New Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp); 7 this.acceptor.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), listenPort)); 8 this.acceptor.Listen(50); 9 this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null); 10 } 11 catch (Exception e) 12 { 13 MessageBox.Show(e.Message); 14 return false ; 15 } 16 return true; 17 }
这个代码很简单,就是创建一个Socket对象,然后,绑定到本机(127.0.0.1)的listenPort(8888)端口
this.acceptor.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), listenPort)); |
然后开始监听这个端口
this.acceptor.Listen(50); |
参数50表示等待处理的连接队列的最大值,这个有点难理解,说简单点就是,当客户端请求连接服务端(我们这个就是服务端)时, 服务端会将监听到的请求都放到一个队列里(50就是设置这个队列的最大值的),然后服务端通过Accept方法,从这个队列里取出来一个请求,然后依据这个请求,创建一个和客户端的连接,并用这个连接和客户端进行通讯。 也就是说如果在服务端,一直都不Accept的话,客户端最多只能连接50次, 超过50次,服务器就会直接拒绝请求,因为等待队列已经满了 。
在这里引出了一个方法:Accept . 这正是我们下一步要用到的。看下一句
this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null); |
这里出现了一个BeginAccept方法
Socket里的方法有个规律,类似Begin***的方法,肯定有个End***方法和他对应,这是用于异步处理的。而没有Begin和End前缀的***方法,则是同步处理用的。因此this.acceptor.Accept 这个方法就是用于同步接收的方法了,当然这里我们是不能使用同步接收的方法的,我们可不想,如果队列里没有连接的时候,程序堵在Accept这里不动。
BeginAccept的第一个参数,是异步处理的委托,这里我们设置成了AcceptConnection方法。 也就是一旦我们的服务端监听到了连接,就会马上调用AcceptConnection方法进行处理。
下面我们继续进入AcceptConnection方法
1 private void AcceptConnection(IAsyncResult ar) 2 { 3 try 4 { 5 ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(Session.CreateAndExecute), this.acceptor.EndAccept(ar)); 6 } 7 catch (ObjectDisposedException) 8 { 9 return; 10 } 11 catch (Exception) 12 { 13 return; 14 } 15 try 16 { 17 this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null); 18 } 19 catch (Exception) 20 { 21 } 22 }
忽略异常处理的部分(当然这里处理的相关粗糙),这段代码,其实只有两句
// 1: ThreadPool.UnsafeQueueUserWorkItem( new WaitCallback(Session.CreateAndExecute), this.acceptor.EndAccept(ar) ); // 2: this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null);
第一句是从线程池里取出来一个线程,并在这个线程里执行Session.CreateAndExecute 方法。具体的说明,可以查MSDN。在这里只要明白,它就相当于在另外一个线程里执行这句
Session.CreateAndExecute(this.acceptor.EndAccept(ar));
就行了
这里this.acceptor.EndAccept(ar) 要注意一下 。
上面也提到了。Begin***,End***是成对出现的,在Proxy.Start方法里,出现了一个BeginAccept,这里自然要出现一个EndAccept和他对应,同时
this.acceptor.EndAccept(ar) 的返回值是一个新的Socket ,这个Socket就是前面提到的用于和客户端进行通讯的Socket了。在这里直接将这个Socket作为参数传递给了 Session.CreateAndExecute 方法
第二句和Proxy.Start里一样的,就是再继续监听有没有其它请求,如果监听到了,就再调用Proxy.AcceptConnection处理 。
这样我们就可以不间断的处理所有来自客户端的请求了。
Net/Session.cs
好了 Proxy.AcceptConnection 讲完了,下面自然要进入 Session.CreateAndExecute 方法了。 Session.CreateAndExecute 方法在 Net目录下的Session.cs 里。
定义如下 :
|
1
2
3
4
5
6
|
public static void CreateAndExecute(object param) { ClientPipe clientPipe = new ClientPipe((Socket)param); Session session = new Session(clientPipe, null); session.Execute();} |
这是一个静态方法,主要是用来创建一个Session类的实例。并执行Session实例的Execute方法,这里只有一个参数,这个参数就是从Proxy.AcceptConnection 方法里传进来的用来和客户端通讯的那个Socket ;
前面已经提到过,Session类是用来封装代理服务器的一次会话,而代理服务器的一次会话表示,从客户端读请求,然后转发至服务器,然后再读取服务器的响应,然后再将响应转发回客户端,所以在代理的一次会话中,需要一个和客户端通讯的SOCKET从客户端读请求,并将响应发回给客户端,另外还需要一个和服务端通讯的SOCKET往服务端转发请求,并读取 服务端的响应 。
所以Session类有一个两个参数的构造方法(第二句)
public Session(ClientPipe clientPipe, ServerPipe serverPipe) |
前面也提到过,ClientPipe 和 ServerPipe 其实就是对于Socket的封装。
所以ClientPipe有一个Socket类型参数的构造方法
ClientPipe clientPipe = new ClientPipe((Socket)param); |
在这里第二个参数是 null 。 也就是ServerPipe是null,后面我们会看到,这个是在转发请求到服务器时创建的(NEW出来的)。为什么要延迟到那里再NEW,其实很容易理解,因为到目前为止,我们还没有开始读取客户端的信息,这样我们就不知道客户端究竟要将请求发送到哪里,自然就没办法建立一个到目标服务端的管道了。Fiddler创建一个这样的构造方法,是因为ServerPipe的重用,但ServerPipe的重用我们给简化掉了,不过这样的构造形式仍然保留了下来,至于理由嘛,就是这样看起来很对称 :) 。
下面我们来看看Session的这个构造方法。
1 public Session(ClientPipe clientPipe, ServerPipe serverPipe) 2 { 3 this.Timers = new SessionTimers(); 4 this.Timers.ClientConnected = DateTime.Now; 5 this.Flags = new StringDictionary(); 6 if (clientPipe != null) 7 { 8 this.clientIP 9 = (clientPipe.Address == null) ? null : clientPipe.Address.ToString(); 10 11 this.clientPort = clientPipe.Port; 12 this.Flags["x-clientIP"] = this.clientIP; 13 this.Flags["x-clientport"] = this.clientPort.ToString(); 14 if (clientPipe.LocalProcessID != 0) 15 { 16 this._localProcessID = clientPipe.LocalProcessID; 17 this.Flags["x-ProcessInfo"] 18 = string.Format( 19 "{0}:{1}", 20 clientPipe.LocalProcessName, 21 this._localProcessID 22 ); 23 this._localProcessName = clientPipe.LocalProcessName; 24 } 25 } 26 27 this.Response = new ServerChatter(this); 28 this.Request = new ClientChatter(this); 29 this.Request.ClientPipe = clientPipe; 30 this.Response.ServerPipe = serverPipe; 31 }
这里开始就实例化了一个SessionTimers。
this.Timers = new SessionTimers();
这个类在Fiddler里是用来记录一次会话过程中的各个阶段的时间点的,主要是用来分析会话的过程中各个阶段所花费的时间。在这里我们保留下来以备后用,但是对这次例子用处不大。可以忽略掉。
下面又实例化了一个StringDictionary类型的Flags对象,
this.Flags = new StringDictionary(); |
用来存储Session过程中的一些标志信息,例如客户端的进程信息,是否设置了断点,断点类型等等。这个标志信息是很重要的,他可以使我们方便的在不同的方法甚至类中传递一些信息.
下面一句是判断clientPipe是否为空。如果不为空(这里不为空,我们在Session.CreateAndExecute方法里已经构造了一个实例,并传递过来了). 就读取一些信息,写到 this.Flags 里。
这里稍稍再详细一点的对ClientPipe做个说明,刚才讲过了,ClientPipe其实就是对负责和客户端通讯的那个Socket的一个封装,所以除了提供基本的通讯功能外,又进一步对一些客户端的信息进行了封装,例如,客户端的IP,端口,进程名和进程ID等等。
再往下四句
this.Response = new ServerChatter(this);this.Request = new ClientChatter(this);this.Request.ClientPipe = clientPipe;this.Response.ServerPipe = serverPipe; |
不知道还记不记得最开始的那段分析,我们提到了。ClientChatter,ServerChatter和ClientPipe,ServerPipe的区别,ClientChatter,ServerChatter是较高层次的封装,例如ClientChatter类就提供了对于客户端请求头的封装,你要想获取请求的头信息,可以这样获取 ClientChatter的实例.Headers ,Headers是一个HTTPRequestHeaders类型的对象,后面我们会详细的讲讲这个类,这里先点出来一下。当然在封装这些信息之前,需要先从客户端读取原始的HTTP头和内容信息,这个就要通过ClientChatter调用ClientPipe完成了。
所以总结一下就是, ClientPipe负责从客户端读取原始请求信息,并简单的封装一下客户端的相关信息,而ClientChatter会对这些原始请求进行进一步的封装。 以方便后续的调用。 ServerChatter和ServerPipe是同样的道理 。
知道了这些,上面那四句话,自然就容易理解了。
创建一个ServerChatter类型的Response对象,用来和服务端进行通讯和获取相关的信息。再创建一个 ClientChatter 类型的Request对象,用来和客户端进行通讯并获取相关的信息,可以看到Request有一个ClientPipe属性,我们将在Session.CreateAndExecute里创建的clientPipe对象赋值给了它,而这个对象就是对和客户端通讯的Socket的一个封装
好了,看完了Session的这个构造方法,我们重新回到Session.CreateAndExecute方法。现在只剩下最后一句了。
session.Execute(); |
这个就不用讲了吧,基本上是个人类都能看明白了,就是调用Session类的Execute方法。
所以下面我们进入Session.Execute方法 。
当然在进入Execute方法之前,
我们需要先回顾一下刚才所分析出来的东西:
session 这个对象里现在已经有了哪些东西?
session:Session -- Request:ClientChatter -- ClientPipe:ClientPipe = new ClientPile(和客户端通讯的Socket) -- Response:ServerChatter -- ServerPipe:ServerPipe = null -- Flags:StringDictionary
好的,有了这些储备后,我们就可以正式进入Session.Execute方法了 。
Execute里面代码相对较多,为使思路清晰,我们去掉一些细节部分,只保留主干部分,如下所示:
1 internal void Execute() 2 { 3 if (!this.ObtainRequest()){return;} // 获取请求信息 4 if (this.State < SessionStates.ReadingResponse) 5 { 6 if (!this.Response.ResendRequest()) // 将包装后的请求重新发到目标服务器 7 { 8 this.CloseSessionPipes(true); 9 this.State = SessionStates.Aborted; 10 return; 11 } 12 13 Intercepter.UpdateSession(this); 14 15 if (!this.Response.ReadResponse ()) // 读取从目标服务器返回的信息 16 { 17 if (this.State != SessionStates.Aborted) 18 { 19 this.Request.FailSession(0x1f8, 20 "Receive Failure", "ReadResponse() failed: The server did not return a response for this request." 21 ); 22 } 23 this.CloseSessionPipes(true); 24 this.State = SessionStates.Aborted; 25 } 26 this.ResponseBodyBytes = this.Response.TakeEntity(); 27 if (this.Response.ServerPipe != null) 28 { 29 this.Response.ServerPipe.End(); 30 } 31 32 if (this.ReturnResponse()) // 将从目标服务器读取的信息返回给客户端 33 { 34 this.State = SessionStates.Done; 35 } 36 else 37 { 38 this.State = SessionStates.Aborted; 39 } 40 if (this.Request != null && this.Request.ClientPipe != null) 41 { 42 this.Request.ClientPipe.End(); 43 } 44 this.Response.ReleaseServerPipe(); 45 } 46 }
看一下上面有注释的四句,是不是感觉有点熟悉,是的,这正是前面讲过的,代理服务器一次会话的四个步骤。
this.ObtainRequest() // 获取请求信息 this.Response.ResendRequest() // 将请求报文重新包装后转发给目标服务器 this.Response.ReadResponse () // 读取从目标服务器返回的信息 this.ReturnResponse() // 将从目标服务器读取的信息返回给客户端

浙公网安备 33010602011771号