Skip to content

Commit

Permalink
Merge branch 'ytisf:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
wisdark committed Feb 23, 2023
2 parents 599363d + d544783 commit eb1de4e
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ pip setup.py --user install
* [Audio](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#audio) - *No listener*.
* [QR Codes](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#qr-codes)
* [WiFi - On Payload](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#wifi-frame-payload)
* [3.5mm Jack](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#3.5mm-jack)
* **Steganography**
* [Binary Offset](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#image-binary-offset)
* [Video Transcript to Dictionary](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#video-dictionary)
* [Braille Text Document](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#braille-text-document)
* [PNG Transparency](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#png-transparency)
* [ZIPCeption](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#zip-loop)
* [DataMatrix over LSB](https://github.com/ytisf/PyExfil/blob/master/USAGE.md#datamatrix-over-lsb)

For usage per modules have a look at the [USAGE](https://www.github.com/ytisf/PyExfil/USAGE.md) file.

Expand Down
34 changes: 34 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,29 @@ a.Send("Hello world")

### Physical

#### 3.5mm Jack
This module is for machines that are air-gapped from other networks and have severe hardening. It's meant to convert a file to a set of tones to be played through a 3.5mm jack and then be able to make sense of them. It also assumes that hardware devices are strictly limited and new device registration might trigger an alert.

In this example code, we use a 16-QAM modulation scheme to encode 4 bits per symbol. We generate a random digital signal consisting of 1024 bits and reshape it into a 2D array with 4 bits per symbol. We then QAM modulate the digital signal using the qammod() function in the scipy.signal library.

We add noise to the modulated signal using the stats.norm.rvs() function in the scipy.stats library to simulate the effects of noise and interference in the transmission. We then play the modulated signal over the default audio output device using the sounddevice library.

```python
from pyexfil.physical.35jack import QAMReceiver, QAMTransmitter

recvr = QAMTransmitter( fs=48000, fc=2000, symbol_duration=0.01, bits_per_symbol=4,
modulation_order=16, noise_level=0.1)

sender.transmit(sender.encode_data("Hello world!".encode('utf-8'))

# Now listen to the audio output and you should hear the data being transmitted
sender = QAMReceiver( fs=48000, fc=2000, symbol_duration=0.01, bits_per_symbol=4, modulation_order=16)
recvr.receive()
```

> Make sure you `pip install -r requirements.txt`!


#### Audio
The Audio module will compile a file into a WAV function. When it's being played a listener can collect the tones and should be able (in a clean environment!) to reconstruct the file. This should be relatively useful for AirGapped networks which are extremely hardened.

Expand Down Expand Up @@ -656,6 +679,17 @@ packed = BipExport.Load('/etc/passwd') # Encode
print(BipExport.Decode(packed)) # Decode
```

#### Datamatrix over LSB
This module will take a file and encode it into a datamatrix code. The datamatrix code will be encoded into the LSB of an image. The resulting image will be saved in the same directory as the original image. It works in a similar manner to other LSB mechanisms, however, since the data is encoded by using a DataMatrix code, the data is more reliable and can be decoded with a higher success rate, and more importantly, the data is not legible to the human eye - hence stealthy.

```python
from pyexfil.Stega.datamatrix import DataMatrixOverLSB

this = DataMatrixOverLSB()

this.Encode('hello wrld'.encode('utf8')*50, 'image.png', 'Encoded.png')
this.Decode(image_path='Encoded.png')
```

#### Image Binary Offset
The binary offset technique will take a file, (zlib it), convert it into a binary string *b01010101...*, and then take an image, and convert it into a pixel array with 3 entities per pixel `(int(R),int(G),int(B))`. In case where the image has a transperancy pixel (PNG for example) the PyExfil will currently ignore it. Then the binary string will be incorprated into the pixel array and saved in another location.
Expand Down
221 changes: 221 additions & 0 deletions pyexfil/Stega/datamatrix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#!/usr/bin/env python3

try:
import cv2
except ImportError:
print("OpenCV library not found. Please install OpenCV to use this script with pip install opencv-python.")
exit(1)
import hashlib
import os
import sys

try:
from PIL import Image
except ImportError:
print("PIL library not found. Please install PIL to use this script with pip install pillow.")
exit(1)


try:
# import pylibdmtx
from pylibdmtx.pylibdmtx import decode, encode
except ImportError as e:
if "Unable to find dmtx" in str(e):
print("libdmtx not found. Please install it with:")
print("\tOn MacOS: brew install libdmtx")
print("\tOn Ubuntu: sudo apt-get install libdmtx-dev")
exit(1)
else:
print("pylibdmtx not found. Please install it with pip install pylibdmtx.")
exit(1)

import numpy as np


KEY = b'\x29\x12\x88\x16\x05\x16\x31\x05\x19\x05\x02\x21'



class DataMatrixOverLSB():
def __init__(self, verbose=True, debug=True):
self._VERBOSE = verbose
self.DEBUG = debug
self.SUCCESS = 'success'
self.ERROR = 'error'
self.INFO = 'info'

def _print(self, message, level='info') -> None:
message = message.strip()
if level == self.INFO:
# print with blue [-] prefix:
sys.stdout.write(f'\033[94m[-]\033[0m {message}\n')
elif level == self.ERROR:
# print with red [!] prefix:
sys.stdout.write(f'\033[91m[!]\033[0m {message}\n')
elif level == self.SUCCESS:
# print with green [+] prefix:
sys.stdout.write(f'\033[92m[+]\033[0m {message}\n')
else:
raise ValueError('level must be one of info, error, success')

def clear_bit(self, value, bit_index=0):
if bit_index < 8:
return value & ~(1 << bit_index)
else:
return value

def get_bit(self, value, bit_index=0):
if bit_index < 8:
return value & (1 << bit_index)
else:
return 0

def set_bit(self, value, bit_index=0):
if bit_index < 8:
return value | (1 << bit_index)
else:
return value

def _xor_bytes(self, a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))

def _read_image_into_rgb_array(self, image_path: str) -> list:
'''
Reads an image into an RGB array
args:
image_path: path to image
returns:
list of RGB values
'''
img = cv2.imread(image_path)
return img.reshape((img.shape[0] * img.shape[1], 3)), img.shape[0], img.shape[1]

def _create_datamatrix_file(self, data: bytes, filename: str) -> str:
encoded = encode(data)
img = Image.frombytes('RGB', (encoded.width, encoded.height), encoded.pixels)
img.save(filename)
return filename

def _zero_lsb(self, pixels: list) -> list:
empty = []
for r,g,b in pixels:
empty.append([self.clear_bit(r), self.clear_bit(g), self.clear_bit(b)],)
return empty

def _save_pixels_to_image(self, pixels: list, size: tuple, filename: str) -> str:
final_packet_pixels = np.array(pixels, dtype=np.uint8)
final_packet_pixels = final_packet_pixels.reshape(size)
cv2.imwrite(filename, final_packet_pixels)
return filename

def _pull_down_pixels(self, pixels: list) -> list:
output = []
for i, _ in enumerate(pixels):
r,g,b = pixels[i]
nr,ng,nb = r,g,b
# if r is bigger than 100 set nr = 1 bit:
if r > 100:
nr = 1
else:
nr = 0
if b > 100:
nb = 1
else:
nb = 0
if g > 100:
ng = 1
else:
ng = 0
output.append([nr,ng,nb])
return output

def _merge_arrays(self, pixels_original, pylled_arr):
for i, px in enumerate(pylled_arr):
# if bit of pylled_arr is 1, set LSB of pixels_original to 1:
if px[0] == 1:
pixels_original[i][0] = self.set_bit(pixels_original[i][0])
if px[1] == 1:
pixels_original[i][1] = self.set_bit(pixels_original[i][1])
if px[2] == 1:
pixels_original[i][2] = self.set_bit(pixels_original[i][2])
return pixels_original

def _get_only_last_bit(self, pixel_array):
# create empty array the size and shape of the input array:
output = np.zeros(pixel_array.shape, dtype=np.uint8)

for i in range(pixel_array.shape[0]):
for j in range(3):
# get last bit of each pixel:
if self.get_bit(pixel_array[i][j]) == 1:
output[i][j] = 255
else:
output[i][j] = 0
return output

def Encode(self, data: bytes, mapping_file_path: str, output_file_path: str) -> str:
pixels_array_original,h,w = self._read_image_into_rgb_array(mapping_file_path)
dm_file_path = self._create_datamatrix_file(data, f'_1_datamatrix.png')

# read the image into pixels array:
pixels_array_dm = self._read_image_into_rgb_array(dm_file_path)
# os.remove(dm_file_path)
if self._VERBOSE:
self._print(f'Data was converted into DataMatrix and stored in memory.', level=self.SUCCESS)

# zero the LSB of the pixels:
zeroed_pixels_array_original = self._zero_lsb(pixels_array_original)
self._save_pixels_to_image(zeroed_pixels_array_original,(h,w,3) ,'_2_zeroed.png')
if self._VERBOSE:
self._print(f'LSB of the pixels was zeroed.', level=self.INFO)

# # convert data matrix pixels to pull up or down:
pixels_array_dm = self._pull_down_pixels(self._read_image_into_rgb_array(dm_file_path)[0])
if self._VERBOSE:
self._print(f'DataMatrix pixels were converted to pull up or down.', level=self.INFO)

# # merge the two arrays:
pixels_array_original = self._merge_arrays(zeroed_pixels_array_original, pixels_array_dm)
if self._VERBOSE:
self._print(f'DataMatrix pixels were merged with the original image map.', level=self.INFO)

self._save_pixels_to_image(pixels_array_original,(h,w,3) , output_file_path)
if self._VERBOSE:
self._print(f'DataMatrix pixels were merged with the original image map.', level=self.SUCCESS)

return output_file_path

def Decode(self, image_path: str) -> str:
encoded_pixels, w, h = self._read_image_into_rgb_array(image_path)
if self._VERBOSE:
self._print(f'Image was read into memory.', level=self.SUCCESS)

# turn encoded_pixels into a numpy array:
output_array = np.array(encoded_pixels, dtype=np.uint8)

# read only last bit from each pixel
output_array = self._get_only_last_bit(pixel_array=output_array)
if self._VERBOSE:
self._print(f'Last bit of each pixel was read.', level=self.INFO)

# convert output_array to a 1D array of '0' and '1' characters
string_output_array = ''.join(str(e) for e in output_array.flatten())

# pad the string with zeros so that its length is a multiple of 8
remainder = len(string_output_array) % 8
if remainder != 0:
string_output_array += '0' * (8 - remainder)

# split the string into 8-bit groups and convert to integers
output_list = [int(string_output_array[i:i+8], 2) for i in range(0, len(string_output_array), 8)]

# reshape for the output:
output_array = np.array(output_list, dtype=np.uint8).reshape((h, w, 3))

# save to image file:
self._save_pixels_to_image(output_array,(w,h, 3), '_decoded_.png')


if __name__ == "__main__":
sys.stderr.write("This is not a standalone script. Please import it as a module.\n")
sys.exit(1)
3 changes: 3 additions & 0 deletions pyexfil/Stega/datamatrix/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pylibdmtx
opencv-python
pillow
Loading

0 comments on commit eb1de4e

Please sign in to comment.