diff --git a/README.md b/README.md index 7a0408d..bdd5ede 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,35 @@ Run the script: Hit ctrl-C when done. The Hikvision TFTP handshake (for both cameras and NVRs) is stupid but easy -enough. The client uses the address 192.0.0.64 and expects a TFTP server -running on address 192.0.0.128. It sends a particular packet to the server's -port 9978 from the client port 9979 and expects the server to echo it back. -Once that happens, it proceeds to send a tftp request (on the standard tftp -port, 69) for the file "digicap.dav", which it then installs. The tftp server -must reply from port 69 (unlike the tftpd package that comes with Debian). +enough. The client sends a particular packet to the server's port 9978 from +the client port 9979 and expects the server to echo it back. Once that +happens, it proceeds to send a tftp request (on the standard tftp port, 69) +for a specific file, which it then installs. The tftp server must reply +from port 69 (unlike the tftpd package that comes with Debian). This script handles both the handshake and the actual TFTP transfer. The TFTP server is very simple but appears to be good enough. +Note the expected IP addresses and file name appear to differ by model. So far +there are two known configurations: + +| client IP | server IP | filename | +| ------------ | ------------ | ------------- | +| 192.0.0.64 | 192.0.0.128 | `digicap.dav` | +| 172.9.18.100 | 172.9.18.80 | `digicap.mav` | + +This program defaults to the former. The latter requires commandline overrides: + + $ sudo ./hikvision_tftp.py --server-ip=172.9.18.80 --filename=digicap.mav + +If nothing happens when your device restarts, your device may be expecting +another IP address. tcpdump may be helpful in diagnosing this: + + $ sudo tcpdump -i eth0 -vv -e -nn ether proto 0x0806 + tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes + 16:21:58.804425 28:57:be:8a:aa:53 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Request who-has 172.9.18.80 tell 172.9.18.100, length 46 + 16:22:00.805251 28:57:be:8a:aa:53 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Request who-has 172.9.18.80 tell 172.9.18.100, length 46 + +Feel free to open an issue for help. + See [discussion thread](https://www.ipcamtalk.com/showthread.php/3647-Hikvision-DS-2032-I-Console-Recovery). diff --git a/hikvision_tftpd.py b/hikvision_tftpd.py index c09c251..f5bceea 100755 --- a/hikvision_tftpd.py +++ b/hikvision_tftpd.py @@ -1,35 +1,6 @@ #!/usr/bin/env python """ -Unbrick a Hikvision device. Use as follows: - -Setup the expected IP address: - - linux$ sudo ifconfig eth0:0 192.0.0.128 - osx$ sudo ifconfig en0 alias 192.0.0.128 255.255.255.0 - -Download the firmware to use: - - $ curl -o digicap.dav - -Run the script: - - $ sudo ./hikvision_tftpd.py - -Hit ctrl-C when done. - -The Hikvision TFTP handshake (for both cameras and NVRs) is stupid but easy -enough. The client uses the address 192.0.0.64 and expects a TFTP server -running on address 192.0.0.128. It sends a particular packet to the server's -port 9978 from the client port 9979 and expects the server to echo it back. -Once that happens, it proceeds to send a tftp request (on the standard tftp -port, 69) for the file "digicap.dav", which it then installs. The tftp server -must reply from port 69 (unlike the tftpd package that comes with Debian). - -This script handles both the handshake and the actual TFTP transfer. -The TFTP server is very simple but appears to be good enough. - -See discussion thread: -https://www.ipcamtalk.com/showthread.php/3647-Hikvision-DS-2032-I-Console-Recovery +Unbrick a Hikvision device. See README.md for usage information. """ from __future__ import division @@ -38,6 +9,7 @@ __license__ = 'MIT' __email__ = 'slamb@slamb.org' +import argparse import errno import os import select @@ -47,10 +19,8 @@ import time HANDSHAKE_BYTES = struct.pack('20s', 'SWKH') -_SERVER_IP = '192.0.0.128' -_HANDSHAKE_SERVER_ADDR = (_SERVER_IP, 9978) -_TFTP_SERVER_ADDR = (_SERVER_IP, 69) -_FILENAME = 'digicap.dav' +_HANDSHAKE_SERVER_PORT = 9978 +_TFTP_SERVER_PORT = 69 _TIME_FMT = '%c' @@ -62,21 +32,22 @@ class Server(object): _TFTP_OPCODE_RRQ = 1 _TFTP_OPCODE_DATA = 3 _TFTP_OPCODE_ACK = 4 - _TFTP_RRQ_PREFIX = struct.pack('>h', _TFTP_OPCODE_RRQ) + _FILENAME + '\x00' _TFTP_ACK_PREFIX = struct.pack('>h', _TFTP_OPCODE_ACK) BLOCK_SIZE = 512 - def __init__(self, handshake_addr, tftp_addr, file_contents): + def __init__(self, handshake_addr, tftp_addr, filename, file_contents): self._file_contents = file_contents self._total_blocks = ((len(file_contents) + self.BLOCK_SIZE) // self.BLOCK_SIZE) + self._tftp_rrq_prefix = (struct.pack('>h', self._TFTP_OPCODE_RRQ) + + filename + '\x00') if self._total_blocks > 65535: raise Error('File is too big to serve with %d-byte blocks.' % self.BLOCK_SIZE) self._handshake_sock = self._bind(handshake_addr) self._tftp_sock = self._bind(tftp_addr) print 'Serving %d-byte %s (block size %d, %d blocks)' % ( - len(file_contents), _FILENAME, self.BLOCK_SIZE, self._total_blocks) + len(file_contents), filename, self.BLOCK_SIZE, self._total_blocks) def _bind(self, addr): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -87,11 +58,11 @@ def _bind(self, addr): raise Error( ('Address %s:%d not available.\n\n' 'Try running:\n' - 'linux$ sudo ifconfig eth0:0 192.0.0.128\n' - 'osx$ sudo ifconfig en0 alias 192.0.0.128 ' + 'linux$ sudo ifconfig eth0:0 %s\n' + 'osx$ sudo ifconfig en0 alias %s ' '255.255.255.0\n\n' '(adjust eth0 or en0 to taste. see "ifconfig -a" output)') - % addr) + % (addr[0], addr[1], addr[0], addr[0])) if e.errno == errno.EADDRINUSE: raise Error( ('Address %s:%d in use.\n' @@ -125,13 +96,13 @@ def _handshake_read(self): self._handshake_sock.sendto(pkt, addr) print '%s: Replied to magic handshake request.' % now else: - print '%s: received unexpected bytes %r from %s:%d' % ( + print '%s: received unexpected handshake bytes %r from %s:%d' % ( now, pkt.encode('hex'), addr[0], addr[1]) def _tftp_read(self): pkt, addr = self._tftp_sock.recvfrom(65536) now = time.strftime(_TIME_FMT) - if pkt.startswith(self._TFTP_RRQ_PREFIX): + if pkt.startswith(self._tftp_rrq_prefix): print '%s: starting transfer' % now self._tftp_maybe_send(0, addr) elif pkt.startswith(self._TFTP_ACK_PREFIX): @@ -139,7 +110,7 @@ def _tftp_read(self): '>H', pkt[len(self._TFTP_ACK_PREFIX):]) self._tftp_maybe_send(block, addr) else: - print '%s: received unexpected bytes %r from %s:%d' % ( + print '%s: received unexpected tftp bytes %r from %s:%d' % ( now, pkt.encode('hex'), addr[0], addr[1]) def _tftp_maybe_send(self, prev_block, addr): @@ -159,18 +130,26 @@ def _tftp_maybe_send(self, prev_block, addr): if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--filename', default='digicap.dav', + help='file to serve; used both to read from the local ' + 'disk and for the filename to expect from client') + parser.add_argument('--server-ip', default='192.0.0.128', + help='IP address to serve from.') + args = parser.parse_args() try: - file_contents = open(_FILENAME, mode='rb').read() + file_contents = open(args.filename, mode='rb').read() except IOError, e: - print 'Error: can\'t read %s' % _FILENAME + print 'Error: can\'t read %s' % args.filename if e.errno == errno.ENOENT: print 'Please download/move it to the current working directory.' sys.exit(1) raise try: - server = Server(_HANDSHAKE_SERVER_ADDR, _TFTP_SERVER_ADDR, - file_contents) + server = Server((args.server_ip, _HANDSHAKE_SERVER_PORT), + (args.server_ip, _TFTP_SERVER_PORT), + args.filename, file_contents) except Error, e: print 'Error: %s' % e.message sys.exit(1) diff --git a/hikvision_tftpd_test.py b/hikvision_tftpd_test.py index f8a5157..2448520 100755 --- a/hikvision_tftpd_test.py +++ b/hikvision_tftpd_test.py @@ -34,7 +34,7 @@ def tearDown(self): def _setup(self, data): self._server = hikvision_tftpd.Server( - ('127.0.0.1', 0), ('127.0.0.1', 0), data) + ('127.0.0.1', 0), ('127.0.0.1', 0), 'digicap.dav', data) self._handshake_client = socket.socket( socket.AF_INET, socket.SOCK_DGRAM) self._handshake_client.connect( @@ -61,7 +61,7 @@ def test_eaddrinuse(self): try: hikvision_tftpd.Server(self._server._handshake_sock.getsockname(), self._server._tftp_sock.getsockname(), - '') + 'digicap.dav', '') except hikvision_tftpd.Error, e: self.assertTrue('in use' in e.message, 'Unexpected: %r' % e) else: @@ -73,7 +73,8 @@ def test_eaddrnotavail(self): # The local machine shouldn't have such an IP address. # (Okay, according to the RFCs, it shouldn't be using 192.0.0.128 # either, but we do what we must.) - hikvision_tftpd.Server(('192.0.2.1', 0), ('192.0.2.1', 0), '') + hikvision_tftpd.Server(('192.0.2.1', 0), ('192.0.2.1', 0), + 'digicap.dav', '') except hikvision_tftpd.Error, e: self.assertTrue('not available' in e.message, 'Unexpected: %r' % e) else: @@ -84,7 +85,8 @@ def test_eaddrnotavail(self): 'Skip check for root permissions on Windows') def test_eaccess(self): try: - hikvision_tftpd.Server(('127.0.0.1', 1), ('127.0.0.1', 3), '') + hikvision_tftpd.Server(('127.0.0.1', 1), ('127.0.0.1', 3), + 'digicap.dav', '') except hikvision_tftpd.Error, e: self.assertTrue('permission' in e.message, 'Unexpected: %r' % e) else: