Di cosa si tratta?
Riprendendo l’idea di inviare dati a un server esterno simulando il caricamento di un’immagine [Leggi l’articolo “SVG Keylogger”], ho deciso di estendere la tecnica per riuscire anche a ricevere dati da remoto senza usare le normali chiamate Ajax. Per fare ciò, sono ricorso all’antichissima ma sempre valida tecnica della “steganografia“.
Ma perché tanto sbattimento? Perché così facendo si bypassa il CORS ed altri controlli standard di sicurezza del browser.
Cenni sulla steganografia
“La steganografia è una tecnica che si prefigge di nascondere la comunicazione tra due interlocutori. […] Generalmente, i messaggi nascosti sembrano essere (o fanno parte di) qualcos’altro: immagini, articoli, liste della spesa o un altro testo di copertura. Ad esempio, il messaggio nascosto può essere un inchiostro invisibile tra le linee di una lettera privata. […] La steganografia, a differenza della crittografia, consente di nascondere un messaggio all’interno di un vettore che possa consentirne il trasporto senza destare sospetti” – Cit. Wikipedia
La stenografia nella storia
Un esempio curioso di uso della steganografia nella storia è quello narrato da Erodoto: Demarato di Sparta, per avvisare i compatrioti di una possibile invasione persiana scrisse su una tavoletta un messaggio da nascondere, poi coprì la tavoletta di cera e sulla cera scrisse un messaggio innocuo. Dato che nell’antichità le tavolette di cera erano normalmente usate per scrivere testi provvisori, questa non destò sospetti. (Per chi avesse visto l’ultimo Indiana Jones “Il quadrante del destino”, la scena sulla barca in cui scoprono il messaggio nascosto nella tavoletta di cera)
Steganografia digitale
Per la steganografia digitale esistono molteplici sistemi di implementazione, quello più utilizzato di solito è il nascondere il messaggio all’interno di un’immagine sostituendo ogni carattere nel bit meno significativo di ogni pixel. In parole povere, il colore dei pixel modificati varierebbe talmente poco che all’occhio umano non risulterebbe nessuna differenza apparente tra l’immagine originale e quella contenente il messaggio.
(per dettagli invito come sempre ad approfondire a parte)
Mani al codice
Il progetto è sviluppato in NodeJs e si occuapa anche della creazione e gestione del webserver, ed è composto da due applicativi distinti: la parte client e la parte server. Perché questa scelta? Per emulare la comunicazione tra due domini distinti e superare i limiti del CORS (il server risponde sulla porta 8080 e il client sulla porta 7070).
PS: Il progetto inoltre è stato sviluppato nel tempo di qualche ora in una notte in cui quest’idea non mi faceva dormire finché non avessi provato a realizzarla, quindi è tutto molto “grezzo” e ridotto all’osso solo per dimostrarne il funzionamento.
Server
L’applicativo server espone un’api che prende in input il testo da codificare e ci genera un’immagine (“text_to_image”). Per semplicità, la routine non inserisce il messaggio in un’immagine già esistente ma ne genera una nuova codificando ogni carattere del messaggio nelle componenti colore di ogni pixel.
Es. codifica messaggio “ABC”:
A => codice ascii 0x41 (65)
B => codice ascii 0x42 (66)
C => codice ascii 0x43 (67)
Colore del pixel risultante: RGBA(65,66,67,255) (alpha sempre al massimo!)
Passiamo all’api che si occupa di intercettare la chiamata, generare l’immagine con il testo ricevuto e inviarla in risposta al client:
app.get('/text.jpg', (req, res) =>
{
let _p = req.query['p'];
let _buffer = text_to_image(_p);
res.contentType('image/jpeg');
res.send(_buffer);
});
Ed ora la routine che si occupa del lavoro sporco, cioè quello di prendere il testo in chiaro e splittarlo nei vari pixel di una nuova immagine generata a runtime:
function text_to_image(text)
{
const width = Math.ceil(text.length / 3);
const height = 1;
const canvas = createCanvas(width, height);
const context = canvas.getContext("2d");
let _char_index = 0;
let _x = 0;
let _y = 0;
while(_char_index < text.length)
{
let _image_data = context.createImageData(1, 1);
let _pixel_data = _image_data.data;
for(let _byte_index = 0; _byte_index < 3; _byte_index++)
{
let _ascii_code = (_char_index >= text.length ? 0 : text.charCodeAt(_char_index));
_pixel_data[_byte_index] = _ascii_code;
_char_index++;
}
_pixel_data[3] = 255; // alpha a 255 è fondamentale altrimenti i colori non vengono decodificati!
context.putImageData(_image_data, _x, _y);
_x++;
}
const buffer = canvas.toBuffer("image/png");
return buffer;
}
Spiegazione
La routine prende il testo passato come parametro e genera un oggetto “Canvas” monodimensionale (per semplicità tutti i pixel generati staranno sullo stesso asse). A quel punto scorre ogni elemento del testo da nascondere e ne inserisce il codice ascii nelle componenti colore dei vari pixel in modo sequenziale. Alla fine la routine restituisce un buffer contenente i byte dell’immagine in formato “png”, pronto da essere inviato nel buffer di risposta della chiamata web.
Client
Il principio di funzionamento di base si riaggancia a quello già discusso nell’articolo sopra citato [Leggi l’articolo “SVG Keylogger”], cioè viene generato un nuovo oggetto “img” a cui viene impostata la proprietà “src” con l’url dell’api remota. Il limite è che essendo un oggetto immagine, e la risposta ricevuta è un’immagine (valida o no che sia), non si può interpretare la risposta direttamente come se fosse del semplice testo (json o altri formati).
Di conseguenza, per estrarre la risposta ricevuta dal server, bisogna effettuare il processo inverso e cioè scorrere ogni pixel e riconvertire il valore di ogni componente colore in caratteri ascii.
Chiaramente per fare questo è necessario prima impostare una callback all’evento “onload” dell’oggetto “img” creato, altrimenti non sapremo mai con certezza quando il browser ha completato il caricamento dell’immagine con la nostra risposta.
Di seguito il cuore del meccanisco che si occupa di effettuare la chiamata e recuperarne la risposta è inserito all’interno della routine “call” che accetta in input il testo da inviare all’api remota e un’eventuale callback alla quale verrà passata la risposta.
function call(parameter, callback)
{
var img = new Image();
img.src = 'http://localhost:8080/text.jpg?p=' + encodeURIComponent(parameter);
img.crossOrigin = "anonymous"; // Essenziale per bypass CORS!
img.onload = function()
{
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
let _text = "";
console.log('Image width: ' + img.width);
console.log('Image height: ' + img.height);
let _image_data = ctx.getImageData(0, 0, img.width, img.height);
console.log(_image_data);
let _srgb_index = 0;
for(let _byte_index = 0; _byte_index < img.width * 4; _byte_index++)
{
if(_srgb_index < 3)
{
let _char_code = _image_data.data[_byte_index];
if(_char_code == 0)
{
// EOF
console.log("EOF!");
break;
}
//console.log(_image_data);
_text += String.fromCharCode(_char_code);
_srgb_index++;
}
else
{
_srgb_index = 0;
}
}
console.log(_text);
if(callback != undefined)
{
callback(_text);
}
};
}
Nota fondamentale: è essenziale che all’oggetto “img” creato per effettuare la chiamata, venga impostata la proprietà “crossOrigin” con il valore “anonymous“, altrimenti il solito CORS bloccherà la chiamata e tutto finirà in un nulla di fatto.
img.crossOrigin = "anonymous"; // Essenziale per bypass CORS!
Link al codice
Have fun! 🙂