本文为看雪论坛优秀文章
看雪论坛作者ID:xwtwho
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
"https://10.0.117.217/about", new MyUrlRequestCallback(), executor);
tls.handshake.ja3_full:
771,64250-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,2570-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-10794-21,2570-29-23-24,0
[JA3: ca6385bf726b3d5f2b4db30186da0fec]
这个JA3,其实就是md5(ja3_full)。
771: Version: TLS 1.2 (0x0303)
64250-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53:
2570-29-23-24:
Supported Groups (4 groups)
Supported Group: Reserved (GREASE) (0x0a0a)
Supported Group: x25519 (0x001d)
Supported Group: secp256r1 (0x0017)
Supported Group: secp384r1 (0x0018)
public CronetEngineBuilderImpl(Context context) {
this.mApplicationContext = context.getApplicationContext();
enableQuic(true); //这里可以设置打开Quic
enableHttp2(true);
enableBrotli(false);
addQuicHint("m.youtube.com",443,443); //这里如果不设置,就会走tlsv1.3协议,设置后才会走QUIC协议。 某音中途有个版本升到QuicVersion:c9f35932 2021-09-15,后来又降到了QuicVersion:68cae75d 2021-08-12,最新19.5版本的quic也还是这个版本。 这里也能通过: cronetEngine.startNetLogToFile("/data/local/tmp/cronet_log.json",true); 输出日志,然后使用下面地址可以分析这个日志:https://netlog-viewer.appspot.com/#events
这个其实也可以用在某音里面截包:
Java.choose("org.chromium.CronetClient", {
onMatch: function (instance) {
instance.getCronetEngine().startNetLogToFile("/data/local/tmp/cronet_log.json", true);
},
onComplete: function () { }
});
到这里后,已经能够调用libsscronet.so走quic协议了,就想回过头来继续访问某音协议,从头分析了下数据,在某音启动时候发现了quic数据包:
看到了这个域名api5-core-c-lf.amemv.com。
这里提一下,Quic是带竞速模式的,同时也会发送tcp链接,如遇网络或服务器不支持quic/udp,客户端标记quic为broken,后面会尝试重试quic,一旦再次成功采用quic并把broken标记取消:后来发现了这个url,正好跟前面发现的域名一致的: https://api5-core-c-lf.amemv.com/top/v1?Action=ApplyUploadInner&Region=CN&SpaceName=aweme&UseQuic=false&Version= 这里还有个字段UseQuic=false,更加说明这个协议涉及到的相关功能会用到quic(后来发现是视频上传相关的),虽然这个url请求本身不一定会走quic,但是这个域名跟之前截到的quic数据包中的一致,可以拿来测试。 使用app测试访问这个域名,也能正常走quic协议。 到这里后,就要继续看看这个协议格式及Cronet模块中的quic请求流程了, 调试时候断在sendto能看到quic数据包: 这里碰到了知识盲区,发现sendto调用的时候没有设置目标地址参数,一下懵了,这怎么知道数据发哪里去的,也尝试断在sendmsg,发现不是,查资料后发现: UDP也可以有connect连接
若sendto()函数和sendmsg()函数向已连接的进程中发送消息,则忽略参数dest_addr、addrlen和msg结构体中用于传递地址的成员。此时若参数dest_addr和addrlen不为NULL,则可能会返回错误EISCONN或0; 连接套接字是需要一定开销的,比如需要查找路由表信息。 所以,UDP 客户端程序通过 connect 可以获得一定的性能提升。 断在connect后,发现了目标IP和端口: 调试分析流程过程中,发现要是有个本地服务器会更方便,看网上有提到node支持quic,打算用node.js架设一个支持quic的服务器。 直接下了个V16+的,结果发现不支持,后来查了下提交log,发现有次提交,去掉了这个quic支持: quic: remove experimental quic by jasnell · Pull Request #37067 · nodejs/node (github.com) 需要拿15.X的代码编译,带上experimental_quic git clone https://github.com/nodejs/node git checkout c4cab1f408 在本地架好TCP UDP服务器后测试,也验证了开启quic请求url确实会同时发送tcp和udp的请求: 在调试协议过程中,发现走不通,翻了下代码,找到ngtcp2相关,查了下 ngtcp2/ngtcp2: ngtcp2 project is an effort to implement IETF QUIC protocol (github.com) ngtcp2项目是实现IETF QUIC的协议,那跟某音这个还不同,协议帧最小数据长度也不一致,实际用这个测试需要基于某音版本修改。 现在这个quic还没统一,很多资料不是gquic的,就算是也可能是不同版本的。 头部格式目前就有几种: enum PacketHeaderFormat : uint8_t { IETF_QUIC_LONG_HEADER_PACKET, IETF_QUIC_SHORT_HEADER_PACKET, GOOGLE_QUIC_PACKET, }; 想着还是直接用google版本的,把quiche拖下来了: https://chromium.googlesource.com/chromium/src; https://quiche.googlesource.com/quiche。 这里面的协议就是跟某音quic协议一致的了,这里提下Pulic Flags的bit3: 0x08: Bit3被置上就表示header中包含8字节的connection id 这个标志位在所有的包中都应该被设置 google代码中的注释:
// All versions of Google QUIC up to and including Q043 set
// FLAGS_DEMULTIPLEXING_BIT to one on all client-to-server packets. Q044
// and Q045 were never default-enabled in production. All subsequent
// versions of Google QUIC (starting with Q046) require FLAGS_FIXED_BIT to
// be set to one on all packets. All versions of IETF QUIC (since
// draft-ietf-quic-transport-17 which was earlier than the first IETF QUIC
// version that was deployed in production by any implementation) also
// require FLAGS_FIXED_BIT to be set to one on all packets. If a packet
// has the FLAGS_LONG_HEADER bit set to one, it could be a first flight
// from an unknown future version that allows the other two bits to be set
// to zero. Based on this, packets that have all three of those bits set
// to zero are known to be invalid.
看资料说google去掉了win上的编译支持,就打算改下,希望能用VS编译,方便调试,后来越改感觉工作量越多,放弃了,搜索发现了lsquic,还是c实现的,拿代码下来编译了个版本(基本按照文档走,需要先编译boringssl,perl,go都需要安装,perl装好后要手动加入环境变量,之前就卡这里了): litespeedtech/lsquic: LiteSpeed QUIC and HTTP/3 Library (github.com) lsquic文档: Tutorial — lsquic 3.0.4 documentation
开启自带的测试程序中的服务器后,app上发起连接,服务器正常收到了请求,要记得设置证书启动,不然处理CHLO Frame数据会异常: http_server.exe -r ./ -s 10.0.117.217:443 -c 10.0.117.217,file.crt,private.pem 再链接发现有交互数据了: 但是随后就被客户端断了,发现是证书校验错误,我这个测试证书是自己生成的: 之前已经是修改了证书校验的(java层和so的都修改了),能够连接我本地的https服务器,但是连quic服务器不行,就算设置enablePublicKeyPinningBypassForLocalTrustAnchors 属性也不行,说明走quic时候还有一个校验,根据字符串找到下面地方: 在google代码中也找到对应的: 根据代码找到修改点: 这个时候服务器没收到客户端因为校验断开的包了,截包发现认证也过了,不会像之前那样断开了:
到这里后,基本上有了完整的调试环境,后面需要分析quic协议算法就可以很方便的带源码调试了。 目前看起来某音并没有大规模使用quic,从测试url看是上传视频可能会用到,但是我的测试环境也没发现走quic,最近的几个版本看见有协议中增加了下面信息:
current_network_quality_info=
{"http_rtt":27,"tcp_rtt":15,"quic_rtt":15,"downstream_throughput_kbps":1600,"net_effective_connection_type":7,"video_download_speed":3311,"quic_receive_loss_rate":-1,"quic_send_loss_rate":-1}
看起来是quic相关的,不知道是不是在收集用户环境信息,评估用户网络环境,然后再决定是否部署quic。 就算后面一些功能http协议都转向quic,那也有很多切入点: 1、对于走Cronet的,那还是可以在JAVA层对应jni函数及Cronet回调函数拿到请求数据。 2、Cronet最终还是走native层的,在so层收发数据对应函数也可以拿到数据。 3、用上面提到的cronetEngine.startNetLogToFile直接写网络日志(这个实际测试发现不全,还不确定是不是参数设置问题)。 4、直接在quic实现部分的收发函数拿数据。 5、了解quic协议过程后,还可以把各个通讯层级加密key记录下来,直接解析udp截包数据。 6、可以像上面测试那样,搭建本地quic服务器,把请求转到本地服务器来。
看雪ID:xwtwho
https://bbs.pediy.com/user-home-44250.htm
# 往期推荐
5.栈与栈帧的调试
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
原文始发于微信公众号(看雪学苑):通过某音Cronet模块学习Quic协议
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论