wororo.

esteganografia

2017/09/30

Esteganografía

Este proyecto consiste en esconder texto dentro de una imagen de tal forma que tanto el texto como la imagen no se vean (afectada en el caso de la imagen) a simple vista. Dicha técnica se conoce como Esteganografía.

Se trabajará con Python para generar un mensaje codificado, de tal forma que contenta el número de bits significativos de la imagen sobre los que se esconde el texto, el texto como tal y un mensaje de fin. Dicho mensaje de término se agregó para indicarle al código que la lectura debería llegar hasta dicho punto.

Repo en Github

Reporte en Overleaf

Introducción

La Esteganografía existe desde hace siglos. Según Wikipedia se aplicó en las primeras veces por Heródoto en el libro de las Historias:

“En este libro describe cómo un personaje tomó un cuadernillo de dos hojas o tablillas; rayó bien la cera que las cubría y en la madera misma grabó el mensaje y lo volvió a cubrir con cera regular.

Otra historia, en el mismo libro, relata cómo otro personaje había rasurado a navaja la cabeza de su esclavo de mayor confianza, le tatuó el mensaje en el cuero cabelludo, esperó después a que le volviera a crecer el cabello y lo mandó al receptor del mensaje, con instrucciones de que le rasuraran la cabeza.”

Imports

1
from skimage import io

Imagen original

Para abrir la imagen y mostrarla, ocuparemos scikit-image.io.imread()

1
2
imageFilename = r"imagen.png"
image = io.imread(imageFilename)

Preview

1
2
io.imshow(image)
io.show()

png

Lectura del archivo

El archivo se puede ver como un arreglo.

En este caso se trata de una imagen en blanco y negro, por lo que el arreglo tiene un shape de 248x300.

Imagen como Array

1
2
print(image.shape)
image
(246, 300)





array([[49, 50, 48, ..., 58, 55, 55],
       [47, 47, 49, ..., 56, 57, 55],
       [48, 48, 49, ..., 57, 56, 57],
       ..., 
       [71, 70, 70, ..., 70, 69, 70],
       [69, 71, 71, ..., 70, 72, 71],
       [69, 68, 69, ..., 71, 72, 71]], dtype=uint8)

Idea central

Imagen

Cada pixel de la imagen corresponde a un número del arreglo:

[49 50 48 … 58 55 55]

Corresponde a la primera fila de la imagen.

Binario

Cada uno de esos bits se puede representar como un número binario:

49, por ejemplo es 00110001 en binario. De esta forma, la imagen quedará como:

[ 00110001 00110010 00110000 … 00111010 00110111 00110111 ]

La idea en esteganografía, es esconder el texto en la imagen, usando los bits menos significativos.

Máscara

Si se utilizaran 2 bit significativo, los bits que se ocuparían serían los que tienen x:

[ 001100xx 001100xx 001100xx … 001110xx 001101xx 001101xx ]

Texto

Así, para esconder una letra, por ejemplo la “h”, se debe convertir esta imagen a un número.

El número ASCII para la h es 104.

104 en binario, sería: 01101000

Esteganografía

Lo que implica, que se pueden colocar 2 números en cada pixel de la imagen

h: 01 10 10 00 en [ 001100xx 001100xx 001100xx … 001110xx 001101xx 001101xx ]:

[ 00110001 00110010 00110010 … ]

Haciendo esto y ocupando un número de bits bajo, el texto puede quedar escondido en la imagen, sin que sea vea a simple vista.

Texto de prueba

El texto se carga por open, con permisos unicamente de lectura.

1
2
3
textFilename = r"texto.txt"
text = open(textFilename,'r').read()
print(text)
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus auctor maximus dignissim. Etiam luctus elit nec risus varius volutpat. Vivamus arcu ante, tempus nec luctus commodo, blandit vel neque. Pellentesque semper posuere purus, et finibus leo accumsan non. Sed sed felis vel erat pretium molestie. Nulla facilisi. Proin vel lacus et leo tristique luctus eu nec nulla. Nullam finibus lobortis porta. Vestibulum sit amet lacus eros. Aliquam tempus augue quis lacus gravida aliquet.

