software 1996 · unas noches · una antena parabólica y vergüenza ajena

Decodificando el Canal Plus a lo bestia (y mal)

Allá por el 96 se me ocurrió intentar decodificar las imágenes estáticas del Canal Plus en ensamblador. La idea era lista. La ejecución, más o menos. El resultado, gloriosamente cutre. Aquí lo cuento como lo recordaría el chaval que lo hizo — que era yo, aunque firmaba como |ncubuX por aquello de mantener cierto misterio en los grupos de la BBS.

01 — Qué hacía el Canal Plus con la imagen

El Canal Plus de los 90 usaba un sistema llamado Nagravision (o Syster, según a quién le preguntes) para cifrar la señal de vídeo analógica. No cifraba el audio — ese iba en claro, que si no los descodificadores oficiales habrían costado el doble y la gente se habría cabreado más todavía.

Lo que hacía con la imagen era básicamente dos cosas:

1. Cortar cada línea horizontal en un punto aleatorio y
   pegar el trozo de la derecha a la izquierda (line cut).

2. Reordenar las líneas de la imagen en un orden
   pseudoaleatorio que dependía de la clave del frame.

La clave cambiaba varias veces por segundo. Para descodificar "bien" necesitabas la clave, que solo tenía el descodificador oficial (que la recibía cifrada en la línea 16 del teletexto, de memoria). Yo no tenía la clave. Nadie en la BBS la tenía. Pero había una cosa que sí teníamos: física.

02 — La idea (brillante) y el problema (gordo)

En una imagen natural — una película, un partido de fútbol, lo que sea — las líneas horizontales adyacentes se parecen mucho entre sí. La línea 100 y la línea 101 son casi iguales porque el mundo no cambia de golpe de un pixel al siguiente en vertical. Esto se llama correlación espacial y es la base de casi toda la compresión de vídeo.

Así que la idea fue: si tengo las líneas desordenadas, puedo intentar reordenarlas buscando, para cada posición, qué línea del montón tiene menos diferencia con la anterior. La línea que "encaje mejor" es probablemente la que tocaba ahí.

Para cada línea de la imagen (de arriba a abajo):
    Para cada línea candidata en el buffer:
        error = suma de |pixel_actual - pixel_candidato|
    La candidata con menor error => esa va aquí
    Intercambiar

Elegante. El problema era uno solo pero gordo: no sabes cuál es la primera línea. La clave del Nagravision determinaba el orden de las líneas Y el punto de corte de cada una, y eso iba variando frame a frame. Así que incluso si mi algoritmo ordenaba las líneas perfectamente a partir de la primera... si la primera era la equivocada, la imagen salía igualmente hecha una pena.

La solución que se me ocurrió fue la de andar probando: la constante s0 en el código era el desplazamiento inicial — qué línea tomaba como "punto de partida". Ponía un número (entre 0 y 399), compilaba, ejecutaba, miraba si la imagen tenía mejor o peor pinta, y cambiaba a mano. Ingeniería de las de toda la vida.

03 — El algoritmo del mínimo error

El núcleo del programa en pseudocódigo decente:

; Intercambia la línea s0 con la primera como punto de arranque
; (apuesta inicial sobre cuál es la "primera" línea real)

para cada línea_actual (de la primera a la última):
    error_min = 0xFFFFFFFF
    mejor_candidata = ninguna

    para cada línea_candidata en el buffer:
        error = 0
        para cada pixel en la línea (640 pixels):
            error += abs(pixel_actual[i] - pixel_candidata[i])
        si error < error_min:
            error_min = error
            mejor_candidata = línea_candidata

    intercambiar(línea_actual, mejor_candidata)

En la práctica, el bucle de cálculo de error lo hace en 32 bits (resta bytes pero acumula en DX), y usa el timestamp counter del 486 (RDTSC, el opcode 0Fh 31h que ni existía oficialmente todavía en los manuales que yo tenía) para medir cuánto tardaba. Eso era el "profiler" que menciona la constante stamp del principio.

04 — El código (con DOS32 y todo)

El programa usa DOS32 como extensor de modo protegido. Esto es porque una imagen de 640x480 ocupa 307.200 bytes, que no caben en los 640KB del modo real del DOS. Con DOS32 se podía pedir memoria extendida vía INT 31h (la interfaz DPMI) y trabajar con punteros de 32 bits en modo plano. Nada de segmentación, nada de límites de 64KB. Lujo.

La cabecera del .ASM lo decía todo sobre el autor y sus intenciones:

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
;        Enredando con el PLUS... nada de copyright |ncubuX 96-97
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
;  Esta fuente emplea DOS32 como extensor de modo protegido... El programa
;  este trata de decodificar el plus (como le sale de los huevos)... juega
;  con la constante s0 para alcanzar mejores resultados (del 0 al 399).
;  No me hago responsable de nada, naturalmente porque aqui no violo absolu-
;  tamente nada. Al programa le entra un .raw (yo los hago con el alchemy)
;  y escupe un decoded.raw :)
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

