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.

  • Switch your station to direct mode, to channel 1 (380 MHz)
  • gcc -std=gnu99 -Wall -lm -O3 -g -ggdb3 forward_voice.c -o forward_voice
  • Put traffic you want to decode to the file named “voice”. If you don't have anything, you can try voice_tones, which are some, uh, tones.
  • The frame format must be exactly this: 20 ASCII 0s and 1s for FEC-protected header, one ASCII underscore (_), 100 ASCII 0s and 1s for misc. bits, \n character. One frame per line, one newline at the end of the file.
  • ./run
  • ./ -i ./transmit.bits
    • You may need to recompile it with grcc and you may need to change your SDR parameters. We have tested it with bladeRF.
  • Protip: If your radio does not receive the generated signal, you may have incompatible scrambling code (the “SCR=4;” line in forward_voice.c)
    • You will also have to XOR all the *.txt files back and forth or capture these frames from your radio. There is no automated tool for that.

We are running an online decoder at

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).

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`
  if echo "$line" | grep -q rx_time; then
    rt=`echo "$line" | grep -oE "rx_time\": [^ ]+" | cut -d : -f 2`
  if echo "$line" | grep -q CHANNEL_ID; then
    cid=`echo "$line" | cut -d = -f 2`
  if echo "$line" | grep -q "KEY_TYPE=0 KEY_INDEX=0"; then
    echo "$cid $ro + $rt"

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.

Except where otherwise noted, content on this wiki is licensed under the following license: CC Attribution-Noncommercial-Share Alike 4.0 International
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki