[转载]HTTP代理实现请求报文的拦截与篡改10–大结局 篡改部分的代码分析 – jivi – 博客园.
上节我们把篡改的功能简单的介绍了下,这些功能看起来挺玄乎的,但明白了原理后,其实没什么东西,下面我们就来分析一下这部分的代码。
- HTTP代理实现请求报文的拦截与篡改1--开篇
- HTTP代理实现请求报文的拦截与篡改2--功能介绍+源码下载
- HTTP代理实现请求报文的拦截与篡改3--代码分析开始
- HTTP代理实现请求报文的拦截与篡改4--从客户端读取请求报文并封装
- HTTP代理实现请求报文的拦截与篡改5--将请求报文转发至目标服务器
- HTTP代理实现请求报文的拦截与篡改6--从目标服务器接收响应报文并封装
- HTTP代理实现请求报文的拦截与篡改7--将接收到的响应报文返回给客户端
- HTTP代理实现请求报文的拦截与篡改8--自动设置及取消代理+源码下载
- HTTP代理实现请求报文的拦截与篡改8补--自动设置及取消ADSL拔号连接代理+源码下载
- HTTP代理实现请求报文的拦截与篡改9--实现篡改功能后的演示+源码下载
- HTTP代理实现请求报文的拦截与篡改10--大结局 篡改部分的代码分析
同样的,分析前我们先来回顾一下前面分析出来的内容。
一。 一次会话(Session)的有四个过程 。
this.ObtainRequest() // 获取请求信息 this.Response.ResendRequest() // 将请求报文重新包装后转发给目标服务器 this.Response.ReadResponse () // 读取从目标服务器返回的信息 this.ReturnResponse() // 将从目标服务器读取的信息返回给客户端
二. 每个会话(Session)都是在独立的线程中执行的。
知道了上面二点, 对于实现篡改来说就已经足够了,我们都知道线程有个很重要的特性,就是可以挂起和恢复,这就让我们的篡改实现起来非常容易了,在转发前挂起会话线程,然后在主线程(界面的那个线程)修改报文,然后恢复会话线程让其继续执行,那么这时候它转发到服务器的那个请求报文就是我们改过后的报文了。
我们先来看看不实现篡改的情况下会话线程的执行步骤 (其中黑线为顺序流,红线为数据流)
再来看看实现篡改功能的情况下会话线程和主线程的执行顺序
从上面两张丑图可以看到,在实现篡改的情况下,运行到转发请求至服务器那一步的时候,转发的请求报文其实已经是在主线程里被修改过的报文了。 🙂 是不是很简单,下面我们就来看看代码是如何写的。
相较于前面的代码,实现篡改后,我们增加了一个BreakPoint.cs的文件,这个文件里有两个类,一个BreakPoint类,一个是BreakPointManager类 ,还有一个枚举
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace JrIntercepter.Net 7 { 8 enum BreakPointType 9 { 10 Request, 11 Response 12 } 13 14 class BreakPoint 15 { 16 public BreakPointType Type; 17 public String Url; 18 } 19 20 class BreakPointManager { 21 private static IList<BreakPoint> breakPoints = new List<BreakPoint>(); 22 public static void Add(BreakPointType type, String url) 23 { 24 foreach (BreakPoint bp in breakPoints) 25 { 26 if (bp.Url.Trim().Equals(url.Trim())) 27 { 28 return; 29 } 30 } 31 breakPoints.Add(new BreakPoint { Type = type, Url=url}); 32 } 33 34 public static BreakPoint Get(String url) 35 { 36 foreach (BreakPoint bp in breakPoints) 37 { 38 if (bp.Url.Trim().Equals(url.Trim())) 39 { 40 return bp; 41 } 42 } 43 return null; 44 } 45 } 46 }
这两个类都很简单,就是用来记录断点的 。
BreakPointManager.Add(BreakPointType.Request,“www.baidu.com”) ;
这样就对www.baidu.com这个网址下了一个断点,当在浏览器里 打开www.baidu.com的时候就会被断下来了。虽然BreakPointType里有一个Response类型,但没有实现,只是装装样子的。
刚才讲过了,如果要实现篡改,需要在读取请求后挂起线程,而要想实现挂起线程自然要在Session的Execute方法里做文章了。因为前面回顾的一次会话的四个过程就是在这个方法体里实现的。
我们还是只列出更改后的主干代码
1 // 从客户端获取请求信息 2 if (!this.ObtainRequest()) 3 { 4 return; 5 } 6 7 // 如果当前状态小于ReadingResponse 8 if (this.State < SessionStates.ReadingResponse) 9 { 10 // 通知界面有新SESSION 11 Intercepter.UpdateSession(this); 12 13 String requestPath = this.Request.Headers.RequestPath.Trim().Trim('/') ; 14 string fullUrl = this.Request.Host.Trim().Trim('/') + (string.IsNullOrEmpty(requestPath)? "":("/" + requestPath)); 15 fullUrl = fullUrl.Split('?')[0] ; 16 // 在这里截断 17 // 判断此在此网址下了断点 18 BreakPoint bp = BreakPointManager.Get(fullUrl); 19 if (bp != null) 20 { 21 // 通知界面有断点被断 22 Intercepter.BreakPoint(this, bp); 23 this.Pause(); 24 } 25 26 // 将请求转发至目的服务器 27 this.State = SessionStates.SendingRequest; 28 if (!this.Response.ResendRequest()) 29 { 30 return; 31 } 32 33 if (!this.Response.ReadResponse ()) 34 { 35 } 36 }
分析前先提一下,
// 通知界面有新SESSION Intercepter.UpdateSession(this);
这个是原来就实现的,用来通知界面获得了新的Session。不过如果你翻看了上个版本的代码你就会发现,他被写在了this.Response.ResendRequest() 后面,这样的写法是错误的,因为这样我们就没办法实现篡改了,因为要想实现篡改就必须在请求转发前修改请求报文,而请求报文的两个部分,Headers(请求报文头)和RequestBodyBytes也就是请求报文体都被封装在了这个Session里 ,那么如果我们在转发请求后才将这个Session传给界面,那么又如何在转发前在主线程(界面线程) 来修改他呢,所以这次我们把他提到了ResendRequest之前 。
下面我们来看看这个方法的具体实现 和作用 。
这个方法在Intercepter.cs 里.
1 internal delegate void DelegateUpdateSession(Session session); 2 internal static event DelegateUpdateSession OnUpdateSession; 3 4 internal static void UpdateSession(Session session) 5 { 6 if (OnUpdateSession != null) 7 { 8 OnUpdateSession(session); 9 } 10 }
方法很简单。就是如果有 OnUpdateSession 事件,就执行 OnUpdateSession 事件 。
再看看FrmMain.cs 里 FrmMain类的构造函数,里面有一句
Intercepter.OnUpdateSession += new Intercepter.DelegateUpdateSession(this.OnUpdateSession);
是不是已经串起来了 🙂
再看OnUpdateSession方法(还是FrmMain类的)
1 private IList<Session> sessions = new List<Session>(); 2 internal void OnUpdateSession(Session session) 3 { 4 try 5 { 6 lock (lvSessions) 7 { 8 sessions.Insert(0, session); 9 ListViewItem lvi = new ListViewItem(); 10 lvi.Text = session.id.ToString(); 11 // FullUrl 12 lvi.SubItems.Add(session.Host); 13 lvi.SubItems.Add(session.Request.Headers.RequestPath); 14 lvi.SubItems.Add(session.Request.Headers.HTTPMethod); 15 lvi.SubItems.Add(session.LocalProcessName); 16 17 this.lvSessions.Items.Insert(0, lvi); 18 } 19 } 20 catch {} 21 }
上面的lvSessions 就是左边的那个ListView (列表框) 。
看了上面的代码,绝大部分人应该已经明白它的作用了,这个类就相当于界面和Session之间的一个桥梁,是实现界面和Session的通讯 的 , 当然Session也可以直接和界面通讯,但这样写,可以大大的降低两者的耦合性,当然我们这个系列不是讲设计的,所以 这个只是提一下,主要的还是来看 他 看的作用,他的作用就是,每当 在 会话中(Session的一个实例) 获取了请求后 , 就会执行一次 FrmMain.cs 里的 OnUpdateSession 方法,然后在FrmMain.OnUpdateSession 方法里往一个全局变量 sessions 的顶部插入当前的这个session , 然后 再在左边的列表框里显示出当前的这个Session的主机名,请求的文件路径,HTTP的方法,以及进程名等信息 。
注:我们这个类实现的是相当的不好的,是会出现问题的,你们可以改成更为安全的方式。
好了,至此我们知道了执行完
Intercepter.UpdateSession(this);
后 frmMain 里的sessions里就已经存储了当前这个Session,另外在左边的列表框里也会列出当前这个Session的相关信息。
那么继续。 Intercepter.UpdateSession(this) 后面的代码如下
String requestPath = this.Request.Headers.RequestPath.Trim().Trim('/') ; string fullUrl = this.Request.Host.Trim().Trim('/') + (string.IsNullOrEmpty(requestPath)? "":("/" + requestPath)); fullUrl = fullUrl.Split('?')[0] ; // 在这里截断 // 判断此在此网址下了断点 BreakPoint bp = BreakPointManager.Get(fullUrl); if (bp != null) { // 通知界面有断点被断 Intercepter.BreakPoint(this, bp); this.Pause(); }
这里就是中断的核心代码了。 代码不多,也不难理解。先是获取当前请求的完整Url,这里的Url地址不包括http://或者https://这部分,也不包括?后面的部分。
获取了完整的Url后,就从断点列表里找有没有这个网址的中断,如果没找到,继续执行后面的,如果找到了 就调用
Intercepter.BreakPoint(this, bp);
通知界面已经找到断点了。
然后
this.Pause();
暂停本线程的执行。
我们先来看看 Intercepter.BreakPoint(this, bp);
这个和前面的Intercepter.UpdateSession(this); 一样,最终会去执行FrmMain.cs里的 OnBreakPoint 方法。
OKAY FrmMain 的 OnBreakPoint 方法
1 internal void OnBreakPoint(Session session, BreakPoint breakPoint) 2 { 3 lock (lvSessions) 4 { 5 int sessionID = session.id; 6 foreach (ListViewItem li in lvSessions.Items) 7 { 8 Session tmp = this.sessions[li.Index]; 9 if (tmp.id == sessionID) 10 { 11 li.BackColor = Color.Red; 12 break; 13 } 14 } 15 16 this.Activate(); 17 lvSessions.Focus(); 18 } 19 }
这段代码同样不多,也不难理解,就是在sessions(存储所有session的列表)里找当前session所在的索引,这个索引也就是当前Session在ListView里的索引, 然后,把ListView的那一行变红,并激活当前 程序 (弹到最前面,或者任务栏 闪动提示) 。
注 ListView的索引和sessions的索引是一致的,也就是ListView里索引为1的位置显示的正是 sessions 里索引为1的Session 的信息
OKAY,把刚才的连起来,我们知道了执行完 Intercepter.UpdateSession(this); 后,程序会被弹到最前面或者在任务栏里闪动提示,同时,对应当前Session的那一行ListView 的背景会被变成红色
Intercepter.UpdateSession(this) 讲完了,后面自然是 thid.Pause()了。
我们看一下他的代码。
Session的Pause 方法 , 顺便把Resume方法也列出来
1 private AutoResetEvent syncEvent; 2 internal void Pause() 3 { 4 this.SState = SessionState.Pasue; 5 if (this.syncEvent == null) 6 { 7 this.syncEvent = new AutoResetEvent(false); 8 } 9 this.syncEvent.WaitOne(); 10 } 11 12 internal void Resume() 13 { 14 if (this.syncEvent != null) 15 { 16 this.syncEvent.Set(); 17 } 18 this.SState = SessionState.Executing; 19 }
可以看到这里我们暂停线程使用的不是Suspend 而是AutoResetEvent 。因为Suspend是一个过时的方法。其它没什么讲的,反正执行了 this.Pause() 当前Session对应的线程会被挂起。这样ResendRequest (将请求转发至服务器的方法)将不会被执行,直到有人调用了Session.Resume 方法。
OKAY ,到此,我们已经将当前的请求中断下来了。下面我们就来修改 。
怎么修改???
还记得上节里的操作说明吗,首先在左边的列表框里选中红色背景的那行(被断下来的那个Session) 。
注:刚才挂起的是Session线程,主线程也就是界面线程是没有被挂起,所以是可以继续进行 操作的。
既然是列表项的选中操作,我们自然要来看一下 ListView的 SelectedIndexChanged 事件的处理方法了,
这个方法在FrmMain里
1 private void lvSessions_SelectedIndexChanged(object sender, EventArgs e) 2 { 3 // 假如有被选中的行 4 if (this.lvSessions.SelectedItems.Count > 0) 5 { 6 // 选取第一个被选中的行,我们只处理单选的情况 7 int index = this.lvSessions.SelectedItems[0].Index; 8 // 调出选中的这行对应的session 9 Session session = sessions[index]; 10 11 // 如果对应的session的状态是执行中 12 if (session.SState == SessionState.Executing) 13 { 14 // 运行至完成 按钮变灰 就是不可用 15 this.btnRunToComplete.Enabled = false; 16 } 17 else 18 { 19 // 运行至完成 按钮变亮 可以用 20 this.btnRunToComplete.Enabled = true; 21 } 22 23 // 在右边的请求文本框里(右边上面的文本框) 里显示选中行对应的session的报文头 24 tbRequest.Text = session.Request.Headers.ToString(); 25 // 加个换行 26 tbRequest.Text += "\r\n"; 27 // 显示请求报文体 28 tbRequest.Text += Encoding.UTF8.GetString(session.RequestBodyBytes); 29 30 // 如果 session.Response 头不为NULL 31 if (session.Response != null && session.Response.Headers != null) 32 { 33 // 在响应文本框(右边下面的文本框)显示响应的报文头 34 tbResponse.Text = session.Response.Headers.ToString(); 35 tbResponse.Text += "\r\n"; 36 } 37 } 38 }
这个方法也没什么好讲的,看看注释就能明白了,就是在右边上面的文本框显示请求报文,如果有响应报文的话,就在下面的文本框里显示响应报文头 。 另外如果对应的session不是Executing状态,也就是挂起状态,说明,那个session 被中断了,这时候就把 运行至完成按钮 变亮 。
请求报文已经都被显示出来了,要想修改的话,只要在右边上面的文本框里修改就行了,这里我们为了方便就直接修改请求报文了,但这样不安全,如果你只想改GET和POST数据,可以继续处理 。
修改完成后,我们再点一下 运行至完成 按钮,那么整个篡改的过程就算完成了。
再看 运行至完成 按钮的单击事件,仍然在FrmMain 里
1 private void btnRunToComplete_Click(object sender, EventArgs e) 2 { 3 if (this.lvSessions.SelectedItems.Count > 0) 4 { 5 // 同样的获取选中行对应的session 6 ListViewItem selectItem = this.lvSessions.SelectedItems[0]; 7 int index = selectItem.Index; 8 Session session = sessions[index]; 9 // 利用Parse.ParseRequest方法分析文本框里的请求报文并重新封装成一个 10 // HttpRequestHeaders类型的变量,并替换session.Request里原来的Headers 11 // 这样报文头就被用修改后的报文头替换了 12 session.Request.Headers = Parser.ParseRequest(this.tbRequest.Text); 13 // 替换报文主体部分 14 // 先将修改后的全部报文转化为byte[]数组。 15 byte[] arrData = Encoding.UTF8.GetBytes(this.tbRequest.Text); 16 int lenHeaders = 0; 17 int entityBodyOffset = 0; 18 HTTPHeaderParseWarnings warnings; 19 // 利用Parse.FindEntityBodyOffsetFromArray 方法,获得报文体在整个报文中的偏移。 20 Parser.FindEntityBodyOffsetFromArray(arrData, out lenHeaders, out entityBodyOffset, out warnings); 21 // 整个报文的长度-报文体的偏移 自然就是报文体的大小了 22 int lenBody = arrData.Length - entityBodyOffset; 23 // 如果请求报文头时有Content-Length首部 24 if (!String.IsNullOrEmpty(session.Request.Headers["Content-Length"])) 25 { 26 // 修改Content-Lenght首部的值为修改后的主体部分的大小 . 27 session.Request.Headers["Content-Length"] = lenBody.ToString(); 28 } 29 // 构造一个和报文主体长度一样的MemoryStream的实例 ms 30 MemoryStream ms = new MemoryStream(lenBody); 31 // 将报文体部分写到这个 ms 里 32 ms.Write(arrData, entityBodyOffset, arrData.Length - entityBodyOffset); 33 // 将ms缓冲区地址赋值给 session.RequestBodyBytes 这样报文体也被修改了 34 session.RequestBodyBytes = ms.GetBuffer(); 35 36 // 恢复线程,线程恢复后做的是什么呢,当然是ResendRequest.翻翻前面的 37 // ResendRequest就是将 session.Request.Headers 38 // 转化成byte[]后和 session.ReqeustBodyBytes 一起被转发到目标服务器, 39 // 但此时这两个都已经被我们改过了 :) 40 session.Resume(); 41 42 // 将选中行的红色背景改回成白色 43 selectItem.BackColor = Color.White ; 44 // 运行到完成按钮变灰 45 this.btnRunToComplete.Enabled = false; 46 } 47 } 48
注释已经写的异常详细了,各位看看应该就可以明白了 。
这个系列终于是完成了。 刚才粗略的统计了,二万五千多字。尼玛,再次不容易啊