Skip to content

Instantly share code, notes, and snippets.

@caquino
Last active November 27, 2017 12:33
Show Gist options
  • Save caquino/270958014580ddbf297acce8ab9fe71f to your computer and use it in GitHub Desktop.
Save caquino/270958014580ddbf297acce8ab9fe71f to your computer and use it in GitHub Desktop.

Dog Training Collar

I was inspired by the Wireless Village CTF to try to reverse engineer a Dog Training Collar, I first saw this challenge at Hak5, and because sadly I never had the opportunity to go to a defcon I acquired a unit that comes with 2 collars for a single controller to try to do it myself at home.

For the reverse engineer I used a HackRF One and gqrx to locate the signal, after finding the signal hackrf_transfer was used to record the signal.

With the signal recorded, inspectrum was used to analyze it, and to retransmit the signal a Yardstick One was used

The protocol format can be seen on main.py headers, based on my limited SDR experience.

Feels like most of the people uses bits as string to do this sort of work, but I went using real bits, it pose some interesting challenges but it was a good refresher.

PS: I'm not sharing this publicly as I don't want to spoil Wireless Village CTF game, so please keep it to yourself

#!/usr/bin/env python
# *-- encoding: utf-8 --*
from bitarray import bitarray
# Protocol Format
# byte * 2 | byte | byte | byte | byte | byte | byte
# Preamble? | id | mode | fixed? | level | cksum | trailing (is it needed?)
# 1111110000 | 1000 (8) | 0001 (shock) | 00001001 | 11001010 | 00010100 | 01111110 | 0 (id + mode + inverted + inversed)
# | 1111 (15) | 0010 (vibrate) |
# | 0100 (beep) |
# | 1000 (blink) |
# DTC - Dog Training Collar
class DTC(object):
# Symbol lookup table
symbols = {
0: [0, 0, 0, 1],
1: [1, 1, 0, 1]
}
baud_rate = 944 * 4 # Each bit has 944hz and each simbol has 4 bits
# Preamble WIP - This doesn't look right, I need to review it
preamble = [0b00000011, 0b11110000]
# Action lookup table
actions = {
'shock': 0b1,
'vibrate': 0b10,
'sound': 0b100,
'light': 0b1000
}
def __init__(self, collar_id=8, level=0, action='vibrate'):
self._collar_id = collar_id
self._level = level
self._action = action
# This is the first nibble of the first byte after preamble
def getCollarId(self):
return self._collar_id
collar_id = property(getCollarId)
# This is the second nibble of the first byte after preamble
def getAction(self):
return self.actions[self._action]
action = property(getAction)
# Join collar id and action nibbles into a single byte
def getIdMode(self):
return (self.collar_id << 4) | (self.action & 0x0F)
id_mode = property(getIdMode)
# First fixed byte in the protocol
# At least is fixed on the units I have
def getFixedOne(self):
return 0b00001001
fixed_one = property(getFixedOne)
# Second fixed byte in the protocol
# At least is fixed on the units I have
def getFixedTwo(self):
return 0b11001010
fixed_two = property(getFixedTwo)
# Level is just binary representation of the level number
def getLevel(self):
return self._level
level = property(getLevel)
# This looks like a mechanism to avoid spurious activation than a checksum itself
# It invert and reverse bits
# eg: if the first byte (id + action) = 0b01100001 the result will be 0b01111001
# https://stackoverflow.com/questions/19204750/how-do-i-perform-a-circular-rotation-of-a-byte
def getChecksum(self):
cksum = self.id_mode ^ 0xFF # invert bits
cksum = (cksum & 0x55) << 1 | (cksum & 0xAA) >> 1 # swap adjacent bits
cksum = (cksum & 0x33) << 2 | (cksum & 0xCC) >> 2 # swap adjacent pairs
cksum = (cksum & 0x0F) << 4 | (cksum & 0xF0) >> 4 # swap nibbles
return cksum
checksum = property(getChecksum)
# Convert bitarray to symbols based on the lookup table
def bitsToSymbols(self, value):
bits = [(value >> bit) & 1 for bit in range(8 - 1, -1, -1)]
for position, bit in enumerate(bits):
bits[position] = self.symbols[bit]
return sum(bits, []) # (+) operator overload, flatten multi dimentional list
# Convert raw bits to bitarray format without symbol lookup
def rawBits(self, value):
return [(value >> bit) & 1 for bit in range(8 - 1, -1, -1)]
# Assemble the whole packet
def getPacket(self):
packet = self.rawBits(self.preamble[0])
packet += self.rawBits(self.preamble[1])
packet += self.bitsToSymbols(self.id_mode)
packet += self.bitsToSymbols(self.fixed_one)
packet += self.bitsToSymbols(self.fixed_two)
packet += self.bitsToSymbols(self.level)
packet += self.bitsToSymbols(self.checksum)
packet += self.rawBits(0)
return bitarray(packet).tobytes()
if __name__ == "__main__":
Collar = DTC(8, 20, 'shock')
print("getIdMode: %s" % bin(Collar.getIdMode()))
print("getChecksum: %s" % bin(Collar.getChecksum()))
print("getLevel: %s" % bin(Collar.getLevel()))
packet = ''.join(format(x, '08b') for x in bytearray(Collar.getPacket()))
print("getPacket bits: %s" % packet)
print("getPacket bin: %s" % Collar.getPacket())
#!/usr/bin/env python
import sys
from dtc import DTC
from rflib import *
from time import sleep
s = DTC(8, 20, 'shock')
spacket = s.getPacket()
d = RfCat()
d.setModeIDLE()
d.setMdmModulation(MOD_ASK_OOK)
d.setFreq(434000000)
d.setMdmSyncMode(0)
d.setMdmDRate(s.baud_rate)
d.setMaxPower()
d.makePktFLEN(len(spacket))
d.RFxmit(spacket, repeat=10)
d.setModeIDLE()
d.cleanup()
sys.exit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment