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所在的索引,這個索引也就是當前SessionListView里的索引, 然后,把ListView的那一行變紅,並激活當前 程序 (彈到最前面,或者任務欄 閃動提示)  。 

  注 ListView的索引和sessions的索引是一致的,也就是ListView里索引為1的位置顯示的正是 sessions 里索引為1Session 的信息 

  OKAY,把剛才的連起來,我們知道了執行完 Intercepter.UpdateSession(this); 后,程序會被彈到最前面或者在任務欄里閃動提示,同時,對應當前Session的那一行ListView 的背景會被變成紅色  

  Intercepter.UpdateSession(this)  講完了,后面自然是 thid.Pause()了。

  我們看一下他的代碼。

  SessionPause 方法 , 順便把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 被中斷了,這時候就把 運行至完成按鈕 變亮   。 

  請求報文已經都被顯示出來了,要想修改的話,只要在右邊上面的文本框里修改就行了,這里我們為了方便就直接修改請求報文了,但這樣不安全,如果你只想改GETPOST數據,可以繼續處理 。  

  修改完成后,我們再點一下 運行至完成 按鈕,那么整個篡改的過程就算完成了。

  再看 運行至完成 按鈕的單擊事件,仍然在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 

  注釋已經寫的異常詳細了,各位看看應該就可以明白了 。 

  這個系列終於是完成了。 剛才粗略的統計了,二萬五千多字。尼瑪,再次不容易啊                

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM