Table of Contents

Decoding audio

Although software implementation of the codec is not available, we can use an existing Tetrapol station in Direct mode to decode audio. This has been tested on G2.

There is an out-of-tree code for this.

We are running an online decoder at http://nat.brmlab.cz:8064/.

Sniffing real-world network

Despite “being encrypted”, our network analysis discovered there is 0.01 % of unencrypted traffic. This is a very low amount, but this also means that on a big network with one million connections per day there is one hundred unencrypted connections a day. We can use this as a demo for sniffing audio off-the-air. Further analysis shows that these unencrypted calls are emergency calls.

Sniffing channel-hopped data

The traffic itself is on a separate channel (and as Tetrapol does not have timeslots, it is always on a separate channel). This poses little challenge with modern wideband SDRs.

Start sniffing CCH as usual and note what channels are stations sent to (the CHANNEL parameter in the dumper). Start sniffing these channels too. You may just start the sniffing after you learn the number, or you can keep buffer of spectrum data and really follow the call (this is not implemented). Save the raw demodulated bits, not only the dumper output.

For the next step, you need aligned data. This means that your CCH and TCH bits must start at the same sample (actually, missing several thousands of samples does not really matter, but missing more is a problem).

Now let's pretend that you have cch.bits and (maybe several) tch.bits satisfying the above conditions.

Finding unencrypted data

Have a look at the dumper output and open PAS 0001-3-2: Version 2.3.4 5.3.39 KEY_REFERENCE. You can see that KEY_TYPE=0, KEY_INDEX=0 and KEY_TYPE=15, KEY_INDEX=0 means unencrypted traffic (we have seen lots of 0, 0 cases and no 15, 0 case). Check the “CHANNEL” parameter to see if you have recorded its TCH.

Now we need to find this connection in TCH, which is a big mess without any synchronization. As the bitrate is constant, it will be on the exact same bit position as is the corresponding message in CCH.

The dumper prints stream offset in JSON messages. There is a script that gives you bit positions of unencrypted frames, just pipe tetrapol_dump output to it (don't forget 2>&1).

ro=-1
rt=-1
grep -B 20 "KEY_TYPE=0 KEY_INDEX=0" $1 | grep -E "(KEY_TYPE=0 KEY_INDEX=0|rx_offs|rx_time|CHANNEL_ID)" \
| while read line; do
  if echo "$line" | grep -qE "rx_offs\": [0-9]+,"; then
    ro=`echo "$line" | grep -oE "rx_offs\": [0-9]+" | cut -d : -f 2`
  fi
  if echo "$line" | grep -q rx_time; then
    rt=`echo "$line" | grep -oE "rx_time\": [^ ]+" | cut -d : -f 2`
  fi
  if echo "$line" | grep -q CHANNEL_ID; then
    cid=`echo "$line" | cut -d = -f 2`
  fi
  if echo "$line" | grep -q "KEY_TYPE=0 KEY_INDEX=0"; then
    echo "$cid $ro + $rt"
  fi
done

Once you know the bit position, copy that data with dd skip=xxx count=xxx and run the dumper in TCH mode on it. You need blobutils.

tetrapol_dump -t TCH -i infile.bits 2>/dev/null | grep '"type": "VOICE"' | grep -oE '"value": "[0-9a-f]{30}"' | cut -d \" -f 4 | blhexbit | sed -re "s/(.)(.)(.)(.) (.)(.)(.)(.) /\8\7\6\5\4\3\2\1/g" | sed -re "s/^([01]{20})/\1_/g"

You now have raw audio frames. You can use the aforementioned method to decode them.