15
\$\begingroup\$

This is my implementation of a prng steganography tool written in Python. You can also find the code on GitHub.

Steganography is the art of hiding messages in (images, videos or even audio)

I've decided to make my own steganography program because I wanted to understand how it works. Instead of just using a tool to get what I want, I wanted to fully grasp how these tools work.

My first programs were quite simple but after a few iterations I've come up with a decent program I think. I make use of Cryptography tools to make it even harder to extract the hidden message from the program by encrypting the message with a password. Lastly I added some randomness so each bit from the message to hide is put in a pseudo-random location of the image. The pseudo-random location is also dependend on the password.

It works by seeding the random module and getting the next unique random integer with that seed. Then it puts the bit of the encrypted text in the lsb [Least Significant Bit]. Decryption works sort of the same and at decryption it knows when to stop looking for bits if it finds the endbyte [0]*7

# Imports
from PIL import Image
import numpy as np
import sys
import os
import getopt
import base64
import random
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend


# Set location of directory we are working in to load/save files
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))


# Methods by Cryptography module used for encrypting and decrypting the message
def get_key(password):
    digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
    digest.update(password)
    return base64.urlsafe_b64encode(digest.finalize())


def encrypt_text(password, token):
    f = Fernet(get_key(password))
    return f.encrypt(bytes(token))


def decrypt_text(password, token):
    f = Fernet(get_key(password))
    return f.decrypt(bytes(token))


# Main encryption method
def encrypt(filename, text, magic):
    # check whether the text is a file name
    if len(text.split('.')[1:]):
        text = read_files(os.path.join(__location__, text))
    t = [int(x) for x in ''.join(text_ascii(encrypt_text(magic, text)))] + [0]*7  # endbit
    try:
        # Change format to png because .jpeg and .bmp extensions do not work
        filename = change_image_form(filename)

        # Load Image
        d_old = load_image(filename)

        # Check if image can contain the data
        if d_old.size < len(t):
            print '[*] Image not big enough'
            sys.exit(0)

        # get new data and save to image
        d_new = encrypt_lsb(d_old, magic, t)
        save_image(d_new, 'new_'+filename)
    except Exception, e:
        print str(e)


# Main decryption method
def decrypt(filename, magic):
    try:
        # Load image
        data = load_image(filename)

        # Retrieve text
        text = decrypt_lsb(data, magic)
        print '[*] Retrieved text: \n%s' % decrypt_text(magic, text)
    except Exception, e:
        print str(e)


# Random methods used in program
def text_ascii(text):
    return map(lambda char: '{:07b}'.format(ord(char)), text)


def ascii_text(byte_char):
    return chr(int(byte_char, 2))


def next_random(random_list, data):
    next_random_number = random.randint(0, data.size-1)
    while next_random_number in random_list:
        next_random_number = random.randint(0, data.size-1)
    return next_random_number


def generate_seed(magic):
    seed = 1
    for char in magic:
        seed *= ord(char)
    print '[*] Your magic number is %d' % seed
    return seed



# Encrypt via lsb method
def encrypt_lsb(data, magic, text):
    print '[*] Starting Encryption'

    # We must alter the seed but for now lets make it simple
    random.seed(generate_seed(magic))

    random_list = []
    for i in range(len(text)):
        next_random_number = next_random(random_list, data)
        random_list.append(next_random_number)
        data.flat[next_random_number] = (data.flat[next_random_number] & ~1) | text[i]

    print '[*] Finished Encryption'
    return data


# Decrypt via lsb method
def decrypt_lsb(data, magic):
    print '[*] Starting Decryption'
    random.seed(generate_seed(magic))

    random_list = []
    output = temp_char = ''

    for i in range(data.size):
        next_random_number = next_random(random_list, data)
        random_list.append(next_random_number)
        temp_char += str(data.flat[next_random_number] & 1)
        if len(temp_char) == 7:
            if int(temp_char) > 0:
                output += ascii_text(temp_char)
                temp_char = ''
            else:
                print '[*] Finished Decryption'
                return output


