Back in '96 I got the idea to try decoding Canal+'s scrambled still images in assembly — Canal+ being Spain's analog pay-TV channel at the time. The idea was clever. The execution, more or less. The result, gloriously crappy. Here's the story as the kid who did it would tell it — that kid being me, although I signed as |ncubuX to keep a bit of mystery going in the BBS scene.
01 — What Canal+ did to the picture
Canal+ in the 90s used a system called Nagravision (or Syster, depending on who you ask) to scramble the analog video signal. It didn't scramble the audio — that went out in the clear, because otherwise the official decoder boxes would have cost twice as much and people would have been even more furious.
What it did to the picture was basically two things:
1. Cut each horizontal line at a random point and swap the right-hand chunk over to the left (line cut). 2. Reorder the image's lines in a pseudorandom order that depended on the frame's key.
The key changed several times a second. To decode it "properly" you needed the key, and only the official decoder box had it (it received it encrypted on teletext line 16, if memory serves). I didn't have the key. Nobody on the BBS had the key. But there was one thing we did have: physics.
02 — The (brilliant) idea and the (huge) problem
In a natural image — a movie, a football match, whatever — adjacent horizontal lines look a lot like each other. Line 100 and line 101 are nearly identical because the world doesn't suddenly change from one vertical pixel to the next. This is called spatial correlation, and it's the foundation of almost all video compression.
So the idea was: if I have the lines all shuffled, I can try putting them back in order by finding, for each position, which line in the pile has the least difference from the previous one. The line that "fits best" is probably the one that belonged there.
For each line in the image (top to bottom):
For each candidate line in the buffer:
error = sum of |current_pixel - candidate_pixel|
The candidate with the lowest error => goes here
Swap
Elegant. There was just one problem, but a big one: you don't know which line is the first one. The Nagravision key determined both the line order AND the cut point of each line, and that kept changing frame by frame. So even if my algorithm sorted the lines perfectly from the first one onward... if the first one was wrong, the image still came out a mess.
The solution I came up with was to just try things out by hand: the constant s0 in the code was the initial offset — which line it took as the "starting point". I'd plug in a number (between 0 and 399), compile, run it, look at whether the image looked better or worse, and change it by hand. Old-school engineering.
03 — The minimum-error algorithm
The core of the program, in decent pseudocode:
; Swaps line s0 with the first line as a starting point
; (initial guess at which line is the real "first" one)
for each current_line (from the first to the last):
min_error = 0xFFFFFFFF
best_candidate = none
for each candidate_line in the buffer:
error = 0
for each pixel in the line (640 pixels):
error += abs(current_pixel[i] - candidate_pixel[i])
if error < min_error:
min_error = error
best_candidate = candidate_line
swap(current_line, best_candidate)
In practice, the error-calculation loop does it in 32 bits (it subtracts bytes but accumulates into DX), and it uses the 486's timestamp counter (RDTSC, opcode 0Fh 31h, which wasn't even officially documented yet in the manuals I had) to measure how long it took. That was the "profiler" the stamp constant at the top refers to.
04 — The code (DOS32 and all)
The program uses DOS32 as its protected mode extender. That's because a 640x480 image takes up 307,200 bytes, which doesn't fit in DOS real mode's 640KB. With DOS32 you could request extended memory via INT 31h (the DPMI interface) and work with 32-bit pointers in flat mode. No segmentation, no 64KB limits. Luxury.
The .ASM file's header said it all about the author and his intentions:
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░ ; 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" — roughly "however the hell it feels like" — was exactly the level of documentation you got back then. The program's flow:
inicializa:
Text mode (INT 10h), hide cursor
Show "DECODIFICADOR KUTUSKIANO v1.0 / Setabia's Hell 1.996"
Parse arguments (name of the input .raw)
Open the source file
Request extended memory via DPMI (INT 31h, function EE42h)
Read the header (size_fichero - 640*480 bytes)
Create and open decoded.raw
Write the header to the destination
Dump the data into the memory buffer
vuelve:
Swap line 0 with line s0 (initial guess)
For each line: find the minimum error and swap
Write the buffer to decoded.raw
Close files and exit
And the error-search loop, which is the heart of it:
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
Simple and direct. It computes the sum of absolute differences (SAD) between two 640-byte lines, accumulating into DX. If the result is smaller than the stored minimum, that becomes the new best candidate.
05 — Why it never fully worked
It worked. A little. More or less. Depends on the image.
The fundamental problem is that the greedy algorithm — "always pick the line with the lowest error against the previous one" — doesn't guarantee the globally optimal solution. Line 47 might be the best candidate for position 5, but if you "spend" it on position 3 because it also fit reasonably well there, then you're out of luck at position 5.
The second problem was the one I already mentioned: without knowing which line is line 0, you're playing the lottery with s0. On images with lots of motion or little vertical correlation (highly variable backgrounds) the algorithm went haywire. On calm images — a static shot from a movie, say — something recognizable would come out every once in a while.
The third problem is that Nagravision also cut each line horizontally at a random point, and I never handled that. So even when the line order came out correct, every single line was still split and glued back together the wrong way round. The result looked like an image seen through venetian blinds hung by a madman.
Was it good for watching football? No. Was it good for learning protected mode, DPMI, RDTSC and minimum-error algorithms? Absolutely yes. And that's what it was for.
06 — The complete source (P2011.ASM)
In case anyone feels like reading it. Without changing a single comma.
; ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
; 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. No copyright, naturally.