手打目录
-
前言
-
App的选取
- 投屏测试
-
抓包分析
- 抓包
- mDNS分析
- 握手协议
- info
- pair-setup
- pair-verify
- 第二个pair-verify
- 两次fp-setup
-
镜像数据
- 第一次SETUP
- 第二次SETUP
- 镜像数据发送分析
- 镜像数据解密
-
音频数据
- 抓包分析
- 第三个SETUP
- 代码分析
- timingPort交互
- controlPort交互
- 音频数据发送分析
- 音频数据解密
-
其他请求
- GET_PARAMETER
- SET_PARAMETER
- feedback
- TEARDOWN-1
-
实现
-
参考链接
-
附件
前言
本文针对AirPlay2协议,选取了一款上线的app并进行逆向分析,实现了对AirPlay2协议的破解,并实现了投屏demo。
以下是本文涉及到的一些知识点:
- 协议:RTP,RTSP,DNS,DNS-SD,mDNS,NTP
- 加解密算法:curve25519,ed25519,AES(cbc&ctr)
- 音视频基础:h264,aac
- Android中dex格式与so的逆向
App的选取
对于AirPlay的破解,国内外均有相关App实现,注意用途是将iPhone设备的内容投屏到电视或者PC上。
这里选取了国内投屏用的比较多的X播投屏,下载了最新版发现是加壳了,所以找了下老版本,所幸找到了7.1.0版本并未加壳,用IOS12测试可以正常投屏,本文针对此版本App进行逆向分析。
投屏测试
打开投屏App,然后在iPhone中上滑->屏幕镜像中找到投屏设备,点击即可在App上看到iPhone画面了。
IOS是如何发现设备?连接过程是怎么样?数据是怎么传输的?带着这些疑问我们往下分析
以下会用server代表Android设备,client代表IOS设备
抓包分析
抓包
这里用了root过的小米,小米root比较简单,刷个开发版本即可,装个最新版本tcpdump
开始抓包
./data/local/tmp/tcpdump -i any -p -s 0 -w /sdcard/airplay.pcapng
打开app前开始抓包,一直到投屏结束,并使用WireShark进行数据包分析
mDNS分析
server发布了mDNS广播如下图所示:

这里有4条DNS记录:
-
一条A记录
MIX2S-xiaomishouji.local: type A
-
3条SRV记录:代表三个服务,用于DNS-SD
\344\271\220\346\212\225V2._airplay._tcp.local
aa5401afc3c1@\344\271\220\346\212\225V2._raop._tcp.local
\344\271\220\346\212\225V2._leboremote
因为X播里面不止是iPhone投屏还有其他投屏,需要对这些服务进行筛选,所以这里需要看下client最终用了哪些服务。
过滤条件修改为ip.src==172.18.145.3 && mdns
,看下client端查询的服务

这里可以看到client只使用了raop和airplay两个服务,查询SRV记录
内容可以得知airplay
使用了52233
端口,raop
使用了52244
端口
在frame146中,server发送了回复的组播包

由此可以得知,client和server是通过mDNS和DNS-SD实现了零配置网络,找到server之后便是client和server的交互
握手协议
过滤条件修改为(ip.src==172.18.145.2 && ip.dst==172.18.145.3) || (ip.src==172.18.145.3 && ip.dst==172.18.145.2)
,过滤结果如图所示:

首先是三次握手,从frame230
开始发送请求,可以发现server端口是52244
即使用了raop
服务
过滤52233
端口发现没有结果,说明client是没有使用airplay
服务。
下面针对选中230,Analyze->Follow->TCP Stream可以看到整个交互过程

具体内容如下
GET /info RTSP/1.0
X-Apple-ProtocolVersion: 1
Content-Length: 70
Content-Type: application/x-apple-binary-plist
CSeq: 0
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
bplist00...Yqualifier..ZtxtAirPlay..................................."
RTSP/1.0 200 OK
Content-Length: 836
Date: Tue, 18 Dec 2018 01:32:17 GMT
Content-Type: application/x-apple-binary-plist
Server: AirTunes/220.68
bplist00.......YaudioType........
.....$&(*.... .
...%')+TtypeXdisplaysTuuid_..audioInputFormatsXfeatures[refreshRate.. "..!!._..aa:54:01:af:c3:c1...dUmodel.<VheightZAppleTV2,1]sourceVersion_..keepAliveLowPower.-/123456(9;<.0!!!0.78:!=]widthPhysicalV220.68.......[overscanned[widthPixelsO. .w'...n....R^....R..h?.!....$eT.ZmacAddress...,.....\audioFormatsTname.Rvv.....Z..._..inputLatencyMicros[statusFlagsWAppleTV.. "..!!.Wdefault_.$2e388006-13ba-4041-9a67-25dd4a43d536......._..outputLatencyMicros^audioLatenciesXrotation..\heightPixelsVmaxFPSXdeviceID_..audioOutputFormats_.$e0ff8a27-6738-3d56-8a16-cc53aacee925_..keepAliveSendStatsAsBody^heightPhysical.eUwidthRpiRpk..#..8............R.C...".d...j.N.....g.....W.T...+.
.:...M...............v.i...v... .....a.m.?.P.....................j...........H.@...............>................
POST /pair-setup RTSP/1.0
Content-Length: 32
Content-Type: application/octet-stream
CSeq: 1
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
...............f.......|..1...Rt
RTSP/1.0 200 OK
Content-Type: application/octet-stream
Content-Length: 32
Server: AirTunes/220.68
CSeq: 1
....M.r..Ej!S.......d...r...l.P3
POST /pair-verify RTSP/1.0
X-Apple-PD: 1
X-Apple-AbsoluteTime: 566789538
Content-Length: 68
Content-Type: application/octet-stream
CSeq: 2
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
.....K>..2?}.c...Z8...y.....X..h.i37...............f.......|..1...Rt
RTSP/1.0 200 OK
Content-Type: application/octet-stream
Content-Length: 96
Server: AirTunes/220.68
CSeq: 2
..f..(..abme......&>
|k.....g.4'....r...'/jb..J
..p.tl..g.....?....F..+..\ ..7u.~.xs..|..
.F....
POST /pair-verify RTSP/1.0
X-Apple-PD: 1
X-Apple-AbsoluteTime: 566789538
Content-Length: 68
Content-Type: application/octet-stream
CSeq: 3
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
.............|.<....-..s.w...w....r...K.Lp...}.L
..Q....r_o...T.k2."
RTSP/1.0 200 OK
Content-Type: application/octet-stream
Content-Length: 0
Server: AirTunes/220.68
CSeq: 3
POST /fp-setup RTSP/1.0
X-Apple-ET: 32
Content-Length: 16
Content-Type: application/octet-stream
CSeq: 4
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
FPLY............
RTSP/1.0 200 OK
Content-Length: 142
Date: Tue, 18 Dec 2018 01:32:17 GMT
Server: AirTunes/220.68
Content-Type: application/octet-stream
FPLY..............G.....W.i5...........F....}....vd.Jk.E._6.l@.F.7.,.o^..?..zeW`...h..A>
SK-<....g.e.-F.YE..|y....GF).......z....V.@...=.u....
POST /fp-setup RTSP/1.0
X-Apple-ET: 32
Content-Length: 164
Content-Type: application/octet-stream
CSeq: 5
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
FPLY..................U.B..^S,..}.m#W.].I.X......D.dd.D....#....^n.s..]~/. ....Z....g....q...f.'/CT.(..L...+.?....[.r..t..E.......O.up.....6q>2.....L........C.....%
RTSP/1.0 200 OK
Content-Length: 32
Date: Tue, 18 Dec 2018 01:32:17 GMT
Server: AirTunes/220.68
Content-Type: application/octet-stream
FPLY............L........C.....%
接下来一个一个看相关请求
1. info
请求包内容如下图所示:

请求和回包都是bplist格式,解析出来看下
client->server
<plist version="1.0">
<dict>
<key>qualifier</key>
<array>
<string>txtAirPlay</string>
</array>
</dict>
</plist>
server->client
<plist version="1.0">
<dict>
<key>sourceVersion</key>
<string>220.68</string>
<key>statusFlags</key>
<integer>4</integer>
<key>macAddress</key>
<string>aa:54:01:af:c3:c1</string>
<key>deviceID</key>
<string>aa:54:01:af:c3:c1</string>
<key>name</key>
<string>AppleTV</string>
<key>vv</key>
<integer>2</integer>
<key>keepAliveLowPower</key>
<integer>1</integer>
<key>keepAliveSendStatsAsBody</key>
<integer>1</integer>
<key>pi</key>
<string>2e388006-13ba-4041-9a67-25dd4a43d536</string>
<key>audioFormats</key>
<array>
<dict>
<key>audioOutputFormats</key>
<integer>33554428</integer>
<key>type</key>
<integer>100</integer>
<key>audioInputFormats</key>
<integer>33554428</integer>
</dict>
<dict>
<key>audioOutputFormats</key>
<integer>33554428</integer>
<key>type</key>
<integer>101</integer>
<key>audioInputFormats</key>
<integer>33554428</integer>
</dict>
</array>
<key>audioLatencies</key>
<array>
<dict>
<key>audioType</key>
<string>default</string>
<key>inputLatencyMicros</key>
<false/>
<key>outputLatencyMicros</key>
<false/>
<key>type</key>
<integer>100</integer>
</dict>
<dict>
<key>audioType</key>
<string>default</string>
<key>inputLatencyMicros</key>
<false/>
<key>outputLatencyMicros</key>
<false/>
<key>type</key>
<integer>101</integer>
</dict>
</array>
<key>pk</key>
<data>
sHcn1vbNbgi1jt5SXsPN6qJSrZ9oP+shLviiBSRlVOc=
</data>
<key>model</key>
<string>AppleTV2,1</string>
<key>features</key>
<integer>130367356919</integer>
<key>displays</key>
<array>
<dict>
<key>height</key>
<integer>1080</integer>
<key>width</key>
<integer>1920</integer>
<key>rotation</key>
<false/>
<key>widthPhysical</key>
<false/>
<key>heightPhysical</key>
<false/>
<key>widthPixels</key>
<integer>1920</integer>
<key>heightPixels</key>
<integer>1080</integer>
<key>refreshRate</key>
<integer>60</integer>
<key>features</key>
<integer>14</integer>
<key>maxFPS</key>
<integer>30</integer>
<key>overscanned</key>
<false/>
<key>uuid</key>
<string>e0ff8a27-6738-3d56-8a16-cc53aacee925</string>
</dict>
</array>
</dict>
</plist>
这里基本是回复server支持的特性,具体可看参考链接[8],这里不做分析
2. pair-setup
client发送32字节 <-> server回复32字节
搜索字符串"pair-setup",找到如下代码

FdkDecodeAudioFun8
参数分析
- 第1个参数 client请求包
- 第2个参数 client请求包长度
- 第3个参数jg server->client回包内容
- 第4个参数out_size 回包长度
- 第5个参数 1
- 第6个参数pairSessionId 连接上下文,看代码是支持最多16个设备连接
进一步分析so,使用ida打开libhpplayaudio.so找到FdkDecodeAudioFun8
查下这个函数的调用关系,如下图所示

看起来挺复杂,分析中找到ed25519这个关键词,是个,搜索发现是已有算法,找到源码之后和汇编代码进行对比是可以匹配上。
现在只需要关注FdkDecodeAudioFun8
的实现
.text:000C7BF8 ; signed int __fastcall Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8(_JNIEnv *a1, int a2, int a3, int a4, int a5, int a6, int a7, unsigned int a8)
.text:000C7BF8 EXPORT Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8
.text:000C7BF8 Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8
.text:000C7BF8 ; DATA XREF: LOAD:0000149C↑o
.text:000C7BF8
.text:000C7BF8 var_38 = -0x38
.text:000C7BF8 var_2C = -0x2C
.text:000C7BF8 arg_0 = 0
.text:000C7BF8 arg_4 = 4
.text:000C7BF8 arg_C = 0xC
.text:000C7BF8 arg_558 = 0x558
.text:000C7BF8 arg_C7D04 = 0xC7D04
.text:000C7BF8
.text:000C7BF8 ; __unwind {
.text:000C7BF8 PUSH.W {R4-R11,LR}
.text:000C7BFC SUB SP, SP, #0x14
.text:000C7BFE MOV R7, R0
.text:000C7C00 MOV R10, R2
.text:000C7C02 LDR R6, [SP,#0x38+arg_C]
.text:000C7C04 CMP R6, #0x10
.text:000C7C06 BHI loc_C7CEA
.text:000C7C08 LDR R4, =(unk_254118 - 0xC7C10)
.text:000C7C0A LSLS R6, R6, #2
.text:000C7C0C ADD R4, PC ; unk_254118
.text:000C7C0E ADD R4, R6
.text:000C7C10 LDR.W R3, [R4,#0x558] ; unk_25518+sessionid*4+0x558
.text:000C7C14 CMP R3, #0
.text:000C7C16 BEQ loc_C7CF0
.text:000C7C18 MOV R1, R2
.text:000C7C1A MOVS R2, #0
.text:000C7C1C BL _ZN7_JNIEnv20GetByteArrayElementsEP11_jbyteArrayPh ; _JNIEnv::GetByteArrayElements(_jbyteArray *,uchar *)
.text:000C7C20 MOV R8, R0 ; raw_data
.text:000C7C22 CMP R0, #0
.text:000C7C24 BEQ loc_C7CF6
.text:000C7C26 MOV R0, R7
.text:000C7C28 LDR R1, [SP,#0x38+arg_4]
.text:000C7C2A MOVS R2, #0
.text:000C7C2C BL _ZN7_JNIEnv19GetIntArrayElementsEP10_jintArrayPh ; _JNIEnv::GetIntArrayElements(_jintArray *,uchar *)
.text:000C7C30 LDR.W R11, [R4,#0x558] ; R11=unk_25518+sessionid*4+0x558
.text:000C7C34 LDR.W R5, [R11,#4] ; unk_25518+sessionid*4+0x558 地址的值 + 4,再取值,说明是个结构体
.text:000C7C38 MOV R9, R0 ; out_size
.text:000C7C3A CBNZ R5, loc_C7C86
.text:000C7C3C MOVS R0, #0xE4 ; size
.text:000C7C3E BLX malloc ; E4=228
.text:000C7C42 MOV R1, R5 ; c
.text:000C7C44 MOVS R2, #0xE4 ; n
.text:000C7C46 STR.W R0, [R11,#4] ; 写入申请内存的地址
.text:000C7C4A LDR.W R3, [R4,#0x558] ; R4=unk_25518+sessionid*4
.text:000C7C4E LDR R0, [R3,#4] ; s
.text:000C7C50 BLX memset
.text:000C7C54
.text:000C7C54 loc_C7C54 ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+7A↓j
.text:000C7C54 LDR.W R3, [R4,#0x558]
.text:000C7C58 LDR R3, [R3,#4] ; R3=unk_25518+sessionid*4+0x558+4
.text:000C7C5A STR R3, [SP,#0xC]
.text:000C7C5C BLX lrand48
.text:000C7C60 MOV R11, R0
.text:000C7C62 BLX lrand48
.text:000C7C66 LDR R3, [SP,#0xC]
.text:000C7C68 SMULBB.W R0, R0, R11 ; R0=lrand48*lrand48
.text:000C7C6C STRB R0, [R3,R5]
.text:000C7C6E ADDS R5, #1
.text:000C7C70 CMP R5, #0x20 ; ' '
.text:000C7C72 BNE loc_C7C54
.text:000C7C74 LDR.W R3, [R4,#0x558]
.text:000C7C78 LDR R2, [R3,#4]
.text:000C7C7A ADD.W R0, R2, #0x60
.text:000C7C7E ADD.W R1, R2, #0x20
.text:000C7C82 BL ed25519_create_keypair
.text:000C7C86
.text:000C7C86 loc_C7C86 ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+42↑j
.text:000C7C86 LDR R3, =unk_18C486
.text:000C7C88 ADD.W R0, R8, #0x20 ; R8=raw_data
.text:000C7C8C MOV R2, R8
.text:000C7C8E ADD R3, PC ; unk_254118
.text:000C7C90 ADD R6, R3
.text:000C7C92 LDR.W R1, [R6,#0x558]
.text:000C7C96 LDR R3, [R1,#4]
.text:000C7C98 ADDS R3, #0x80 ; 从128字节开始写入
.text:000C7C9A
.text:000C7C9A loc_C7C9A ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+AC↓j
.text:000C7C9A LDR.W R4, [R2],#4
.text:000C7C9E CMP R2, R0
.text:000C7CA0 STR.W R4, [R3],#4 ; 复制raw_data到内存
.text:000C7CA4 BNE loc_C7C9A
.text:000C7CA6 LDR R3, [R1,#4]
.text:000C7CA8 MOV R0, R7 ; a1
.text:000C7CAA LDR R1, [SP,#0x38+arg_0] ; jg
.text:000C7CAC MOVS R2, #0 ; jsize
.text:000C7CAE ADDS R3, #0x60 ; '`'
.text:000C7CB0 STR R3, [SP,#0x38+var_38] ; sp[0]=堆栈地址+96即pk的地址
.text:000C7CB2 MOVS R3, #dword_20 ; 32字节
.text:000C7CB4 BL _ZN7_JNIEnv18SetByteArrayRegionEP11_jbyteArrayiiPKa ; SetByteArrayRegion(this, array(R1), start(R2), len(R3), buf)
.text:000C7CB8 MOVS R3, #0x20 ; ' '
.text:000C7CBA MOV R0, R7
.text:000C7CBC STR.W R3, [R9]
.text:000C7CC0 LDR R1, [SP,#0x38+arg_4]
.text:000C7CC2 MOVS R2, #0
.text:000C7CC4 MOVS R3, #1
.text:000C7CC6 STR.W R9, [SP,#0x38+var_38]
.text:000C7CCA BL _ZN7_JNIEnv17SetIntArrayRegionEP10_jintArrayiiPKi ; _JNIEnv::SetIntArrayRegion(_jintArray *,int,int,int const*)
.text:000C7CCE MOV R0, R7
.text:000C7CD0 MOV R1, R10
.text:000C7CD2 MOV R2, R8
.text:000C7CD4 MOVS R3, #0
.text:000C7CD6 BL _ZN7_JNIEnv24ReleaseByteArrayElementsEP11_jbyteArrayPai ; _JNIEnv::ReleaseByteArrayElements(_jbyteArray *,signed char *,int)
.text:000C7CDA MOV R0, R7
.text:000C7CDC LDR R1, [SP,#0x38+arg_4]
.text:000C7CDE MOV R2, R9
.text:000C7CE0 MOVS R3, #0
.text:000C7CE2 BL _ZN7_JNIEnv23ReleaseIntArrayElementsEP10_jintArrayPii ; _JNIEnv::ReleaseIntArrayElements(_jintArray *,int *,int)
.text:000C7CE6 MOVS R0, #0
.text:000C7CE8 B loc_C7CFA
.text:000C7CEA ; ---------------------------------------------------------------------------
.text:000C7CEA
.text:000C7CEA loc_C7CEA ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+E↑j
.text:000C7CEA MOV R0, #0xFFFFFFF8
.text:000C7CEE B loc_C7CFA
.text:000C7CF0 ; ---------------------------------------------------------------------------
.text:000C7CF0
.text:000C7CF0 loc_C7CF0 ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+1E↑j
.text:000C7CF0 MOV R0, #0xFFFFFFF7
.text:000C7CF4 B loc_C7CFA
.text:000C7CF6 ; ---------------------------------------------------------------------------
.text:000C7CF6
.text:000C7CF6 loc_C7CF6 ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+2C↑j
.text:000C7CF6 MOV.W R0, #0xFFFFFFFF
.text:000C7CFA
.text:000C7CFA loc_C7CFA ; CODE XREF: Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+F0↑j
.text:000C7CFA ; Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8+F6↑j ...
.text:000C7CFA ADD SP, SP, #0x14
.text:000C7CFC POP.W {R4-R11,PC}
.text:000C7CFC ; End of function Java_com_hpplay_happyplay_aaceld_FdkDecodeAudioFun8
根据分析,画下目前内存里面的数据

重点观察下ed25519_create_keypair
函数
void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed) {
ge_p3 A;
sha512(seed, 32, private_key);
private_key[0] &= 248;
private_key[31] &= 63;
private_key[31] |= 64;
ge_scalarmult_base(&A, private_key);
ge_p3_tobytes(public_key, &A);
}
根据.text:000C7CB4 BL _ZN7_JNIEnv18SetByteArrayRegionEP11_jbyteArrayiiPKa ;
可以发现server发送给client的32字节是ed25519生成的publickey
3. pair-verify
client发送68字节(前4个字节是 01 00 00 00) <-> server回复96字节
client请求包剩余64个字节内容

同样的,找到FdkDecodeAudioFun9
函数
FdkDecodeAudioFun9
参数分析
- 第1个参数 client请求包
- 第2个参数 client请求包长度(68)
- 第3个参数jg server->client回包内容
- 第4个参数out_size 回包长度
- 第5个参数 1
- 第6个参数pairSessionId 连接上下文,看代码是支持最多16个设备连接
查看下FdkDecodeAudioFun9
函数调用关系图如下

大体看下整个函数结构,还是有点复杂,具体函数分析如下
文章过长此部分删除
经过分析之后,此时内存结构图如下

回包是96字节
第1部分是(32字节)是ecdh_ours
第2部分是(64字节)是(ecdh_ours + ecdh_theirs)的ed25519签名,再经过AES加密之后的数据
- 关键代码1,生成ecdh_ours和ecdh_private
curve25519_donna
看到这个关键函数,Curve25519目前应用广泛的Diffie-Hellman函数,通过交换一些公开的数据就能让通讯双方相互算出密钥的算法
还是搜搜curve25519_donna
找到这个函数的源码
int curve25519_donna(unsigned char *mypublic, const unsigned char *secret, const unsigned char *basepoint);
是否和我们猜测一致,或者有什么坑,我们继续往下分析
ida中unk_1FDE68 一个9 31个0 和 basepoint是一致的,再对比下相关调用函数可以确认是对应的
- 关键代码2,生成ed25519签名
void ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key)
函数参数
sig_msg = ecdh_ours + ecdh_theirs
public_key = ed_ours
private_key = ed_private(后32字节被替换,确认了多次)
- 关键代码3,生成AES加密key
生成key
sha512_init
sha512_update -> "Pair-Verify-AES-Key"
sha512_update -> ecdh_secret
sha512_final -> sha512_1
- 关键代码4,生成AES加密iv
生成iv
sha512_init
sha512_update -> "Pair-Verify-AES-IV"
sha512_update -> ecdh_secret
sha512_final -> sha512_2
- 加密算法判断
有了key和iv然后是加密
需要关注的是 f1116c0
和 f1117b0
函数,一开始不知道这两个函数是干嘛的,不过看算法比较复杂,应该是已有算法,我们进行跟进发现
f1117b0 用到了unk_118FE8
进去看

根据这个线索我们google下0xC3 0x72 0x16 0x1D
找到https://gnupg.org/ftp/gcrypt/historic/rijndael.c
,所以这个是aes加密算法,这样就比较简单了,继续搜索找到了https://www.ghostscript.com/doc/base/aes.c
和此函数对应关系更大是对应的,f1117b0
为初始化key函数 对应 aes_setkey_enc
函数
f1117b0
参数分析
- 第1个参数 304基地址()
- 第2个参数 sha512_1 前16字节作为key
- 第3个参数 128
f1116c0
是最终加密函数,通过与AES几个加密方式的对比,确认为CTR加密
f1116c0
参数分析
- 第1个参数 304基地址
- 第2个参数 64
- 第3个参数 iv_off
- 第4个参数 sha512_2 前16字节作为初始iv
- 第5个参数 16b字节的地址
- 第6个参数 输入字符串
- 第7个参数 输出字符串
4. 第二个pair-verify
client发送68字节(前4个字节是 00 00 00 00) <-> server回复0字节
和第一个pair-verify一样,在FdkDecodeAudioFun9
函数中
- 关键函数
int ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key)
ed25519_verify
参数分析
- 第1个参数 是client发过来的64字节签名,用作校验
- 第2个参数 是签名的消息
- 第3个参数 签名消息的长度
- 第4个参数 client的公钥
根据校验结果,正确则继续,错误则断开连接
5. 两次fp-setup
- 第一次fp-setup
client发送16字节 <-> server回复142字节
- 第二次fp-setup
client发送164 <->server回复 32
两次fp-setup分别对应FdkDecodeAudioFun1和FdkDecodeAudioFun2,两个函数的调用关系如下图


看起来非常复杂,分析起来耗时耗力
这里有两个思路:
- 导出此部分汇编代码,直接调用函数运行
- 直接调用so中的函数
虽然这两个方法都可以达到目的,代码就变得不可控了,后经过各种搜索偶然找到一个airplay1的项目shairplay,里面有这两个函数实现,但是代码比较老了,是七年前的项目。
抱着试一试的心态集成了下,发现是可以正常使用的,本文demo也是基于此开源代码,做了大量修改。
镜像数据
在握手协议之后,发送镜像数据之前会有两次SETUP请求(在数据分析中会用到)
SETUP rtsp://172.18.145.2/4882189185445544350 RTSP/1.0
Content-Length: 535
Content-Type: application/x-apple-binary-plist
CSeq: 6
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
bplist00...........
..
.................RetSeiv^timingProtocol[sessionUUIDVosName^osBuildVersion]sourceVersionZtimingPort_..isScreenMirroringSessionYosVersionTekeyXdeviceIDUmodelTnameZmacAddress. O....mD..9o.YRR.0./SNTP_.$43C10532-7CBC-419E-9BB3-528F7D6F9AE0YiPhone OSV16A404W371.4.7... V12.0.1O.HFPLY.......<.....nT=......9..X......w.Jw9.t.v..iK.c....Tj.u..G..KL.....X_..DC:0C:5C:B7:D6:DAYiPhone9,1jT..2v.. .i.P.h.o.n.e_..DC:0C:5C:B7:D6:D8...).,.0.?.K.R.a.o.z.......................
....... .'.r......................................
RTSP/1.0 200 OK
Content-Length: 0
Server: AirTunes/220.68
CSeq: 6
SETUP rtsp://172.18.145.2/4882189185445544350 RTSP/1.0
Content-Length: 188
Content-Type: application/x-apple-binary-plist
CSeq: 10
DACP-ID: D18733453E686899
Active-Remote: 252920595
User-Agent: AirPlay/371.4.7
bplist00...Wstreams.........Ttype]timestampInfo_..streamConnectionID.n. .....
.TnameUSubSu.
UBePxT.
.UAfPxT.
.UBefEn.
.UEmEnc.D...6QD......!/DFLOTZ]cfloux~................................
RTSP/1.0 200 OK
Content-Length: 120
Date: Tue, 18 Dec 2018 01:32:18 GMT
Content-Type: application/x-apple-binary-plist
Server: AirTunes/220.68
bplist00..l.n.....YeventPort...ZtimingPortWstreamsXdataPort....cTtype...
. .E*;
2.@....=...............................L
1. 第一次SETUP
client -> server
<plist version="1.0">
<dict>
<key>et</key>
<integer>32</integer>
<key>eiv</key>
<data>
Bp5tRB8BOW/MWVJSGzALLw==
</data>
<key>timingProtocol</key>
<string>NTP</string>
<key>sessionUUID</key>
<string>43C10532-7CBC-419E-9BB3-528F7D6F9AE0</string>
<key>osName</key>
<string>iPhone OS</string>
<key>osBuildVersion</key>
<string>16A404</string>
<key>sourceVersion</key>
<string>371.4.7</string>
<key>timingPort</key>
<integer>60373</integer>
<key>isScreenMirroringSession</key>
<true/>
<key>osVersion</key>
<string>12.0.1</string>
<key>ekey</key>
<data>
RlBMWQECAQAAAAA8AAAAALFuVD0C1qvRjZI5wtJY4v0AAAAQd5dKdzn2dNJ2ysNpS4VjnfmFHlRqEnXFqUeXzEtMDLIdF/5Y
</data>
<key>deviceID</key>
<string>DC:0C:5C:B7:D6:DA</string>
<key>model</key>
<string>iPhone9,1</string>
<key>name</key>
<string>xxx的 iPhone</string>
<key>macAddress</key>
<string>DC:0C:5C:B7:D6:D8</string>
</dict>
</plist>
server->client
空
2. 第二次SETUP
client->server
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>type</key>
<integer>110</integer>
<key>timestampInfo</key>
<array>
<dict>
<key>name</key>
<string>SubSu</string>
</dict>
<dict>
<key>name</key>
<string>BePxT</string>
</dict>
<dict>
<key>name</key>
<string>AfPxT</string>
</dict>
<dict>
<key>name</key>
<string>BefEn</string>
</dict>
<dict>
<key>name</key>
<string>EmEnc</string>
</dict>
</array>
<key>streamConnectionID</key>
<integer>4964383553955644435</integer>
</dict>
</array>
</dict>
</plist>
server->client
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>dataPort</key>
<integer>7020</integer>
<key>type</key>
<integer>110</integer>
</dict>
</array>
<key>eventPort</key>
<integer>52244</integer>
<key>timingPort</key>
<integer>7011</integer>
</dict>
</plist>
3. 镜像数据发送分析
第二次setup之后开始发送数据,加入过滤条件(ip.src==172.18.145.2 || ip.src==172.18.145.3) && (ip.dst==172.18.145.3 || ip.dst==172.18.145.2) && ( udp || (tcp.srcport != 52244 && tcp.dstport != 52244))
,结果如下图所示

这里server端使用了7020(tcp)和7011(udp)两个端口,client端使用了60373和59694两个端口
根据发送包的大小确认7020是接收镜像数据端口,下面具体分析
- 7011(udp)端口分析
7011 -> 60373 48b
60373 -> 7011 48b
7011 -> 60373 48b
60373 -> 7011 48b
7011端口由UDPListenerScreenTC处理

看了下代码,是ntp协议,每隔3秒发送一次
在wireshark中右键udp包,decode as,选取ntp即可看到解析,这块比较简单,我们直接进入解析镜像数据的分析
- 7020(tcp)端口分析
通过端口信息找到多个镜像service,主要包括以下几个类

修改LeLog 中sLevel为0,重新编包,根据打印出的log辅助定位,确认是由GeneralMirrorService处理(需要附件)
分析之后,我们知道,有两个数据类型,先判断前4个字节是GET/POST,如果不是按照镜像数据处理,格式如下图

payloadsize即镜像数据,根据分析得知这里的数据是加密的。
4. 镜像数据解密
我们看GeneralMirrorService
中的这段
if (!this.mIsAesInited) {
mainServer.this.initAESUseRAOPKey();
this.mIsAesInited = true;
}
mReturn = mainServer.this.decryptAES(vstreamdata_in, 0, payloadsize, mainServer.this.vstreamdata, 0);
关注mainServer中的initAESUseRAOPKey和decryptAES两个函数,可以确认的是这个也是aes加密数据
public void initAESUseRAOPKey() {
try {
this.sks = new SecretKeySpec(this.mPlaybackService.mKey, "AES");
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.cipher.init(1, this.sks, new IvParameterSpec(this.mPlaybackService.mIv));
this.mNextDecryptCount = 0;
Arrays.fill(this.og, (byte) 0);
} catch (Throwable e) {
LeLog.m1097w("Server", e);
}
}
其中我们再看,使用的是CTR解密,需要知道key和iv才能解密,
通过在mainServer
类中搜索mkey
找到如下代码
r4 = com.hpplay.happyplay.mainServer.this;
r48 = r4.mAaceld;
r0 = r200;
r4 = com.hpplay.happyplay.mainServer.this;
r49 = r4.aesekey;
r50 = 16;
r0 = r200;
r4 = com.hpplay.happyplay.mainServer.this;
r53 = r4.ver_signal;
r0 = r200;
r4 = com.hpplay.happyplay.mainServer.this;
r54 = r4.streamid;
r0 = r200;
r0 = r0.pairSessionId;
r55 = r0;
// r49 = r4.aesekey; 16 out outsize ver_signal streamid pairSessionId
r90 = r48.FdkDecodeAudioFun10(r49, r50, r51, r52, r53, r54, r55);
r4 = 0;
r4 = r52[r4];
r6 = 32;
if (r4 != r6) goto L_0x69ec;
L_0x69ba:
r4 = 0;
r0 = r200;
r6 = com.hpplay.happyplay.mainServer.this;
r6 = r6.mPlaybackService;
r6 = r6.mKey;
r9 = 0;
r10 = 16;
r0 = r51;
// 取r51前16个字节
java.lang.System.arraycopy(r0, r4, r6, r9, r10);
r4 = 16;
r0 = r200;
r6 = com.hpplay.happyplay.mainServer.this;
r6 = r6.mPlaybackService;
r6 = r6.mIv;
r9 = 0;
r10 = 16;
r0 = r51;
// 取r51的16-31
java.lang.System.arraycopy(r0, r4, r6, r9, r10);
r0 = r200;
r4 = com.hpplay.happyplay.mainServer.this;
r4 = r4.mPlaybackService;
r6 = 1;
分析得知,key和iv均来自FdkDecodeAudioFun10
先明确几个入参的含义
- aesekey
第一次的SETUP中的ekey是72字节,aesekey是ekey通过FdkDecodeAudioFun3解密后输出为16字节,FdkDecodeAudioFun3和FirePlay相关,复杂度类似于FdkDecodeAudioFun1和FdkDecodeAudioFun2。这个函数和两次fp-setup
一样,在shairplay
里面找到了相关实现
- ver_signal
版本判断,在airplay2中为1
- streamid
第二次SETUP的streamConnectionID字段的值
- pairSessionId
会话标识,可忽略
- out为32字节的输出
FdkDecodeAudioFun10
是native层代码,根据前面的分析这个函数比较简单,不再做分析,感兴趣的可以自行阅读,以下是关键代码:
sha512_init
sha512_update->eaeskey
sha512_update->ecdh_secret
sha512_final->eaeskey
sha512_init(&ctx);
sha512_update->"AirPlayStreamKey"+streamConnectionID
sha512_update->eaeskey
sha512_final->hash1
sha512_init
sha512_update->"AirPlayStreamIV"+streamConnectionID
sha512_update->eaeskey
sha512_final->hash2
AES中的key为hash1的前16字节
AES中的iv为hash2的前16字节
有了key和iv之后可以解密了,此时解密完的数据为avcc格式的H264裸流
至此,屏幕镜像数据解析完毕。
tips:这里撸代码的时候,streamConnectionID转换为%lld格式,导致投屏时一会可以显示正确图像,一会全是错误数据,后面看log发现输出的streamConnectionID为负值,导致解密错误,正确应为%llu格式
音频数据
1. 抓包分析
抓取镜像包的时候没有抓音频包,这里重新抓取含音频的包。
过滤下端口信息,看下音频的交互

