En plus de permettre d’échanger des messages texte, l’application SeaTalk permet aussi de passer des appels vocaux.
J’ai donc tenté d’interpréter le trafic réseau auquel donne lieu un appel vocal, et j’ai jeté un œil à l’implémentation sous-jacente.
Le trafic induit par un appel vocal
Pour se faire une première idée de la cinématique d’un appel, on réalise différentes captures réseau.
Regardons les serveurs TLS contactés lorsque aucun appel n’est effectué :
$ tshark -r captures/seatalk_nocall.pcap -Y tls.handshake.type==1 -T fields -e tls.handshake.extensions_server_name | grep -vE '^$' | sort -u
api.haiserve.com
edge-co.haiserve.com
oa.haiserve.com
s.haiserve.com
$ tshark -r captures/seatalk_call_failure_1.pcap -Y tls.handshake.type==1 -T fields -e tls.handshake.extensions_server_name | grep -vE '^$' | sort -u
api.haiserve.com
call.haiserve.com
edge-co.haiserve.com
oa.haiserve.com
play.googleapis.com
s.haiserve.com
www.google.com
Regardons ensuite ce qui se produit lorsqu’un appel est effectué mais que le correspondant ne décroche pas :
$ tshark -r captures/seatalk_call_failure_2.pcap -Y tls.handshake.type==1 -T fields -e tls.handshake.extensions_server_name | grep -vE '^$' | sort -u
api.haiserve.com
call.haiserve.com
edge-co.haiserve.com
oa.haiserve.com
s.haiserve.com
Enfin, lorsqu’une communication vocale est réellement établie :
$ tshark -r captures/seatalk_call_success_1.pcap -Y tls.handshake.type==1 -T fields -e tls.handshake.extensions_server_name | grep -vE '^$' | sort -u
api.haiserve.com
call.haiserve.com
edge-co.haiserve.com
f.haiserve.com
oa.haiserve.com
s.haiserve.com
En conclusion, on constate que du trafic est initié avec le serveur call.haiserve.com
par un terminal lorsqu’il initie un appel.
De plus (et cela n’apparaît pas dans ces grep
), on constate que du trafic UDP est échangé lorsque le correspondant décroche : le trafic vocal proprement dit est donc très probablement transporté par ce canal.
Trafic échangé avec call.haiserve.com
Intéressons-nous d’un peu plus près au trafic échangé avec call.haiserve.com
lorsqu’un appel est initié.
Pour cela, lançons l’application en interceptant le trafic avec Burp.
L’échange HTTPS avec call.haiserve.com
se réduit à un unique couple requête-réponse :

Cette requête sert donc à établir une session websocket avec call.haiserve.com
.
Une fois la websocket établie, on peut voir dans l’Interceptor de Burp qu’elle est visiblement utilisée pour échanger de la signalisation :
Commande RING
:

Commande JOIN
:

Commande STATE
:

Réponse du serveur à la commande RING
:

Réponse du serveur à la commande JOIN
:

Dans cette réponse, on observe la présence d’un token.
Réponse du serveur à la commande STATE
:

Certaines réponses du serveur informent le client de certains événements concernant le correspondant, par exemple lorsque son téléphone sonne :

ou lorsqu’il décroche :

Des messages qui semblent être des heartbeats sont régulièrement échangés (message SignalRTT
).
Ces messages comportent un champ ts
(vraisemblablement pour timestamp), dont la valeur est strictement croissante :