# File handling methods
def load_image(filename):
    img = Image.open(os.path.join(__location__, filename))
    img.load()
    data = np.asarray(img, dtype="int32")
    return data


def save_image(npdata, outfilename):
    img = Image.fromarray(np.asarray(np.clip(npdata, 0, 255), dtype="uint8"), "RGB")
    img.save(os.path.join(__location__, outfilename))


def change_image_form(filename):
    f = filename.split('.')
    if not (f[-1] == 'bmp' or f[-1] == 'BMP' or f[-1] == 'PNG' or f[-1] == 'png'):
        img = Image.open(os.path.join(__location__, filename))
        img.load()
        filename = ''.join(f[:-1]) + '.png'
        img.save(os.path.join(__location__, filename))
    return filename


def read_files(filename):
    if os.path.exists(filename):
        with open(filename, 'r') as f:
            return ''.join([i for i in f])
    return filename.replace(__location__, '')[1:]


# Usage and main
def usage():
    print "Steganography prng-Tool @Ludisposed & @Qin"
    print ""
    print "Usage: prng_stego.py -e -m magic filename text "
    print "-e --encrypt              - encrypt filename with text"
    print "-d --decrypt              - decrypt filename"
    print "-m --magic                - encrypt/decrypt with password"
    print ""
    print ""
    print "Examples: "
    print "python prng_stego.py -e -m pass test.png howareyou"
    print 'python prng_stego.py -e -m magic test.png tester.sh'
    print 'python prng_stego.py -e -m magic test.png file_test.txt'
    print 'python prng_stego.py --encrypt --magic password test.png "howareyou  some other text"'
    print ''
    print "python prng_stego.py -d -m password test.png"
    print "python prng_stego.py --decrypt --magic password test.png"
    sys.exit(0)

if __name__ == "__main__":
    if not len(sys.argv[1:]):
        usage()
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hedm:", ["help", "encrypt", "decrypt", "magic="])
    except getopt.GetoptError as err:
        print str(err)
        usage()

    magic = to_encrypt = None
    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
        elif o in ("-e", "--encrypt"):
            to_encrypt = True
        elif o in ("-d", "--decrypt"):
            to_encrypt = False
        elif o in ("-m", "--magic"):
            magic = a
        else:
            assert False, "Unhandled Option"

    if magic is None or to_encrypt is None:
        usage()

    if not to_encrypt:
        filename = args[0]
        decrypt(filename, magic)
    else:
        filename = args[0]
        text = args[1]
        encrypt(filename, text, magic) 

Things I'd like to improve

  • Any general Coding tips are welcome!
  • I would also love some tips on how the use the magic to create a wierd seed.
  • Furthermore I'm interested in how Random this is, to my understanding the random module uses Mersenne Twister implementation and that is not cryptographically secure.
  • Lastly I think this is harder to decrypt then just normal lsb_stego, but can not prove anything.

EDIT I've made a follow up-questions which can be found here

\$\endgroup\$

3 Answers 3

12
\$\begingroup\$

Your next_random function can be improved a lot. This function will get slower and slower, the more often you call it, because in is \$\mathcal{O}(n)\$ for lists. In addition the while loop will run more often as well, because more values have already been taken.

If your size is not too big (less than a million or so), you could just create a list with all possible values, shuffle it and yield from that list:

def random_ints(size):
    values = range(size)
    random.shuffle(values)
    for value in values:
        yield value

The usage of this is slightly different:

def encrypt_lsb(image_data, magic, text):
    print '[*] Starting Encryption'
    assert len(text) < len(image_data)

    # We must alter the seed but for now lets make it simple
    random.seed(generate_seed(magic))

    for char, i in zip(text, random_ints(image_data.size)):
        image_data.flat[i] = (image_data.flat[i] & ~1) | char            

    print '[*] Finished Encryption'
    return image_data