和音频相关都是udp协议,这里发现了几个端口
- server端
42820 音频端口
46440 controlport && timeport(其实是两个端口,X播用了同一个)
- client端
63658 controlport
59593 timeport
client需要知道server端口肯定有个交互,继续往前,找到了第三个SETUP
2. 第三个SETUP
SETUP rtsp://172.18.145.2/16274097868445272520 RTSP/1.0
Content-Length: 199
Content-Type: application/x-apple-binary-plist
CSeq: 17
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
bplist00...Wstreams........
..
.
......ZlatencyMax^redundantAudioZlatencyMinRctSspf[controlPort[usingScreen[audioFormatTtype............. ......`....(3BMPT`lx}.......................................
RTSP/1.0 200 OK
Content-Length: 118
Date: Mon, 14 Jan 2019 06:56:49 GMT
Content-Type: application/x-apple-binary-plist
Server: AirTunes/220.68
bplist00..D........ ..
..ZtimingPortWstreams..hXdataPort.`Ttype[controlPort.$.
/.?,:8................................K
client->server
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>latencyMax</key>
<integer>3750</integer>
<key>redundantAudio</key>
<integer>2</integer>
<key>latencyMin</key>
<integer>3750</integer>
<key>ct</key>
<integer>8</integer>
<key>spf</key>
<integer>480</integer>
<key>controlPort</key>
<integer>63658</integer>
<key>usingScreen</key>
<true/>
<key>audioFormat</key>
<integer>16777216</integer>
<key>type</key>
<integer>96</integer>
</dict>
</array>
</dict>
</plist>
server->client
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>dataPort</key>
<integer>42820</integer>
<key>controlPort</key>
<integer>46440</integer>
<key>type</key>
<integer>96</integer>
</dict>
</array>
<key>timingPort</key>
<integer>46440</integer>
</dict>
</plist>
3. 代码分析
由于代码是java写的,用的是udp,通过搜索DatagramSocket和相关日志确认了代码的关系

为了确认3个端口的作用,需要分析AudioServer中的代码
- 关键代码一
class C07971 extends Thread {
C07971() {
}
public void run() {
AudioServer.this.mStartClockTime = System.currentTimeMillis();
AudioServer.this.mLastSyncPacketTime = System.currentTimeMillis();
AudioServer.this.mSessionStartTime = System.currentTimeMillis();
AudioServer.this.mDiffToSource = 0;
while (!AudioServer.this.mStopped) {
// ntp 毫秒
AudioServer.this.writeTimeStamp(AudioServer.this.request, 24, (System.currentTimeMillis() - AudioServer.this.mLastSyncPacketTime) + AudioServer.this.mDiffToSource);
try {
AudioServer.this.csock.send(new DatagramPacket(AudioServer.this.request, AudioServer.this.request.length, AudioServer.this.mSocket.getInetAddress(), AudioServer.this.session.getTimingPort()));
try {
Thread.sleep(3000);
} catch (Throwable e) {
LeLog.m1097w("AudioServer", e);
return;
}
} catch (Throwable e2) {
LeLog.m1097w("AudioServer", e2);
return;
} catch (Throwable npe) {
LeLog.m1097w("AudioServer", npe);
return;
}
}
}
}
- 关键代码二
public void packetReceivedTC(byte[] packet, int len) {
if (!this.mStopped) {
this.type_tc = packet[1] & TransportMediator.KEYCODE_MEDIA_PAUSE;
if (this.type_tc == 83) {
this.mDiffToSource = readTimeStamp(packet, 24);
this.mLastSyncPacketTime = System.currentTimeMillis();
if (this.audioBuf != null) {
if (((this.mLastTimeStampsAp - this.audioBuf.getPlayPts()) * 10) / 441 > ((long) this.synctime)) {
LeLog.m1087d("AudioServer", "Sync Audio ...");
this.audioBuf.setSync();
}
if (readTimeStamp(packet, 24) - this.mLastTimeStampsTp <= 2500) {
}
}
} else if (this.type_tc == 84) {
this.mCurrentSeqNo_tc = ((packet[2] & 255) << 8) + (packet[3] & 255);
this.mLastTimeStampsPc = read32(packet, 4);
this.mDiffToSource = readTimeStamp(packet, 8);
this.mLastSyncPacketTime = System.currentTimeMillis();
this.mSessionStartTime = System.currentTimeMillis();
if (this.audioBuf != null && ((read32(packet, 4) * 10) / 441) - ((this.audioBuf.getPlayPts() * 10) / 441) > 150) {
}
} else if (this.type_tc == 86) {
int seqn = ((packet[6] & 255) * 256) + (packet[7] & 255);
this.mCurrentSeqNo_tc = ((packet[6] & 255) << 8) + (packet[7] & 255);
if (len > 16) {
Arrays.fill(this.packet_buffer_retry, (byte) 0);
System.arraycopy(packet, 16, this.packet_buffer_retry, 0, (len - 12) - 4);
if (this.mType != 1) {
this.audioBuf.putPacketInBuffer(this.mCurrentSeqNo_tc, this.packet_buffer_retry, len - 16);
this.audioBuf.putAacEldPacketPts(this.mCurrentSeqNo_tc, read32(packet, 4));
}
}
} else {
LeLog.m1087d("AudioServer", "type=" + this.type_tc + "-->unkown\n");
this.mCurrentSeqNo_tc = (short) (((packet[2] & 255) << 8) + (packet[3] & 255));
}
}
}
4. timingPort交互
server 46440 <-> client 59593
数据内容如下
server->client
80 d2 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 aa 7e 80 00 00 00 f3
client->server
80 d3 00 07 00 00 00 00 83 aa 7e 80 00 00 00 f3 83 b7 bc e9 3b d6 ea c8 83 b7 bc e9 3b e1 ae 70
46440先向59593发送了32字节数据,对应
前24字节固定为
0x80,0xd2,0x00,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
后8个字节为ntp时间的发送时间,
收到的也是32字节,前8个字节固定80,d3,00,07,00,00,00,00
后32个字节是Origin_Timestamp
,Receive_Timestamp
和Transmit_Timestamp
所以timingPort是用作ntp对时用的
5. controlPort交互
server 46440 <-> client 63658
看这行代码this.type_tc = packet[1] & TransportMediator.KEYCODE_MEDIA_PAUSE;
,TransportMediator.KEYCODE_MEDIA_PAUSE
的值是0x7F,type_tc
的值有两种84和86
84在代码中没看出什么作用,可以先不管,86在代码中是重传数据音频数据,包中会含有音频包
6. 音频数据发送分析
server接收音频数据的端口为42820
视频数据是加密的,那么音频加密可能性很大,继续分析代码
public void packetReceived(byte[] packet, int len) {
if (!this.mStopped) {
playbackService com_hpplay_happyplay_playbackService = this.mPlaybackService;
com_hpplay_happyplay_playbackService.mStreamCount += len;
this.type = packet[1] & TransportMediator.KEYCODE_MEDIA_PAUSE;
if (this.type == 96 || this.type == 86) {
int off = 0;
if (this.type == 86) {
off = 4;
}
this.mCurrentSeqNo = ((packet[off + 2] & 255) << 8) + (packet[off + 3] & 255);
if (this.mPreSeqNo > 0 && Math.abs(this.mCurrentSeqNo - this.mPreSeqNo) > 12) {
this.mBadSeqCount++;
if (this.mBadSeqCount >= 6) {
LeLog.m1091i("AudioServer", "bad seq count " + this.mBadSeqCount);
if (!(playbackService.getInstance().getPlayer() || Mirror.MirrorActivityStatus)) {
LeLog.m1091i("AudioServer", "showActivity audio server");
Intent intent = new Intent(this.mContext, MirrorCourseActivity.class);
intent.putExtra("type", 4);
intent.addFlags(268435456);
this.mContext.startActivity(intent);
this.mStopped = true;
this.mContext.sendBroadcast(new Intent(mainConst.MIRROR_FORCE_STOP));
}
this.mBadSeqCount = 0;
this.mPreSeqNo = -1;
}
}
this.mPreSeqNo = this.mCurrentSeqNo;
this.mLastTimeStampsAp = read32(packet, 4);
if ((len - 12) - off == 4 && packet[12] == (byte) 0 && packet[13] == (byte) 104 && packet[14] == (byte) 52 && packet[15] == (byte) 0) {
if (this.audioBuf.getStopstatus()) {
this.audioBuf.setSync();
}
this.mCurrentPlaySeqNo = this.mCurrentSeqNo;
this.mIsSync = false;
return;
}
if (!this.mIsSync) {
this.mCurrentPlaySeqNo = this.mCurrentSeqNo - 1;
this.mIsSync = true;
}
if (this.mType == 0) {
//使用alac解码
Arrays.fill(this.packet_buffer, (byte) 0);
System.arraycopy(packet, off + 12, this.packet_buffer, 0, (len - 12) - off);
this.audioBuf.putPacketInBuffer(this.mCurrentSeqNo, this.packet_buffer, (len - 12) - off);
this.audioBuf.putAacEldPacketPts(this.mCurrentSeqNo, this.mLastTimeStampsAp);
} else if ((this.mCurrentSeqNo & SupportMenu.USER_MASK) >= ((this.mCurrentPlaySeqNo + 1) & SupportMenu.USER_MASK)) {
//使用aac-eld解码
Arrays.fill(this.packet_buffer, (byte) 0);
System.arraycopy(packet, off + 12, this.packet_buffer, 0, (len - 12) - off);
this.audioBuf.putAacEldPacketInBuffer(this.mCurrentSeqNo, this.packet_buffer, (len - 12) - off);
this.audioBuf.putAacEldPacketPts(this.mCurrentSeqNo, this.mLastTimeStampsAp);
this.mSeqCount++;
this.mCurrentPlaySeqNo = this.mCurrentSeqNo;
} else if ((this.mCurrentSeqNo & SupportMenu.USER_MASK) != 0 && this.mPlaybackService.channel < 14 && !this.audioBuf.audioBuffer[this.mCurrentSeqNo % 512].ready) {
LeLog.m1087d("AudioServer", "Frame " + this.mCurrentSeqNo + " not ready, pushed");
Arrays.fill(this.packet_buffer, (byte) 0);
System.arraycopy(packet, off + 12, this.packet_buffer, 0, (len - 12) - off);
this.audioBuf.putAacEldPacketInBuffer(this.mCurrentSeqNo, this.packet_buffer, (len - 12) - off);
this.audioBuf.putAacEldPacketPts(this.mCurrentSeqNo, this.mLastTimeStampsAp);
}
}
}
}
音频包格式如图所示

type = 第2个字节 & 0x7F
,只处理type为86的数据
这里需要注意的是:如果音频数据为4个字节且为{0x0,0x68,0x34,0x0}时,表示没有音频数据,不处理。
7. 音频数据解密
继续分析AudioBuffer的putAacEldPacketInBuffer方法,音频数据又是AES加密,不过这次解密为CBC方式解密,关键代码如下
public void initAES() {
try {
this.f867k = new SecretKeySpec(this.session.getAESKEY(), "AES");
this.f866c = Cipher.getInstance("AES/CBC/NoPadding");
this.f866c.init(2, this.f867k, new IvParameterSpec(this.session.getAESIV()));
} catch (Throwable e) {
LeLog.m1097w("Music", e);
}
}
private int decryptAES(byte[] array, int inputOffset, int inputLen, byte[] output, int outputOffset) {
try {
return this.f866c.update(array, inputOffset, inputLen, output, outputOffset);
} catch (Throwable e) {
LeLog.m1097w("Music", e);
return -1;
}
}
同样的,我们需要知道key和iv。
通过分析,key为native函数FdkDecodeAudioFun11
产出,分析代码可得出
eaeskey
为72字节解密出的16字节ekey,hash之后用作key,关键代码如下
sha512_init
sha512_update->eaeskey
sha512_update->ecdh_secret
sha512_final->eaeskey
取eaeskey的前16字节作为key,iv是第一次SETUP
时client发送数据bplist中的eiv
得到key和iv之后,即可进行解密
这里解出的是AAC裸流,接入fdk-aac解码为pcm,这里面遇到两个问题
- 问题1:pcm的时长是原始时长的3倍
通过打印序号,然后看了下收到的包,发现每个序号会发3遍,需要做过滤

做了过滤,时长正常
- 问题2:pcm播放是杂音,非正常音乐
通过日志发现fdk-aac返回错误0x4006,表示数据源错误。经过各种可能方法查找问题,最后使用java层解密的方式才确认是C中选用的aes库的问题,在每次解密需要重新初始化aes_context再进行解密,之前是缓存了aes_context导致错误。
其他请求
GET_PARAMETER rtsp://172.18.145.2/16274097868445272520 RTSP/1.0
Content-Length: 8
Content-Type: text/parameters
CSeq: 8
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
volume
RTSP/1.0 200 OK
Content-Type: text/parameters
Content-Length: 13
Server: AirTunes/220.68
CSeq: 8
volume: 0.0
SET_PARAMETER rtsp://172.18.145.2/16274097868445272520 RTSP/1.0
Content-Length: 20
Content-Type: text/parameters
CSeq: 18
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
volume: -20.000000
RTSP/1.0 200 OK
Server: AirTunes/220.68
CSeq: 18
POST /feedback RTSP/1.0
CSeq: 26
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
RTSP/1.0 200 OK
Server: AirTunes/220.68
CSeq: 26
TEARDOWN rtsp://172.18.145.2/16274097868445272520 RTSP/1.0
Content-Length: 69
Content-Type: application/x-apple-binary-plist
CSeq: 30
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
bplist00...Wstreams.....Ttype.`......................................RTSP/1.0 200 OK
Connection: close
Server: AirTunes/220.68
CSeq: 30
TEARDOWN rtsp://172.18.145.2/16274097868445272520 RTSP/1.0
Content-Length: 69
Content-Type: application/x-apple-binary-plist
CSeq: 31
DACP-ID: 2F4085FA856F2D7D
Active-Remote: 3115937391
User-Agent: AirPlay/366.74.2
bplist00...Wstreams.....Ttype.n......................................
1. GET_PARAMETER
获取音量数据
2. SET_PARAMETER
调整音量数据
3. feedback
心跳
4. TEARDOWN-1
client->server
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>type</key>
<integer>96</integer>
</dict>
</array>
</dict>
</plist>
根据type=96可得出是销毁音频服务
- 5.TEARDOWN-2
<plist version="1.0">
<dict>
<key>streams</key>
<array>
<dict>
<key>type</key>
<integer>110</integer>
</dict>
</array>
</dict>
</plist>
根据type=110可得出是销毁镜像服务
实现
目前已经开源,直接看源码即可。
git.code.oa.com
Github
下面是投屏的演示图

参考链接
- shairplay
- ed25519算法集
- ed25519算法实现
- curve25519-donna算法实现
- RTP协议文档
- Multicast DNS
- 局域网设备发现之Bonjour协议
- Unofficial AirPlay Protocol Specification
- 通过NTP协议进行时间同步
- rtsp协议详解
- AES算法实现
附件
- classes.dex
- ida分析文件
网友评论