HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

栏目: 后端 · 前端 · 发布时间: 5年前

内容简介:今天发布了HttpCanary2.0版本,除了修复了部分bug以及优化性能外,最主要的是支持了HTTP2协议。HttpCanary是什么?Android平台第二强大的HTTP抓包和注入工具,不了解的同学可以阅读下关于HttpCanary的介绍:HttpCanary2.0已经发布到GooglePlay,欢迎大家下载并给予评价建议,传送门:

今天发布了HttpCanary2.0版本,除了修复了部分bug以及优化性能外,最主要的是支持了HTTP2协议。

HttpCanary是什么?Android平台第二强大的HTTP抓包和注入工具,不了解的同学可以阅读下关于HttpCanary的介绍: juejin.im/post/5c1e37…

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

HttpCanary2.0已经发布到GooglePlay,欢迎大家下载并给予评价建议,传送门: play.google.com/store/apps/…

干货为主,废话不多说,下面开始本篇的正文。

HTTP2.0和HTTP1.x的区别

先简单介绍一下HTTP2.0协议的概况,熟悉的同学可以跳过。

HTTP2.0协议是由SPDY协议进化而来,标准于2015年5月正式发布,算起来不到四年时间,属于比较新的技术。所以部分主流的抓包 工具 都不支持HTTP2,比如Fiddler,而Charles则是在4.0版本后开始支持。

HTTP2.0协议和HTTP/1.x协议在请求方法、状态码乃至URI和绝大多数HTTP头部字段等部分保持高度兼容性,即常说的请求行、请求头、请求体、响应行、响应头、响应体这些格式都具有一致性。

但是,HTTP2.0协议在对头部数据的压缩、多路复用、服务器主动推送三个方面做了支持和优化。

  • 头部数据压缩。对请求行、请求头、响应行、响应头这些头部数据进行压缩,采用Hpack算法。
  • 多路复用。每个connection以stream的形式组织,数据包按照frame(数据帧)的形式通信,同时增加了流量控制等功能。
  • 服务器主动推送。HTTP2.0协议支持双向通信,以及half-close这种单向通信。

HTTP2.0协议虽然没有明确要求加密,但目前的实现都是默认使用TLS加密,所以可以认为使用HTTP2.0则必须使用HTTPS。

为了实现对HTTP1.x的兼容,HTTP2.0协议为此额外定义了应用层协商标准(Application-Layer Protocol Negotiation,简称ALPN),以便客户端和服务端能够从HTTP/1.0、HTTP/1.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协商的图解:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

粗略一看非常简单,但是由于HTTP2.0协议强制使用TLS/SSL加密,所以只能使用中间人MITM方式进行解密抓包。而中间人MITM又分为中间人Client和中间人Server,所以ClientHello握手包的通信流程是Client -> MITM Server -> MITM Client -> Server,而ServerHello握手包的通信流程则是 Server -> MITM Client -> MITM Server -> Client,由原先的一来一回两步,变成了来回六步,复杂性上增加了许多。

增加了MITM层的ALPN协商的图解:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

这里有个小技巧,最开始的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。

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

我们依次遍历并找到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型),用图解表示为如下:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

解析出来的protocol值,可能为HTTP/1.0、HTTP/1.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.1、h2这些协议组织成byte[]呢?

其实这个byte[]是按照protocols的length+protocol依次组织的,图解如下:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

代码实现是:

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,两个表之间互不干扰,完全独立。除此之外,为了尽量压缩头部数据,还是用了霍夫曼编码,编码后再存入动态表中。

静态表和动态表都是以二进制编码的方式组织的,编码状态机和规则如下图:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

以上就是Hpack相关的知识点,下面来分析NetBare是如何进行Hpack编解码设计的。

2.2 NetBare的Hpack解码及重编码

NetBare库的VirtualGateway维护了四个Hpack表,MITM Client和MITM Server各两个,目的是先解码还原成我们常见的HTTP协议格式,然后再重新编码,图解如下:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

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的数据。这就是真正意义上的多路复用。

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

3.2 多路复用请求的拦截和注入

普通的HTTP请求是一个Connection一个请求响应,结束后销毁Connection,也就是常说的握手挥手,不留下一片云彩。虽然性能低,但是对请求和响应的拦截和注入就方便多了。所以NetBare对于HTTP1.x的拦截器设计是:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

很明显,这种拦截器设计只能满足一个Connection一个请求响应的情况。如果是HTTP2.0协议那种frame单元传输而且交错的数据传输,Interceptors很难做逻辑处理。唯一的方案就是对各个stream的frame单元进行组包,还原成HTTP1.x格式的数据,交给Interceptors做拦截注入,最后再拆包成frame单元发给终端。另外,由于请求并发,同一个时间有多个stream的frame在传输,所以还需要对各个stream进行隔离。

所以,修改之后的拦截器设计如下:

HttpCanary实现对HTTP2协议的抓包和注入(原理篇)

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

以上所述就是小编给大家介绍的《HttpCanary实现对HTTP2协议的抓包和注入(原理篇)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Two Scoops of Django

Two Scoops of Django

Daniel Greenfeld、Audrey M. Roy / CreateSpace Independent Publishing Platform / 2013-4-16 / USD 29.95

Two Scoops of Django: Best Practices For Django 1.5 is chock-full of material that will help you with your Django projects. We'll introduce you to various tips, tricks, patterns, code snippets, and......一起来看看 《Two Scoops of Django》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具