Note that I made the names more explicit (I had to figure out first if data was the data to encrypt or the data to encrypt it in), used zip to iterate over two sequences at once and made random_ints a generator as explained above. I also used the fact that you can directly iterate over a string, giving you the individual characters.

After having restructured the code like this, you could realize that what you want is actually drawing a random sample (without replacing). For this there exists the random.sample function, which simplifies this a bit more:

def random_ints2(size):
    return random.sample(xrange(size), size)

This is also slightly faster:

In [6]: %timeit list(random_ints(1000000))
1 loop, best of 3: 645 ms per loop
In [7]: %timeit random_ints2(1000000)
1 loop, best of 3: 603 ms per loop

(The main speed-boost here comes from the fact that we don't need to consume the generator into a list.)

Both of these are way faster than your original function, here slightly modified to put it into a single function with the same interface:

def random_ints_op(size):
    random_list = []
    for i in range(size):
        next_random_number = random.randint(0, size - 1)
        while next_random_number in random_list:
            next_random_number = random.randint(0, size - 1)
        random_list.append(next_random_number)
    return random_list

This is waaay slower:

In [18]: %timeit random_ints_op(1000)
10 loops, best of 3: 55.4 ms per loop
In [19]: %timeit random_ints_op(10000)
1 loop, best of 3: 5.81 s per loop

Note that the input is a factor 1000 and 100 smaller, respectively. To visualize the difference in the runtime complexity, have a look at this graph of the execution time as a function of size (for size smaller than a 100, but already random_ints and random_ints2 are indistinguishable on this scale):

enter image description here

For larger inputs, random_ints2 scales slightly better, but both are basically linear:

enter image description here

As you correctly sad in your question, the random module does not produce cryptographically secure pseudo-random numbers. This is why in Python 3, we have the secrets module, which has the secrets.randbelow function, but no shuffle and no sample, so it would be a bit hard to use here.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ @Peilonrayz using that would be counter-productive, since the seed does nothing there, but it is needed for the decryption \$\endgroup\$
    – Graipher
    Jul 5, 2017 at 16:24
  • \$\begingroup\$ @Graipher Oh, I didn't realize that, my bad. :) \$\endgroup\$
    – Peilonrayz
    Jul 5, 2017 at 16:28
  • 1
    \$\begingroup\$ @Graipher I only noticed because I'm guilty of doing that far too many times... and don't forget to edit the for loop and the return statement \$\endgroup\$
    – airstrike
    Jul 5, 2017 at 20:44
  • 1
    \$\begingroup\$ @AndreTerra Done (also the second part now). \$\endgroup\$
    – Graipher
    Jul 5, 2017 at 20:46
6
+50
\$\begingroup\$

Edit: I'm leaving my original answer about encryption alone, and re-summarizing the relevant parts (that we worked out after discussing things in more details) here. I've also added some practical pointers about my code suggestions at the end.

Encryption

To be clear, the difference between naming your methods encrypt_lsb and encode_lsb isn't just a matter of naming convention, but a question of how you approach the problem all together. It is important because the encryption and the LSB encoding are two different steps that serve two different purposes.

When it comes to encryption, the best way to do that is to simply use a well established (and vetted) algorithm, which you are already doing via fernet (at least, I assume that is a strong encryption algorithm: I'm not familiar with it myself). Once you are using a strong encryption algorithm any additional attempts at encrypting your data won't actually make you more secure: instead they just provide more vectors for attacks, and more room for mistakes that can be exploited. The problem is that you really have to be an expert to do encryption right. As an example, it is interesting to read up on timing attacks, which have resulted in real world exploits of otherwise-secure encryption algorithms. You could follow that up by browsing through the list of attacks at the bottom of the wiki page. It is very fascinating. The point is that when it comes to encryption it is very easy to make mistakes that render you vulnerable to exploits. As a result, the best bet is to use well vetted libraries exactly as they are intended and leave it at that.

So having thought about it some more I wanted to address a question from your comment again: is it possible to use the PRNG sequence to guess the password? It may not be. However, PRNGs are designed to be fast, so what this probably does is give a much easier route to brute-force the answer. So my overall answer remains the same: I think that using the encryption password as the seed for your PRNG makes you less secure overall, not more.

I would say that you have two options moving forward. You could ditch the PRNG all together. It's main purpose is not really to encrypt so much as to make the LSB encoding less-obvious. I would say that, by its nature, LSB encodings are already very well hidden, so I personally suspect that you are already secure without the addition of a PRNG.

However, the PRNG itself is not a bad idea. The problem is your use of the encryption password as the seed of the PRNG. So another option would be to do something akin to what good passwords do: generate a random seed that you use to drive the PRNG (much like a password salt) and then encode the seed (without encryption and in a fixed location) at the very "beginning" of your image. Decoding the message then has an extra step, but is still very straight-forward. First you read the seed hidden at the "beginning" of your image. Then you use that seed to run your PRNG and retrieve the encrypted payload. Finally, use the password to decrypt the payload. Does that make sense?

This gets you the best of both worlds: the password is only used once to encrypt/decrypt the message using a strong encryption algorithm, minimizing the surface area for attack vectors and maximizing security. Now you use a randomly generated seed to encode your message via PRNG, which means your data isn't all just stored sequentially at the "beginning" of the image. Your seed still gets stored at a fixed "location" in your image, but this way you get your randomness, your secure password, and you can still decode your images.

Code

My biggest comments about your code concern separation of concerns. Your functions themselves are well separated. Each one makes for a good "unit" without being too long or being split into too small parts. However, the next step is better organizing the whole code base. In the long run, a good application organization leads to better reusability and much simpler long-term maintenance. You entire application effectively does four things:

  1. Process command line input
  2. Read/write files
  3. Encryption/decryption
  4. LSB encoding/decoding

However, those four responsibilities are intermingled with eachother throughout your code base, making it impossible to separate one from the other. This hampers the reusability of your application, the ease with which you might maintain it, and also it's agility. Instead of thinking of your application as something that gets run from the command line, think of it as a module that other people might want to use. In that context, what are the answers to these questions:

  1. What if I want to encode a file in memory?
  2. What if I want to use a different encryption mechanism?
  3. What if I don't want to encrypt at all?

With your current setup the answer is that all of that is impossible. Rather than being a reusable module, this is a single purpose script. Learning how to re-organize it into the former and do that well is what will really set you apart from the pack, so to speak.

In terms of how you actually do that, it isn't half as complicated as you would think. Break everything down in terms of the four responsibilities I outlined above (process, i/o, encryption, encoding), and split up any functions that cross lines. That mainly includes:

save_image encrypt decrypt change_image_form

Once every function has just one responsibility, split the functions into separate files according to responsibility. A simple implementation would therefore still just have four files:

  1. A python script that drives the command line tool
  2. A module that reads/writes files (although you could probably skip this because it is so simple, and let the command line tool manually read/write files)
  3. A module that handles LSB encoding/decoding, and operates on an image array. It should make no attempt at encryption, but simply work with whatever data is passed, whether encrypted or not.
  4. A separate module to handle encryption/decryption.

Your command line runner acts like the glue that brings it all together, and each aspect of the system is its own reusable module. This allows others to import your modules into their own code and use them as necessary. Need to make a script that downloads images off your webserver, hides data in them, and then re-uploads them? No problem. Do your own I/O, import the LSB encoding/decoding module, and the encryption module (if needed, or use your own). The point is that you get to pick and choose the parts that you use. Your application also gets divided up into functional parts, so it is much easier to understand what everything does. When you need to modify the code related to LSB encoding you only see the code related to LSB encoding, and don't have to dig through stuff about file I/O that you don't (currently care about).

Once you master separation of concerns the next trick is inversion of control, which could aid your encryption module substantially.

Code Example

It's hard to make an example out of this without just re-writing everything, so I think the simplest thing to do is show you how to break apart one of your more involved functions, encrypt(). Your current encrypt looks like this:

def encrypt(filename, text, magic):
    # check whether the text is a file name
    if len(text.split('.')[1:]):
        text = read_files(os.path.join(__location__, text))
    t = [int(x) for x in ''.join(text_ascii(encrypt_text(magic, text)))] + [0]*7  # endbit
    try:
        # Change format to png because .jpeg and .bmp extensions do not work
        filename = change_image_form(filename)

        # Load Image
        d_old = load_image(filename)

        # Check if image can contain the data
        if d_old.size < len(t):
            print '[*] Image not big enough'
            sys.exit(0)

        # get new data and save to image
        d_new = encrypt_lsb(d_old, magic, t)
        save_image(d_new, 'new_'+filename)
    except Exception, e:
        print str(e)

And we can just run through and see the different "kinds" of things it is doing:

[I/O] Read data in from a file:

if len(text.split('.')[1:]):
    text = read_files(os.path.join(__location__, text))

[Encryption] Apply Fernet encryption

t = [int(x) for x in ''.join(text_ascii(encrypt_text(magic, text)))] + [0]*7  # endbit

[I/O] Convert file to a new format

filename = change_image_form(filename)

[I/O] Read file in

d_old = load_image(filename)

[Image Processing] Validate image:

if d_old.size < len(t):

[I/O] Communicate with terminal window

    print '[*] Image not big enough'
    sys.exit(0)

[Encoding] LSB encoding:

d_new = encrypt_lsb(d_old, magic, t)

[I/O] Write to file:

save_image(d_new, 'new_'+filename)

As you can see, this method performs a little bit of everything. That is what you are aiming to fix. The solution is actually very simple. Your new encode method (instead of encrypt) will do exactly this (in pseudo-code):

def encode( data, message, seed ):

    # store seed at beginning of message
    with_seed = store_seed( message, seed )

    # then encode the message
    encoded = lsb_encode( data, message, seed )

    # actually, that's it
    return encoded

This keeps things very simple and straight-forward. The idea is that you receive an array of image data and return an array of image data with the message encoded. This function doesn't care if the message is encrypted or not. If the message needs to be encrypted, then someone else does the encryption before it gets here. The function doesn't care whether the image came from a file or was just generated. It works either way. Those responsibilities now fall on whoever is using the code. Your script that runs this has now grown a bit, but that's okay. It is primarily responsible for input and output and otherwise just puts some pieces together: read file in, use encryption module to apply encryption, use encoding module to apply encoding, write file back out. If you aren't working from the command line then you can still use the encryption and encoding modules as needed.

That's not a super detailed example, but there are two ways to learn more about this: practice (which you are already doing), and lots of reading. The concepts that you are approaching are SOLID. These concepts are specific to object-oriented programming, but the general principles are still very applicable to any programming paradigm.

I think that's all I got...

\$\endgroup\$
1
  • 1
    \$\begingroup\$ @Ludisposed I edited my answer to add in the encryption comments and and example/more code breakdown. \$\endgroup\$ Jul 7, 2017 at 19:06
4
\$\begingroup\$

I have a two part answer, and due to time constraints I'll come back for part two later. Part 1 = encrypt/decrypt LSB, Part 2 = code suggestions.

encrypt/decrypt LSB

I think it is good to be clear that your encrypt_lsb and decrypt_lsb functions aren't actually encrypting or decrypting anything. It doesn't really matter of your random function is cryptographically secure because your actual process is not a cryptographic process. LSB is much more akin to an encoding than it is to an encryption algorithm. Granted, the message can't be (immediately) decoded without understanding your algorithm and the password, but I would suggest that your algorithm is no more secure than a simple substitution cipher.

Sure, your average person who tries to decode one of your images will be unable to do so, but this is not uncrackable in the same way that modern encryption methods are. So to be clear, your message is hidden: it isn't encrypted. For these purposes of course I doubt that is a big deal. I suspect you aren't trying to hide life-or-death secrets in your images.

As I was thinking through this it occurred to me that a simple solution would be to actually encrypt the data before you encode it via LSB. If you want your data actually encrypted, you definitely don't want to try to implement the encryption algorithm yourself. OpenSSL can be used to do the actual encryption/decryption for you, and there are definitely wrappers for openssl available for python (although in the worst case scenario you could always hack it with command line calls).

Encrypted + LSB = very secure. In that case the password would be used for the encryption, and you would no longer need the magic to run the LSB encoding. I did a quick google search for using encryption + LSB for steganography and it turns out (not surprisingly) that I'm not the first to think of it. Here is a paper where someone outlines a method to do exactly that. It may be of interest to you.

Finally, I would say that there really isn't any point to making your magic "more weird". Apparent randomness of output doesn't actually imply that the data itself has more entropy, and without adding entropy into the system you are not actually more random or more secure. The short of it is that if you take a fixed input (i.e. the password) and run it through a deterministic method (i.e. generate_seed) your output will be no more random than your input. Adjusting your algorithm to make your seed look "weirder" will do nothing to change that underlying truth, and you can't randomize the password because doing so will make it impossible to recover the original message. The randomness comes exclusively from the person who typed in the password. There is nothing your system can do to add to that. So just use the password as is (or convert it as needed if it needs to be in a different form to be used as a seed for a random number generator). What you are doing is no different than hashing a password, which all the experts will tell you is useless:

https://crypto.stackexchange.com/a/12509 https://security.stackexchange.com/a/104871/149676

\$\endgroup\$
9
  • \$\begingroup\$ While I do agree with you I should rename my lsb functions since you are correctly saying these do not actualy encrypt nor decrypt. I don't only use the password for randomness though, but also use it together with the Cryptopraphy.fernet module to actually encrypt the text. I am currently working on a RSA implementation of my program, to imporve encryption of the text. Alltogether I thank you for the time you spent in reviewing my code. \$\endgroup\$
    – Ludisposed
    Jul 6, 2017 at 7:37
  • \$\begingroup\$ I see that now. A lot more time code is devoted to the encrypt/decrypt_lsb functionality, and I just didn't see the similar encrypt/decrypt_text hiding at the top. So here is my question for you, since I am not an LSB expert: what exactly is the purpose of the randomness in your LSB encoding? To the best of my (limited) knowledge, LSB does not need to have any randomness to do its job. If not, then I think it is counterproductive to apply a "pseudo-encryption" in your LSB encoding when you have already encrypted your text with an actual encryption cipher. \$\endgroup\$ Jul 6, 2017 at 12:12
  • \$\begingroup\$ To be honest, I was unsure if it did add more security or only a false sense of security. That was one of the questions I had that prompted me to post here. I realise now that it actually does not really improve anything, in my head it was supposed to be more difficult to decrypt because when I started from the top-left the pattern is more obvious. \$\endgroup\$
    – Ludisposed
    Jul 6, 2017 at 12:21
  • \$\begingroup\$ 'Lastly I think this is harder to decrypt then just normal lsb_stego, but can not prove anything.' as quoted in my questions. I guess I was wrong on that account \$\endgroup\$
    – Ludisposed
    Jul 6, 2017 at 12:22
  • \$\begingroup\$ You said "I think it is counterproductive." A random New Question do you think by observing the pattern of the prng you can reverse engineer the password more easily? Since then you can extraxt the seed which relates to the password? \$\endgroup\$
    – Ludisposed
    Jul 6, 2017 at 12:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.