HttpCanary實現對HTTP2協議的抓包和注入(原理篇)

今天發佈了HttpCanary2.0版本,除了修復了部分bug以及優化性能外,最主要的是支持了HTTP2協議。html

HttpCanary是什麼?Android平臺第二強大的HTTP抓包和注入工具,不瞭解的同窗能夠閱讀下關於HttpCanary的介紹:juejin.im/post/5c1e37…java

HttpCanary2.0已經發布到GooglePlay,歡迎你們下載並給予評價建議,傳送門:play.google.com/store/apps/…android

乾貨爲主,廢話很少說,下面開始本篇的正文。git

HTTP2.0和HTTP1.x的區別

先簡單介紹一下HTTP2.0協議的概況,熟悉的同窗能夠跳過。github

HTTP2.0協議是由SPDY協議進化而來,標準於2015年5月正式發佈,算起來不到四年時間,屬於比較新的技術。因此部分主流的抓包工具都不支持HTTP2,好比Fiddler,而Charles則是在4.0版本後開始支持。算法

HTTP2.0協議和HTTP/1.x協議在請求方法、狀態碼乃至URI和絕大多數HTTP頭部字段等部分保持高度兼容性,即常說的請求行、請求頭、請求體、響應行、響應頭、響應體這些格式都具備一致性。服務器

可是,HTTP2.0協議在對頭部數據的壓縮、多路複用、服務器主動推送三個方面作了支持和優化。cookie

  • 頭部數據壓縮。對請求行、請求頭、響應行、響應頭這些頭部數據進行壓縮,採用Hpack算法。
  • 多路複用。每一個connection以stream的形式組織,數據包按照frame(數據幀)的形式通訊,同時增長了流量控制等功能。
  • 服務器主動推送。HTTP2.0協議支持雙向通訊,以及half-close這種單向通訊。

HTTP2.0協議雖然沒有明確要求加密,但目前的實現都是默認使用TLS加密,因此能夠認爲使用HTTP2.0則必須使用HTTPS。session

爲了實現對HTTP1.x的兼容,HTTP2.0協議爲此額外定義了應用層協商標準(Application-Layer Protocol Negotiation,簡稱ALPN),以便客戶端和服務端可以從HTTP/1.0、HTTP/1.一、HTTP/2乃至其餘非HTTP協議中作出選擇。ALPN衍生於SPDY協議的NPN標準,都是基於TLS的擴展標準。併發

Android是從5.0開始支持ALPN,而Java是從OpenJDK 8和JDK 9開始支持,能夠認爲從這些時候開始才真正支持HTTP2.0協議。

HttpCanary的HTTP2之旅

我在發佈HttpCanary2.0的同時,已經將HTTP2.0協議的實現代碼更新到了github,也就是HttpCanary的核心庫NetBare,對代碼感興趣的能夠對照着本文理解。

HTTP2.0的支持難點主要有三個:

  • 如何進行應用層協議協商,即ALPN協商。
  • 對請求和響應頭部進行Hpack解碼並從新編碼。
  • 將HTTP2.0的stream、frame並還原成HTTP1.x協議格式並從新生成stream、frame,以及多路複用的分離。

下面,講解NetBare是如何解決這四個難題,從而實現對HTTP2.0協議的抓包和注入的。

1. ALPN協商

Android從5.0開始支持ALPN協商,NetBare庫的最低支持版本也是5.0,因此在理論上是徹底能夠實現的。

1.1 ALPN協商圖解

簡單歸納ALPN協商的過程:SSL握手的時候,Client將支持的協議版本列表發給Server,Server務端從列表中選擇一個協議版本併發給Client做爲協商版本,SSL握手完成後,Client和Server都使用協商版本進行通訊。ALPN的協商是在Client發給Server的ClientHello握手包以及Server回給Client的ServerHello握手包兩步直接完成的。

下圖是ALPN協商的圖解:

粗略一看很是簡單,可是因爲HTTP2.0協議強制使用TLS/SSL加密,因此只能使用中間人MITM方式進行解密抓包。而中間人MITM又分爲中間人Client和中間人Server,因此ClientHello握手包的通訊流程是Client -> MITM Server -> MITM Client -> Server,而ServerHello握手包的通訊流程則是 Server -> MITM Client -> MITM Server -> Client,由原先的一來一回兩步,變成了來回六步,複雜性上增長了許多。

增長了MITM層的ALPN協商的圖解:

這裏有個小技巧,最開始的ClientHello報文並無直接交給MITM Server開始握手,而是經過一個Parser直接解析出list of protocols並交給MITM Client,讓MITM Client先和Server進行握手。獲取到selected protocol後,MITM Server在和Client開始握手。這樣的設計的目的主要是,下降兩組SSL握手之間的邏輯依賴。

接下來,按照這個圖解流程,實現新的ALPN協商過程。

1.2 解析ClientHello報文

第一個核心步驟,MITM Server須要解析出ClientHello握手包中的協議列表(list of protocols)。因爲ALPN extension是基於TLS的extension標準,因此解析方式相似於SNI的解析方式。

TLS extensions數據區位於ClientHello包的Compression Method以後,TLS extensions(注意複數s)是支持多個extension擴展的,而SNI和APLN協商只是其中的一種。每一個extension是按照type + length + data的格式依次組織的。其中SNI的type是0,而ALPN的type是16。

咱們依次遍歷並找到type等於16的數據區域,並按照length讀取data數據區,這裏就是ALPN的list of protocols內容了。

下一步是繼續解析list of protocols中具體的協議,好比是HTTP1.0、HTTP1.1或者HTTP2.0。list of protocols的數據組織形式是count+(length+protocol)s,其中count表示協議列表中的協議個數,length表示其後的協議值長度(注意length所佔字節數是1,也就是byte型),用圖解表示爲以下:

解析出來的protocol值,可能爲HTTP/1.0、HTTP/1.一、h2等,其中h2表示HTTP2.0協議。

1.2 MITM Client設置list of protocols

第二個核心步驟,MITM Client將解析出來的protocols加入到ClientHello包中發給真正的Server。因爲Android並無公開相關的API,因此咱們只能經過反射方式調用隱藏API。經過閱讀org.conscrypt.OpenSSLEngineImpl的源碼,發現能夠經過反射其成員變量sslParameters設置ClientHello的list of protocols。

sslParameters變量類型是SSLParametersImpl,咱們來簡單看下其內部參數:

public class SSLParametersImpl implements Cloneable {
    ...
    byte[] npnProtocols;
    byte[] alpnProtocols;
    boolean useSessionTickets;
    boolean useSni;
    ...
}

複製代碼

這裏除了ALPN外,還有NPN(SPDY協議的協商標準),因此反射ALPN設置list of protocols的代碼是:

Field sslParametersField = mSSLEngine.getClass().getDeclaredField("sslParameters");
sslParametersField.setAccessible(true);
Object sslParameters = sslParametersField.get(mSSLEngine);
if (sslParameters == null) {
   throw new IllegalAccessException("sslParameters value is null");
}
Field alpnProtocolsField = sslParameters.getClass().getDeclaredField("alpnProtocols");
alpnProtocolsField.setAccessible(true);
alpnProtocolsField.set(sslParameters, listOfProtocols);
複製代碼

必須注意這裏的alpnProtocols是byte[]類型的變量,那麼咱們如何把HTTP/1.0、HTTP/1.一、h2這些協議組織成byte[]呢?

其實這個byte[]是按照protocols的length+protocol依次組織的,圖解以下:

代碼實現是:

ByteArrayOutputStream os = new ByteArrayOutputStream();
for (HttpProtocol protocol : protocols) {
    String protocolStr = protocol.toString();
    os.write(protocolStr.length());
    os.write(protocolStr.getBytes(Charset.forName("UTF-8")), 0, protocolStr.length());
}
byte[] alpnProtocols = os.toByteArray();
複製代碼

細心的同窗,仔細一對比會發現,這個和上面解析的list of protocols數據相比就相差一個count,那爲何還要費這麼大力氣來先解析出protocol值呢?

由於從Android P開始支持Java OpenJDK 8,以上經過反射OpenSSLEngineImpl的方式已經行不通了。因爲OpenJDK 8已經支持直接經過SSLParameter類設置list of protocols,故Android對此做了相應的兼容,具體的兼容類是org.conscrypt.Java8EngineWrapper。閱讀其源碼,能夠找到setApplicationProtocols方法傳入list of protocols。

final class Java8EngineWrapper extends AbstractConscryptEngine {
    ...
    @Override
    void setApplicationProtocols(String[] protocols) {
    delegate.setApplicationProtocols(protocols);
    } 
    ...
}
複製代碼

咱們一樣須要經過反射調用此方法:

Method setApplicationProtocolsMethod = mSSLEngine.getClass().getDeclaredMethod("setApplicationProtocols", String[].class);
setApplicationProtocolsMethod.setAccessible(true);
setApplicationProtocolsMethod.invoke(mSSLEngine, new Object[]{protocols});
複製代碼

這裏使用的是String[],這就是爲何要解析出protocol值的緣故了。

1.3 解析ServerHello報文中的selected protocol

當真正的Server收到MITM Client發過去的ClientHello包後,須要回一個ServerHello包,同時將服務端選擇的協議版本加入其中。MITM Client收到ServerClient包後須要解析出selected protocol,這裏講解下是如何解析出selected protocol的。

從ServerHello包中解析selected protocol有兩種方式,一種是如同以前處理ClientHello同樣,強解析。由於selected protocol同list of protocols同樣,都是使用的TLS extensions標準。第二種方式,將ServerHello直接交給SSLEngine,開始正常的SSL握手流程,而後從SSLEngine中直接獲取解析後的selected protocol。兩種方法,都沒有任何問題,我這裏採用的是第二種。

這種方式須要反射SSLEngine,按照以前的經驗,要區分系統版本。

Android P如下,SSLEngine的實現類是org.conscrypt.OpenSSLEngineImpl,如何來反射selected protocol呢?仔細閱讀源碼後,會發現OpenSSLEngineImpl類中並無相關ALPN selected protocol的代碼,這個就很是捉急了。可是若是熟悉okhttp源碼的同窗,可能會知道okhttp對ALPN協商的支持使用過反射OpenSSLSocketImpl來完成的,因此再來看一下OpenSSLSocketImpl的代碼,就找到ALPN selected protocol相關的代碼了,以下:

private long sslNativePointer;
...
/** * Returns the protocol agreed upon by client and server, or {@code null} if * no protocol was agreed upon. */
public byte[] getAlpnSelectedProtocol() {
    return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
}
複製代碼

它是經過調用NativeCrypto的靜態方法SSL_get0_alpn_selected來獲取selectedProtocol的,如此一看,最關鍵的就是sslNativePointer這個參數了。sslNativePointer是個JNI層指針,一樣出現於OpenSSLEngineImpl類中,那麼是不是同一個呢?答案是確定的,都是由SessionContext建立的,同一個Session下的sslNativePointer是相同的。

由此就找到了解決方案:先反射取到sslNativePointer,再反射NativeCrypto.SSL_get0_alpn_selected方法獲取ALPN selected protocol。

Class<?> nativeCryptoClass = Class.forName("com.android.org.conscrypt.NativeCrypto");
Method SSL_get0_alpn_selectedMethod = nativeCryptoClass.getDeclaredMethod("SSL_get0_alpn_selected", long.class);
SSL_get0_alpn_selectedMethod.setAccessible(true);

Field sslNativePointerField = mSSLEngine.getClass().getDeclaredField("sslNativePointer");
sslNativePointerField.setAccessible(true);
long sslNativePointer = (long) sslNativePointerField.get(mSSLEngine);
byte[] selectedProtocol = (byte[]) SSL_get0_alpn_selectedMethod.invoke(null, sslNativePointer);
複製代碼