"Como le sale de los huevos" era exactamente el nivel de documentación de la época. El flujo del programa:

inicializa:
    Modo texto (INT 10h), esconde cursor
    Muestra "DECODIFICADOR KUTUSKIANO v1.0 / Setabia's Hell 1.996"
    Parsea argumentos (nombre del .raw de entrada)
    Abre el fichero fuente
    Pide memoria extendida vía DPMI (INT 31h, función EE42h)
    Lee la cabecera (size_fichero - 640*480 bytes)
    Crea y abre decoded.raw
    Escribe la cabecera en el destino
    Vuelca los datos al buffer en memoria

vuelve:
    Intercambia línea 0 con línea s0 (apuesta inicial)
    Para cada línea: busca mínimo error e intercambia
    Escribe el buffer a decoded.raw
    Cierra ficheros y sale

Y el bucle de búsqueda de error, que es el corazón:

busca_error:
        mov     al,[esi+ebx]    ; pixel de la línea actual
        sub     al,[edi+ebx]    ; menos pixel de la candidata
        jns     oks             ; si positivo, está bien
        neg     al              ; si no, valor absoluto
oks:
        add     dx,ax           ; acumula error
        dec     ebx
        jnz     busca_error

Sencillo y directo. Calcula la suma de diferencias absolutas (SAD) entre dos líneas de 640 bytes, acumulando en DX. Si el resultado es menor que el mínimo guardado, esa es la nueva mejor candidata.

05 — Por qué no funcionaba del todo

Funcionaba. Un poco. Más o menos. Depende de la imagen.

El problema fundamental es que el algoritmo greedy de "coge siempre la línea con menor error respecto a la anterior" no garantiza la solución global óptima. Puede que la línea 47 sea la mejor candidata para la posición 5, pero si se la "gastas" en la posición 3 porque ahí también encajaba bastante bien, luego en la 5 te quedas sin ella.

El segundo problema era el que ya mencioné: sin saber cuál es la línea 0, estás jugando a la lotería con s0. En imágenes con mucho movimiento o poca correlación vertical (fondos muy variables) el algoritmo se volvía loco. En imágenes tranquilas — un plano fijo de una película, por ejemplo — a veces salía algo reconocible.

El tercer problema es que el Nagravision también cortaba cada línea horizontalmente por un punto aleatorio, y eso yo no lo trataba. Así que incluso cuando el orden de las líneas era correcto, cada línea estaba partida y pegada al revés. El resultado parecía una imagen vista a través de una persiana puesta por un loco.

¿Servía para ver el fútbol? No. ¿Servía para aprender modo protegido, DPMI, RDTSC y algoritmos de mínimo error? Absolutamente sí. Y para eso se hizo.

06 — El fuente completo (P2011.ASM)

Por si a alguien le apetece leerlo. Sin cambiar ni una coma.

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
;        Enredando con el PLUS... nada de copyright |ncubuX 96-97
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
;  Esta fuente emplea DOS32 como extensor de modo protegido... El programa
;  este trata de decodificar el plus (como le sale de los huevos)... juega
;  con la constante s0 para alcanzar mejores resultados (del 0 al 399).
;  No me hago responsable de nada, naturalmente porque aqui no violo absolu-
;  tamente nada. Al programa le entra un .raw (yo los hago con el alchemy)
;  y escupe un decoded.raw :)
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

;        includelib debugs.lib

;        debugg   =      1       ; Poner a 1 si incluyo el debug del DOS32
        stamp    =      1       ; Poner a 1 para el profiler
        s0      EQU     32
        xmax    EQU     640
        ymax    EQU     480
        cont    =       0

        CuentaFin MACRO
                pushad
                db      0Fh, 31h
                sub     eax,co_low
                sbb     edx,co_hi
                sub     eax,oh_low
                sbb     edx,0
                mov     baja,eax
                popad
        ENDM

        Contar MACRO
                pushad
                db      0Fh, 31h
                mov     ebx,eax
                db      0Fh, 31h
                sub     eax,ebx
                mov     oh_low,eax
                db      0Fh, 31h
                mov     co_low,eax
                mov     co_hi,edx
                popad
        ENDM

      ;if debugg
;        extrn Debug : near
     ; endif

