├── poc.gif ├── examples ├── simple.gif └── complex.jpg ├── LICENSE ├── README.MD ├── .gitignore └── stegextract /poc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evyatarmeged/stegextract/HEAD/poc.gif -------------------------------------------------------------------------------- /examples/simple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evyatarmeged/stegextract/HEAD/examples/simple.gif -------------------------------------------------------------------------------- /examples/complex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evyatarmeged/stegextract/HEAD/examples/complex.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Evyatar Meged 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Stegextract 2 | Bash script to extract hidden files and strings from images. 3 | 4 | ## Description 5 | Stegextract extracts any trailing data after the image's closing bytes, and any hidden files (or other images) embedded within the image.
6 | Short byte combinations such as JPEG's `FFD8 FFE0` might sometimes create false positives.
7 | Manually reviewing the hexdump is sometimes inevitable in cases of highly complex embedded files.
8 | Stegextract is not the solution for any color/pixel/filter/LSB related Steganography, nor does it try to be. It 9 | relies on magic numbers, hexdumps and binary data alone.
10 | Currently supports PNG, JPG, and GIF. 11 | 12 | **Update**: `--analyze` flag was deprecated and is now being performed automatically with every scan. 13 | 14 | ## Installation 15 | ``` 16 | sudo sh -c 'curl https://raw.githubusercontent.com/evyatarmeged/stegextract/master/stegextract > /usr/local/bin/stegextract' 17 | sudo chmod +x /usr/local/bin/stegextract 18 | ``` 19 | 20 | ## Usage 21 | ``` 22 | Usage: stegextract [options] 23 | 24 | -h, --help Print this and exit 25 | -o, --outfile Specify an outfile 26 | -s, --strings Extract strings from file 27 | -q, --quiet Do not output to stdout 28 | --force-format Force this image format instead of detecting 29 | ``` 30 | 31 | ## POC: 32 | ![poc](poc.gif) 33 | 34 | Image examples from the above GIF can be found in the [examples](https://github.com/evyatarmeged/stegextract/tree/master/examples) folder. 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | [Ss]cripts 12 | pyvenv.cfg 13 | .venv 14 | pip-selfcheck.json 15 | ### Python template 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | .static_storage/ 70 | .media/ 71 | local_settings.py 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .idea/ 119 | xtrx.py -------------------------------------------------------------------------------- /stegextract: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | command -v xxd >/dev/null 2>&1 || { 4 | echo "This program requires command 'xxd' which is not installed. Please install it before running this program."; 5 | exit 1; 6 | } 7 | command -v grep >/dev/null 2>&1 || { 8 | echo "This program requires command 'grep' which is not installed. Please install it before running this program."; 9 | exit 1; 10 | } 11 | 12 | exec 6>&1 13 | 14 | if [ $# -eq 0 ]; then 15 | echo "Usage: stegextract [options]" 16 | echo "stegextract -h for help" 17 | exit 0 18 | fi 19 | 20 | while (( "$#" )); do 21 | case "$1" in 22 | -h|--help) 23 | echo "Extract hidden data from images" 24 | echo " " 25 | echo "Usage: stegextract [options]" 26 | echo "-h, --help Print this and exit" 27 | echo "-o, --outfile Specify an outfile" 28 | echo "-s, --strings Extract strings from file" 29 | echo "-q, --quiet Do not output to stdout" 30 | echo "--force-format Force this image format instead of detecting" 31 | exit 0 32 | ;; 33 | "-o"|"--outfile") 34 | outfile="$2" 35 | shift 2 36 | ;; 37 | "--force-format") 38 | ext="$2" 39 | shift 2 40 | ;; 41 | "-s"|"--strings") 42 | get_strings="true" 43 | shift 1 44 | ;; 45 | "-q"|"--quiet") 46 | exec > /dev/null 47 | shift 1 48 | ;; 49 | --) # end argument parsing 50 | shift 51 | break 52 | ;; 53 | -*|--*=) # unsupported flags 54 | echo "Error: Unsupported flag $1" >&2 55 | exit 1 56 | ;; 57 | *) # preserve positional arguments 58 | image="$1" 59 | stripped=${image%.*} 60 | shift 61 | ;; 62 | esac 63 | done 64 | 65 | file_type="" 66 | 67 | readonly jpg_start="ffd8" 68 | readonly jpg_end="ff d9" 69 | readonly png_start="8950 4e47 0d0a 1a0a" 70 | readonly png_end="49 45 4e 44 ae 42 60 82" 71 | readonly gif_start="4749 4638 3961" 72 | readonly gif_end="00 3b" 73 | 74 | if [ ! -f "$image" ]; then 75 | echo "$0: File $image not found." 76 | exit 1 77 | fi 78 | 79 | if [ -z ${outfile+x} ]; then 80 | outfile=$stripped"_dumps"; 81 | elif [ -f "$outfile" ]; then 82 | read -p "$outfile exists, overwrite ? (y/n) " choice 83 | case "$choice" in 84 | y|Y ) :;; 85 | n|N ) echo "Exiting"; exit 1;; 86 | * ) echo "Invalid option"; exit 1;; 87 | esac 88 | fi 89 | 90 | extract_trailing() { 91 | curr=${@:1} 92 | xxd -c1 -p $image | tr "\n" " " | sed -n -e "s/.*\( $curr \)\(.*\).*/\2/p" | xxd -r -p > $outfile 93 | } 94 | 95 | jpeg() { 96 | file_type="jpg" 97 | # Grab everything after 0xFF 0xD9 98 | echo "Detected image format: JPG" 99 | extract_trailing "$jpg_end" 100 | } 101 | 102 | png() { 103 | file_type="png" 104 | # Grab everything after "IEND.B`" chunk 105 | echo "Detected image format: PNG" 106 | extract_trailing "$png_end" 107 | } 108 | 109 | gif() { 110 | file_type="gif" 111 | # Grab everything after "0x00 0x3B" 112 | echo "Detected image format: GIF" 113 | extract_trailing "$gif_end" 114 | } 115 | 116 | extract_embedded() { 117 | local curr_ext="$1"; shift 118 | magic="$@" 119 | 120 | magic_no_ws=$(echo -e "$magic" | tr -d '[:space:]') 121 | located=$(xxd -ps -c1000 $image | grep $magic_no_ws) 122 | if [[ $located ]]; then 123 | upper_ext=$(echo "$curr_ext" | tr /a-z/ /A-Z/) 124 | echo "Found embedded: $upper_ext" 125 | echo "$magic" | xxd -r -p > "$stripped"."$curr_ext" 126 | xxd -c1 -p "$image" | tr "\n" " " | sed -n -e "s/.*\( $magic \)\(.*\).*/\2/p" | xxd -r -p >> $stripped.$curr_ext 127 | fi 128 | } 129 | 130 | analysis() { 131 | # Did not include zlib, bzip because of a high false positive percentage 132 | extensions=("png" "jpg" "gif" "zip" "rar" "7z") 133 | for i in "${extensions[@]}"; do 134 | # Look for magic numbers in file except for the already detected image type 135 | if [ "$i" != "$file_type" ]; then 136 | case "$i" in 137 | "png") 138 | extract_embedded "png" "89 50 4e 47 0d 0a 1a 0a" 139 | ;; 140 | "jpg") 141 | extract_embedded "jpg" "ff d8 ff e0" 142 | ;; 143 | "gif") 144 | extract_embedded "gif" "47 49 46 38 39 61" 145 | ;; 146 | "zip") 147 | extract_embedded "zip" "50 3b 03 04" 148 | ;; 149 | "rar") 150 | extract_embedded "rar" "52 61 72 21 1a 07" 151 | ;; 152 | "7z") 153 | extract_embedded "7z" "37 7a bc af 27 1c" 154 | ;; 155 | esac 156 | fi 157 | done 158 | } 159 | 160 | if [[ ! -z ${ext+x} ]]; then 161 | case ${ext,,} in 162 | # Lazy format detection 163 | "jpg"|"jpeg") 164 | jpeg 165 | ;; 166 | "png") 167 | png 168 | ;; 169 | "gif") 170 | gif 171 | ;; 172 | *) 173 | echo "Unsupported image format" 174 | exit 1 175 | ;; 176 | esac 177 | else 178 | # Look for SOI bytes in xxd output to detect image type 179 | head_hexdump=$(xxd $image 2> /dev/null | head) 180 | if [[ $(grep "$png_start" <<< $head_hexdump) ]]; then 181 | png 182 | elif [[ $(grep "$jpg_start" <<< $head_hexdump) ]]; then 183 | jpeg 184 | elif [[ $(grep "$gif_start" <<< $head_hexdump) ]]; then 185 | gif 186 | else 187 | echo "Unknown or unsupported image format" 188 | exit 1 189 | fi 190 | fi 191 | 192 | data=$(file "$outfile") 193 | data=${data##*:} 194 | result=$(echo $data | head -n1 | sed -e 's/\s.*$//') 195 | if [ $result = "empty" ]; then 196 | echo "No trailing data found in file" 197 | rm "$outfile" 198 | elif [ "$result" = "data" ]; then 199 | echo "Extracted trailing file data: binary data, might contain embedded files." 200 | else 201 | echo "Extracted trailing file data: $data" 202 | fi 203 | 204 | 205 | if [[ "$get_strings" ]]; then 206 | echo "Extracting strings..." 207 | strings -6 "$image" > "$stripped".txt 208 | fi 209 | 210 | echo "Performing deep analysis" 211 | analysis 212 | echo "Done" 213 | 214 | exec 1>&6 6>&- 215 | --------------------------------------------------------------------------------