這裏的byte[]不須要再解析了,能夠直接轉換成UTF-8字符串。

對於Android P而言,獲取ALPN selected protocol就容易多了,Java8EngineWrapper中直接提供了相關方法,直接反射就能夠了:

final class Java8EngineWrapper extends AbstractConscryptEngine {
    ...
    @Override
    public String getApplicationProtocol() {
        return delegate.getApplicationProtocol();
    }
    ...
}
複製代碼

如此,就知曉了服務端選擇的協議類型了,也就是本次Connection通訊使用的協議類型了,若是是h2那就表示這次通訊使用的是HTTP2協議。

1.4 MITM Server設置selected protocol

ALPN協商的最後一步,就將selected protocol加入到ServerHello報文中,由MITM Server發給Client完成SSL握手。這一步同1.2 MITM Client設置list of protocols幾乎相同,惟一的區別是protocol列表變成了單個的selected protocol。

當SSL握手完成後,就開始進行請求和響應數據通訊了。

2. Hpack編解碼

Hpack是爲了精簡要是HTTP頭部數據而設計的,HTTP2.0協議就使用了Hpack算法,來提高性能。

2.1 Hpack算法概念及原理

因爲HTTP協議headers部分包含了大量相同的字段,好比Content-Type,Cookie,Host等等,這些都是能夠經過字典的方式進行編碼壓縮,好比Client和Server都約定1表示Content-Type,2表示cookie,如此數據就顯得很是小了。Hpack算法的原理和做用就是相似這樣的。

Hpack只做用於HTTP頭部信息,包括請求行、請求頭、響應行、響應頭這四個部分,而不只僅是請求頭和響應頭。

首先,Hpack算法定義了兩種Table,一種是靜態表(Static Table),一種是動態表(Dynamic Table)。

靜態表是由IETF統一制定的標準,定義了大部分經常使用的字段:

Index Header Name Header Value
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
6 :scheme http
7 :scheme https
8 :status 200
... ... ...
14 :status 500
15 accept-charset
16 accept-encoding gzip, deflate
... ... ...

靜態表一共定義了61個字段,索引從1開始,完整的表可參考:http2.github.io/http2-spec/…

動態表,顧名思義就是針對不肯定內容動態處理的表,它維護了一個索引和頭部值,好比訪問一個圖片,content-type爲image/jpeg,image/jpeg這個字符串數據就存放於動態索引表中。動態索引表的大小是能夠動態增加的,而最大上限由SETTINGS幀的SETTINGS_HEADER_TABLE_SIZE來設置。

動態表由服務端和客戶端共同維護,每一條Connection讀數據和寫數據各有且僅有一個動態表,也就是說Client和Server各有兩個動態表,動態表做用於此Connection下的全部HTTP請求和響應。Client發送請求,編碼使用動態表1,Server接收請求,解碼也使用動態表1;Server發送響應,編碼使用動態表2,Client接收響應,解碼使用動態表2。此Connection下的全部HTTP請求和響應,都是使用的動態表1和動態表2,兩個表之間互不干擾,徹底獨立。除此以外,爲了儘可能壓縮頭部數據,仍是用了霍夫曼編碼,編碼後再存入動態表中。

靜態表和動態表都是以二進制編碼的方式組織的,編碼狀態機和規則以下圖:

以上就是Hpack相關的知識點,下面來分析NetBare是如何進行Hpack編解碼設計的。

2.2 NetBare的Hpack解碼及重編碼

NetBare庫的VirtualGateway維護了四個Hpack表,MITM Client和MITM Server各兩個,目的是先解碼還原成咱們常見的HTTP協議格式,而後再從新編碼,圖解以下:

Hpack算法的實現,是基於OKHttp開源庫中的Hpack類並作了一些修改。值得注意的是,Hpack算法有多種編碼規則,極有可能相同的數據先解碼再從新編碼後和原先不一樣。當時未注意到這一點,覺得是bug,還給OkHttp提了issue,囧。