.386P
.MODEL FLAT
.STACK 1024
.CODE
        caa     DB      8 DUP(0)
                DB      "$"
        oh_low  DD      ?       ; TIMESTAMP
        co_low  DD      ?
        co_hi   DD      ?

        baja            DD      0
        inicio          DB            ">>> DECODIFICADOR KUTUSKIANO v1.0 <<<"
                        DB      13,10,"             Setabia's Hell 1.996$"
        letras          DB      13,10,"Error: debes indicar el archivo a ser"
                        DB      " procesado",10,13,"$"
        buffer_ptr      DD      ?                ; puntero de cabecera
        Fichero_SI      DW      ?                ; manip del archivo fuente
        Fichero_DI      DW      ?                ; manip del archivo destino
        Error_min       DD      0FFFFFFFFh       ; error minimo encontrado
        Error_off       DD      ?                ; offset de la minerrorline
        size_fichero    DD      ?                ; tamanyo del fichero en bytes
        size_arg        DB      ?                ; longitud de los argumentos.
        print_fich      DB      "Fichero:           $"      ; Mensaje
        print_memo      DB      "Memoria requerida: $"
        argumentos      DB      0FFh DUP(0)      ; Linea de comando
        crr             DB      10,13,"$"        ; Retorno de carro
        dos_error       DB      "- Error del DOS -$"
        sin_memoria     DB      13,10,"No hay memoria libre",13,10,"$"
        size_cab        DD      ?                ; tamanyo de la cabecera
        fic_sal         DB      "decoded.raw",0

     comienzo:
;        if debugg
;                call    Debug
 ;       endif

        jmp     inicializa
     vuelve:
        call    cr

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

        mov     esi,buffer_ptr          ; se coge la primera linea
        mov     edi,xmax*s0
        add     edi,buffer_ptr
        xor     ebx,ebx
     er34:
        mov     al,[esi+ebx]
        xchg    al,[edi+ebx]
        mov     [esi+ebx],al
        inc     ebx
        cmp     ebx,xmax
        jna     er34

        mov     esi,buffer_ptr
     pilla:
        mov     edi,esi
        add     edi,xmax
     error_linea:
        mov     ebx,xmax-4
        xor     edx,edx
        xor     eax,eax

  ;      if stamp
        contar
   ;     endif

     busca_error:                               ; Bucle de busqueda de errores
        mov     al,[esi+ebx]
        sub     al,[edi+ebx]
        jns     oks
        neg     al
oks:
        add     dx,ax
        dec     ebx
        jnz     busca_error

    ;    if stamp
        cuentafin
     ;   endif

        cmp     edx,dword ptr error_min
        ja      sigue_buscando
        mov     dword ptr error_off,edi         ; Linea con maximo error
        mov     dword ptr error_min,edx
     sigue_buscando:
        mov     edx,buffer_ptr
        add     edx,xmax*ymax
        cmp     edi,edx
        jae     otra_linea
        add     edi,xmax
        jmp     error_linea
     otra_linea:
        add     esi,xmax
        mov     edi,dword ptr error_off
        xor     ebx,ebx                         ; Intercambio de lineas
     cambia_linea:
        mov     eax,dword ptr [esi+ebx]
        xchg    eax,dword ptr [edi+ebx]
        mov     dword ptr ds:[esi+ebx],eax
        add     bx,4
        cmp     bx,xmax
        jne     cambia_linea

        mov     dword ptr error_min,0FFFFFFFFh  ;(-1) Anyadir desplazamiento
        mov     edx,xmax*ymax                   ; de la siguiente linea
        add     edx,buffer_ptr
        cmp     esi,edx
        ja      final1
        jmp     pilla                           ; Aun quedan lineas


; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
     final1:
        mov     ah,40h                  ; Salva los datos a "decoded.raw"
        mov     bx,fichero_DI
        mov     ecx,size_fichero
        sub     ecx,size_cab
        mov     edx,buffer_ptr
        int     21h
        jc      errores
        mov     ax,3Eh                  ; Cierra el fichero destino
        mov     bx,word ptr fichero_DI
        jc      errores
     final_close:
        mov     ax,3Eh                  ; Cierra el fichero origen
        mov     bx,word ptr fichero_SI
        jc      errores
     final:
        call    Pon_cursor              ; Restaura el tamanyo del cursor

        mov     eax,baja

        mov     ah,4Ch                  ; Salida al DOS
        int     21h                     ; y libera la memoria usada

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; ALLOCATE: meter en size_fichero el tamanyo que quieras coger
;           EDX: puntero donde esta el bloque
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

     allocate PROC
        pushad
        mov     ax,0EE42h
        mov     edx,dword ptr size_fichero
        int     31h
        sub     eax,dword ptr size_fichero
        jz      allocate_ok
        xor     edx,edx
        stc
        jmp     fin_allocate
       allocate_ok:
        clc
       fin_allocate:
        popad
        ret
     allocate ENDP

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

     Quita_cursor PROC                  ; Esconde el cursor
        pushad
        mov     ch,32
        xor     cl,cl
        mov     ah,1
        int     10h
        popad
        ret
     Quita_cursor ENDP

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

     Pon_cursor PROC                    ; Restaura el cursor
        pushad
        mov     ch,6
        mov     cl,7
        mov     ah,1
        int     10h
        popad
        ret
     Pon_cursor ENDP

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

     CR PROC                            ; Imprime un retorno de carro
        push    edx
        push    ax
        mov     ah,9
        mov     edx,offset crr
        int     21h
        pop     ax
        pop     edx
        ret
     CR ENDP

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
;  ERRORHANDLER
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

     info:                              ; Como se usa el programa
        mov     ah,9
        mov     edx,offset letras
        int     21h
        jmp     final
     errores:                           ; Los errores del DOS
        mov     ah,9
        mov     edx,offset dos_error
        int     21h
        jmp     final
     no_mem:
        mov     ah,9
        mov     edx,offset sin_memoria
        int     21h
        jmp     final_close

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
     VAL32 PROC
        mov     cl,8
        xor     bl,bl
        xor     ch,ch
     cro:
        rol     eax,4
        mov     dl,al
        and     dl,00001111b
        cmp     bl,0
        jne     siguiendo
        cmp     dl,0
        setne   bl
     siguiendo:
        cmp     bl,1
        jne     loo
        cmp     dl,9
        ja      letra
        add     dl,48
        jmp     printer
     letra:
        add     dl,55
     printer:
        push    ax
        mov     ah,2
        int     21h
        pop     ax
     loo:
        loop    cro
        mov     dl,"h"
        mov     ah,2
        int     21h
        ret
     VAL32 ENDP

; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
     inicializa:
        mov     ax,3                    ; Modo texto
        int     10h
        push    ds                      ; Inicializa selectores
        pop     es
        call    Quita_cursor            ; Hace invisible el cursor
        call    cr                      ; Muestra informacion general
        mov     ah,9
        mov     edx,offset inicio
        int     21h
        call    cr
        mov     ax,0EE02h               ; Pilla los argumentos necesarios
        int     31h
        xor     ebx,ebx
        add     esi,80h
        mov     bl,[esi]
        add     esi,2
        push    esi
        add     esi,ebx
        dec     esi
        mov     al,0
        mov     [esi],al
        add     esi,1
        mov     al,"$"
        mov     [esi],al
        pop     esi
        push    esi
        call    cr
        mov     ah,9
        mov     edx,offset print_fich
        int     21h
        mov     edx,esi
        int     21h
        call    cr
        pop     esi
        mov     ax,3D02h                ; Abre el archivo
        xor     cx,cx
        mov     edx,esi
        int     21h
        jc      errores
        mov     word ptr fichero_SI,ax
        mov     ax,4202h                ; Coge el tamanyo del fichero fuente
        mov     edx,0
        mov     bx,word ptr fichero_SI
        int     21h
        jc      errores
        mov     dword ptr size_fichero,eax
        mov     edx,eax
        call    allocate                ; Pide memoria de acuerdo con el fich.
        mov     ax,0EE42h
        int     31h
        mov     dword ptr buffer_ptr,edx
        cmp     eax,0
        jz      no_mem
        push    eax
        mov     ah,9
        mov     edx,offset print_memo
        int     21h
        pop     eax
;        call    val32
        mov     edx,size_fichero        ; Tamanyo de la cabecera
        sub     edx,xmax*ymax
        mov     size_cab,edx
        mov     ax,4200h                ; Restaura al principio
        mov     edx,0
        mov     bx,word ptr fichero_SI
        int     21h
        jc      errores
        mov     ah,3Fh                  ; vuelca la cabecera hacia el buffer
        mov     bx,fichero_SI
        mov     ecx,size_cab
        mov     edx,dword ptr buffer_ptr
        int     21h
        jc      errores
        mov     ah,3Ch                 ; Crea el fichero de salida (y abre)
        mov     cx,0
        mov     edx,offset fic_sal
        int     21h
        jc      errores
        mov     ax,fichero_DI
        mov     ax,3D02h                ; Abre el archivo
        xor     cx,cx
        mov     edx,offset fic_sal
        int     21h
        jc      errores
        mov     word ptr fichero_DI,ax
        mov     ah,40h                  ; Salva la cabecera
        mov     bx,fichero_DI
        mov     ecx,size_cab
        mov     edx,buffer_ptr
        int     21h
        jc      errores
        mov     ah,3Fh                  ; vuelca los datos al buffer
        mov     bx,fichero_SI
        mov     ecx,size_fichero
        sub     ecx,size_cab
        mov     edx,dword ptr buffer_ptr
        int     21h
        jc      errores
        jmp     vuelve
END     comienzo

— |ncubuX / Setabia's Hell, 1.996. Nada de copyright, naturalmente.