AirPlay2技术浅析

作者: dsafa22 | 来源:发表于2019-03-13 15:28 被阅读49次

手打目录

  • 前言

  • App的选取

    • 投屏测试
  • 抓包分析

    • 抓包
    • mDNS分析
    • 握手协议
        1. info
        1. pair-setup
        1. pair-verify
        1. 第二个pair-verify
        1. 两次fp-setup
  • 镜像数据

      1. 第一次SETUP
      1. 第二次SETUP
      1. 镜像数据发送分析
      1. 镜像数据解密
  • 音频数据

      1. 抓包分析
      1. 第三个SETUP
      1. 代码分析
      1. timingPort交互
      1. controlPort交互
      1. 音频数据发送分析
      1. 音频数据解密
  • 其他请求

      1. GET_PARAMETER
      1. SET_PARAMETER
      1. feedback
      1. TEARDOWN-1
  • 实现

  • 参考链接

  • 附件

前言

本文针对AirPlay2协议,选取了一款上线的app并进行逆向分析,实现了对AirPlay2协议的破解,并实现了投屏demo。

以下是本文涉及到的一些知识点:

  1. 协议:RTP,RTSP,DNS,DNS-SD,mDNS,NTP
  2. 加解密算法:curve25519,ed25519,AES(cbc&ctr)
  3. 音视频基础:h264,aac
  4. 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广播如下图所示:


image

这里有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端查询的服务

image

这里可以看到client只使用了raop和airplay两个服务,查询SRV记录内容可以得知airplay使用了52233端口,raop使用了52244端口

在frame146中,server发送了回复的组播包


image

由此可以得知,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),过滤结果如图所示:

image

首先是三次握手,从frame230开始发送请求,可以发现server端口是52244即使用了raop服务

过滤52233端口发现没有结果,说明client是没有使用airplay服务。

下面针对选中230,Analyze->Follow->TCP Stream可以看到整个交互过程


image

具体内容如下

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

请求包内容如下图所示:

image

请求和回包都是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",找到如下代码

image
FdkDecodeAudioFun8参数分析
  • 第1个参数 client请求包
  • 第2个参数 client请求包长度
  • 第3个参数jg server->client回包内容
  • 第4个参数out_size 回包长度
  • 第5个参数 1
  • 第6个参数pairSessionId 连接上下文,看代码是支持最多16个设备连接

进一步分析so,使用ida打开libhpplayaudio.so找到FdkDecodeAudioFun8
查下这个函数的调用关系,如下图所示

image

看起来挺复杂,分析中找到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

根据分析,画下目前内存里面的数据

image

重点观察下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个字节内容


image

同样的,找到FdkDecodeAudioFun9函数
FdkDecodeAudioFun9参数分析

  • 第1个参数 client请求包
  • 第2个参数 client请求包长度(68)
  • 第3个参数jg server->client回包内容
  • 第4个参数out_size 回包长度
  • 第5个参数 1
  • 第6个参数pairSessionId 连接上下文,看代码是支持最多16个设备连接

查看下FdkDecodeAudioFun9函数调用关系图如下

image

大体看下整个函数结构,还是有点复杂,具体函数分析如下

文章过长此部分删除

经过分析之后,此时内存结构图如下


image

回包是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然后是加密

需要关注的是 f1116c0f1117b0 函数,一开始不知道这两个函数是干嘛的,不过看算法比较复杂,应该是已有算法,我们进行跟进发现
f1117b0 用到了unk_118FE8进去看

image

根据这个线索我们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,两个函数的调用关系如下图
​​


image
image

看起来非常复杂,分析起来耗时耗力
这里有两个思路:

  • 导出此部分汇编代码,直接调用函数运行
  • 直接调用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)),结果如下图所示

image

这里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处理

image

看了下代码,是ntp协议,每隔3秒发送一次

在wireshark中右键udp包,decode as,选取ntp即可看到解析,这块比较简单,我们直接进入解析镜像数据的分析

  • 7020(tcp)端口分析

通过端口信息找到多个镜像service,主要包括以下几个类

image

修改LeLog 中sLevel为0,重新编包,根据打印出的log辅助定位,确认是由GeneralMirrorService处理(需要附件)

分析之后,我们知道,有两个数据类型,先判断前4个字节是GET/POST,如果不是按照镜像数据处理,格式如下图


image

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. 抓包分析

抓取镜像包的时候没有抓音频包,这里重新抓取含音频的包。
过滤下端口信息,看下音频的交互

image

和音频相关都是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和相关日志确认了代码的关系

image

为了确认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_TimestampReceive_TimestampTransmit_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);
            }
        }
    }
}

音频包格式如图所示

image

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遍,需要做过滤


image

做了过滤,时长正常

  • 问题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

下面是投屏的演示图


image

参考链接

  1. shairplay
  2. ed25519算法集
  3. ed25519算法实现
  4. curve25519-donna算法实现
  5. RTP协议文档
  6. Multicast DNS
  7. 局域网设备发现之Bonjour协议
  8. Unofficial AirPlay Protocol Specification
  9. 通过NTP协议进行时间同步
  10. rtsp协议详解
  11. AES算法实现

附件

  1. classes.dex
  2. ida分析文件

相关文章

  • AirPlay2技术浅析

    手打目录 前言 App的选取投屏测试 抓包分析抓包mDNS分析握手协议infopair-setuppair-ver...

  • 2016/12/23

    技术 Weex之Android端的浅析(一) SQLite表结构和数据的导入导出

  • OCR 技术浅析

    本文为 ReinhardHuang 原创,著作权归作者所有。如需转载请联系作者,并取得作者的明示同意后方可转载。 ...

  • OCR技术浅析

    姓名:吴兆阳 学号:14020199009 转自机器人学习研究会 嵌牛导读:OCR(Optical Charact...

  • Hook技术浅析

    1. 什么是 Hook(钩子) Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发...

  • 浅析索引技术

    1. 认识索引 索引是一种常见的数据库优化手段,设计优良的索引能够大幅提升数据库的查询效率,提升并发能力。 举一个...

  • 【前沿技术】浅析搜狗AI主播背后的核心技术​

    首发于微信公众号《有三AI》 【前沿技术】浅析搜狗AI主播背后的核心技术​ 今天是新专栏《前沿技术》,技术的更新迭...

  • Linux中的零拷贝技术

    参考文章:浅析Linux中的零拷贝技术[https://www.jianshu.com/p/fad3339e344...

  • webpack技术内幕

    webpack技术内幕 入口文件 对于多入口文件,如何巧妙地编写入口 Module浅析 开启对Webpack多核的...

  • 人脸识别技术浅析

    第一次尝试写专业领域方面的文章,一方面想通过写的方式,对产品和系统进行一些思考,发现和形成一些自己的看法。另一方面...

网友评论

    本文标题:AirPlay2技术浅析

    本文链接:https://www.haomeiwen.com/subject/asxtmqtx.html