3. Stream+Frame的多路複用機制

3.1 HTTP2.0的多路複用設計

HTTP2.0協議最大的特性就是多路複用,下降HTTP延時提升性能。雖然HTTP1.1引入了管道機制(Pipelining)使用keep-alive也可以實現多路複用,可是多個請求和響應必須依次排隊,未能將多路複用發揮到極致。

HTTP2.0協議的多路複用,一樣也是基於keep-alive,另外因爲強制使用HTTPS,還須要開啓session ticket,其一樣是一個TLS extensions擴展。而開啓session ticket的方式,相似處理ALPN,都是經過反射完成的,很少贅述。

HTTP2.0協議多路複用最大的革新,是使用stream+frame的形式來組織HTTP請求和響應,來實現多個請求和響應能夠併發而不用依次排隊。每個stream表明一個請求+響應,一個Connection中能夠同時存在多個stream,每一個stream中的數據發送和接收的最小處理單元就是frame。同一時間內能夠有多個stream的各自的frame存在於管道中,每一個frame中包含stream id,接收端用此區分frame是屬於哪一個stream的數據。這就是真正意義上的多路複用。

3.2 多路複用請求的攔截和注入

普通的HTTP請求是一個Connection一個請求響應,結束後銷燬Connection,也就是常說的握手揮手,不留下一片雲彩。雖然性能低,可是對請求和響應的攔截和注入就方便多了。因此NetBare對於HTTP1.x的攔截器設計是:

很明顯,這種攔截器設計只能知足一個Connection一個請求響應的狀況。若是是HTTP2.0協議那種frame單元傳輸並且交錯的數據傳輸,Interceptors很難作邏輯處理。惟一的方案就是對各個stream的frame單元進行組包,還原成HTTP1.x格式的數據,交給Interceptors作攔截注入,最後再拆包成frame單元發給終端。另外,因爲請求併發,同一個時間有多個stream的frame在傳輸,因此還須要對各個stream進行隔離。

因此,修改以後的攔截器設計以下:

HTTP2 Codec Interceptor分爲Decode Interceptor和Encode Interceptor,分別用於Frame解碼和Frame編碼。而每一個Stream的攔截使用各自徹底獨立的攔截器實例,這樣就能夠在自定義攔截器中對HTTP2的明文請求及響應進行注入等操做。

4. HTTP2.0的其它特性支持

HTTP2.0協議比HTTP1.x要複雜地多,除了以上一些特性外,還有服務端推送,Stream優先級、Stream重置和數據流控制等特性。因爲不影響正常的抓包和注入主功能,NetBare暫未作支持,有需求了後面會考慮。

關於NetBare和HttpCanary2.0

NetBare最新的代碼已經開源到Github,有興趣的一塊兒交流探討:github.com/MegatronKin…

HttpCanary2.0的下載推薦使用Google Play,或者百度雲:pan.baidu.com/s/147pSK2mP… 提取碼: 363b

下個版本的主要計劃:

  • 支持multipart/form-data數據格式解析
  • Websocket的抓包和注入。

最後,感謝各位的閱讀和支持!奉上10枚HttpCanary付費版本的兌換碼,能夠在GooglePlay中進行兌換。

兌換碼
5Q5JYB4Z306WJQXJLQAXFPC
YTAYSLHGBEYZHMDU9A7H27J
TR1WFDAMGPBJ8KZM350LG8E
SJ6720KE369T5YPK8WRGEHA
MUBP7HE9NLJCVU7AVQJ8SG9
5XENFC9L1UGUT1KUZ9SMUZ2
EHL8BHRJFNYLS1SN818KW9P
YPBGFBML1APSSR4J9DPHLFT
6Q1L3EG4NSC8LFGG3VV0Y3Q
K1C761A389BWPMUYYVTXK2Y
相關文章
相關標籤/搜索