HttpCanary实现对HTTP2协议的抓包和注⼊(原理篇)
今天发布了HttpCanary2.0版本,除了修复了部分bug以及优化性能外,最主要的是⽀持了HTTP2协议。
HttpCanary是什么?Android平台第⼆强⼤的HTTP抓包和注⼊⼯具,不了解的同学可以阅读下关于HttpCanary的介绍:
HttpCanary2.0已经发布到GooglePlay,欢迎⼤家下载并给予评价建议,传送门:
⼲货为主,废话不多说,下⾯开始本篇的正⽂。
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的核⼼库,对代码感兴趣的可以对照着本⽂理解。
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.1、h2等,其中h2表⽰HTTP2.0协议。
1.2 MITM Client设置list of protocols
第⼆个核⼼步骤,MITM Client将解析出来的protocols加⼊到ClientHello包中发给真正的Server。由于Android并没有公开相关的API,所以我们只能通过反射⽅式调⽤隐藏API。通过阅读scrypt.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 = Class().getDeclaredField("sslParameters");
sslParametersField.setAccessible(true);
Object sslParameters = (mSSLEngine);
if (sslParameters == null) {
throw new IllegalAccessException("sslParameters value is null");
}
Field alpnProtocolsField = Class().getDeclaredField("alpnProtocols");
alpnProtocolsField.setAccessible(true);
alpnProtocolsField.set(sslParameters, listOfProtocols);
复制代码
必须注意这⾥的alpnProtocols是byte[]类型的变量,那么我们如何把HTTP/1.0、HTTP/1.1、h2这些协议组织成byte[]呢?
其实这个byte[]是按照protocols的length+protocol依次组织的,图解如下:
代码实现是:
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (HttpProtocol protocol : protocols) {
String protocolStr = String();
os.write(protocolStr.length());
os.Bytes(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对此作了相应的兼容,具体的兼容类是
final class Java8EngineWrapper extends AbstractConscryptEngine {
...
@Override
void setApplicationProtocols(String[] protocols) {
delegate.setApplicationProtocols(protocols);
}
...
}
复制代码
我们同样需要通过反射调⽤此⽅法:
Method setApplicationProtocolsMethod = Class().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的实现类是scrypt.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("nscrypt.NativeCrypto");
Method SSL_get0_alpn_selectedMethod = DeclaredMethod("SSL_get0_alpn_selected", long.class);
SSL_get0_alpn_selectedMethod.setAccessible(true);
Field sslNativePointerField = Class().getDeclaredField("sslNativePointer");
sslNativePointerField.setAccessible(true);
long sslNativePointer = (long) (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() {
ApplicationProtocol();
}
...
}
复制代码
如此,就知晓了服务端选择的协议类型了,也就是本次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:status200
.........
14:status500
15accept-charset
16accept-encoding gzip, deflate
.........ssl协议是指什么
静态表⼀共定义了61个字段,索引从1开始,完整的表可参考:
动态表,顾名思义就是针对不确定内容动态处理的表,它维护了⼀个索引和头部值,⽐如访问⼀个图⽚,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解码及重编码

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。