Ces messages de heartbeat sont échangés tout au long de l’appel.
Le Software-Defined-Network agora
Différents indices indiquent que SeaTalk utilise le SDK agora (https://www.agora.io), du moins au plus probant :
- le mot « agora » dans la réponse à la commande
JOIN
(voir plus haut), - la très grande quantité de classes dans le package
io.agora.*
:

- les très nombreuses bibliothèques partagées
libagora*.so
:
$ find SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/ -type f -name "*libagora*so" -exec ls -lh {} \;
-rw-r--r-- 1 thomas thomas 2,9M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_ai_noise_suppression_extension.so
-rw-r--r-- 1 thomas thomas 679K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-fdkaac.so
-rw-r--r-- 1 thomas thomas 174K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-soundtouch.so
-rw-r--r-- 1 thomas thomas 1,4M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_video_encoder_extension.so
-rw-r--r-- 1 thomas thomas 23M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-rtc-sdk.so
-rw-r--r-- 1 thomas thomas 3,0M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_udrm3_extension.so
-rw-r--r-- 1 thomas thomas 1,9M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_audio_beauty_extension.so
-rw-r--r-- 1 thomas thomas 1,3M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_clear_vision_extension.so
-rw-r--r-- 1 thomas thomas 895K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_video_decoder_extension.so
-rw-r--r-- 1 thomas thomas 191K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_drm_loader_extension.so
-rw-r--r-- 1 thomas thomas 1011K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_video_quality_analyzer_extension.so
-rw-r--r-- 1 thomas thomas 2,9M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_segmentation_extension.so
-rw-r--r-- 1 thomas thomas 1,2M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_face_detection_extension.so
-rw-r--r-- 1 thomas thomas 5,9M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-ffmpeg.so
-rw-r--r-- 1 thomas thomas 488K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-core.so
-rw-r--r-- 1 thomas thomas 3,9M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_ai_echo_cancellation_extension.so
-rw-r--r-- 1 thomas thomas 4,5M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_spatial_audio_extension.so
-rw-r--r-- 1 thomas thomas 379K 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_screen_capture_extension.so
-rw-r--r-- 1 thomas thomas 1,7M 25 nov. 21:44 SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora_content_inspect_extension.so
Le sdk agora est développé par l’entreprise de même nom (agora.io
) et permet d’ajouter rapidement à une application des fonctionnalités d’appels vocaux ou vidéos. Ces appels transitent par l’agora SD-RTN (Software-Defined Real-Time Network) : Le client d’agora n’a pas à mettre en place une infrastructure d’acheminement du flux vocal, c’est l’infrastructure agora qui fait ce travail.
Le modèle économique de Agora est de
- mettre gratuitement à disposition le SDK pour la plateforme de votre choix (https://docs.agora.io/en/sdks?platform=android),
- facturer au développeur l’usage du SD-RTN fait par son application.
Un client de agora aura un APP_ID
, qui est un identifiant propre à chaque client, destiné à des fins de facturation et divers services statistiques.
Ce client aura également accès à un agora_certificate qui est une chaîne de caractères à partir de laquelle le backend du client calculera les tokens (observé dans la réponse de call.haiserve.com
à la commande JOIN
dans le trafic de signalisation) en utilisant les snippets de code fournis par agora.
Le token est communiqué par le backend client à ses utilisateurs, et est construit cryptographiquement à partir d’un timestamp, de l’APP_ID
et de l’agora_certificate.
Après avoir été communiqué par le backend du développeur à l’application cliente, le token est ensuite transmis avec l’APP_ID
au SDN agora comme élément d’authentification.
Une vulnérabilité a été découverte en 2020 par McAfee (CVE-2020-25605) : l’APP_ID
et le token d’authentification transitaient en clair dans le trafic entre l’application cliente et le SDN agora.
Dans SeaTalk, l’APP_ID
est codé en dur dans un appel à la méthode RtcEngine.create
dans RtcEngineInstance
:

Chiffrement du flux vocal
Dans l’architecture conçue par Agora, il est possible de protéger cryptographiquement le flux vocal entre l’appelant et l’appelé.
Dans ce cas, la distribution du matériel cryptographique (une clé et un sel) incombe au backend client.
Concrètement, l’activation du chiffrement passe par un appel à la méthode enableEncryption
de la classe RtcEngine
:
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_join) {
if (!joined) {
if (encry_mode.getSelectedItem().toString().equals(getString(R.string.custom))) {
enablePacketProcessor(true);
} else {
// Creates an EncryptionConfig instance.
EncryptionConfig config = new EncryptionConfig();
// Sets the encryption mode as AES_128_XTS.
config.encryptionMode = EncryptionConfig.EncryptionMode.valueOf(encry_mode.getSelectedItem().toString());
// Sets the encryption key.
config.encryptionKey = et_password.getText().toString();
System.arraycopy(getKdfSaltFromServer(), 0, config.encryptionKdfSalt, 0, config.encryptionKdfSalt.length);
// Enables the built-in encryption.
engine.enableEncryption(true, config);
}
Le code de l’application comporte bien une méthode enableEncryption
, qui fait appel à la méthode native correspondante nativeEnableEncryption
:

Toutefois, la méthode enableEncryption
n’est jamais appelée :

Par conséquent, tracer la méthode enableEncryption
:
$ frida-trace -U -p $(frida-ps -U | grep SeaTalk | awk -F ' ' '{print $1}') -j '*!*enableEncryption*'
Instrumenting...
RtcEngineImpl.enableEncryption: Auto-generated handler at "/home/thomas/tools/dirsearch/__handlers__/io.agora.rtc2.internal.RtcEngineImpl/enableEncryption.js"
RtcEngine.enableEncryption: Auto-generated handler at "/home/thomas/tools/dirsearch/__handlers__/io.agora.rtc2.RtcEngine/enableEncryption.js"
Started tracing 2 functions. Press Ctrl+C to stop
ou bien la méthode native correspondante nativeEnableEncryption
:
$ frida-trace -U -p $(frida-ps -U | grep SeaTalk | awk -F ' ' '{print $1}') -i '*nativeEnableEncryption*'
Instrumenting...
Java_io_agora_rtc2_internal_RtcEngineImpl_nativeEnableEncryption: Auto-generated handler at "/home/thomas/tools/dirsearch/__handlers__/libagora_rtc_sdk.so/Java_io_agora_rtc2_internal_RtcE_8d545876.js"
Started tracing 1 function. Press Ctrl+C to stop.
Ne donne lieu à aucune trace, configurant que ces fonctions ne sont effectivement jamais appelées.
L’application SeaTalk ne suit donc pas les conseils d’utilisation prodigués par agora, mais cela ne signifie pas nécessairement l’absence totale de chiffrement : il est possible que les flux entre l’application et le SDN agora soient de toute façon chiffrés, auquel cas l’absence d’utilisation de la fonction enableEncryption
aurait pour conséquence l’absence de chiffrement de bout en bout entre deux utilisateurs de SeaTalk.
Tout ceci reste cependant de l’ordre de l’hypothèse, et demanderait une étude approfondie du SDK agora pour être confirmé ou non.
Par où commencer ?
Comme on l’a vu plus haut, le volume de code représenté par les différentes bibliothèques partagées du SDK agora embarquées par l’application est relativement important.
En particulier, les deux bibliothèques libagora-rtc-sdk.so et libagora-core.so exportent un nombre considérable de fonctions :
$ aarch64-linux-gnu-objdump -T SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-rtc-sdk.so | grep 'DF .text' | sort -u | wc -l
517
$ aarch64-linux-gnu-objdump -T SeaTalk_3.49.1_APKPure.xapk.out/unknown/config.arm64_v8a/lib/arm64-v8a/libagora-core.so | grep 'DF .text' | sort -u | wc -l
462
Dès lors, par quel bout débuter l’analyse de l’application ?
Les fonctions natives
On peut commencer par lister les méthodes java natives, qui sont des wrappers vers des méthodes implémentées dans une des bibliothèques partagées embarquées par l’application.
Ces méthodes sont préfixées par le mot-clé native :

Comme on peut le voir, toutes ces méthodes ont des noms débutant par « native ».
On peut donc, pour les différents packages de l’application, tracer les méthodes dont le nom contient le mot « native » :
Le package le plus prometteur est io.agora.rtc2, qui embarquent 367 méthodes « native« .
frida-trace -U -p $(frida-ps -U | grep SeaTalk | awk -F ' ' '{print $1}') -j 'io.agora.rtc2*!*native*'
(...)
La fonction la plus intéressante est peut-être nativeJoinChannel
(dont le pendant natif s’appelle Java_io_agora_rtc2_internal_RtcEngineImpl_nativeJoinChannel
dans libagora-rtc-sdk.so) : En traçant cette fonction on constate que son deuxième argument est un token (similaire à celui retourné par le backend de l’application et observé dans les échanges de signalisation sur websocket), tandis que le dernier argument est l’identifiant numérique de l’utilisateur :
(...)
18119 ms RtcEngineImpl.nativeJoinChannel("-5476376667000352608", "0067656ab67ab154d18a1cf32bfb01446d1IADobfyXeZRJlhohshNaJrj+JfDA3se/T5Umq14gOoyzoQGMrchm26d2IgDLh8gDb+WEZwQAAQAfvoNnAgAfvoNnAwAfvoNnBAAfvoNn", "01JHCZKDS9GWSC9TD8HSJQHZWH", "", 641926)
(...)
logcat et server hello
La journalisation logcat contient de nombreuses références à agora, symptomatiques de différents appels de fonctions, et constitue donc une source d’information appréciable.
Une entrée particulièrement intéressante est celle mentionnant la réception d’un message « server hello » :

La chaîne de caractères en question est présente dans la bibliothèque libagora-rtc-sdk.so :

Dans cette librairie, cette chaîne de caractères n’est utilisée que par la fonction FUN_008232f0
:

Interceptons donc les appels à cette fonction avec frida :
const ghidraImageBase = 0x00100000
const libagoraBaseAddr = Module.findBaseAddress('libagora-rtc-sdk.so');
const FUN_008232f0_RealAddr = libagoraBaseAddr.add(0x008232f0 - ghidraImageBase);
Le hook positionné est configuré pour dumper les arguments d’appel de la fonction et lister la pile d’appel à la fonction :
Interceptor.attach(FUN_008232f0_RealAddr, {
onEnter: function(args) {
console.log("entering the server hello-related function");
console.log('FUN_008232f0 called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log('x0:' + this.context.x0.toString());
console.log('x1:' + this.context.x1.toString());
console.log('x2:' + this.context.x2.toString());
console.log('x3:' + this.context.x3.toString());
console.log('x4:' + this.context.x3.toString());
console.log('content at x0 : ' + hexdump(Memory.readByteArray(this.context.x0, 256)));
console.log('content at x1 : ' + hexdump(Memory.readByteArray(this.context.x1, 256)));
console.log('content at x2 : ' + hexdump(Memory.readByteArray(this.context.x2, 256)));
console.log('content at x3 : ' + hexdump(Memory.readByteArray(this.context.x3, 256)));
console.log('content at x4 : ' + hexdump(Memory.readByteArray(this.context.x4, 256)));
}
});
On répète la démarche pour les appelants successifs de FUN_008232f0
.
La « 5ème fonction appelante » (la « 1ère fonction appelante » étant une fonction appelant directement FUN_008232f0
, la « 2ème fonction appelante » étant une fonction appelant une fonction qui appelle FUN_008232f0
, et ainsi de suite) est la fonction FUN_0079786c
.
Il apparaît que le 3ème argument de cette fonction est un paquet qui peut également être observé dans une capture réseau, tandis que le 4ème argument est la taille du paquet en question :
[Pixel 7a::PID::20321 ]-> entering the server hello-related function 5th caller
FUN_0079786c called from:
0x78524f3fb0 libagora-rtc-sdk.so!0x695fb0
0x78524f3fb0 libagora-rtc-sdk.so!0x695fb0
0x78524f14c8 libagora-rtc-sdk.so!0x6934c8
0x7852b8c008 libagora-rtc-sdk.so!0xd2e008
0x7852c8df4c libagora-rtc-sdk.so!0xe2ff4c
0x7852c8e8b4 libagora-rtc-sdk.so!0xe308b4
0x78d1b24228 libagora-core.so!0x13228
0x78d1b21ed8 libagora-core.so!0x10ed8
0x78d1b26d88 libagora-core.so!0x15d88
0x78d1b29170 libagora-core.so!0x18170
0x78d1b282f0 libagora-core.so!0x172f0
0x78d1b39254 libagora-core.so!0x28254
0x7cd6cf3fc0 libc.so!_ZL15__pthread_startPv+0xd0
0x7cd6ce5d64 libc.so!__start_thread+0x44
x0:0xb400007b1b839590
x1:0xb400007a6aceed10
x2:0xb400007b4ad003a0
x3:0xc2
content at x2 : 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 02 1f cd 26 fc 5c 6b bb 56 6e af e1 00 00 23 00 ...&.\k.Vn....#.
00000010 83 53 19 00 01 43 50 54 4f a7 00 4e 4f 4e 43 20 .S...CPTO..NONC
00000020 00 03 e8 e8 32 ec b3 f2 f2 d3 0b 97 b8 22 a7 6f ....2........".o
00000030 67 7a a7 87 f2 32 2d 56 40 79 3c b6 19 b7 28 f5 gz...2-V@y<...(.
00000040 bf 41 45 41 44 04 00 41 45 53 47 53 43 49 44 20 .AEAD..AESGSCID
00000050 00 70 33 71 a9 c5 2e 35 bc fe 7e e5 b0 a7 12 a5 .p3q...5..~.....
00000060 de 1f 9d e7 aa 7c 8a b7 c9 f4 50 8e bc 7b 1a f0 .....|....P..{..
00000070 6a 50 55 42 53 41 00 04 22 41 a2 5e 24 f7 b5 e3 jPUBSA.."A.^$...
00000080 20 49 c6 4f d7 0c 9e a8 99 64 0e 54 47 c8 b6 fc I.O.....d.TG...
00000090 23 12 98 eb 87 a5 53 52 a7 3c 11 52 cf 95 81 14 #.....SR.<.R....
000000a0 4e bb 31 39 18 7e 27 61 af 06 ab f6 08 60 30 f6 N.19.~'a.....`0.
000000b0 03 aa dc ff 9a 3b 7b d3 4b 45 58 53 04 00 50 32 .....;{.KEXS..P2
000000c0 35 36 56

Dans les journaux logcat on retrouve aussi trace de messages « client hello » :

Cette terminologie laisse penser qu’on a affaire à un protocole de transport sécurisé inspiré de TLS.
Si c’est le cas, le server hello est alors reçu par le client avant que la négociation d’algorithmes et de secrets cryptographiques communs n’ait été terminée.
Le message server hello serait alors pleinement manipulable par un attaquant (pas de hmac ou de tag gcm empêchant de le modifier !) et pourrait être utilisé comme vecteur d’attaque pour déclencher une vulnérabilité présentes dans une des fonctions traitant ce server hello.