├── requirements.txt ├── test └── integration │ ├── test-files │ ├── test.txt │ ├── test-lsb-obfuscated.png │ ├── test-append-obfuscated.jpg │ └── test-header-obfuscated.jpg │ └── test_data_obfuscator.py ├── defaults ├── blank.jpg └── blank-big.jpg ├── .gitignore ├── README.md └── dataobfuscator.py /requirements.txt: -------------------------------------------------------------------------------- 1 | bitstring 2 | Pillow 3 | -------------------------------------------------------------------------------- /test/integration/test-files/test.txt: -------------------------------------------------------------------------------- 1 | This is some text. -------------------------------------------------------------------------------- /defaults/blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AttackIQ/data-obfuscator/HEAD/defaults/blank.jpg -------------------------------------------------------------------------------- /defaults/blank-big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AttackIQ/data-obfuscator/HEAD/defaults/blank-big.jpg -------------------------------------------------------------------------------- /test/integration/test-files/test-lsb-obfuscated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AttackIQ/data-obfuscator/HEAD/test/integration/test-files/test-lsb-obfuscated.png -------------------------------------------------------------------------------- /test/integration/test-files/test-append-obfuscated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AttackIQ/data-obfuscator/HEAD/test/integration/test-files/test-append-obfuscated.jpg -------------------------------------------------------------------------------- /test/integration/test-files/test-header-obfuscated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AttackIQ/data-obfuscator/HEAD/test/integration/test-files/test-header-obfuscated.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | ChangeLog 120 | AUTHORS 121 | 122 | download_file_to_memory/ 123 | save_file_to_disk/ 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Obfuscator 2 | 3 | Data Obfuscator is a simple **python3** tool to obfuscate data inside image-like files. It can obfuscate/deobfuscate files using the following 4 | methods: 5 | 6 | - `header`: The obfuscated data consists of a JPEG header followed by the original data. The result is not a valid image 7 | - `append`: The obfuscated data consists of a blank JPEG image followed by the original data. The result is a valid 8 | image 9 | - `lsb`: The data is obfuscated using Least Significant Byte (LSB) steganography using a blank image. The result is a 10 | valid image. 11 | 12 | The accepted parameters are the following: 13 | ```bash 14 | $ python dataobfuscator.py -h 15 | usage: dataobfuscator.py [-h] -i input [-m method] [-b bits] action 16 | 17 | Data Obfuscator 18 | 19 | positional arguments: 20 | action Action to do (obfuscate or deobfuscate data) 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -i input, --input input 25 | Input to be obfuscated 26 | -m method, --method method 27 | Obfuscation/deobfuscation method. Valid options: 28 | header, append, lsb, all (default) 29 | -b bits, --bits bits Number of bits of obfuscated payload (only for LSB 30 | deobfuscation) 31 | ``` 32 | 33 | ## Examples 34 | 35 | ### Obfuscation 36 | The following command will obfuscate `mimikatz.exe` in using all available methods (header, append and LSB). 37 | ```bash 38 | $ python dataobfuscator.py -i examples/mimikatz/mimikatz.exe obfuscate 39 | ``` 40 | 41 | Alternatively, we can only obfuscate the payload using only one method using the `-m` argument, for instance: 42 | ```bash 43 | $ python dataobfuscator.py -i examples/mimikatz/mimikatz.exe -m append obfuscate 44 | ``` 45 | 46 | ### Deobfuscation 47 | 48 | Deobfuscate `mimikatz.exe` obfuscated via the header method: 49 | ```bash 50 | $ python dataobfuscator.py -i examples/mimikatz/mimikatz-header.jpg -m header deobfuscate 51 | ``` 52 | 53 | Deobfuscate `mimikatz.exe` obfuscated via the append method: 54 | ```bash 55 | $ python dataobfuscator.py -i examples/mimikatz/mimikatz-append.jpg -m append deobfuscate 56 | ``` 57 | 58 | Deobfuscate `mimikatz.exe` obfuscated via the LSB method (note that we need to pass the number of bytes of the original 59 | Mimikatz binary): 60 | ```bash 61 | $ python dataobfuscator.py -i examples/mimikatz/mimikatz-lsb.jpg -m lsb -b 7275776 deobfuscate 62 | ``` -------------------------------------------------------------------------------- /test/integration/test_data_obfuscator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from dataobfuscator import * 5 | 6 | PATH = path.dirname(os.path.realpath(__file__)) 7 | JPEG_HEADER = bytes(b"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01") 8 | 9 | 10 | class TestDataObfuscator(unittest.TestCase): 11 | 12 | input_file = path.join(PATH, '..', '..', 'defaults/blank.jpg') 13 | input_file_lsb = path.join(PATH, '..', '..', 'defaults/blank-big.jpg') 14 | data_file = os.path.join(PATH, 'test-files', 'test.txt') 15 | header_output_file = '{}-header.jpg'.format(os.path.splitext(data_file)[0]) 16 | header_deobfuscate_file = '{}-header-obfuscated.jpg'.format(os.path.splitext(data_file)[0]) 17 | header_output_deobfuscate_file = '{}-header-obfuscated-recovered'.format(os.path.splitext(data_file)[0]) 18 | append_output_file = '{}-append.jpg'.format(os.path.splitext(data_file)[0]) 19 | append_deobfuscate_file = '{}-append-obfuscated.jpg'.format(os.path.splitext(data_file)[0]) 20 | append_output_deobfuscate_file = '{}-append-obfuscated-recovered'.format(os.path.splitext(data_file)[0]) 21 | lsb_output_file = '{}-lsb.png'.format(os.path.splitext(data_file)[0]) 22 | lsb_deobfuscate_file = '{}-lsb-obfuscated.png'.format(os.path.splitext(data_file)[0]) 23 | lsb_output_deobfuscate_file = '{}-lsb-obfuscated-recovered'.format(os.path.splitext(data_file)[0]) 24 | 25 | @classmethod 26 | def tearDownClass(cls): 27 | try: 28 | os.remove(cls.header_output_file) 29 | os.remove(cls.header_output_deobfuscate_file) 30 | os.remove(cls.append_output_file) 31 | os.remove(cls.append_output_deobfuscate_file) 32 | os.remove(cls.lsb_output_file) 33 | os.remove(cls.lsb_output_deobfuscate_file) 34 | except IOError: 35 | pass 36 | 37 | def test_obfuscate_header(self): 38 | obfuscate_via_header(self.data_file, self.header_output_file) 39 | self.assertTrue(os.path.exists(self.header_output_file)) 40 | self._compare_file_contents(self.header_output_file, self.header_deobfuscate_file) 41 | 42 | def test_deobfuscate_header(self): 43 | deobfuscate_via_header(self.header_deobfuscate_file, self.header_output_deobfuscate_file) 44 | self.assertTrue(os.path.exists(self.header_output_deobfuscate_file)) 45 | self._check_deobfuscated_text(self.header_output_deobfuscate_file) 46 | 47 | def test_obfuscate_append(self): 48 | obfuscate_via_append(self.data_file, self.input_file, self.append_output_file) 49 | self.assertTrue(os.path.exists(self.append_output_file)) 50 | self._compare_file_contents(self.append_output_file, self.append_deobfuscate_file) 51 | 52 | def test_deobfuscate_append(self): 53 | deobfuscate_via_append(self.append_deobfuscate_file, self.append_output_deobfuscate_file) 54 | self.assertTrue(os.path.exists(self.append_output_deobfuscate_file)) 55 | self._check_deobfuscated_text(self.append_output_deobfuscate_file) 56 | 57 | def test_obfuscate_lsb(self): 58 | obfuscate_via_lsb(self.data_file, self.input_file_lsb, self.lsb_output_file) 59 | self.assertTrue(os.path.exists(self.lsb_output_file)) 60 | self._compare_file_contents(self.lsb_output_file, self.lsb_deobfuscate_file) 61 | 62 | def test_deobfuscate_lsb(self): 63 | deobfuscate_via_lsb(self.lsb_deobfuscate_file, self.lsb_output_deobfuscate_file) 64 | self.assertTrue(os.path.exists(self.lsb_output_deobfuscate_file)) 65 | self._check_deobfuscated_text(self.lsb_output_deobfuscate_file) 66 | 67 | def _compare_file_contents(self, file1, file2): 68 | with open(file1, "rb") as f: 69 | data1 = f.read() 70 | with open(file2, "rb") as f: 71 | data2 = f.read() 72 | self.assertEqual(data1, data2) 73 | 74 | def _check_deobfuscated_text(self, file): 75 | with open(file, "rb") as f: 76 | data = f.read() 77 | with open(self.data_file, "rb") as f: 78 | text = f.read() 79 | self.assertEqual(text, data) 80 | -------------------------------------------------------------------------------- /dataobfuscator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from bitstring import BitArray 4 | from hashlib import md5 5 | from os import path 6 | from PIL import Image 7 | 8 | 9 | PATH = path.dirname(path.realpath(__file__)) 10 | JPEG_HEADER = b"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01" 11 | APPEND_FILE_MARKER = md5(b'').digest() 12 | LSB_PAYLOAD_LENGTH_BITS = 32 13 | 14 | 15 | def obfuscate_via_header(data_file, output_file): 16 | jpeg_header = bytes(JPEG_HEADER) 17 | data = read_data(data_file) 18 | write_data(output_file, jpeg_header + data) 19 | 20 | 21 | def deobfuscate_via_header(input_file, output_file): 22 | data = read_data(input_file) 23 | write_data(output_file, data[12:]) 24 | 25 | 26 | def obfuscate_via_append(data_file, input_file, output_file): 27 | data = read_data(data_file) 28 | input_file_data = read_data(input_file) 29 | write_data(output_file, input_file_data + APPEND_FILE_MARKER + data) 30 | 31 | 32 | def deobfuscate_via_append(input_file, output_file): 33 | data = read_data(input_file) 34 | file_start = data.find(APPEND_FILE_MARKER) 35 | write_data(output_file, data[file_start+len(APPEND_FILE_MARKER):]) 36 | 37 | 38 | def obfuscate_via_lsb(data_file, input_file, output_file): 39 | data = read_data(data_file) 40 | data = BitArray(uint=len(data) * 8, length=LSB_PAYLOAD_LENGTH_BITS).bin + BitArray(bytes=data).bin 41 | 42 | i = 0 43 | try: 44 | with Image.open(input_file) as img: 45 | width, height = img.size 46 | if len(data) > width * height * 3: 47 | print("Data is too large to be embedded in the image. Data contains {} bytes, maximum is {}".format( 48 | int(len(data) / 8), int(width * height * 3 / 8))) 49 | exit(1) 50 | for x in range(0, width): 51 | for y in range(0, height): 52 | pixel = list(img.getpixel((x, y))) 53 | for n in range(0, 3): 54 | if i < len(data): 55 | pixel[n] = pixel[n] & ~1 | int(data[i]) 56 | i += 1 57 | img.putpixel((x, y), tuple(pixel)) 58 | if i >= len(data): 59 | break 60 | if i >= len(data): 61 | break 62 | img.save(output_file, "png") 63 | except IOError: 64 | print("Could not open {}. Check that the file exists and it is a valid image file.".format(input_file)) 65 | exit(1) 66 | print("Data written to {}".format(output_file)) 67 | 68 | 69 | def deobfuscate_via_lsb(input_file, output_file): 70 | try: 71 | with Image.open(input_file) as img: 72 | payload_length = int("".join([str(x) for x in decode_img_nbits(img, LSB_PAYLOAD_LENGTH_BITS)]), 2) 73 | data = decode_img_nbits(img, payload_length + LSB_PAYLOAD_LENGTH_BITS)[LSB_PAYLOAD_LENGTH_BITS:] 74 | data = BitArray(bin="".join([str(x) for x in data])).bytes 75 | except IOError: 76 | print("Could not open {}".format(input_file)) 77 | exit(1) 78 | write_data(output_file, data) 79 | 80 | 81 | def decode_img_nbits(img, nbits): 82 | data = [] 83 | i = 0 84 | width, height = img.size 85 | for x in range(0, width): 86 | for y in range(0, height): 87 | pixel = list(img.getpixel((x, y))) 88 | for n in range(0, 3): 89 | if i < nbits: 90 | data.append(pixel[n] & 1) 91 | i += 1 92 | if i >= nbits: 93 | break 94 | if i >= nbits: 95 | break 96 | return data 97 | 98 | 99 | def read_data(input_file): 100 | try: 101 | with open(input_file, "rb") as f: 102 | data = f.read() 103 | except IOError: 104 | print("Could not open file {}".format(input_file)) 105 | exit(1) 106 | return data 107 | 108 | 109 | def write_data(output_file, data): 110 | try: 111 | with open(output_file, "wb") as f: 112 | f.write(data) 113 | except IOError: 114 | print("Could not open file {}".format(output_file)) 115 | exit(1) 116 | print("Data written to {}".format(output_file)) 117 | 118 | 119 | def parse_args(): 120 | parser = argparse.ArgumentParser(description='Data Obfuscator') 121 | parser.add_argument('action', metavar='action', choices=['obfuscate', 'deobfuscate'], type=str, 122 | help='Action to do (obfuscate or deobfuscate data)') 123 | parser.add_argument('-d', '--data', metavar='data', type=str, help='Data to be obfuscated (required if action is ' 124 | '"obfuscate")', required=False) 125 | parser.add_argument('-i', '--input', metavar='input', type=str, 126 | help="Image where the data will be hidden (if action is \"obfuscate\") or from where the data " 127 | "will be extracted (if method is \"deobfuscate\"). Required if action is \"deobfuscate\"." 128 | "If action is \"obfuscate\", it will only be used in append and LSB methods " 129 | "(default is a blank image)", 130 | required=False) 131 | parser.add_argument('-o', '--output', metavar='output', type=str, help="Name of the output file", required=False) 132 | parser.add_argument('-m', '--method', choices=['header', 'append', 'lsb'], metavar='method', type=str, 133 | help='Obfuscation/deobfuscation method. Valid options: header, append, lsb', required=True) 134 | 135 | args = parser.parse_args() 136 | extension = 'png' if args.method == 'lsb' else 'jpg' 137 | # Further parameter validation 138 | if args.action == 'obfuscate': 139 | if args.method == 'header' and args.input: 140 | print("Method 'header' does not take any input file. Input file {} will be ignored".format(args.input)) 141 | if not args.data: 142 | print("Parameter -d/--data is required for the obfuscate method\n") 143 | parser.print_help() 144 | exit(1) 145 | if args.action == 'deobfuscate': 146 | if not args.input: 147 | print("Parameter -i/--input is required for the deobfuscate method\n") 148 | parser.print_help() 149 | exit(1) 150 | if not args.output: 151 | if args.action == 'obfuscate': 152 | args.output = '{}-{}.{}'.format(path.splitext(args.data)[0], args.method, extension) 153 | else: 154 | args.output = '{}-{}'.format(path.splitext(args.input)[0], 'recovered') 155 | if not args.input: 156 | args.input = path.join(PATH, 'defaults/blank-big.jpg') if args.method == 'lsb' \ 157 | else path.join(PATH, 'defaults/blank.jpg') 158 | 159 | return args 160 | 161 | 162 | if __name__ == "__main__": 163 | args = parse_args() 164 | 165 | if args.action == 'obfuscate': 166 | if args.method == 'header': 167 | obfuscate_via_header(data_file=args.data, output_file=args.output) 168 | elif args.method == 'append': 169 | obfuscate_via_append(data_file=args.data, input_file=args.input, output_file=args.output) 170 | elif args.method == 'lsb': 171 | obfuscate_via_lsb(data_file=args.data, input_file=args.input, output_file=args.output) 172 | else: 173 | if args.method == 'header': 174 | deobfuscate_via_header(input_file=args.input, output_file=args.output) 175 | elif args.method == 'append': 176 | deobfuscate_via_append(input_file=args.input, output_file=args.output) 177 | elif args.method == 'lsb': 178 | deobfuscate_via_lsb(input_file=args.input, output_file=args.output) 179 | --------------------------------------------------------------------------------