Maecenas eget purus est. Etiam ac lectus ac magna commodo molestie in eget ante. Vestibulum porta mauris nec risus finibus, nec elementum purus vulputate. Nunc non eros lobortis, vestibulum nibh eget, sagittis justo. Suspendisse eget tellus vitae dolor fermentum elementum. Nulla luctus nulla vel volutpat dictum. Morbi ac nulla interdum, semper lacus quis, eleifend eros.

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse potenti. Suspendisse potenti. Cras eget massa urna. Pellentesque quam neque, euismod sagittis eros sed, consequat fringilla augue. In ac euismod diam. Morbi maximus semper erat, pulvinar efficitur ligula congue eu. Donec ante arcu, feugiat sit amet tincidunt in, interdum id nibh. In non posuere nisl. Vivamus vitae justo in tellus tincidunt facilisis in ut dui. Vestibulum sed mauris aliquet, blandit lacus sit amet, ultrices nisl. Aliquam eget pharetra mi, eget venenatis enim. Vivamus a placerat enim.

Donec varius aliquet velit, id mollis massa ultrices sit amet. Mauris lacus nunc, euismod non dignissim at, consectetur vitae enim. Phasellus volutpat leo urna, a posuere felis volutpat quis. Ut elementum urna sed arcu fermentum ultricies. Mauris at massa diam. Vestibulum id ipsum rutrum, ultrices metus id, pharetra tortor. Vestibulum lacinia odio nec faucibus lobortis. Curabitur convallis, velit quis feugiat scelerisque, eros elit lobortis tellus, non tempor lectus dolor non risus. Suspendisse a condimentum mauris. Cras finibus nec lacus at consequat. Donec et neque et nibh placerat efficitur vel non magna. Mauris augue nunc, placerat quis tellus eu, viverra fermentum quam. Mauris at metus pretium, rhoncus nisi vitae, scelerisque elit.

In massa erat, dignissim quis enim in, egestas aliquam dolor. Morbi condimentum blandit placerat. Cras ultricies, nisi in tincidunt facilisis, lacus nibh iaculis mi, vitae cursus quam sem sit amet urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed nisi lectus, placerat at scelerisque sollicitudin, cursus eget erat. Cras eu volutpat nisl, sit amet tincidunt nisl. Proin a elit elit. Sed et lectus fermentum, congue est vel, mattis mi.

Codificación

El proceso de codificación y decodificación se realizarán por medio de operaciones a nivel de bits. El procedimiento se podría resumir de la siguiente manera:

- Calcular la máscara
- Generar una lista de bits que contienen el texto codificado
- Aplicar la codificacion a la imagen

Calculo de la máscara

Para calculár la máscara, se definirá una variable que indica el número de bits (nbit) a usar. Para el valor de la máscara, se calcula el valor máximo para un byte/pixel (0-255) y se le resta el $2^nbits$. De esta forma, por ejemplo, para un nbit de 3 se tiene una máscara de 0b 1111 1011.

Representación binaria de máscara

1
2
nbits = 4
"{0:b}".format(nbits)
'100'

De largo 8

1
"{0:{fill}8b}".format(nbits, fill='0')
'00000100'

Completando los números anteriores

1
2
trailing = 2**(int(nbits))-1
trailing
15
1
"{0:{fill}8b}".format(trailing, fill='0')
'00001111'

A la inversa

1
2
inv = 255 - (2**(int(nbits))-1)
inv
240
1
"{0:{fill}8b}".format(inv, fill='0')
'11110000'

Refactor

1
2
def getMask(nbits):
return 255-(2**(int(nbits)-1))

Así, las máscaras que ocuparemos para los distintos bytes serán:

Test

1
2
for i in range(1,9):
print(bin(getMask(i)))
0b11111110
0b11111101
0b11111011
0b11110111
0b11101111
0b11011111
0b10111111
0b1111111

Codificación del texto a bits

La generación de una lista con el texto codificado está compuesta de 3 partes:

  • Agregar 4 bits que indican la cantidad de bits significativos con los que se encriptó la imagen.
  • Convertir los caracteres (bytes) a bits.
  • Agregar una marca para indicar el final del archivo.

El proceso final, debe dar por resultado una imagen del siguiente tipo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
%%html
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
.tg .tg-a080{background-color:#9aff99;vertical-align:top}
.tg .tg-gr78{background-color:#ffce93;vertical-align:top}
.tg .tg-ufe5{background-color:#34cdf9;vertical-align:top}
</style>
<table class="tg">
<tr>
<th class="tg-a080">Numero de bits</th>
<th class="tg-gr78">-----------------Texto Codificado-----------------</th>
<th class="tg-ufe5">Final del texto</th>
</tr>
</table>







Numero de bits —————–Texto Codificado—————– Final del texto

Texto Codificado

Convertir los caracteres a bits se logra de la misma manera. En la medida que se desplazan ($>>$) y se aislan ($\& 1$) se van agregando a la lista.

El código se puede apreciar a continuación.

Caracter a codificar

1
2
test = "h"
test
'h'

En números

1
2
char = ord(test)
char
104

En Binario

1
"{0:{fill}8b}".format(char, fill='0')
'01101000'

A Array

1
2
3
4
byte = []
for i in range(0,8):
byte.insert(0, char & 1)
char = char >> 1
1
byte
[0, 1, 1, 0, 1, 0, 0, 0]

Con un texto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getEncodedText(nbits, text):
encodedText = []
#Convertir cada caracter a bits y agregarlos a una lista:
for char in text:
byte = []
tmp = ord(char)
for i in range(0,8):
byte.append(tmp & 1)
tmp = tmp >> 1
# Agregar bytes a la lista:
[encodedText.append(i) for i in byte[::-1]]
return encodedText
1
2
3
4
5
def testEncoding(letter):
print("letter\t", letter)
print("Ordinal\t", ord(letter))
print("Binary\t", bin(ord(letter)))
print("Encode\t", getEncodedText(0, letter))
1
testEncoding("h")
letter     h
Ordinal     104
Binary     0b1101000
Encode     [0, 1, 1, 0, 1, 0, 0, 0]

Número de bits

Para agregar el número de bits significativos con los que se encriptó la imagen se desplaza hacia la derecha 1 uno la cantidad de veces que se especifica con nBits. El proceso se realiza con los operadores “>>” y $”\&”$. El código se aprecia a continuación:

1
2
3
4
5
6
7
8
9
def getEncodedNbits(nbits):
encodedNbits = []
tmp = int(nbits)
for i in range(0,4):
encodedNbits.append(tmp & 1)
tmp = tmp >> 1
return encodedNbits

Test

1
2
for i in range(0,32):
print(i,"\t", getEncodedNbits(i))
0      [0, 0, 0, 0]
1      [1, 0, 0, 0]
2      [0, 1, 0, 0]
3      [1, 1, 0, 0]
4      [0, 0, 1, 0]
5      [1, 0, 1, 0]
6      [0, 1, 1, 0]
7      [1, 1, 1, 0]
8      [0, 0, 0, 1]
9      [1, 0, 0, 1]
10      [0, 1, 0, 1]
11      [1, 1, 0, 1]
12      [0, 0, 1, 1]
13      [1, 0, 1, 1]
14      [0, 1, 1, 1]
15      [1, 1, 1, 1]
16      [0, 0, 0, 0]
17      [1, 0, 0, 0]
18      [0, 1, 0, 0]
19      [1, 1, 0, 0]
20      [0, 0, 1, 0]
21      [1, 0, 1, 0]
22      [0, 1, 1, 0]
23      [1, 1, 1, 0]
24      [0, 0, 0, 1]
25      [1, 0, 0, 1]
26      [0, 1, 0, 1]
27      [1, 1, 0, 1]
28      [0, 0, 1, 1]
29      [1, 0, 1, 1]
30      [0, 1, 1, 1]
31      [1, 1, 1, 1]

Final del Texto

El mismo proceso se ocupara para la marca del EOF. En ese caso se ocupó la siguiente variable.

1
END_OF_FILE = "$EOF"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def getEncodedText(nbits, text):
encodedText = []
#Convertir cada caracter a bits y agregarlos a una lista:
for char in text:
byte = []
tmp = ord(char)
for i in range(0,8):
byte.append(tmp & 1)
tmp = tmp >> 1
# Agregar byts a la lista:
[encodedText.append(i) for i in byte[::-1]]
# Convertir cada caracter a bits y agregarlos a una lista:
for char in END_OF_FILE:
byte = []
tmp = ord(char)
for i in range(0, 8):
byte.append(tmp & 1)
tmp = tmp >> 1
# Agregar byts a la lista:
[encodedText.append(i) for i in byte[::-1]]
#Insertar al inicio los NBits para poder después decriptar:
[encodedText.insert(0, i) for i in getEncodedNbits(nbits)]
return encodedText

Codificación del texto en la imagen

Para aplicar la codificación a la imagen se recorre la imagen como un arreglo de dos dimensiones, entendiendo que cada pixel se comporta como un byte, que compone una matriz que describe los distintos tonos de grises que componen la imagen. En este loop se aplica la máscara a cada byte y se agrega el texto desde la lista calculada anteriormente. El proceso se realiza por el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def encode(imageFilename, textFilename, nbits):
image = io.imread(imageFilename)
text = open(textFilename,'r').read()
mask = getMask(nbits)
encodedText = getEncodedText(nbits, text)
textCount = 0
for row in range(0,len(image)):
for column in range(0,len(image[row])):
#TODO: Check que el texto entre en la imagen; Tirar exception;
if(textCount < len(encodedText)):
if(textCount < 4):
image[row][column] = (image[row][column] & getMask(1)) | (encodedText[textCount])
else:
image[row][column] = (image[row][column] & mask) | (encodedText[textCount] << int(nbits)-1)
textCount += 1
return image

Pruebas de codificación

Ha de notarse que los primeros 4 bits, que describen los nBits, se aplican siempre al bit menos significativo para que el decodificador los pueda leer desde la misma entrada. Como se revisará más adelante, la idea de agregarlos en el LSB es no alterer visualmente la imagen.

El resultado de aplicar la codificación tiene distintos resultados según el bit significativo que se ocupe. Es recomendable aplicarlo para un N<=2. Para N mayores, la imagen empieza a distorcionarse por la modificación de los colores de la misma. En las siguientes imagenes se puede apreciar los distintos resultados:

1
2
3
image = encode(imageFilename, textFilename, 1)
io.imshow(image)
io.show()

png

1
2
3
image = encode(imageFilename, textFilename, 4)
io.imshow(image)
io.show()

png

1
2
3
image = encode(imageFilename, textFilename, 5)
io.imshow(image)
io.show()

png

1
2
3
image = encode(imageFilename, textFilename, 8)
io.imshow(image)
io.show()

png

Decodificación

Para la decodificación, se realiza el proceso inverso. En primera instancia se leen los nbits de los primeros 4 bits y posteriormente se lee el resto de los caracteres a un arreglo. Se termina este proceso cuando se encuentra el EOF designado. El proceso se puede apreciar por el siguiente código:

1
2
def bitstring_to_bytes(s):
return int(s, 2).to_bytes(len(s) // 8, byteorder='big')

Decodificar texto

1
2
3
4
5
def decodeText(encodedText):
encodedTextNoBits = encodedText
bitString = "".join([str(i) for i in encodedTextNoBits])
decodedText = str(bitstring_to_bytes(bitString))[2:-1]
return decodedText

Decodificación de imagen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def decode(imageFilename):
image = io.imread(imageFilename)
encodedText = []
textCount = 0
nbits = 0
for row in range(0,len(image)):
for column in range(0,len(image[row])):
if textCount < 4:
encodedText.append(image[row][column] & 1)
else:
if textCount == 4:
nbits = 8*encodedText[0] + 4*encodedText[1] + 2*encodedText[2]+ 1*encodedText[3]
encodedText.append((image[row][column] >> (nbits-1)) & 1)
if((textCount+5)%8 == 0 and textCount > 36):
if(decodeText(encodedText[-32:]) == END_OF_FILE):
return decodeText(encodedText[4:-32])
textCount += 1

Pruebas de decodificación

1
print(decode("imagen_out.png"))
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus auctor maximus dignissim. Etiam luctus elit nec risus varius volutpat. Vivamus arcu ante, tempus nec luctus commodo, blandit vel neque. Pellentesque semper posuere purus, et finibus leo accumsan non. Sed sed felis vel erat pretium molestie. Nulla facilisi. Proin vel lacus et leo tristique luctus eu nec nulla. Nullam finibus lobortis porta. Vestibulum sit amet lacus eros. Aliquam tempus augue quis lacus gravida aliquet.\n\nMaecenas eget purus est. Etiam ac lectus ac magna commodo molestie in eget ante. Vestibulum porta mauris nec risus finibus, nec elementum purus vulputate. Nunc non eros lobortis, vestibulum nibh eget, sagittis justo. Suspendisse eget tellus vitae dolor fermentum elementum. Nulla luctus nulla vel volutpat dictum. Morbi ac nulla interdum, semper lacus quis, eleifend eros.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse potenti. Suspendisse potenti. Cras eget massa urna. Pellentesque quam neque, euismod sagittis eros sed, consequat fringilla augue. In ac euismod diam. Morbi maximus semper erat, pulvinar efficitur ligula congue eu. Donec ante arcu, feugiat sit amet tincidunt in, interdum id nibh. In non posuere nisl. Vivamus vitae justo in tellus tincidunt facilisis in ut dui. Vestibulum sed mauris aliquet, blandit lacus sit amet, ultrices nisl. Aliquam eget pharetra mi, eget venenatis enim. Vivamus a placerat enim.\n\nDonec varius aliquet velit, id mollis massa ultrices sit amet. Mauris lacus nunc, euismod non dignissim at, consectetur vitae enim. Phasellus volutpat leo urna, a posuere felis volutpat quis. Ut elementum urna sed arcu fermentum ultricies. Mauris at massa diam. Vestibulum id ipsum rutrum, ultrices metus id, pharetra tortor. Vestibulum lacinia odio nec faucibus lobortis. Curabitur convallis, velit quis feugiat scelerisque, eros elit lobortis tellus, non tempor lectus dolor non risus. Suspendisse a condimentum mauris. Cras finibus nec lacus at consequat. Donec et neque et nibh placerat efficitur vel non magna. Mauris augue nunc, placerat quis tellus eu, viverra fermentum quam. Mauris at metus pretium, rhoncus nisi vitae, scelerisque elit.\n\nIn massa erat, dignissim quis enim in, egestas aliquam dolor. Morbi condimentum blandit placerat. Cras ultricies, nisi in tincidunt facilisis, lacus nibh iaculis mi, vitae cursus quam sem sit amet urna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed nisi lectus, placerat at scelerisque sollicitudin, cursus eget erat. Cras eu volutpat nisl, sit amet tincidunt nisl. Proin a elit elit. Sed et lectus fermentum, congue est vel, mattis mi.

Conclusiones

El proceso de codificación de texto en imágenes permite esconder texto. Una de las limitaciones principales tiene que ver con que el texto que se utilice debe ser menor en longitud al tamaño de la imagen.

Lo anterior podría compensarse cuando se utilizan imánenes en colores ya que se triplica la cantidad de bytes disponibles para la codificación. Si se ocupan imágenes con transparencias (alpha). Podría agregarse una dimensión más, siempre y cuando considerando que sería más oportuno hacerlo con el bit menos significativo.

Uno de los principales problemas para manejar la encriptación tuvo que ver con el órden de los bits en la lista de los bits.

Otro punto interesante fue el delimitador final que se utilizó. Se considera que podria ser posible incluir el tamaño de la imagen al principio, sin embargo esto sería variable dependiento del tamaño.

Finalmente se puede mencionar que existen varios paquetes públicos para realizar esta encriptación. Algunos con varias opciones de configuración.

CATALOG
  1. 1. Esteganografía
  2. 2. Introducción
  3. 3. Imports
  4. 4. Imagen original
    1. 4.1. Preview
    2. 4.2. Lectura del archivo
    3. 4.3. Imagen como Array
    4. 4.4. Idea central
  5. 5. Texto de prueba
  6. 6. Codificación
    1. 6.1. Calculo de la máscara
      1. 6.1.1. Representación binaria de máscara
      2. 6.1.2. De largo 8
      3. 6.1.3. Completando los números anteriores
      4. 6.1.4. A la inversa
    2. 6.2. Refactor
    3. 6.3. Test
    4. 6.4. Codificación del texto a bits
    5. 6.5. Texto Codificado
      1. 6.5.1. Caracter a codificar
      2. 6.5.2. En números
      3. 6.5.3. En Binario
      4. 6.5.4. A Array
      5. 6.5.5. Con un texto
    6. 6.6. Número de bits
      1. 6.6.1. Test
    7. 6.7. Final del Texto
    8. 6.8. Codificación del texto en la imagen
    9. 6.9. Pruebas de codificación
  7. 7. Decodificación
    1. 7.1. Decodificar texto
    2. 7.2. Decodificación de imagen
    3. 7.3. Pruebas de decodificación
  8. 8. Conclusiones