├── .gitattributes ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── cpp ├── CMakeLists.txt ├── Doxyfile ├── README.md ├── adapter.cpp ├── adapter.h ├── checksum.cpp ├── checksum.h ├── ethernet.cpp ├── ethernet.h ├── eventing.h ├── sadplib.cpp ├── sadplib.h └── sadptool.cpp ├── docs ├── Makefile ├── conf.py ├── csadp.rst ├── fmod.rst ├── index.rst ├── make.bat ├── requirements.txt └── sadp.rst ├── gists ├── csadp │ └── CPacketSender.cpp ├── firmwarelist.json ├── fmod │ └── decode_xor16.c └── libsadp.so ├── hiktools ├── __init__.py ├── csadp │ ├── CService.py │ ├── __init__.py │ ├── checksum.py │ ├── model.py │ ├── payloads.py │ └── uarray.py ├── fmod │ ├── __init__.py │ ├── __main__.py │ ├── digicap.py │ └── export.py └── sadp │ ├── __init__.py │ ├── client.py │ └── message.py ├── lua └── sadp.lua ├── pyproject.toml └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | exclude/ 2 | __pychache__/# Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | tests/ 7 | *.dav 8 | 9 | # C extensions 10 | # *.so 11 | resources/ 12 | .vscode/ 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | /cpp/docs 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | # pytype static type analyzer 148 | .pytype/ 149 | 150 | # Cython debug symbols 151 | cython_debug/ 152 | 153 | # PyCharm 154 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 155 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 156 | # and can be added to the global gitignore or merged into this file. For a more nuclear 157 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 158 | #.idea/ 159 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | # python: 28 | # install: 29 | # - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MatrixEditor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hiktools 2 | 3 | ![Module](https://img.shields.io:/static/v1?label=Module&message=hiktools&color=9cf) 4 | ![Build](https://img.shields.io:/static/v1?label=Python&message=>=3.5&color=green) 5 | ![Platform](https://img.shields.io:/static/v1?label=Platforms&message=Linux|Windows&color=yellowgreen) 6 | ![PyPi](https://img.shields.io:/static/v1?label=PyPi&message=1.2.2&color=green) 7 | 8 | 9 | This respository was former known as `hikvision-sdk-cam`, but has changed since the old content of this repository was deleted. This is now a small project with four main functionalities: 10 | * A Wireshark dissector for the Search Active Devices Protocol, 11 | * Decrypt and extract hikvision firmware, 12 | * Send raw SADP packets (only Linux) and 13 | * Send commands via UDP Broadcast. 14 | 15 | To get familiar with the API provided in this repository, take a quick look at the python documentation available **[here »](https://hiktools.readthedocs.io/)** or the 16 | C++ documentation available at Github-Pages **[here »](https://matrixeditor.github.io/hiktools/docs/html/d3/dcc/md__r_e_a_d_m_e.html)**. 17 | 18 | ### Installation 19 | 20 | You can now install the `hiktools` module with pip: 21 | 22 | ```bash 23 | $ python3 -m pip install hiktools 24 | ``` 25 | 26 | ### Overview 27 | --- 28 | **Update:** Since feature version `1.1.0` the native packet creation and communication works! It is still only usable on UNIX systems that support sending raw ethernet frames. The checksum algorithm was disassebled and decompiled correctly, just the input parameters were interpreted wrong. The algorithm is implemented in [C/C++](/cpp/checksum.h) and [python3](/hiktools/csadp/checksum.py). 29 | 30 | Communication on UDP works fine at the moment - this API is just a small wrapper which can be used for a more general API. 31 | 32 | Firmware decryption and extraction will only work on newer `digicap.dav` files with at least `1` file entry (otherwise there will be no files to inspect) in the header. All firmware files and updates can be downloaded from the following endpoint (EU): 33 | 34 | * https://www.hikvisioneurope.com/uk/portal 35 | 36 | There is also a full list of files available at this enpoint stored in a JSON file named [firmwarelist.json](/gists/firmwarelist.json). 37 | 38 | > Info: There is an interesting file located in the extracted files of a firmware image: /hroot.img/initrd/etc/passwd. A password is set to the root user: 39 | 40 | Name: passwd 41 | Folder: - 42 | Size: 44 43 | Packed Size: 1 024 44 | Mode: -rwxrwxr-x 45 | Last change: 2016-12-23 08:27:46 46 | Last modification: 2016-12-23 08:27:46 47 | ------------------------------------------- 48 | 49 | root:ToCOv8qxP13qs:0:0:root:/root/:/bin/psh 50 | 51 | The plain password is `hiklinux`, kudos to [Don Bowman](https://blog.donbowman.ca/2018/01/18/hacking-the-hikvision-part-1/). 52 | 53 | > Old exploit with authkey := YWRtaW46MTEK 54 | 55 | ### Basic Usage 56 | --- 57 | 58 | - Firmware inspection and extraction 59 | 60 | ```python 61 | from hiktools import fmod 62 | 63 | # Open the resource at the specified path (loading is done automatically) 64 | # or manually decrypt the firmware file (see documentation for actual code). 65 | with fmod.DigiCap('filename.dav') as dcap: 66 | # Iterate over all files stored in the DigiCap object 67 | for file_info in dcap: 68 | print('> File name="%s", size=%d, pos=%d, checksum=%d' % file_info) 69 | 70 | # get file amount and current language 71 | print("Files: %d, Language: %d" % (dcap.head.files, dcap.head.language)) 72 | 73 | # save all files stored in 74 | fmod.export(dcap, "outdir/") 75 | ``` 76 | 77 | Or you can use the module main script to extract the files via the CLI: 78 | ```console 79 | python3 -m hiktools.fmod input.dav outputDir 80 | ``` 81 | 82 | - Native interface on sending raw packets (only LINUX) 83 | ```python 84 | from hiktools import csadp 85 | 86 | # Because the following module requires root priviledges it 87 | # has to be imported directly. 88 | from hiktools.csadp import CService 89 | 90 | sock = CService.l2socket('wlan0') 91 | counter = 0x115e 92 | 93 | # To build an Inquiry packet, we need the following information: 94 | # - our mac, ipv4 and ipv6 address (and the counter of course) 95 | packet = csadp.inquiry('', '', '', counter) 96 | 97 | # before we can send the message, a checksum has to be calculated 98 | packet.insert_checksum() 99 | 100 | sock.send(bytes(packet)) 101 | response = csadp.SADPPacket(sock.recv(1024)) 102 | 103 | # to view the contents just print the str() version 104 | print(str(response)) 105 | ``` 106 | 107 | - Interact with the device through UDP broadcast 108 | ```python 109 | from hiktools import sadp 110 | from uuid import uuid4 111 | 112 | # create a packet from a simple dict object 113 | inquiry = sadp.fromdict({ 114 | 'Uuid': str(uuid4()).upper(), 115 | 'MAC': 'ff-ff-ff-ff-ff-ff', 116 | 'Types': 'inquiry' 117 | }) 118 | 119 | # Open up a client to communicate over broadcast 120 | with sadp.SADPClient() as client: 121 | # send the inquiry packet 122 | client.write(inquiry) 123 | 124 | # iterate over all received packets (None is returned on error) 125 | for response in client: 126 | if response is None: break 127 | # initiate the response 128 | message = sadp.unmarshal(response.toxml()) 129 | 130 | # message objects contain a dict-like implementation 131 | for property_name in message: 132 | print(message[property_name]) 133 | 134 | # e.g. 135 | print('Device at', message['IPv4Address']) 136 | ``` 137 | -------------------------------------------------------------------------------- /cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0.0) 2 | project(sadplib VERSION 0.1.1) 3 | 4 | set(DIR ${PROJECT_SOURCE_DIR}) 5 | 6 | add_executable(sadptool 7 | ${DIR}/sadptool.cpp 8 | ${DIR}/adapter.cpp 9 | ${DIR}/checksum.cpp 10 | ${DIR}/ethernet.cpp 11 | ${DIR}/sadplib.cpp 12 | 13 | ) -------------------------------------------------------------------------------- /cpp/README.md: -------------------------------------------------------------------------------- 1 | # Native C/++ SADP Library 2 | 3 | _Note: This library is a work in progress. Program execution at your own risk;_ [API-Docs](https://matrixeditor.github.io/hiktools/). 4 | 5 | ## Network Interfaces ## 6 | 7 | Before setting up a client or socket that can send or receive SADP packets, 8 | the system's available network interfaces must be known. The namespace `eth::adapter` covers different utility methods and classes to query all network interfaces, for instance: 9 | 10 | ```{.cpp} 11 | using namespace eth::adapter; 12 | 13 | NetInterfaceList *list = GetNetInterfaces(); 14 | if (list) { 15 | for (int i = 0; i < list.size(); i++) { 16 | // The returned NetInterface stores information about the 17 | // following attributes: 18 | // - MAC address 19 | // - Interface index 20 | // - IPv6 address 21 | // - Current IPv4 address 22 | // - Interface name 23 | const NetInterface &ni = list.at(i); 24 | } 25 | } 26 | ``` 27 | 28 | To refresh the list with its `NetInterface` objects, invoke the `ClearNetInterfaces()` method and query them again. 29 | 30 | ## API Functionalities 31 | 32 | The `eth` namespace offers a fully implemented layer 2 socket to send and receive proprietary packets. These sockets should be created with the `NetInterface` to bind itself to it, which isn't necessary. 33 | 34 | New sockets can be created rather easy, just declare a new variable: 35 | ```{.cpp} 36 | #include "sadplib.h" 37 | 38 | eth::IISocket sock; 39 | ``` 40 | 41 | The declared socket has no use case, because it does not contain any real file descriptor to a raw socket. To create one, call the `Create` function: 42 | 43 | ```{.cpp} 44 | eth::adapter::NetworkInterface &ni = ...; 45 | if (sock.Create(&ni, 0x3380) < 0) { 46 | //report error 47 | } 48 | ``` 49 | 50 | **Warning:** Custom protocol support is currently disabled, so layer 2 sockets will receive all packets. The `Daemon` class automatically filters the right packets out of the received ones. 51 | 52 | **Important:** Before you create the raw socket, make sure you have the `NetworkInterface` instance. This will be used as a reference, so the socket knows where to bind to. 53 | 54 | The created socket can be used for sending **only** - receiveing is not supported yet. To enable packet receiving, just call the `Bind`-method: 55 | 56 | ```{.cpp} 57 | if (sock.Bind() < 0) { 58 | // report error 59 | } 60 | ``` 61 | --- 62 | 63 | Next, we want to create proprietary SADP packets and send them to the link-local broadcast. By now, only inquiry packets can be generated automatically. However, if you want to create packets, take a look at the method documentation of `BuildFrame` provided in [sadplib.h](sadplib.h). 64 | 65 | To create **and** send an inquiry-packet, there is a pre-defined method to simplify the creation process: 66 | ```{.cpp} 67 | // This method will call BuildInquiry(), GetSize() and the 68 | // sock.Send(...) method. 69 | eth::sadp::packet::SendInquiry(sock); 70 | ``` 71 | 72 | ## SADPTool (currently) 73 | 74 | By now, the `sadplib` can be used to communicate with Hikvision devices over the local area network via their Search Active Devices Protocol (SADP). Note that there is currently no possibility to run this small library without `sudo`. 75 | 76 | The [sadptool.cpp](sadptool.cpp) currently sends an Inquiry-Packet and waits for the next response. Further use-case implementation is in progress. 77 | 78 | --- 79 | Below, the full example of how to use the c++ library: 80 | ```{.cpp} 81 | #include "sadplib" 82 | 83 | using namespace eth; 84 | 85 | int main(void) { 86 | // Create a new socket; 87 | IISocket sock; 88 | 89 | // Creates the socket descriptor; Note that the interface 90 | // can be queried by calling eth::adapter::GetNetInterfaces(). 91 | if (sock.Create(&networkInterface, 0x3380) < 0) { 92 | // log error 93 | exit(1); 94 | } 95 | 96 | // To bind the socket in order to receive packets, use Bind() 97 | if (sock.Bind() < 0) { 98 | exit(1); 99 | } 100 | 101 | // To send a simple Inquiry-packet, there is a utility method, 102 | // that automatically builds and sends the packet. 103 | sadp::packet::SendInquiry(sock); 104 | 105 | // Receiving is rather simple, just call Receive() and the bytes 106 | // that have been read are returned. 107 | const int bytes = sock.Receive(); 108 | if (bytes < 0) { 109 | exit(1); 110 | } 111 | 112 | // The packet creation can be done by simply casting the received 113 | // bytes to the packet structure. Note that the receiving buffer 114 | // will be cleared automatically when trying to receive the next 115 | // packet. 116 | const sadp::sadp_hdr *header = (const sadp::sadp_hdr *)sock.GetBuffer(); 117 | 118 | // Check against the SADP identifier 119 | if (ntohns(header->h_proto) == 0x8033) { 120 | // Same as above: simply casting the payload pointer to the 121 | // pre-defined sadp_frame structure. 122 | const sadp::sadp_frame *frame = (const sadp::sadp_frame *)header->payload; 123 | 124 | //handle packet... 125 | } 126 | } 127 | ``` -------------------------------------------------------------------------------- /cpp/adapter.cpp: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | 28 | #include "adapter.h" 29 | 30 | //-----------------------------[NetInterface]----------------------------- 31 | 32 | eth::adapter::NetInterface::NetInterface( 33 | const unsigned int _Index, const std::string &_Name, 34 | const std::string &_Mac, const std::string &_Inet6, 35 | const unsigned int _Flags 36 | ) : index{_Index}, scope{_Flags} 37 | { 38 | this->mac = std::string(_Mac); 39 | this->ipv6 = std::string(_Inet6); 40 | this->name = std::string(_Name); 41 | 42 | const char *inet4 = eth::adapter::GetInet4Address(this->name); 43 | this->ipv4 = std::string(inet4); 44 | } 45 | 46 | const std::string ð::adapter::NetInterface::GetIpv4Address() const 47 | { 48 | return this->ipv4; 49 | } 50 | 51 | const unsigned int eth::adapter::NetInterface::GetScope() const 52 | { 53 | return this->scope; 54 | } 55 | 56 | const unsigned int eth::adapter::NetInterface::GetInterfaceIndex() const 57 | { 58 | return this->index; 59 | } 60 | 61 | const std::string ð::adapter::NetInterface::GetIpv6Address() const 62 | { 63 | return this->ipv6; 64 | } 65 | 66 | const std::string ð::adapter::NetInterface::GetName() const 67 | { 68 | return this->name; 69 | } 70 | 71 | const std::string ð::adapter::NetInterface::GetMacAddress() const 72 | { 73 | return this->mac; 74 | } 75 | 76 | inline const bool eth::adapter::NetInterface::IsCompat() const 77 | { 78 | return (this->GetScope() & IP_ADDR_COMPAT) != 0; 79 | } 80 | 81 | inline const bool eth::adapter::NetInterface::IsGlobal() const 82 | { 83 | return (this->GetScope() & IP_ADDR_GLOBAL) != 0; 84 | } 85 | 86 | inline const bool eth::adapter::NetInterface::IsLinkLocal() const 87 | { 88 | return (this->GetScope() & IP_ADDR_LINKLOCAL) != 0; 89 | } 90 | 91 | inline const bool eth::adapter::NetInterface::IsLoopback() const 92 | { 93 | return (this->GetScope() & IP_ADDR_LOOPBACK) != 0; 94 | } 95 | 96 | inline const bool eth::adapter::NetInterface::IsSiteLocal() const 97 | { 98 | return (this->GetScope() & IP_ADDR_SITELOCAL) != 0; 99 | } 100 | 101 | //-----------------------------[NetInterface]----------------------------- 102 | 103 | //---------------------------[NetInterfaceList]--------------------------- 104 | 105 | /** 106 | * @brief The file path to inet6 address values. 107 | */ 108 | static const char *IF_INET6_PATH = "/proc/net/if_inet6"; 109 | 110 | /** 111 | * @brief The basic path where files for every network interface are stored. 112 | */ 113 | static const char *NET_PATH = "/sys/class/net/"; 114 | 115 | /** 116 | * @brief THe global interface list 117 | * 118 | */ 119 | static eth::adapter::NetInterfaceList globalList; 120 | 121 | void eth::adapter::ClearNetInterfaces() 122 | { 123 | if (globalList.size()) { 124 | globalList.clear(); 125 | } 126 | } 127 | 128 | const eth::adapter::NetInterfaceList *eth::adapter::GetNetInterfaces() 129 | { 130 | if (globalList.size()) { 131 | return &globalList; 132 | } 133 | 134 | std::ifstream if_inet6, address; 135 | std::string ipv6, name, mac; 136 | unsigned int index, prefix, scope, flags; 137 | 138 | if_inet6.open(IF_INET6_PATH); 139 | // This way we indicate that an error has occurred. 140 | if (!if_inet6) { 141 | return &globalList; 142 | } 143 | 144 | do { 145 | if_inet6 >> ipv6; 146 | 147 | // NOTE: Here, we are testing against the EOF. It is necessary, 148 | // because the last loop would read the last interface again. 149 | // This check has to be done AFTER the first reading, otherwise 150 | // the '\n' would be count as a valid character. 151 | if (if_inet6.eof()) { 152 | break; 153 | } 154 | 155 | if_inet6 >> std::hex >> index; 156 | if_inet6 >> std::hex >> prefix; 157 | if_inet6 >> std::hex >> scope; 158 | if_inet6 >> std::hex >> flags; 159 | if_inet6 >> name; 160 | 161 | std::string path = NET_PATH + name + "/address"; 162 | address.open(path); 163 | if (!address) { 164 | return &globalList; 165 | } 166 | 167 | address >> mac; 168 | eth::adapter::NetInterface *interface = new eth::adapter::NetInterface( 169 | index, name, mac, ipv6, scope 170 | ); 171 | 172 | globalList.push_back(*interface); 173 | 174 | address.close(); 175 | } while (if_inet6.peek() == '\n' && !if_inet6.eof()); 176 | 177 | if_inet6.close(); 178 | return &globalList; 179 | } 180 | 181 | //---------------------------[NetInterfaceList]--------------------------- 182 | 183 | // Most of the code was taken from here: 184 | // https://stackoverflow.com/questions/2283494/get-ip-address-of-an-interface-on-linux 185 | const char *eth::adapter::GetInet4Address(const std::string &_Name) 186 | { 187 | int fd; 188 | struct ifreq ifr; 189 | 190 | fd = socket(AF_INET, SOCK_DGRAM, 0); 191 | ifr.ifr_addr.sa_family = AF_INET; 192 | 193 | strncpy(ifr.ifr_name , _Name.c_str(), IFNAMSIZ - 1); 194 | ioctl(fd, SIOCGIFADDR, &ifr); 195 | close(fd); 196 | return inet_ntoa(( (struct sockaddr_in *)&ifr.ifr_addr )->sin_addr); 197 | } 198 | -------------------------------------------------------------------------------- /cpp/adapter.h: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | #if !defined(__ADAPTER_INFO_H__) 24 | #define __ADAPTER_INFO_H__ 25 | 26 | #include 27 | #include 28 | 29 | // These imports are needed to get the ipv4 address via a simple 30 | // UDP socket on the NetInterface. 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | namespace eth 40 | { 41 | 42 | namespace adapter 43 | { 44 | 45 | #define IP_ADDR_GLOBAL 0x0000U 46 | #define IP_ADDR_LOOPBACK 0x0010U 47 | #define IP_ADDR_LINKLOCAL 0x0020U 48 | #define IP_ADDR_SITELOCAL 0x0040U 49 | #define IP_ADDR_COMPAT 0x0080U 50 | 51 | /** 52 | * @brief A network interface. 53 | */ 54 | class NetInterface { 55 | private: 56 | /** 57 | * @brief The scope of this interface. 58 | */ 59 | const unsigned int scope; 60 | 61 | /** 62 | * @brief The interface index. 63 | */ 64 | const unsigned int index; 65 | 66 | /** 67 | * @brief The MAC address. 68 | * 69 | * The address can be found in every /sys/class/net//address 70 | * file in an interface directory. 71 | */ 72 | std::string mac; 73 | 74 | /** 75 | * @brief The raw inet6 address used in Inquiry packets. 76 | */ 77 | std::string ipv6; 78 | 79 | /** 80 | * @brief The interface name. 81 | */ 82 | std::string name; 83 | 84 | /** 85 | * @brief The raw inet4 address used in Inquiry packets. 86 | */ 87 | std::string ipv4; 88 | 89 | public: 90 | /** 91 | * @brief Construct a new NetInterface object. 92 | * 93 | * @param _Index The interface index 94 | * @param _Name The name of this interface (copied) 95 | * @param _Mac The local mac address (copied) 96 | * @param _Inet6 The ipv6 address (copied) 97 | * @param _Flags The flags which will be converted into an 98 | * enumeration object (Scope). 99 | */ 100 | NetInterface( 101 | const unsigned int _Index, const std::string &_Name, 102 | const std::string &_Mac, const std::string &_Inet6, 103 | const unsigned int _Flags 104 | ); 105 | 106 | const std::string &GetIpv4Address() const; 107 | 108 | /** 109 | * @brief Get the Scope of this interface. 110 | * 111 | * @return [const unsigned int] the scope of this interface 112 | */ 113 | const unsigned int GetScope() const; 114 | 115 | /** 116 | * @brief Get the Interface Index. 117 | * 118 | * @return [const unsigned int] the index of this interface. 119 | */ 120 | const unsigned int GetInterfaceIndex() const; 121 | 122 | /** 123 | * @brief Get the Mac Address of this interface. 124 | * 125 | * You should use the eth::mac::ToBytes() method to convert this 126 | * address to bytes. 127 | * 128 | * @return [const char*] the mac address 129 | */ 130 | const std::string &GetMacAddress() const; 131 | 132 | /** 133 | * @brief Get the Ipv6 Address of this interface. 134 | * 135 | * @b Note: The returned string is not normalized. Use the 136 | * eth::ip::ToString() method to convert the returned address 137 | * to a human readable address. 138 | * 139 | * @return [const char*] the ipv6 address 140 | */ 141 | const std::string &GetIpv6Address() const; 142 | 143 | /** 144 | * @brief Get the name of this interface. 145 | * 146 | * @return [const char*] the interface name 147 | */ 148 | const std::string &GetName() const; 149 | 150 | const bool IsLoopback() const; 151 | const bool IsLinkLocal() const; 152 | const bool IsCompat() const; 153 | const bool IsSiteLocal() const; 154 | const bool IsGlobal() const; 155 | 156 | }; 157 | 158 | /** 159 | * @brief Get the Inet4Addres of the specified interface. 160 | * 161 | * @param _Name the network interface name 162 | * @return [const char*] the ip address as a string 163 | */ 164 | const char *GetInet4Address(const std::string &_Name); 165 | 166 | /** 167 | * @brief Simple type definition for a linked list that stores the 168 | * platform's network interfaces. 169 | * 170 | * This class overloads the '[]' operator so that the following code 171 | * example can be used to iterate over the list: 172 | * @code {.cpp} 173 | * NetInterfaceList *list = GetNetInterfaces(); 174 | * for (unsigned int i = 0; i < list->GetSize(); i++) { 175 | * const NetInterface &interface = list->Get(i); 176 | * //or 177 | * const NetInterface &interface = (*list)[i]; 178 | * } 179 | * @endcode 180 | * 181 | * @b Note: This class is designed to store the interfaces parsed from 182 | * /sys/class/net/.* and /proc/net/if_inet6. 183 | */ 184 | using NetInterfaceList = std::vector; 185 | 186 | /** 187 | * @brief Tries to load all network interfaces and stores them in a vector. 188 | * 189 | * @return [const NetInterfaceList*] the interface list 190 | */ 191 | const NetInterfaceList *GetNetInterfaces(); 192 | 193 | /** 194 | * @brief Clears the currently loaded network interfaces. 195 | */ 196 | void ClearNetInterfaces(); 197 | 198 | } // namespace adapter 199 | 200 | } // namespace eth 201 | 202 | #endif // __ADAPTER_INFO_H__ 203 | -------------------------------------------------------------------------------- /cpp/checksum.cpp: -------------------------------------------------------------------------------- 1 | #include "checksum.h" 2 | 3 | unsigned int eth::sadp::Checksum(unsigned short *header, unsigned int prefix) 4 | { 5 | unsigned int checksum = 0; 6 | 7 | int var1 = 0; 8 | int var2 = 0; 9 | int index = 0; 10 | 11 | unsigned short *pHeader = header; 12 | 13 | if (3 < (prefix & 0xFFFFFFFE)) { 14 | // When a SADPTool instance tries build a packet, 0x42 is provided as 15 | // the prefix number. 16 | index = (prefix - 4 >> 2) + 1; 17 | do { 18 | prefix -= 4; 19 | var1 += (unsigned int)pHeader[0]; 20 | var2 += (unsigned int)pHeader[1]; 21 | 22 | pHeader = pHeader + 2; 23 | index -= 1; 24 | } while (index != 0); 25 | } 26 | 27 | if (1 < prefix) { 28 | // On simple inquiry packets, this additional sum computing 29 | // does not change anything. 30 | checksum = (unsigned int)*pHeader; 31 | pHeader++; 32 | prefix -= 2; 33 | } 34 | 35 | checksum += var1 + var2; 36 | if (prefix != 0) { 37 | checksum += *(unsigned char *)(pHeader); 38 | } 39 | 40 | checksum = (checksum >> 16) + (checksum & 0xFFFF); 41 | return ~((checksum >> 16) + checksum); 42 | } -------------------------------------------------------------------------------- /cpp/checksum.h: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | #if !defined(__SADP_CHECKSUM_H__) 23 | #define __SADP_CHECKSUM_H__ 24 | 25 | namespace eth { 26 | 27 | namespace sadp { 28 | 29 | /** 30 | * @brief The code for a SADPClient. 31 | * 32 | * While inspecting the network traffic between an hikvision ip-camera and the 33 | * SADPTool, there was this code value when the SADPTool had send a message. 34 | */ 35 | #define CSADP_CLIENT_TYPE 0x4201 36 | 37 | /** 38 | * @brief The code for a SADPServer. 39 | * 40 | * Assuming the code defined above defines clients as the sender, this code 41 | * indicated that a server built the received packet. 42 | */ 43 | #define CSADP_SERVER_TYPE 0xf601 44 | 45 | /** 46 | * @brief A small checksum generator for SADP Packets (Disassebled with Ghidra). 47 | * 48 | * Note that the type value is always 0x42 (on the client side). To verify that, 49 | * the x32dbg-Debugger was used. 50 | * 51 | * @b Important: The input pointer should reference an array of little endian 52 | * encoded bytes. For example, in wireshark you will see 0x2101 which has to be 53 | * converted to 0x0221 (swap bytes). 54 | * 55 | * @param header an unsinged short pointer to the start of the header of the 56 | * packet 57 | * @param type as defined above, the sender type 58 | * @return an unsigned integer value which contains the actual checksum within 59 | * the first two bytes. 60 | */ 61 | unsigned int Checksum(unsigned short *header, unsigned int type); 62 | 63 | } // namespace sadp 64 | 65 | 66 | } // namespace eth 67 | 68 | 69 | #endif // __SADP_CHECKSUM_H__ 70 | 71 | -------------------------------------------------------------------------------- /cpp/ethernet.cpp: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | #ifdef __unix__ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include // for strcpy 33 | 34 | #include "ethernet.h" 35 | 36 | //-------------------------------[Counter]------------------------------- 37 | 38 | static eth::Counter counter; 39 | 40 | eth::Counter ð::GetCounter() 41 | { 42 | return counter; 43 | } 44 | 45 | eth::Counter::Counter() 46 | { 47 | this->num = static_cast(std::rand()); 48 | } 49 | 50 | eth::Counter::Counter(unsigned int _Start) 51 | : num{_Start} {} 52 | 53 | const unsigned int eth::Counter::Get() const 54 | { 55 | return this->num; 56 | } 57 | 58 | void eth::Counter::Increment() 59 | { 60 | this->num = (unsigned int) (num + 1); 61 | } 62 | 63 | void eth::Counter::Set(const unsigned int _NewValue) 64 | { 65 | this->num = _NewValue; 66 | } 67 | 68 | const unsigned int eth::Counter::GetAndIncrement() 69 | { 70 | const unsigned int count = this->num; 71 | this->Increment(); 72 | return count; 73 | } 74 | 75 | //-------------------------------[ip]------------------------------- 76 | 77 | static inline uint8_t IntFromHex(const uint8_t byte) 78 | { 79 | if (byte >= '0' && byte <= '9') return byte - '0'; 80 | else if (byte >= 'a' && byte <='f') return byte - 'a' + 10; 81 | else if (byte >= 'A' && byte <='F') return byte - 'A' + 10; 82 | return byte; 83 | } 84 | 85 | static inline uint8_t HexFromInt(const uint8_t _Src) 86 | { 87 | if (_Src >= 0 && _Src <= 9) return _Src + '0'; 88 | else if (_Src >= 0xA && _Src <=0xF) return _Src + 'a' - 10; 89 | return _Src; 90 | } 91 | 92 | void eth::ip::ToString(const uint32_t *_IpAddress, char *_DstAddress) 93 | { 94 | uint8_t* ip = (uint8_t *)_IpAddress; 95 | 96 | char result[16] {0}; 97 | sprintf(result, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); 98 | 99 | strcpy(_DstAddress, result); 100 | } 101 | 102 | void eth::ip::ToBytes(const char *_IpAddress, uint32_t *_DstAddress) 103 | { 104 | struct in_addr ip_addr; 105 | inet_aton(_IpAddress, &ip_addr); 106 | 107 | *_DstAddress = (uint32_t) ip_addr.s_addr; 108 | } 109 | 110 | void eth::ip::ToString(const uint8_t *_Ip6Address, uint8_t *_DstAddress) 111 | { 112 | uint8_t *dst = _DstAddress; 113 | for (size_t i = 0; i < 16; i++) { 114 | unsigned char c0 = HexFromInt(_Ip6Address[i] >> 4); 115 | unsigned char c1 = HexFromInt(_Ip6Address[i] & 0xF); 116 | 117 | *(dst) = c0; 118 | *(dst + 1) = c1; 119 | dst +=2; 120 | } 121 | } 122 | 123 | void eth::ip::ToBytes(const uint8_t *_Ip6Address, uint8_t *_DstAddress) 124 | { 125 | unsigned char *dst = _DstAddress; 126 | for (size_t i = 0; i < 32; i+=2) { 127 | unsigned char c = IntFromHex(_Ip6Address[i]); 128 | unsigned char c1 = IntFromHex(_Ip6Address[i + 1]); 129 | 130 | *(dst) = c << 4 | c1 & 0xFF; 131 | dst++; 132 | } 133 | } 134 | 135 | //-------------------------------[mac]------------------------------- 136 | 137 | void eth::mac::ToString(const uint8_t *_MacAddress, uint8_t *_DstAddress) 138 | { 139 | uint8_t *dst = _DstAddress; 140 | for (size_t i = 0; i < 6; i++) { 141 | unsigned char c0 = HexFromInt(_MacAddress[i] >> 4); 142 | unsigned char c1 = HexFromInt(_MacAddress[i] & 0xF); 143 | 144 | *(dst) = c0; 145 | *(dst + 1) = c1; 146 | if (i == 5) break; 147 | 148 | *(dst + 2) = ':'; 149 | dst += 3; 150 | } 151 | } 152 | 153 | void eth::mac::ToBytes(const uint8_t *_MacAddress, uint8_t *_DstAddress) 154 | { 155 | uint8_t *dst = _DstAddress; 156 | for (size_t i = 0; i < 18; i+=3) { 157 | unsigned char c = IntFromHex(_MacAddress[i]); 158 | unsigned char c1 = IntFromHex(_MacAddress[i + 1]); 159 | 160 | *(dst) = c << 4 | c1 & 0xF; 161 | dst++; 162 | } 163 | } 164 | 165 | //-------------------------------[IISocket]------------------------------- 166 | 167 | eth::IISocket::IISocket() 168 | : closed{false}, protocol{ETH_P_ALL}, interface{nullptr} {} 169 | 170 | eth::IISocket::IISocket(const eth::adapter::NetInterface *_Interface) 171 | : IISocket() 172 | { 173 | this->interface = _Interface; 174 | if (interface) { 175 | if (this->Create(interface, protocol)) { 176 | this->Bind(); 177 | } 178 | } 179 | } 180 | 181 | eth::IISocket::~IISocket() 182 | { 183 | this->Close(); 184 | } 185 | 186 | const bool eth::IISocket::Create(const eth::adapter::NetInterface *_Interface) 187 | { 188 | return this->Create(_Interface, ETH_P_ALL); 189 | } 190 | 191 | const bool eth::IISocket::Create( 192 | const eth::adapter::NetInterface *_Interface, const uint16_t _Proto 193 | ) { 194 | if (this->interface != _Interface && _Interface) { 195 | this->interface = _Interface; 196 | } 197 | 198 | if (!this->interface) return false; 199 | 200 | if (this->protocol != _Proto) { 201 | this->protocol = _Proto; 202 | } 203 | 204 | this->sock = socket(AF_PACKET, SOCK_RAW, htons(this->protocol)); 205 | this->closed = false; 206 | 207 | struct ifreq eth; 208 | ioctl(this->sock, SIOCGIFFLAGS, ð); 209 | 210 | eth.ifr_flags |= IFF_PROMISC; 211 | 212 | ioctl(this->sock, SIOCSIFFLAGS, ð); 213 | 214 | return (this->sock != -1); 215 | } 216 | 217 | const bool eth::IISocket::Bind() 218 | { 219 | struct sockaddr_ll socketaddress; 220 | memset(&socketaddress, 0x00, sizeof(socketaddress)); 221 | socketaddress.sll_family = AF_PACKET; 222 | socketaddress.sll_protocol = htons(ETH_P_ALL); 223 | socketaddress.sll_ifindex = interface->GetInterfaceIndex(); 224 | return bind(this->sock, (struct sockaddr *)&socketaddress, sizeof(socketaddress)) >= 0; 225 | } 226 | 227 | const int eth::IISocket::Receive() 228 | { 229 | int bytecount; 230 | 231 | memset((void *)this->buf, 0x00, BUFFER_SIZE); 232 | int size = recv(this->sock, this->buf, BUFFER_SIZE,0); 233 | if (size < 0) { 234 | perror("Unexpected return code"); 235 | } 236 | return size; 237 | } 238 | 239 | void eth::IISocket::Close() 240 | { 241 | if (this->IsClosed()) { 242 | return; 243 | } 244 | 245 | if (this->sock != -1) { 246 | close(this->sock); 247 | } 248 | // Don't delete the NetInterface reference, because the pointer 249 | // points to an internal structure. 250 | this->interface = nullptr; 251 | closed = true; 252 | } 253 | 254 | const int eth::IISocket::Send(char *_Buf, const int _Length) const 255 | { 256 | return (int)send(this->sock, _Buf, _Length, 0x00); 257 | } 258 | 259 | const bool eth::IISocket::IsClosed() const 260 | { 261 | return closed; 262 | } 263 | 264 | const eth::adapter::NetInterface *eth::IISocket::GetInterface() const 265 | { 266 | return interface; 267 | } 268 | 269 | const char *eth::IISocket::GetBuffer() const 270 | { 271 | return buf; 272 | } 273 | 274 | //-------------------------------[sadp]------------------------------- 275 | 276 | #define __case(name) \ 277 | case ((uint8_t)eth::sadp::sadp_query_type::name): return #name; 278 | 279 | const char *eth::sadp::QueryTypeToString( 280 | const unsigned char _QType, 281 | const eth::sadp::sadp_packet_type _PType 282 | ) { 283 | uint8_t qtype = _QType; 284 | if (_PType == eth::sadp::sadp_packet_type::Response) { 285 | qtype--; 286 | } 287 | 288 | switch (qtype) { 289 | __case(Inquiry) 290 | __case(DeviceOnlineRequest) 291 | __case(UpdateIP) 292 | __case(ResetPassword) 293 | __case(CMSInfo) 294 | __case(ModifyNetParam) 295 | 296 | default: return "Unknown"; 297 | }; 298 | } 299 | 300 | #undef __case 301 | 302 | #endif // __unix__ -------------------------------------------------------------------------------- /cpp/ethernet.h: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | #if !defined(__LAYER_2_H__) 24 | #define __LAYER_2_H__ 25 | 26 | #include "adapter.h" 27 | 28 | namespace eth 29 | { 30 | 31 | /** 32 | * @brief Defines the maximum buffer size for receiving tasks. 33 | * 34 | * The max buffer size of the receiving character buffer. 35 | */ 36 | #define BUFFER_SIZE 8192 37 | 38 | /** 39 | * @brief Counter class used to manage the internal counter and its 40 | * access. 41 | * 42 | * A global instance of this class will be created at runtime with a random 43 | * generated number as its starting point. 44 | */ 45 | class Counter { 46 | private: 47 | /** 48 | * @brief the internal counter used to 'sign' the packets. 49 | */ 50 | unsigned int num; 51 | 52 | public: 53 | /** 54 | * @brief Construct a new Counter object. 55 | * 56 | * This object starts at value 0. 57 | */ 58 | Counter(); 59 | 60 | /** 61 | * @brief Construct a new counter with the given start value. 62 | * 63 | * @param _Start the starting point 64 | */ 65 | Counter(unsigned int _Start); 66 | 67 | /** 68 | * @brief Returns the current counter value. 69 | * 70 | * @return [const unsigned int] the counter's value 71 | */ 72 | const unsigned int Get() const; 73 | 74 | /** 75 | * @brief Get the And Increment the counter 76 | * 77 | * @return [const unsigned int] the counter before the increment 78 | */ 79 | const unsigned int GetAndIncrement(); 80 | 81 | /** 82 | * @brief Increments this counter. 83 | */ 84 | void Increment(); 85 | 86 | /** 87 | * @brief Sets the new starting point. 88 | * 89 | * @param _NewValue the new starting point. 90 | */ 91 | void Set(const unsigned int _NewValue); 92 | }; 93 | 94 | /** 95 | * @brief The global counter instance. 96 | */ 97 | Counter &GetCounter(); 98 | 99 | namespace sadp 100 | { 101 | 102 | /** 103 | * @brief A simple struct used to cast the received bytes into that 104 | * structure. 105 | * 106 | * This type definition can be used as follows: 107 | * 108 | * @code {.c} 109 | * IISocket socket = ...; 110 | * //... 111 | * if (socket.Receive() > 0) { 112 | * psadp_hdr packet = (psadp_hdr)(socket.GetBuffer()); 113 | * } 114 | * @endcode 115 | * 116 | * See IISocket for more usage details. 117 | */ 118 | typedef struct sadp_hdr { 119 | 120 | /** 121 | * @brief The packet's destination MAC address. 122 | */ 123 | unsigned char h_dest[6]; 124 | 125 | /** 126 | * @brief The packet's source MAC address. 127 | * 128 | */ 129 | unsigned char h_src[6]; 130 | 131 | /** 132 | * @brief The packet's protocol. 133 | */ 134 | uint16_t h_proto; 135 | 136 | /** 137 | * @brief The paylod of the packet that can be parsed. 138 | */ 139 | char payload[]; 140 | 141 | } sadp_hdr, *psadp_hdr; 142 | 143 | enum class sadp_query_type : unsigned char { 144 | /** 145 | * @brief ? 146 | */ 147 | DeviceOnlineRequest = 0x02, 148 | 149 | /** 150 | * @brief Device location query 151 | */ 152 | Inquiry = 0x03, 153 | 154 | /** 155 | * @brief ? 156 | */ 157 | UpdateIP = 0x06, 158 | 159 | /** 160 | * @brief ? 161 | */ 162 | ResetPassword = 0x0a, 163 | 164 | /** 165 | * @brief ? 166 | */ 167 | CMSInfo = 0x0c, 168 | 169 | /** 170 | * @brief ? 171 | */ 172 | ModifyNetParam = 0x10 173 | 174 | }; 175 | 176 | enum class sadp_packet_type : unsigned char { 177 | /** 178 | * @brief Indicator for a response packet. 179 | */ 180 | Response = 0x01, 181 | 182 | /** 183 | * @brief Indicator for a request packet. 184 | */ 185 | Request = 0x02 186 | }; 187 | 188 | /** 189 | * @brief Converts the given packet type to a string representation. 190 | * 191 | * @param _QType the query type 192 | * @param _PType the packet type 193 | * @return [const char*] the type name 194 | */ 195 | const char *QueryTypeToString( 196 | const unsigned char _QType, 197 | const sadp_packet_type _PType 198 | ); 199 | 200 | 201 | typedef struct sadp_frame { 202 | 203 | /** 204 | * @brief The default start value for a SADP packet. (0x21) 205 | */ 206 | uint8_t f_prefix = 0x21U; 207 | 208 | /** 209 | * @brief The packet type (either request or response). 210 | * 211 | * Defines 0x02 to be a request and 0x01 a response packet. 212 | */ 213 | uint8_t f_packet_type; 214 | 215 | /** 216 | * @brief The header client type, which is defined in the 'checksum.h' file. 217 | * 218 | * There are two possible values of this client type number. 219 | * 220 | * 1. [0x42]: While inspecting the network traffic between an hikvision 221 | * ip-camera and the SADPTool, there was this code value when the SADPTool 222 | * had send a message. 223 | * 224 | * 2. [0xf6]: Assuming the code defined above defines clients as the sender, 225 | * this code indicates that a server has built the received packet. 226 | * 227 | * Note that the client type value will be in the first by of this short 228 | * variable. 229 | */ 230 | uint16_t f_client_type; 231 | 232 | /** 233 | * @brief The checksum counter which is used to verify the packet. 234 | */ 235 | uint32_t f_counter; 236 | 237 | /** 238 | * @brief A simple number used to support calculation of the checksum. 239 | * 240 | * In all received packets this number was equal to 0x0604. 241 | */ 242 | uint16_t f_marker = 0x0604U; 243 | 244 | /** 245 | * @brief The actual packet type. 246 | * 247 | * Use the #PacketTypeToString() method to get the string representation of 248 | * this value. 249 | */ 250 | uint8_t f_type; 251 | 252 | /** 253 | * @brief The parameters used in connection with the actual packet 254 | * type. 255 | * 256 | * Most likely, this value is going to be 0. 257 | */ 258 | uint8_t f_parameters = 0x00; 259 | 260 | /** 261 | * @brief The calculated checksum of this frame. 262 | * 263 | * This checksum is verified by the #Checksum() implementation by this 264 | * library (and in sadp.dll). 265 | */ 266 | uint16_t f_checksum; 267 | 268 | /** 269 | * @brief The source MAC address that is equal to sadp_hdr::h_src. 270 | */ 271 | uint8_t f_src_mac[6]; 272 | 273 | /** 274 | * @brief The source IP address. 275 | */ 276 | uint32_t f_src_ip; 277 | 278 | /** 279 | * @brief The destination MAC address that is equal to sadp_hdr::h_dest. 280 | */ 281 | uint8_t f_dest_mac[6]; 282 | 283 | /** 284 | * @brief The destination ip address. 285 | * 286 | * This IP address which will be 0.0.0.0 in most cases. 287 | */ 288 | uint32_t f_dest_ip = 0x00; 289 | 290 | /** 291 | * @brief The used subnet mask. 292 | */ 293 | uint16_t f_subnet_mask = 0x00; 294 | 295 | /** 296 | * @brief The sadp payload. 297 | */ 298 | char payload[]; 299 | 300 | } sadp_frame, *psadp_frame; 301 | 302 | 303 | typedef struct inquiry_payload { 304 | 305 | /** 306 | * @brief The inet6 address of the sender. 307 | */ 308 | uint8_t f_inet6_address[16]; 309 | 310 | } inquiry_payload, *pinquiry_payload; 311 | 312 | } // namespace sadp 313 | 314 | /** 315 | * @brief Internal namespace to separate IP address conversion 316 | * methods. 317 | */ 318 | namespace ip 319 | { 320 | 321 | #define IPV6_ADDR_LEN 16 322 | #define IPV6_ADDR_STR_LEN (IPV6_ADDR_LEN*2) 323 | 324 | /** 325 | * @brief Converts an ipv4 address to string. 326 | * 327 | * @param _IpAddress the address as an uint32_t 328 | * @param _DstAddress the destination buffer 329 | */ 330 | void ToString(const uint32_t *_IpAddress, char *_DstAddress); 331 | 332 | /** 333 | * @brief Converts an ipv6 address to string. 334 | * 335 | * Returns all hex bytes of the given 16-length input address. The 336 | * returned address has a length of 32, for instance: 337 | * 338 | * - "fe80000000000000b0235af200027250" 339 | * 340 | * @param _Ip6Address the address buffer 341 | * @param _DstAddress the destination buffer 342 | */ 343 | void ToString(const uint8_t *_Ip6Address, uint8_t *_DstAddress); 344 | 345 | /** 346 | * @brief Converts an ipv4 address to a number. 347 | * 348 | * The output buffer should have a length of 4. 349 | * 350 | * @param _IpAddress the address buffer 351 | * @param _DstAddress the number destination 352 | */ 353 | void ToBytes(const char *_IpAddress, uint32_t *_DstAddress); 354 | 355 | /** 356 | * @brief Converts an ipv6 to a byte buffer 357 | * 358 | * The output buffer should have a length of 16 bytes. 359 | * 360 | * @param _Ip6Address the address buffer 361 | * @param _DstAddress the destination buffer 362 | */ 363 | void ToBytes(const uint8_t *_Ip6Address, uint8_t *_DstAddress); 364 | } // namespace ip 365 | 366 | /** 367 | * @brief Internal namespace to separate MAC address conversion 368 | * methods. 369 | */ 370 | namespace mac 371 | { 372 | 373 | /** 374 | * @brief Converts the MAC address to string. 375 | * 376 | * This method expects a number array of length 6. 377 | * 378 | * @param _MacAddress the address buffer 379 | * @param _DstAddress the destination buffer 380 | */ 381 | void ToString(const uint8_t *_MacAddress, uint8_t *_DstAddress); 382 | 383 | /** 384 | * @brief Converts the MAC address to bytes. 385 | * 386 | * The expected structure of the given MAC address would be the following: 387 | * 388 | * - "12:34:56:78:90:12" 389 | * 390 | * @param _MacAddress the address buffer 391 | * @param _DstAddress the destination buffer 392 | */ 393 | void ToBytes(const uint8_t *_MacAddress, uint8_t *_DstAddress); 394 | } // namespace mac 395 | 396 | 397 | /** 398 | * @brief A layer 2 socket used to send and receive SADP packets. 399 | */ 400 | class IISocket { 401 | private: 402 | /** 403 | * @brief The socket used for receiving and sending data. 404 | * 405 | * By creating an object of this class, this socket won't be initialized. That 406 | * can be done by calling the #Create() method. 407 | */ 408 | int sock; 409 | 410 | /** 411 | * @brief The buffer used to receive data. 412 | * 413 | * See the definition of BUFFER_SIZE for more information. 414 | */ 415 | char buf[BUFFER_SIZE]; 416 | 417 | /** 418 | * @brief Stores the interface this socket was bound to. 419 | * 420 | * The binding interface. 421 | */ 422 | const eth::adapter::NetInterface *interface; 423 | 424 | /** 425 | * @brief Specifies the protocol to listen to. 426 | * 427 | * The protocol on layer 2. 428 | */ 429 | uint16_t protocol; 430 | 431 | /** 432 | * @brief Simple variable to indicate whether this socket has been 433 | * closed. 434 | */ 435 | bool closed; 436 | 437 | public: 438 | /** 439 | * @brief Construct a new empty IISocket object 440 | * 441 | * Note that this constructor does not create the system socket. 442 | */ 443 | IISocket(); 444 | 445 | /** 446 | * @brief Construct a new IISocket object and creates the layer 2 447 | * socket. 448 | * 449 | * @param _Interface the interface to bind to 450 | */ 451 | IISocket(const eth::adapter::NetInterface *_Interface); 452 | 453 | /** 454 | * @brief Destroy the IISocket object. 455 | * 456 | * This method will have no effects if this socket has been closed 457 | * already. 458 | */ 459 | ~IISocket(); 460 | 461 | /** 462 | * @brief Creates a new layer 2 socket on the stored interface. 463 | * 464 | * The code that should create the layer 2 socket could be the following 465 | * for all packets: 466 | * @code {.C} 467 | * this->socket = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); 468 | * @endcode 469 | * 470 | * If a specific socket for the SADP protocol should be created, the code 471 | * would be like this: 472 | * @code {.C} 473 | * this->socket = socket(AF_PACKET, SOCK_RAW, htons(0x8033)); 474 | * @endcode 475 | * 476 | * @param _Interface the interface name 477 | * @param _Proto the used protocol 478 | * @return true if the socket creation was successfull 479 | * @return false if no socket could be created 480 | */ 481 | const bool Create( 482 | const eth::adapter::NetInterface *_Interface, 483 | const uint16_t _Proto 484 | ); 485 | 486 | /** 487 | * @brief Creates a new layer 2 socket on the stored interface. 488 | * 489 | * The code that should create the layer 2 socket could be the following 490 | * for all packets: 491 | * @code {.C} 492 | * this->socket = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); 493 | * @endcode 494 | * 495 | * @param _Interface the interface name 496 | * @return true if the socket creation was successfull 497 | * @return false if no socket could be created 498 | */ 499 | const bool Create(const eth::adapter::NetInterface *_Interface); 500 | 501 | /** 502 | * @brief Tries to capture the next packet from wire. 503 | * 504 | * @return [const int] the number of bytes received or -1 on failure 505 | */ 506 | const int Receive(); 507 | 508 | /** 509 | * @brief Binds this socket to the interface provided in #Create(). 510 | * 511 | * This method should be build on the following code: 512 | * @code {.C} 513 | * struct sockaddr_ll socketaddress; 514 | * memset(&socketaddress, 0x00, sizeof(socketaddress)); 515 | * socketaddress.sll_family = AF_PACKET; 516 | * socketaddress.sll_protocol = htons(this->protocol); 517 | * socketaddress.sll_ifindex = interface->GetInterfaceIndex(); 518 | * // bind the socket 519 | * bind(this->sock, (struct sockaddr *)&socketaddress, sizeof(socketaddress)); 520 | * @endcode 521 | * 522 | * 523 | * @return true if the socket has been bound to the interface 524 | * @return false on failure 525 | */ 526 | const bool Bind(); 527 | 528 | /** 529 | * @brief Sends the given data. 530 | * 531 | * This method will expand to the following code: 532 | * @code {.c} 533 | * return (int)send(this->sock, _Buf, _Length, 0x00); 534 | * @endcode 535 | * 536 | * @param _Buf the data to be sent 537 | * @param _Length the data length 538 | * @return [const int] the amount of bytes that have been sent or 539 | * -1 on failure. 540 | */ 541 | const int Send(char *_Buf, const int _Length) const; 542 | 543 | /** 544 | * @brief Closes this socket and releases all system associated 545 | * resources with it. 546 | * 547 | * The basic execution flow should be the following: 548 | * @code {.C} 549 | * close(this->socket); 550 | * free(this->buffer); 551 | * free(this->interface); 552 | * @endcode 553 | * 554 | * This method should not have any effect if this socket has already 555 | * been closed. 556 | */ 557 | void Close(); 558 | 559 | /** 560 | * @brief Returns whether this socket has been closed. 561 | * 562 | * @return true if the socket has been closed 563 | * @return false if the socket is active 564 | */ 565 | const bool IsClosed() const; 566 | 567 | /** 568 | * @brief Get the Interface of this socket. 569 | * 570 | * @return [const NetInterface &] the interface 571 | */ 572 | const eth::adapter::NetInterface *GetInterface() const; 573 | 574 | /** 575 | * @brief Get the Buffer. 576 | * 577 | * @return [const char*] the raw buffer 578 | */ 579 | const char *GetBuffer() const; 580 | }; 581 | 582 | } // namespace eth 583 | 584 | #endif // __LAYER_2_H__ 585 | -------------------------------------------------------------------------------- /cpp/eventing.h: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | #if !defined(__EVENTING_H__) 23 | #define __EVENTING_H__ 24 | 25 | #include "adapter.h" 26 | #include "ethernet.h" 27 | 28 | namespace eth 29 | { 30 | 31 | namespace event 32 | { 33 | 34 | /** 35 | * @brief Simple Event-Object that stores the received message. 36 | */ 37 | class PacketEvent 38 | { 39 | private: 40 | /** 41 | * @brief The received pacekt header 42 | */ 43 | const eth::sadp::sadp_hdr *header; 44 | 45 | /** 46 | * @brief The received message without ethernet header 47 | */ 48 | const eth::sadp::sadp_frame *frame; 49 | 50 | /** 51 | * @brief The socket that captured the packet 52 | */ 53 | const eth::IISocket *sock; 54 | 55 | public: 56 | 57 | PacketEvent( 58 | const eth::sadp::sadp_hdr *_Hdr, 59 | const eth::sadp::sadp_frame *_Frame, 60 | const eth::IISocket *_Socket 61 | ) : header{_Hdr}, frame{_Frame}, sock{_Socket} {}; 62 | 63 | inline const eth::sadp::sadp_hdr *GetHeader() const 64 | { return header; } 65 | 66 | inline const eth::sadp::sadp_frame *GetSADPFrame() const 67 | { return frame; } 68 | 69 | inline const eth::IISocket *GetSocket() const 70 | { return sock; } 71 | 72 | }; 73 | 74 | /** 75 | * @brief The main handler which should be registered to the sadp system. 76 | */ 77 | class PacketListener 78 | { 79 | public: 80 | virtual void onPacketReceived( 81 | const PacketEvent & 82 | ) const = 0; 83 | }; 84 | 85 | using PacketListenerList = std::vector; 86 | 87 | } // namespace event 88 | 89 | } // namespace eth 90 | 91 | #endif // __EVENTING_H__ 92 | -------------------------------------------------------------------------------- /cpp/sadplib.cpp: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | #include "sadplib.h" 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | // #include 29 | // std::thread daemonThread(&Start); 30 | 31 | //--------------------------------------[sadp::packet]-------------------------------------- 32 | 33 | const int eth::sadp::packet::GetSize( 34 | const eth::sadp::sadp_hdr *_Hdr) noexcept 35 | { 36 | if (!_Hdr) { 37 | return -1; 38 | } 39 | 40 | int size = DEFAULT_FRAME_HDR_SIZE + DEFAULT_FRAME_BODY_SIZE; 41 | 42 | char *payload = ((eth::sadp::psadp_frame)_Hdr->payload)->payload; 43 | size += (sizeof(payload) / sizeof(char)); 44 | 45 | return size > MIN_FRAME_SIZE ? size : MIN_FRAME_SIZE; 46 | } 47 | 48 | const bool eth::sadp::packet::SendInquiry( 49 | const eth::IISocket &_Socket) 50 | { 51 | eth::sadp::psadp_hdr hdr = 52 | eth::sadp::packet::BuildInquiry(_Socket.GetInterface()); 53 | 54 | std::cout << "+ hdr at" << hdr << "\n"; 55 | if (!hdr) return false; 56 | 57 | const int size = eth::sadp::packet::GetSize(hdr); 58 | if (size != -1) { 59 | _Socket.Send((char *)hdr, size); 60 | } 61 | return size != -1; 62 | } 63 | 64 | eth::sadp::sadp_hdr* eth::sadp::packet::BuildInquiry( 65 | const eth::adapter::NetInterface *_Interface 66 | ) { 67 | if (!_Interface) { 68 | return nullptr; 69 | } 70 | 71 | eth::sadp::inquiry_payload payload; 72 | 73 | eth::ip::ToBytes( 74 | (const uint8_t *)_Interface->GetIpv6Address().c_str(), 75 | (uint8_t *)payload.f_inet6_address 76 | ); 77 | 78 | eth::sadp::sadp_hdr *_Hdr = eth::sadp::packet::BuildFrame( 79 | _Interface, 80 | eth::sadp::sadp_packet_type::Request, 81 | eth::sadp::sadp_query_type::Inquiry, 82 | (const char *)&payload, 83 | 16 84 | ); 85 | 86 | return _Hdr; 87 | } 88 | 89 | eth::sadp::sadp_hdr *eth::sadp::packet::BuildFrame( 90 | const eth::adapter::NetInterface *_Interface, 91 | const eth::sadp::sadp_packet_type _PacketType, 92 | const eth::sadp::sadp_query_type _QueryType, 93 | const char *_Payload, 94 | const size_t _Payload_Length, 95 | const uint16_t _ClientType 96 | ) noexcept { 97 | 98 | if (!_Interface) { 99 | return nullptr; 100 | } 101 | 102 | eth::sadp::psadp_hdr _Hdr = 103 | (eth::sadp::psadp_hdr)malloc(DEFAULT_FRAME_SIZE); 104 | 105 | memset((void *)_Hdr, 0x00, DEFAULT_FRAME_SIZE); 106 | eth::sadp::psadp_frame _Frame = 107 | (eth::sadp::psadp_frame)_Hdr->payload; 108 | 109 | _Hdr->h_proto = 0x3380; 110 | 111 | _Frame->f_packet_type = (uint8_t)_PacketType; 112 | _Frame->f_client_type = _ClientType; 113 | _Frame->f_counter = htonl(eth::GetCounter().GetAndIncrement()); 114 | _Frame->f_type = (uint8_t)_QueryType; 115 | _Frame->f_prefix = 0x21U; 116 | _Frame->f_marker = 0x0406U; 117 | _Frame->f_parameters = 0x00; 118 | 119 | eth::mac::ToBytes( 120 | (const uint8_t *)_Interface->GetMacAddress().c_str(), 121 | _Frame->f_src_mac 122 | ); 123 | 124 | eth::mac::ToBytes( 125 | (const uint8_t *)"FF:FF:FF:FF:FF:FF", 126 | _Frame->f_dest_mac 127 | ); 128 | 129 | eth::ip::ToBytes( 130 | _Interface->GetIpv4Address().c_str(), 131 | &_Frame->f_src_ip 132 | ); 133 | 134 | memcpy( 135 | (void *)_Frame->payload, 136 | (const void *)_Payload, 137 | _Payload_Length 138 | ); 139 | 140 | eth::mac::ToBytes( 141 | (const uint8_t *)"FF:FF:FF:FF:FF:FF", 142 | (uint8_t *)_Hdr->h_dest 143 | ); 144 | 145 | memcpy( 146 | (void *)_Hdr->h_src, 147 | (const void *)_Frame->f_src_mac, 148 | 6 // mac address size 149 | ); 150 | 151 | _Frame->f_checksum = htons((uint16_t)eth::sadp::Checksum( 152 | (unsigned short *)_Frame, 153 | ((unsigned int)_Frame->f_client_type >> 8) & 0xFF 154 | )); 155 | 156 | return _Hdr; 157 | } 158 | 159 | //--------------------------------------[Daemon]-------------------------------------- 160 | 161 | eth::sadp::Daemon::Daemon(eth::IISocket &_Socket) 162 | : socket{_Socket}, running{false} {} 163 | 164 | void eth::sadp::Daemon::Stop() 165 | { 166 | this->running = false; 167 | } 168 | 169 | const bool eth::sadp::Daemon::IsRunning() const 170 | { 171 | return running; 172 | } 173 | 174 | void eth::sadp::Daemon::Start() 175 | { 176 | if (this->IsRunning()) { 177 | return; 178 | } 179 | 180 | this->running = true; 181 | // this->worker = std::thread([=]() { 182 | // this->Run(); 183 | // }); 184 | // this->worker.join(); 185 | } 186 | 187 | void eth::sadp::Daemon::Run() noexcept 188 | { 189 | // TEMPORARY: will be changed 190 | eth::sadp::psadp_hdr hdr; 191 | eth::sadp::psadp_frame frame; 192 | 193 | while (this->IsRunning()) { 194 | try { 195 | const int count = this->socket.Receive(); 196 | if (count != -1) { 197 | hdr = (const eth::sadp::psadp_hdr)socket.GetBuffer(); 198 | // check against the sadp protocol identifier 199 | if (ntohs(hdr->h_proto) == 0x8033) { 200 | std::cout << "+ SADP-Packet: " << (int)listenerList.size() << "\n"; 201 | frame = (const eth::sadp::psadp_frame)hdr->payload; 202 | eth::event::PacketEvent pe(hdr, frame, &this->socket); 203 | 204 | for (size_t i = 0; i < listenerList.size(); i++) { 205 | listenerList.at(i)->onPacketReceived(pe); 206 | } 207 | } 208 | } 209 | } 210 | catch (...) { 211 | perror("Unexpected error"); 212 | } 213 | } 214 | } 215 | 216 | const bool eth::sadp::Daemon::AddListener( 217 | const eth::event::PacketListener *_Listener) 218 | { 219 | if (_Listener) { 220 | this->listenerList.push_back(_Listener); 221 | } 222 | return _Listener != nullptr; 223 | } 224 | 225 | const bool eth::sadp::Daemon::RemoveListener( 226 | const eth::event::PacketListener &_Listener) 227 | { 228 | return false;//TODO 229 | } 230 | 231 | //--------------------------------------[sadp]-------------------------------------- 232 | -------------------------------------------------------------------------------- /cpp/sadplib.h: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2023 MatrixEditor 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | // DEALINGS IN THE SOFTWARE. 22 | 23 | #if !defined(__SADP_LIB_H__) 24 | #define __SADP_LIB_H__ 25 | 26 | #include "ethernet.h" 27 | #include "adapter.h" 28 | #include "checksum.h" 29 | #include "eventing.h" 30 | 31 | #include 32 | 33 | namespace eth { 34 | 35 | namespace sadp 36 | { 37 | 38 | namespace packet 39 | { 40 | 41 | /** 42 | * @brief Get the size of the given sadp packet. 43 | * 44 | * @param _Hdr the frame to inspect 45 | * @return [const int] the size or -1 on error 46 | */ 47 | const int GetSize(const eth::sadp::sadp_hdr *_Hdr) noexcept; 48 | 49 | /** 50 | * @brief Builds and sends an inquiry packet. 51 | * 52 | * @param _Socket the socket which should send the data 53 | * @return true if the packet has been sent successfully, 54 | * @return false otherwise 55 | */ 56 | const bool SendInquiry(const eth::IISocket &_Socket); 57 | 58 | /** 59 | * @brief Builds an Inquiry packet and returns its size. 60 | * 61 | * @param _Interface the NetInterface 62 | * @return [const int] the packet's size 63 | */ 64 | eth::sadp::sadp_hdr *BuildInquiry( 65 | const eth::adapter::NetInterface *_Interface 66 | ); 67 | 68 | #define DEFAULT_FRAME_BODY_SIZE 38 // body 69 | #define DEFAULT_FRAME_HDR_SIZE 14 // header 70 | #define DEFAULT_FRAME_SIZE 512 // rest for payload 71 | #define MIN_FRAME_SIZE 80 // the minimum packet size 72 | 73 | /** 74 | * @brief Inserts the given attributes to the output buffer. 75 | * 76 | * @param _Interface the network interface storing the IPv4, IPv6 and 77 | * MAC address 78 | * @param _PacketType the packet type 79 | * @param _ClientType the client type 80 | * @param _QueryType the request or response packet type 81 | * @param _Payload the payload pointer (data will be copied) 82 | * @return the created packet 83 | */ 84 | eth::sadp::sadp_hdr *BuildFrame( 85 | const eth::adapter::NetInterface *_Interface, 86 | const eth::sadp::sadp_packet_type _PacketType, 87 | const eth::sadp::sadp_query_type _QueryType, 88 | const char *_Payload, 89 | const size_t _Payload_Length, 90 | 91 | const uint16_t _ClientType = CSADP_CLIENT_TYPE 92 | ) noexcept; 93 | 94 | } // namespace packet 95 | 96 | class Daemon { 97 | private: 98 | /** 99 | * @brief A variable indicating whether this daemon is active. 100 | */ 101 | bool running; 102 | 103 | /** 104 | * @brief The layer 2 socket. 105 | */ 106 | eth::IISocket &socket; 107 | 108 | std::thread worker; 109 | 110 | eth::event::PacketListenerList listenerList{}; 111 | 112 | public: 113 | /** 114 | * @brief Construct a new Daemon. 115 | * 116 | * This constructor uses an editable socket object, because it has to 117 | * send and receive packets. 118 | * 119 | * @param _Socket the socket for networking actions 120 | */ 121 | Daemon(eth::IISocket &_Socket); 122 | 123 | /** 124 | * @brief Starts this daemon. 125 | * 126 | * This method should start a loop that runs until the #Stop() method 127 | * is called. In addition to that, the sadp::Notify() method should be 128 | * called whenever a packet with the protocol defined in this->socket 129 | * is 130 | */ 131 | void Start(); 132 | 133 | /** 134 | * @brief Stops this daemon. 135 | * 136 | * The method should stop the damon @b after the next packet is 137 | * received. 138 | */ 139 | void Stop(); 140 | 141 | /** 142 | * @brief Returns whether this daemon is active. 143 | * 144 | * @return true if the daemon is active, 145 | * @return false on a stopped daemon 146 | */ 147 | const bool IsRunning() const; 148 | 149 | void Run() noexcept; 150 | 151 | /** 152 | * @brief Registers the given handler. 153 | * 154 | * @param _Listener the handler to add 155 | * @return true if the handler could be added successfully; 156 | * @return false otherwise 157 | */ 158 | const bool AddListener(const eth::event::PacketListener *_Listener); 159 | 160 | /** 161 | * @brief Removes the given handler if it exists. 162 | * 163 | * @param _Listener the handler to remove 164 | * @return true if the handler has been removed; 165 | * @return false otherwise 166 | */ 167 | const bool RemoveListener(const eth::event::PacketListener &_Listener); 168 | 169 | }; 170 | 171 | } // namspace sadp 172 | 173 | } // namspace eth 174 | 175 | #endif // __SADP_LIB_H__ 176 | -------------------------------------------------------------------------------- /cpp/sadptool.cpp: -------------------------------------------------------------------------------- 1 | #include "sadplib.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class MyHandler : public eth::event::PacketListener { 8 | public: 9 | virtual void onPacketReceived(const eth::event::PacketEvent &_Event) const override 10 | { 11 | //TODO maybe add source 12 | std::cout << "+ Received a packet (" 13 | << eth::sadp::QueryTypeToString( 14 | _Event.GetSADPFrame()->f_type, 15 | (eth::sadp::sadp_packet_type)_Event.GetSADPFrame()->f_packet_type) 16 | <<")\n"; 17 | } 18 | }; 19 | 20 | int main(int argc, char const *argv[]) 21 | { 22 | eth::GetCounter().Set(0x1c80); 23 | 24 | MyHandler handler; 25 | 26 | std::cout << "i Lookup...\n"; 27 | const eth::adapter::NetInterfaceList *list = eth::adapter::GetNetInterfaces(); 28 | std::cout << "+ List at " << list << " (" << list->size()<< ")\n"; 29 | 30 | const size_t count = list->size(); 31 | for (size_t i = 0; i < count; i++) 32 | { 33 | std::cout.flush(); 34 | const eth::adapter::NetInterface &ni = list->at(i); 35 | 36 | if (ni.GetInterfaceIndex() == 3) { 37 | std::cout << "+ Found NetInterface...\n"; 38 | eth::IISocket sock; 39 | 40 | if (sock.Create(&ni, 0x3380)) { 41 | std::cout << "+ Created socket with proto 0x8033\n"; 42 | } 43 | 44 | sock.Bind(); 45 | 46 | eth::sadp::Daemon daemon(sock); 47 | daemon.AddListener(&handler); 48 | daemon.Start(); 49 | 50 | eth::sadp::packet::SendInquiry(sock); 51 | 52 | daemon.Run(); 53 | daemon.Stop(); 54 | break; 55 | } 56 | } 57 | 58 | return 0; 59 | } 60 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | import sys 9 | import os 10 | 11 | project = 'hiktools' 12 | copyright = '2023, MatrixEditor' 13 | author = 'MatrixEditor' 14 | release = '1.2.2' 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration --------------------------------------------------- 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 23 | extensions = [ 24 | 'sphinx.ext.autodoc', 25 | 'sphinx_rtd_theme' 26 | ] 27 | 28 | templates_path = ['_templates'] 29 | exclude_patterns = [] 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 33 | 34 | import sphinx_rtd_theme 35 | 36 | html_theme = 'sphinx_rtd_theme' 37 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 38 | 39 | html_static_path = ['_static'] 40 | -------------------------------------------------------------------------------- /docs/csadp.rst: -------------------------------------------------------------------------------- 1 | .. _csadp: 2 | 3 | Native Search Active Devices Protocol (CSADP) 4 | ============================================= 5 | 6 | .. automodule:: hiktools.csadp 7 | 8 | .. contents:: Table of Contents 9 | 10 | **Note:** If you can't see any contents below, ReadTheDocs-build fails on this module. Please refer to the documented source code 11 | instead. 12 | 13 | Usage: 14 | 15 | .. code:: python 16 | 17 | from hiktools import csadp 18 | 19 | # Because the following module requires root priviledges it 20 | # has to be imported directly. 21 | from hiktools.csadp import CService 22 | 23 | sock = CService.l2socket('wlan0') 24 | counter = 0x115e 25 | 26 | # To build an Inquiry packet, we need the following information: 27 | # - our mac, ipv4 and ipv6 address (and the counter of course) 28 | packet = csadp.Inquiry('', '', '', counter) 29 | 30 | # before we can send the message, a checksum has to be calculated 31 | packet.insert_checksum() 32 | 33 | sock.send(bytes(packet)) 34 | response = csadp.SADPPacket(sock.recv(1024)) 35 | 36 | # to view the contents just print the str() version 37 | print(str(response)) 38 | 39 | .. autofunction:: get_checksum 40 | 41 | .. autofunction:: inet_stomac 42 | 43 | .. autofunction:: inet_mactos 44 | 45 | .. autofunction:: inet_iptos 46 | 47 | .. autofunction:: inet_stoip 48 | 49 | .. autofunction:: inet6_stoip 50 | 51 | .. autofunction:: inet6_iptos 52 | 53 | .. autofunction:: payload 54 | 55 | .. autoclass:: EthernetHeader 56 | :members: 57 | 58 | .. autoclass:: SADPHeader 59 | :members: 60 | 61 | .. autoclass:: SADPPayload 62 | :members: 63 | 64 | .. autoclass:: SADPPacket 65 | :members: 66 | 67 | .. _hiktools: https://github.com/MatrixEditor/hiktools -------------------------------------------------------------------------------- /docs/fmod.rst: -------------------------------------------------------------------------------- 1 | .. _fmod: 2 | 3 | Firmware modulation (fmod) 4 | ========================== 5 | 6 | .. automodule:: hiktools.fmod 7 | 8 | .. contents:: Table of Contents 9 | 10 | Interface 11 | ~~~~~~~~~ 12 | 13 | .. autofunction:: hiktools.fmod.read_raw_header 14 | 15 | .. autofunction:: hiktools.fmod.decode_xor16 16 | 17 | .. autofunction:: hiktools.fmod.split_header 18 | 19 | .. autofunction:: hiktools.fmod.split_files 20 | 21 | .. autofunction:: hiktools.fmod.fopen_dav 22 | 23 | .. autofunction:: hiktools.fmod.export 24 | 25 | 26 | Classes 27 | ~~~~~~~ 28 | 29 | .. autoclass:: hiktools.fmod.DigiCap 30 | :members: 31 | 32 | Usage: 33 | 34 | .. code:: python 35 | 36 | from hiktools import fmod 37 | 38 | # Open the resource at the specified path (loading is done automatically) 39 | # or manually decrypt the firmware file. 40 | with fmod.DigiCap('filename.dav') as dcap: 41 | # Iterate over all files stored in the DigiCap object 42 | for file_info in dcap: 43 | print('> File name="%s", size=%d, pos=%d, checksum=%d' % file_info) 44 | 45 | # get file amount and current language 46 | print("Files: %d, Language: %d" % (dcap.head.files, dcap.head.language)) 47 | 48 | # save all files stored in 49 | fmod.export(dcap, "outdir/") 50 | 51 | .. autoclass:: hiktools.fmod.DigiCapHeader 52 | 53 | Exceptions 54 | ~~~~~~~~~~ 55 | 56 | .. autoclass:: hiktools.fmod.InvalidFileFormatException 57 | 58 | .. autoclass:: hiktools.fmod.FileAccessException -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. hiktools documentation master file, created by 2 | sphinx-quickstart on Mon Aug 15 18:25:18 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | hiktools' documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :caption: Developer Interface: 12 | 13 | sadp 14 | csadp 15 | fmod 16 | 17 | Features 18 | -------- 19 | 20 | - "Native" interface on sending and receiving SADP packets 21 | - Client for XML based UDP communication on port 37020 22 | - Generation of old reset code 23 | - Hikvision firmware inspector and extractor 24 | - Wireshark dissector written in Lua for Search Active Devices Protocol (SADP) 25 | - Disassebled C/C++ code snippets for checksum algorithm and packet building 26 | 27 | 28 | .. note:: 29 | 30 | Previos versions of hiktools were unstable in terms of working with firmware 31 | files, but since 1.2.2 the while codebase has been refactored to be more robust. 32 | 33 | 34 | Usage 35 | ----- 36 | 37 | Installation 38 | ~~~~~~~~~~~~ 39 | 40 | .. code:: shell 41 | 42 | git clone https://github.com/MatrixEditor/hiktools.git 43 | cd hiktools/ && pip install . 44 | 45 | Build the docs: 46 | ~~~~~~~~~~~~~~~ 47 | 48 | .. code:: shell 49 | 50 | cd docs/ 51 | pip install -U sphinx 52 | sphinx-build -b html source build 53 | 54 | Install dissector 55 | ~~~~~~~~~~~~~~~~~ 56 | 57 | Open up wireshark go to Help > About Wireshark > search for "Global Lua Plugins" and copy the location. 58 | 59 | .. code:: shell 60 | 61 | $LOCATION="..." 62 | # on unix 63 | cp hiktools/lua/sadp.lua $LOCATION/sadp.lua 64 | # on windows 65 | copy "hiktools\lua\sadp.lua" "$LOCATION\sadp.lua" 66 | 67 | Basic Usage 68 | ~~~~~~~~~~~ 69 | 70 | - Firmware inspection and extraction 71 | 72 | .. code:: python 73 | 74 | from hiktools import fmod 75 | 76 | # Open the resource at the specified path (loading is done automatically) 77 | # or manually decrypt the firmware file. 78 | with fmod.DigiCap('filename.dav') as dcap: 79 | # Iterate over all files stored in the DigiCap object 80 | for file_info in dcap: 81 | print('> File name="%s", size=%d, pos=%d, checksum=%d' % file_info) 82 | 83 | # get file amount and current language 84 | print("Files: %d, Language: %d" % (dcap.head.files, dcap.head.language)) 85 | 86 | # save all files stored in 87 | fmod.export(dcap, "outdir/") 88 | 89 | - Native interface on sending raw packets (only LINUX) 90 | 91 | .. code:: python 92 | 93 | from hiktools import csadp 94 | 95 | # Because the following module requires root priviledges it has to be 96 | # imported directly 97 | from hiktools.csadp import CService 98 | 99 | sock = CService.l2socket('wlan0') 100 | counter = 2855 101 | 102 | # Building an inquiry packet 103 | packet = csadp.packet( 104 | 'src_mac', 'src_ip', 0x03, counter, 105 | checksum=csadp.from_counter(counter), 106 | payload='\x00'*28 107 | ) 108 | 109 | # If you want to have the packet as an object use parse() 110 | packet_obj = csadp.parse(packet) 111 | 112 | sock.send(packet) # or sock.send(bytes(packet_obj)) 113 | response = csadp.parse(sock.recv(1024)) 114 | 115 | 116 | - Interact with the device through UDP broadcast 117 | 118 | .. code:: python 119 | 120 | from hiktools import sadp 121 | from uuid import uuid4 122 | 123 | # create a packet from a simple dict object 124 | inquiry = sadp.fromdict({ 125 | 'Uuid': str(uuid4()).upper(), 126 | 'MAC': 'ff-ff-ff-ff-ff-ff', 127 | 'Types': 'inquiry' 128 | }) 129 | 130 | # Open up a client to communicate over broadcast 131 | with sadp.SADPClient() as client: 132 | # send the inquiry packet 133 | client.write(inquiry) 134 | 135 | # iterate over all received packets (None is returned on error) 136 | for response in client: 137 | if response is None: break 138 | # initiate the response 139 | message = sadp.unmarshal(response.toxml()) 140 | 141 | # message objects contain a dict-like implementation 142 | for property_name in message: 143 | print(message[property_name]) 144 | 145 | # e.g. 146 | print('Device at', message['IPv4Address']) 147 | 148 | 149 | Indices and tables 150 | ================== 151 | 152 | * :ref:`genindex` 153 | * :ref:`modindex` 154 | * :ref:`search` 155 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme -------------------------------------------------------------------------------- /docs/sadp.rst: -------------------------------------------------------------------------------- 1 | .. _sadp: 2 | 3 | Search Active Devices Protocol (SADP) 4 | ===================================== 5 | 6 | .. automodule:: hiktools.sadp 7 | 8 | .. contents:: Table of Contents 9 | 10 | Interface 11 | ~~~~~~~~~ 12 | 13 | .. autofunction:: hiktools.sadp.unmarshal 14 | 15 | .. autofunction:: hiktools.sadp.hik_code 16 | 17 | .. autofunction:: hiktools.sadp.fromdict 18 | 19 | Classes 20 | ~~~~~~~ 21 | 22 | Client 23 | ------ 24 | 25 | .. autoclass:: hiktools.sadp.SADPClient 26 | 27 | Example usage 28 | 29 | .. code:: python 30 | 31 | from hiktools import sadp 32 | 33 | with sadp.SADPClient(timeout=3) as client: 34 | # send data with client.write 35 | client.write(some_message) 36 | 37 | # receive bytes with client.recv_next() or just iterate 38 | # over next SADPMessages: 39 | for message in client: 40 | if message is None: break 41 | # de-serialize message 42 | response = sadp.unmarshal(message) 43 | 44 | 45 | Response Classes 46 | ---------------- 47 | 48 | .. autoclass:: hiktools.sadp.ActionResponse 49 | :members: 50 | 51 | .. autoattribute:: hiktools.sadp.ActionResponse.__msg__ 52 | 53 | .. autoclass:: hiktools.sadp.DiscoveryPacket 54 | 55 | .. autoclass:: hiktools.sadp.SafeCode 56 | :members: 57 | 58 | .. autoclass:: hiktools.sadp.DeviceSafeCodePacket 59 | :members: -------------------------------------------------------------------------------- /gists/csadp/CPacketSender.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define __stack_chk_fail(x) 4 | 5 | typedef int __stack_chk_guard; 6 | 7 | class CPacketSender; 8 | 9 | class CAdapterInfo { 10 | public: 11 | static void *Instance(); 12 | static void GetCurAdapterMAC(CAdapterInfo *pInfo, unsigned short index, char *__dst); 13 | static void GetCurAdapterIP(CAdapterInfo *pInfo, unsigned short index, char *__dst); 14 | }; 15 | 16 | void FormatStrToMAC(char *__src, unsigned char *__dst); 17 | void FormatStrToIP(char *__src, unsigned long *__dst); 18 | unsigned long SwapULong(unsigned long value); 19 | unsigned int SwapUInt(unsigned int value); 20 | unsigned int CheckSum(CPacketSender *pSender, unsigned short *buffer, unsigned int param_2); 21 | 22 | void BuildSADPPacket(char *buffer, char *dest_mac, char *ip_address, unsigned char param_4, 23 | char *cur_adapter_ipv6, unsigned char packet_type, unsigned char param_6, 24 | unsigned short membuffer, unsigned short mem_len, unsigned short adapter_index) 25 | { 26 | CAdapterInfo *pInfo; 27 | 28 | size_t __n; 29 | long local8; 30 | 31 | unsigned long checksum; 32 | unsigned long cur_mac; 33 | unsigned long cur_ip; 34 | unsigned long ether_type; 35 | unsigned long header_start; 36 | unsigned long payload_start; 37 | unsigned short actual_size; 38 | unsigned int next_ip; 39 | 40 | char *buffer_start; 41 | unsigned short *buffer_footer; 42 | unsigned long *header_buffer; 43 | unsigned long *out_buffer; 44 | 45 | local8 = __stack_chk_guard(); 46 | // NOTE: At first, there are some checks around the starting point of the 47 | // buffer. Note, that the actual starting address is (buffer + 0x18). 48 | buffer_start = *(char **)(buffer + 0x10); 49 | if (buffer_start == (char *)0x00) { 50 | actual_size = 0; 51 | } 52 | else if (*(long *)(buffer + 0x18) == 0x0) { 53 | // The start of the packet buffer is 0x00 54 | actual_size = 0; 55 | } 56 | else { 57 | *buffer_start = 0; 58 | 59 | // Make sure, the checksum field and starting point are filled up with 0's. 60 | *(unsigned short *)((long)buffer_start + 0xc) = 0x0; 61 | *(unsigned int *)(buffer_start + 0x1) = 0x0; 62 | 63 | // Clear the buffer of 512 bytes. 64 | memset((void *)(buffer + 0x18), 0x0, 0x200); 65 | 66 | // Retrive the current MAC and IP address: 67 | pInfo = (CAdapterInfo *)CAdapterInfo::Instance(); 68 | CAdapterInfo::GetCurAdapterMAC(pInfo, adapter_index, (char *)&cur_mac); 69 | pInfo = (CAdapterInfo *)CAdapterInfo::Instance(); 70 | CAdapterInfo::GetCurAdapterIP(pInfo, adapter_index, (char *)&cur_ip); 71 | 72 | if (mem_len < 0x1C) { 73 | actual_size = 0x50; 74 | __n = 0x1C; 75 | } 76 | else { 77 | actual_size = mem_len + 0x34; 78 | __n = (size_t)(int)(actual_size - 0x34); 79 | } 80 | 81 | 82 | header_start = *(long *)(buffer + 0x10); 83 | FormatStrToMAC(dest_mac, (unsigned char *)header_start); 84 | FormatStrToMAC((char *)&cur_mac, (unsigned char *)(header_start + 6)); 85 | 86 | // Big endian encoding is used. According to IEEE, the ether type 8033 87 | // is registered to: (https://standards-oui.ieee.org/ethertype/eth.txt) 88 | // VIA Systems 89 | // 76 Treble Cove Road 90 | // N. Billerica MA 08162, US 91 | ether_type = SwapULong(0xffff8033); 92 | *(short *)(header_start + 0xc) = (short) ether_type; 93 | 94 | // This code fills up the header values up to byte 12: 95 | payload_start = *(long *)(buffer + 0x18); 96 | *((char *)payload_start) = 0x21; 97 | *((char *)payload_start + 1) = 0x02; 98 | *((char *)payload_start + 2) = 0x01; 99 | *((char *)payload_start + 3) = 0x42; 100 | *((unsigned int *)payload_start + 4) = SwapUInt((unsigned int)(cur_adapter_ipv6)); 101 | *((char *)payload_start + 8) = 0x06; 102 | *((char *)payload_start + 9) = 0x04; 103 | *((unsigned char *)payload_start + 10) = packet_type; 104 | *((char *)payload_start + 11) = (char)(param_6); 105 | *((char *)payload_start + 12) = 0x00; 106 | 107 | // Next, the current mac and destination mac address are set together with the 108 | // local and target ip address. 109 | FormatStrToMAC((char *)&cur_mac, (unsigned char *)(payload_start + 14)); 110 | FormatStrToIP((char *)&cur_ip, (unsigned long *)(*(long *)(payload_start) + 20)); 111 | FormatStrToMAC(dest_mac, (unsigned char *)(payload_start + 24)); 112 | 113 | next_ip = 0x00; 114 | FormatStrToIP(ip_address, (unsigned long *)&next_ip); 115 | *(unsigned int *)(*(long *)(payload_start) + 30) = next_ip; 116 | 117 | FormatStrToIP((char *)(unsigned long)param_4, (unsigned long *)&next_ip); 118 | *(unsigned int *)(*(long *)(payload_start) + 34) = next_ip; 119 | 120 | // Copy given data into buffer. 121 | memcpy( 122 | (void *)(*(long *)(payload_start) + 38), 123 | (const void *)(unsigned long) membuffer, 124 | (unsigned long) mem_len 125 | ); 126 | 127 | buffer_footer = (unsigned short *)(payload_start); 128 | checksum = CheckSum((CPacketSender *)buffer, buffer_footer, (unsigned int) *(char *)((long) buffer_footer + 3)); 129 | buffer_footer[6] = (unsigned short) SwapULong(checksum); 130 | } 131 | 132 | 133 | // At the end there is a check if a stack overflow was detected. 134 | if (local8 == __stack_chk_guard()) { 135 | return; 136 | } 137 | __stack_chk_fail(actual_size) 138 | } -------------------------------------------------------------------------------- /gists/fmod/decode_xor16.c: -------------------------------------------------------------------------------- 1 | typedef char byte; 2 | 3 | void __cdecl DecodeXOR16(const byte *buffer, byte *dest, const int length) 4 | { 5 | int index; 6 | byte key_byte; 7 | unsigned int *keyRef; 8 | unsigned int *pKey; 9 | unsigned int *key[4]; 10 | 11 | // key reference here 12 | keyRef = &DAT_00404000; 13 | pKey = key; 14 | for (index = 4; index != 0; index--) { 15 | *pKey = *keyRef; 16 | keyRef++; 17 | pKey++; 18 | } 19 | if (length > 0) { 20 | do { 21 | key_byte = (byte)((int)key + (index + (index >> 4) & 0xFU)); 22 | *(dest + index) = key_byte ^ *(buffer + index); 23 | index++; 24 | } while (index != length); 25 | } 26 | return; 27 | } -------------------------------------------------------------------------------- /gists/libsadp.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixEditor/hiktools/a05091977389d74159729671b8f76d9946e533dc/gists/libsadp.so -------------------------------------------------------------------------------- /hiktools/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | __version__ = '1.2.2' 24 | __author__ = 'MatrixEditor' 25 | -------------------------------------------------------------------------------- /hiktools/csadp/CService.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | `WARNING`: This module is usable only on linux systems. 24 | 25 | A small module which ensures the right permissions are used to execute the 26 | script base. It covers the creation of a layer 2 socket. 27 | """ 28 | 29 | __all__ = ["l2socket"] 30 | 31 | import socket 32 | 33 | from sys import platform 34 | 35 | # Check if the program is running with root priviledges 36 | if platform != "linux": 37 | raise OSError("Unsupported platform for layer II network operations!") 38 | 39 | try: 40 | import os 41 | 42 | if os.getuid(): 43 | raise PermissionError("This library requires super-user priviledges.") 44 | 45 | except AttributeError as exc: 46 | raise PermissionError("This library requires super-user priviledges.") from exc 47 | 48 | 49 | def l2socket(interface: str) -> socket.socket: 50 | """Creates a layer II socket and binds it to the given interface.""" 51 | if not interface: 52 | raise ValueError("Interface not specified") 53 | 54 | sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW) 55 | sock.settimeout(2) 56 | sock.bind((interface, 0)) 57 | 58 | return sock 59 | -------------------------------------------------------------------------------- /hiktools/csadp/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | A module covering layer II networking elements. 24 | """ 25 | 26 | from hiktools.csadp.uarray import * 27 | from hiktools.csadp.checksum import get_checksum 28 | from hiktools.csadp.model import * 29 | from hiktools.csadp.payloads import * 30 | -------------------------------------------------------------------------------- /hiktools/csadp/checksum.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | __all__ = ["get_checksum"] 23 | 24 | from hiktools.csadp.uarray import UIntArray 25 | 26 | 27 | def get_checksum(buf: UIntArray, prefix: int) -> int: 28 | """The SADP checksum algorithm implemented in python3. 29 | 30 | For a more accurate view on the algorithm, see the C++ source code on the 31 | `hiktools`_ repository on github. 32 | 33 | :param buf: an unsinged int16 array (can be created via a call to 34 | ``to_uint16_buf()``) 35 | :param prefix: the sender's type specification. As defined in the C++ 36 | header file, ``0x42`` specifies a client and ``0xf6`` 37 | an server. 38 | """ 39 | csum: int = 0 40 | lower: int = 0 41 | high: int = 0 42 | index: int = 0 43 | 44 | if 3 < (prefix & 0xFFFFFFFE): 45 | index = (prefix - 4 >> 2) + 1 46 | while index != 0: 47 | prefix -= 4 48 | lower += buf[0] 49 | high += buf[1] 50 | 51 | buf = buf[2:] 52 | index -= 1 53 | 54 | if 1 < prefix: 55 | csum = buf[0] 56 | buf = buf[1:] 57 | prefix -= 2 58 | 59 | csum += lower + high 60 | if prefix != 0: 61 | csum += buf[0] & 0xFF 62 | 63 | csum = (csum >> 16) + (csum & 0xFFFF) 64 | # NOTE: by adding 1 << 32 to the calculated checksum, a virtual 65 | # cast to an unsigned integer is performed. Because python has 66 | # no inbuild unsinged types, this cast has to be done before 67 | # returning the value. 68 | csum = (1 << 32) + ~((csum >> 16) + csum) 69 | return csum 70 | -------------------------------------------------------------------------------- /hiktools/csadp/model.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | __doc__ = """ 23 | CSADP packets are wrapped into objects that support the ``__bytes__`` method to 24 | dynamically create the byte buffer. 25 | 26 | This module covers the conversion between binary data to MAC- and IP addressees 27 | and defines base classes for SADPPackets. 28 | """ 29 | 30 | import struct 31 | import binascii 32 | 33 | from socket import AF_INET6, inet_aton, inet_ntoa, inet_ntop, inet_pton 34 | 35 | from hiktools.csadp.checksum import get_checksum 36 | from hiktools.csadp.uarray import LITTLE_ENDIAN, to_uint16_buf 37 | 38 | __all__ = [ 39 | "inet_stomac", 40 | "inet_mactos", 41 | "inet_stoip", 42 | "inet_iptos", 43 | "EthernetHeader", 44 | "SADPHeader", 45 | "SADPPacket", 46 | "SERVER_PREFIX", 47 | "CLIENT_PREFIX", 48 | "PACKET_TYPE", 49 | "inet6_stoip", 50 | "inet6_iptos", 51 | "SADPPayload", 52 | "payload", 53 | ] 54 | 55 | CLIENT_PREFIX = 0x42 56 | SERVER_PREFIX = 0xF6 57 | 58 | PACKET_TYPE = { 59 | "DeviceOnlineRequest": 0x02, 60 | "Inquiry": 0x03, 61 | "InquiryResponse": 0x04, 62 | "UpdateIP": 0x06, 63 | "UpdateIPResponse": 0x07, 64 | "ResetPassword": 0x0A, 65 | "ResetPasswordResponse": 0x0B, 66 | "CMSInfo": 0x0C, 67 | "CMSInfoResponse": 0x0D, 68 | "ModifyNetParam": 0x10, 69 | "ModifyNetParamResponse": 0x11, 70 | } 71 | 72 | PAYLOAD_TYPE = { 73 | # structure of this dict 74 | # int: class, ... 75 | } 76 | 77 | 78 | def inet_stomac(mac: str) -> bytes: 79 | """Converts the string mac address into a byte buffer.""" 80 | mac = mac.replace(":", "").replace("-", "") 81 | return binascii.unhexlify(mac) 82 | 83 | 84 | def inet_mactos(buffer: bytes, index: int, sep: str = ":") -> str: 85 | """Converts bytes to a MAC address.""" 86 | return binascii.b2a_hex(buffer[index : index + 6], sep=sep).decode("utf-8") 87 | 88 | 89 | def inet_stoip(ip_address: str) -> bytes: 90 | """Converts the string ip address into a byte buffer.""" 91 | return inet_aton(ip_address) 92 | 93 | 94 | def inet_iptos(buffer: bytes, offset: int) -> str: 95 | """Converts bytes to an IP address.""" 96 | return inet_ntoa(buffer[offset : offset + 4]) 97 | 98 | 99 | def inet6_iptos(buffer: bytes, offset: int) -> str: 100 | """Converts bytes to an IP address.""" 101 | return inet_ntop(AF_INET6, buffer[offset : offset + 16]) 102 | 103 | 104 | def inet6_stoip(ip6: str) -> bytes: 105 | """Converts the string ip address into a byte buffer.""" 106 | return inet_pton(AF_INET6, ip6) 107 | 108 | 109 | class EthernetHeader: 110 | """Small wrapper for raw ethernet message headers. 111 | 112 | The basic structure of this header is defined as follows: 113 | 114 | >>> +-------------------------------------------------+ 115 | >>> | EthernetHeader | 116 | >>> +---------------+--------------+------------------+ 117 | >>> | dest: byte[6] | src: byte[6] | eth_type: uint16 | 118 | >>> +---------------+--------------+------------------+ 119 | 120 | :param dest: The destination MAC address of this packet. Usually this field 121 | points to the multicast MAC address (``FF:FF:FF:FF:FF:FF``). 122 | :type dest: str 123 | 124 | :param src: the source MAC address for this packet 125 | :type src: str 126 | 127 | :param eth_type: The specified ethernet type for this packet. This value can 128 | be used for validation, because it has to be ``0x8033``. 129 | :type eth_type: uint16 (int) 130 | """ 131 | 132 | def __init__(self, buf: bytes = None) -> None: 133 | self.dest: str = "FF:FF:FF:FF:FF:FF" 134 | self.src: str = "" 135 | self.eth_type: int = 0x8033 136 | if buf is not None and len(buf) >= 14: 137 | self.dest = inet_mactos(buf, 0) 138 | self.src = inet_mactos(buf, 6) 139 | self.eth_type = struct.unpack("!H", buf[12:14])[0] 140 | 141 | def __bytes__(self) -> bytes: 142 | """Packs this header object into a byte buffer. 143 | 144 | The returned structure is defined in the documentation of this class. 145 | 146 | :returns: all values stored by this header object packed into a byte buffer. 147 | """ 148 | buf = bytearray() 149 | buf += inet_stomac(self.dest) 150 | buf += inet_stomac(self.src) 151 | buf += struct.pack("!H", self.eth_type) 152 | return bytes(buf) 153 | 154 | 155 | class SADPHeader: 156 | """A simple class wrapper to store header variables of SADPPackets.""" 157 | 158 | def __init__(self, buf: bytes = None) -> None: 159 | self.prefix: int = 0 # uint8 160 | self.counter: int = 0 # uint32 161 | self.packet_type: int = 0 # uint8 162 | self.params: int = 0 # uint8 163 | self.checksum: int = 0 # uint16 164 | if buf is not None and len(buf) >= 14: 165 | values = struct.unpack("!HBBIHBBH", buf) 166 | self.prefix = values[2] 167 | self.counter = values[3] 168 | self.packet_type = values[5] 169 | self.params = values[6] 170 | self.checksum = values[7] 171 | 172 | def __bytes__(self) -> bytes: 173 | buf = bytearray(14) 174 | struct.pack_into( 175 | "!HBBIHBBH", 176 | buf, 177 | 0, 178 | 0x2102, 179 | 0x01, 180 | self.prefix, 181 | self.counter, 182 | 0x0604, 183 | self.packet_type, 184 | self.params, 185 | self.checksum, 186 | ) 187 | return bytes(buf) 188 | 189 | 190 | class SADPPayload: 191 | """The base class for all payload types.""" 192 | 193 | def __init__(self, buf: bytes = None) -> None: 194 | self.buf = buf 195 | 196 | def __bytes__(self) -> bytes: 197 | raise NotImplementedError( 198 | "Abstract class SADPPayload does not implement __bytes__()" 199 | ) 200 | 201 | 202 | class SADPPacket: 203 | """A dynamic class for creating SADPPackets for sending and resceiving data.""" 204 | 205 | def __init__(self, buf: bytes = None) -> None: 206 | self.eth_header: EthernetHeader = EthernetHeader(buf[:14] if buf else None) 207 | self.header: SADPHeader = SADPHeader(buf[14:] if buf else None) 208 | self.src_ip: str = "" 209 | self.dest_ip: str = "0.0.0.0" 210 | self.subnet: str = "0.0.0.0" 211 | self.payload: SADPPayload = SADPPayload() 212 | if buf is not None and len(buf) >= 52: 213 | self.src_ip = inet_iptos(buf, 34) 214 | self.dest_ip = inet_iptos(buf, 44) 215 | self.subnet = inet_iptos(buf, 48) 216 | self.payload = SADPPayload() 217 | if self.header.packet_type in PAYLOAD_TYPE: 218 | # Create a payload object of type (class) 219 | self.payload = PAYLOAD_TYPE[self.header.packet_type](buf[52:]) 220 | 221 | def insert_checksum(self): 222 | """Calculates the checksum for this packet.""" 223 | if self.header.checksum == 0: 224 | buf = to_uint16_buf(bytes(self)[14:], encoding=LITTLE_ENDIAN) 225 | self.header.checksum = get_checksum(buf, self.header.prefix) & 0xFFFF 226 | 227 | def __bytes__(self) -> bytes: 228 | buf = bytearray() 229 | buf += bytes(self.eth_header) 230 | buf += bytes(self.header) 231 | buf += inet_stomac(self.eth_header.src) 232 | buf += inet_stoip(self.src_ip) 233 | buf += inet_stomac(self.eth_header.dest) 234 | buf += inet_stoip(self.dest_ip) 235 | buf += inet_stoip(self.subnet) 236 | buf += bytes(self.payload) 237 | return bytes(buf) 238 | 239 | 240 | def payload(ptype: int): 241 | """Simple class descriptor to register payload types 242 | 243 | Use this descriptor to define payload classes that are used within the 244 | parsing process of a ``SADPPacket``. Note that this method will raise 245 | a ``NameError`` if the payload type has already been assigned to another 246 | class. 247 | 248 | Example for registering a payload class for the `UpdateIP` packet type. 249 | 250 | >>> @payload(PACKET_TYPE['UpdateIP']) 251 | >>> class MySADPPayload(SADPPayload): 252 | ... pass 253 | 254 | :param ptype: the packet type the payload class should be mapped to 255 | """ 256 | 257 | def do_register(payload_type): 258 | if ptype in PAYLOAD_TYPE: 259 | raise NameError(f"Payload of type {ptype} already exists") 260 | PAYLOAD_TYPE[ptype] = payload_type 261 | return payload_type 262 | 263 | return do_register 264 | -------------------------------------------------------------------------------- /hiktools/csadp/payloads.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | __doc__ = """ 23 | Known payload implementations. 24 | """ 25 | 26 | __all__ = ["InquiryPayload", "inquiry", "InquiryResponsePayload"] 27 | 28 | from hiktools.csadp.model import ( 29 | SADPPayload, 30 | SADPPacket, 31 | payload, 32 | inet6_iptos, 33 | inet6_stoip, 34 | CLIENT_PREFIX, 35 | PACKET_TYPE, 36 | ) 37 | 38 | 39 | @payload(PACKET_TYPE["Inquiry"]) 40 | class InquiryPayload(SADPPayload): 41 | def __init__(self, ipv6: str = None, buf: bytes = None) -> None: 42 | super().__init__(buf) 43 | self.ipaddress = ipv6 44 | if self.buf is not None: 45 | self.ipaddress = inet6_iptos(self.buf, 0) 46 | 47 | def __bytes__(self) -> bytes: 48 | if self.buf is None: 49 | self.buf = bytearray() 50 | self.buf += inet6_stoip(self.ipaddress) 51 | self.buf += bytes([0 for _ in range(12)]) 52 | self.buf = bytes(self.buf) 53 | return self.buf 54 | 55 | 56 | @payload(PACKET_TYPE["InquiryResponse"]) 57 | class InquiryResponsePayload(SADPPayload): 58 | pass 59 | 60 | 61 | def inquiry(mac: str, ipv4: str, ipv6: str, counter: int) -> SADPPacket: 62 | packet = SADPPacket() 63 | packet.eth_header.src = mac 64 | packet.header.packet_type = PACKET_TYPE["Inquiry"] 65 | packet.header.counter = counter 66 | packet.header.prefix = CLIENT_PREFIX 67 | packet.src_ip = ipv4 68 | packet.payload = InquiryPayload(ipv6=ipv6) 69 | return packet 70 | -------------------------------------------------------------------------------- /hiktools/csadp/uarray.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import struct 24 | 25 | __all__ = ["LITTLE_ENDIAN", "BIG_ENDIAN", "UIntArray", "to_uint16_buf"] 26 | 27 | LITTLE_ENDIAN = "<" 28 | BIG_ENDIAN = ">" 29 | 30 | 31 | class UIntArray(list): 32 | """Simple wrapper class for byte buffers.""" 33 | 34 | def __init__(self, bytes_size: int) -> None: 35 | self.max = 1 << bytes_size * 8 36 | 37 | def __iadd__(self, __x: list) -> "UIntArray": 38 | for i, val in enumerate(__x): 39 | if self.max <= val: 40 | raise TypeError( 41 | "Unsupported value (0 - %#x) at index %d" % (self.max - 1, i) 42 | ) 43 | return super().__iadd__(__x) 44 | 45 | 46 | def to_uint16_buf(buffer: bytes, encoding: str = BIG_ENDIAN) -> UIntArray: 47 | """Converts the given byte buffer into an unsigned 16-bit integer array. 48 | 49 | :param buffer: the raw bytes buffer 50 | :type buffer: bytes 51 | :param encoding: the encoding to use, defaults to BIG_ENDIAN 52 | :type encoding: str, optional 53 | :raises ValueError: if an invalid encoding is provided 54 | :raises ValueError: if an invalid input length is provided 55 | :return: the converted buffer 56 | :rtype: UIntArray 57 | """ 58 | if 62 < ord(encoding) or 60 > ord(encoding): 59 | raise ValueError("Invalid encoding value") 60 | 61 | length = len(buffer) 62 | if length == 0: 63 | return buffer 64 | if length % 2 != 0: 65 | raise ValueError(f"Invalid input array length ({length})") 66 | 67 | seq = UIntArray(2) 68 | for i in range(0, len(buffer), 2): 69 | seq += struct.unpack(encoding + "H", bytes(buffer[i : i + 2])) 70 | return seq 71 | -------------------------------------------------------------------------------- /hiktools/fmod/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from hiktools.fmod.digicap import * 24 | from hiktools.fmod.export import * 25 | -------------------------------------------------------------------------------- /hiktools/fmod/__main__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | import sys 23 | 24 | from hiktools.fmod import DigiCap, export 25 | 26 | if __name__ == '__main__': 27 | args = len(sys.argv) 28 | 29 | if args == 2 and sys.argv[1] == '-h': 30 | print(f'Usage: {__name__} ') 31 | sys.exit(1) 32 | 33 | if args != 3: 34 | print('[-] Expected only 2 arguments (type -h for help).') 35 | sys.exit(1) 36 | 37 | with DigiCap(sys.argv[1]) as dcap: 38 | print("Got", dcap.head.files, "files to save!") 39 | export(dcap, sys.argv[2]) 40 | for resource in dcap: 41 | print('> File name="%s", size=%d, pos=%d, checksum=%d' % resource) 42 | -------------------------------------------------------------------------------- /hiktools/fmod/digicap.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | Decryption module of hikvision firmware files. Note that newer files aren't supported 24 | yet as there is a new decryption key needed. 25 | 26 | The basic process of decrpytion os the following: 27 | - Read the raw header (first 108 bytes) 28 | - Decode the header XOR with the decryption key 29 | - Parse the header 30 | - Decode the rest of the firmware file XOR with the decryption key 31 | - Parse the embedded files 32 | """ 33 | from __future__ import annotations 34 | 35 | __all__ = [ 36 | "InvalidFileFormatException", 37 | "FileAccessException", 38 | "DigiCapHeader", 39 | "read_raw_header", 40 | "decode_xor16", 41 | "split_header", 42 | "split_files", 43 | "fopen_dav", 44 | "DigiCap", 45 | ] 46 | 47 | import logging 48 | 49 | from io import IOBase 50 | from typing import Generator, Iterator 51 | from struct import unpack 52 | 53 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 54 | 55 | logger = logging.getLogger("hiktools-logger") 56 | 57 | 58 | ############################################################################### 59 | # Exception Classes 60 | ############################################################################### 61 | class InvalidFileFormatException(Exception): 62 | """Base class for DAV file exceptions.""" 63 | 64 | 65 | class FileAccessException(Exception): 66 | """Base class for permission related issues.""" 67 | 68 | 69 | ############################################################################### 70 | # Data Types 71 | ############################################################################### 72 | BIG_ENDIAN = ">" 73 | LITTLE_ENDIAN = "<" 74 | 75 | 76 | def uint32(value: bytes, encoding: str = LITTLE_ENDIAN) -> int: 77 | """Unpacks an unsigned 32-bit integer from the given buffer. 78 | 79 | :param value: the input buffer 80 | :type value: bytes 81 | :param encoding: the encoding to use, defaults to LITTLE_ENDIAN 82 | :type encoding: int, optional 83 | :raises TypeError: if the buffer is not a bytes object 84 | :return: the unpacked unsigned 32-bit integer 85 | :rtype: int 86 | """ 87 | if isinstance(value, (bytes, bytearray, tuple, list)): 88 | if len(value) < 4: 89 | raise ValueError( 90 | "Could not verify buffer length - expected at least 4 " 91 | f"bytes, got {len(value)}" 92 | ) 93 | 94 | (result,) = unpack(f"{encoding}I", bytearray(list(value)[:4])) 95 | return result 96 | 97 | raise TypeError(f"Unexpected input type: {type(value)}") 98 | 99 | 100 | def uint24(value: bytes, encoding: int = LITTLE_ENDIAN) -> int: 101 | """Unpacks an unsigned 24-bit integer from the given buffer. 102 | 103 | :param value: the input buffer 104 | :type value: bytes 105 | :param encoding: the encoding to use, defaults to LITTLE_ENDIAN 106 | :type encoding: int, optional 107 | :raises ValueError: if an invalid encoding value is provided 108 | :raises TypeError: if the buffer is not a bytes object 109 | :return: the unpacked unsigned 24-bit integer 110 | :rtype: int 111 | """ 112 | if isinstance(value, (bytes, bytearray, tuple, list)): 113 | if len(value) < 3: 114 | raise ValueError( 115 | f"Invalid buffer length ({len(value)}), expected at least 4 " 116 | "bytes to handle." 117 | ) 118 | 119 | if encoding == BIG_ENDIAN: 120 | return value[0] << 16 | value[1] << 8 | value[2] 121 | if encoding == LITTLE_ENDIAN: 122 | return value[0] | value[1] << 8 | value[2] << 16 123 | 124 | raise ValueError( 125 | f"Unexpected Encoding, got {str(encoding)} ('<' or '>' accepted)" 126 | ) 127 | raise TypeError(f"unexpected input type: {type(value)}") 128 | 129 | 130 | def uint8(value: bytes | int) -> int: 131 | """Converts the input to an unsigned 8-bit integer 132 | 133 | :param value: the input buffer or int 134 | :type value: bytes 135 | :raises TypeError: if an invalid input is provided 136 | :return: the unsigned 8-bit integer 137 | :rtype: int 138 | """ 139 | if isinstance(value, bytes): 140 | return int(value[0]) 141 | if isinstance(value, int): 142 | return int(value & 0xFF) 143 | raise TypeError(f"unexpected input type: {type(value)}") 144 | 145 | 146 | class DigiCapHeader: 147 | """A class covering important configuration information. 148 | 149 | :param magic: magic header bytes indicating the used firmware 150 | :type magic: int 151 | 152 | :param header_checksum: unused. 153 | :type header_checksum: int 154 | 155 | :param header_length: The header length is used to indicate the end of the filesystem index. 156 | :type header_length: int 157 | 158 | :param files: The amount of files stored in this firmware image. 159 | :type files: int 160 | 161 | :param language: The used language 162 | :type language: int 163 | 164 | :param device_class: unidentified 165 | :type device_class: int 166 | 167 | :param oem_code: maybe system verfication key 168 | :type oem_code: int 169 | 170 | :param signature: unidentified 171 | :type signature: int 172 | 173 | :param features: unidentified 174 | :type features: int 175 | 176 | """ 177 | 178 | def __init__( 179 | self, 180 | magic: int = 0x00, 181 | header_checksum: int = 0x00, 182 | header_length: int = 0x00, 183 | files: int = 0x00, 184 | language: int = 0x00, 185 | device_class: int = 0x00, 186 | oem_code: int = 0x00, 187 | signature: int = 0x00, 188 | features: int = 0x00, 189 | ) -> None: 190 | self.magic = magic 191 | self.header_checksum = header_checksum 192 | self.header_length = header_length 193 | self.files = files 194 | self.language = language 195 | self.device_class = device_class 196 | self.oem_code = oem_code 197 | self.signature = signature 198 | self.features = features 199 | 200 | def __repr__(self) -> str: 201 | return f"" 202 | 203 | 204 | ############################################################################### 205 | # Funtions 206 | ############################################################################### 207 | def fopen_dav(file_name: str, mode: str = "rb") -> IOBase: 208 | """Opens a file with te 'dav' extension. 209 | 210 | :param file_name: The absolute or relative path to the file 211 | :type file_name: str 212 | :param mode: The mode this file shoul be opened with (either 'r' or 'rb'), defaults to 'rb' 213 | :type mode: str, optional 214 | 215 | :raises InvalidFileFormatException: on invalid file extension 216 | :raises ValueError: on invalid argument values 217 | :raises FileAccessException: if there are issues with open the file 218 | 219 | :return: A file reader instance. 220 | :rtype: IOBase 221 | """ 222 | if not file_name or not file_name.endswith("dav"): 223 | raise InvalidFileFormatException("Expected a *.dav file on input.") 224 | 225 | if not mode or mode not in ["r", "rb"]: 226 | raise ValueError("Expected a reading mode.") 227 | 228 | try: 229 | res = open(file_name, mode) #:noqa 230 | except OSError as open_error: 231 | raise FileAccessException(open_error) from open_error 232 | 233 | if not res: 234 | raise FileAccessException("Unable to open *.dav file") 235 | 236 | return res 237 | 238 | 239 | def read_raw_header(resource: IOBase | str) -> bytes: 240 | """Reads the first 108 bytes from the resource stream.""" 241 | if isinstance(resource, str): 242 | resource = fopen_dav(resource, "rb") 243 | 244 | if not resource or resource.mode != "rb": 245 | raise ValueError("Expected a reading bytes mode resource.") 246 | 247 | buf_len = 0x6C # 108 bytes 248 | try: 249 | buf = resource.read(buf_len) 250 | except OSError as error: 251 | raise FileAccessException(error) from error 252 | 253 | return buf 254 | 255 | 256 | def decode_xor16(buf: bytes, key: bytes, length: int) -> bytes: 257 | """Decodes (XOR) the given buf with a key.""" 258 | result = bytearray() 259 | 260 | if length > 0 or len(key) != 0xF: 261 | for index in range(length): 262 | key_byte = key[index + (index >> 4) & 0xF] 263 | result.append(key_byte ^ buf[index]) 264 | 265 | return bytes(result) 266 | 267 | 268 | def split_header(buf: bytes) -> DigiCapHeader: 269 | """Extracts information from the decoded firmware header.""" 270 | if not buf or len(buf) == 0: 271 | raise ValueError("Invalid buf object len() == 0 or object is None.") 272 | 273 | # REVISION: maybe add magic value check to validate the right firmware 274 | # file is going to be inspected. 275 | magic = uint32(buf[:4]) # 9 the buffer should have only four bytes 276 | header_checksum = uint32(buf[4:8]) 277 | header_length = uint32(buf[8:12]) 278 | files = uint32(buf[12:16]) 279 | language = uint32(buf[16:20]) 280 | device_class = uint32(buf[20:24]) 281 | oem_code = uint32(buf[24:28]) 282 | signature = uint32(buf[28:32]) 283 | features = uint32(buf[32:36]) 284 | 285 | checksum = uint8(buf[8]) + (uint24(buf[9:12]) * 0x100) 286 | if checksum != header_length: 287 | raise InvalidFileFormatException( 288 | f"Invalid header size: expected {checksum}, got {header_length}" 289 | ) 290 | 291 | return DigiCapHeader( 292 | magic, 293 | header_checksum, 294 | header_length, 295 | files, 296 | language, 297 | device_class, 298 | oem_code, 299 | signature, 300 | features, 301 | ) 302 | 303 | 304 | def split_files( 305 | buf: bytes | IOBase, length: int = 0x40 306 | ) -> Generator[tuple, None, None]: 307 | """Iterates over all files located in the given filesystem index 308 | 309 | :param buf: the input buffer or file pointer 310 | :type buf: bytes 311 | :param length: the amount of bytes to read, defaults to 0x40 312 | :type length: int, optional 313 | :raises ValueError: if the reading mode is not 'rb' 314 | :raises ValueError: if the amount of bytes to read is <= 0 315 | :yield: a tuple storing the file name, file position, and file checksum 316 | :rtype: Generator[tuple, None, None] 317 | """ 318 | if isinstance(buf, IOBase): 319 | if not buf or buf.mode != "rb": 320 | raise ValueError("Expected a reading bytes mode resource.") 321 | 322 | if length <= 0: 323 | raise ValueError("Expected a length > 0.") 324 | 325 | buf.seek(0, 0) 326 | buf = buf.read(length) 327 | 328 | index = 0x40 329 | amount = uint32(buf[12:16]) 330 | for _ in range(amount): 331 | file_name = buf[index : index + 32].replace(b"\x00", b"") 332 | index += 32 333 | 334 | file_length = uint32(buf[index : index + 4]) 335 | file_pos = uint32(buf[index + 4 : index + 8]) 336 | file_checksum = uint32(buf[index + 8 : index + 12]) 337 | index += 12 338 | yield file_name.decode("utf-8"), file_length, file_pos, file_checksum 339 | 340 | 341 | ############################################################################### 342 | # Classes 343 | ############################################################################### 344 | class DigiCap: 345 | """The base class for operating with digicap.dav files.""" 346 | 347 | KEY_XOR = b"\xBA\xCD\xBC\xFE\xD6\xCA\xDD\xD3\xBA\xB9\xA3\xAB\xBF\xCB\xB5\xBE" 348 | """The key used to encrypt/decrypt the firmware files.""" 349 | 350 | def __init__(self, resource: str | IOBase = None) -> None: 351 | self._file = None 352 | self._name = None 353 | self._filelist = [] 354 | self._len = 0 355 | self._head = None 356 | 357 | if resource is not None: 358 | if isinstance(resource, str): 359 | if not self.fopen(resource): 360 | raise InvalidFileFormatException(f"Invalid input file: {resource}") 361 | 362 | elif isinstance(resource, IOBase): 363 | self._file = resource 364 | self._name = resource.name 365 | else: 366 | raise ValueError(f"Invalid argument type: {type(resource)}") 367 | 368 | def fopen(self, resource: str) -> bool: 369 | """Opens the given resource. 370 | 371 | Will be called automatically when this class is used in a with statement. 372 | """ 373 | if resource is not None: 374 | self._file = fopen_dav(resource) 375 | self._name = resource 376 | return resource is not None 377 | 378 | def fclose(self): 379 | """Closes the current file reader. 380 | 381 | Will be called automatically when this class is used in a with statement. 382 | """ 383 | self._file.close() 384 | 385 | def reset(self) -> bool: 386 | """Sets the reader's position to the start of the stream.""" 387 | self._file.seek(0, 0) 388 | return self._file.seekable() 389 | 390 | def fread(self, length: int, offset: int = -1) -> bytes: 391 | """Reads the given amount of bytes from the underlying stream.""" 392 | if self._file.closed: 393 | raise ValueError("FileInoutStream is closed!") 394 | 395 | if offset >= 0: 396 | self._file.seek(offset) 397 | return self._file.read(length) 398 | 399 | def fparse(self): 400 | """Parses the firmware file.""" 401 | if self._file is not None: 402 | raw_head = read_raw_header(self._file) 403 | decoded_head = decode_xor16(raw_head, self.KEY_XOR, 0x6C) 404 | 405 | self._head = split_header(decoded_head) 406 | self.reset() 407 | raw_data = decode_xor16( 408 | self._file.read(self.head.header_length), 409 | self.KEY_XOR, 410 | self.head.header_length, 411 | ) 412 | self._filelist = list(split_files(raw_data)) 413 | if len(self) == 0: 414 | logger.warning("Could not decode firmware - detected 0 files!") 415 | else: 416 | raise ValueError("Input source is None.") 417 | 418 | @property 419 | def name(self) -> str: 420 | """The file name (absolute or relative)""" 421 | return self._name 422 | 423 | @property 424 | def head(self) -> DigiCapHeader: 425 | """The header object storing important configuration data.""" 426 | return self._head 427 | 428 | def __enter__(self) -> "DigiCap": 429 | self.fparse() 430 | return self 431 | 432 | def __exit__(self, exc_type, exc_value, traceback): 433 | self.fclose() 434 | 435 | def __len__(self) -> int: 436 | return len(self._filelist) if not self._len else self._len 437 | 438 | def __iter__(self) -> Iterator[tuple]: 439 | return iter(self._filelist) 440 | 441 | def __getitem__(self, index: int) -> tuple: 442 | return self._filelist[index] 443 | 444 | def __repr__(self) -> str: 445 | return f'' 446 | -------------------------------------------------------------------------------- /hiktools/fmod/export.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | __all__ = ["export"] 23 | 24 | from pathlib import Path 25 | from hiktools.fmod.digicap import DigiCap, FileAccessException 26 | 27 | 28 | def export(dfile: DigiCap, location: str) -> bool: 29 | """Extracts all files stored in the given digicap file.""" 30 | if location is None or dfile is None: 31 | raise ValueError("Input file or location is null") 32 | 33 | path = Path(location) 34 | try: 35 | path.mkdir(exist_ok=True) 36 | except OSError as error: 37 | raise FileAccessException(error) from error 38 | 39 | for fname, flen, fpos, _ in dfile: 40 | try: 41 | with open(str(path / fname), "wb") as exp_file: 42 | exp_file.write(dfile.fread(flen, fpos)) 43 | except OSError as err: 44 | print(str(err)) 45 | return False 46 | 47 | return True 48 | -------------------------------------------------------------------------------- /hiktools/sadp/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | This module covers basic operations and classes related to the UDP broadcast 24 | communication with hikvision devices. 25 | """ 26 | 27 | from hiktools.sadp.message import * 28 | from hiktools.sadp.client import * 29 | -------------------------------------------------------------------------------- /hiktools/sadp/client.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | UDP-Client implementation to communicate with Hikvision cameras. 24 | """ 25 | 26 | __all__ = ["SADPClient", "hik_code"] 27 | 28 | import socket 29 | 30 | from hiktools.sadp.message import SADPMessage 31 | 32 | 33 | class SADPClient: 34 | """A simple UDP wrapper client. 35 | 36 | This clas implements basic funtionalities of an UDP socket sending data 37 | from UDP broadcast. The with directive can be used and __iter__ is also 38 | implemented in order to iterate over reveiced SADPMessages. 39 | """ 40 | 41 | def __init__(self, port: int = 37020, timeout: int = 2) -> None: 42 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 43 | self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 44 | self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 45 | self._sock.settimeout(timeout) 46 | self._address = ("239.255.255.250", port) 47 | self.buf_size = 2048 48 | 49 | def __enter__(self) -> "SADPClient": 50 | return self 51 | 52 | def __exit__(self, exc_type, exc_value, traceback): 53 | self.close() 54 | 55 | def close(self): 56 | """Closes the underlying socket.""" 57 | self._sock.close() 58 | 59 | def write(self, message: SADPMessage) -> int: 60 | """Writes the given message and returnes the amount of bytes sent.""" 61 | return self.write_bytes(bytes(message)) if message else -1 62 | 63 | def write_bytes(self, buffer: bytes) -> int: 64 | """Directly writes bytes to the UDP broadcast""" 65 | return self._sock.sendto(buffer, self._address) 66 | 67 | def recv_next(self, bufsize: int = 1024) -> bytes: 68 | """Receives the next bytes.""" 69 | return self._sock.recv(bufsize) 70 | 71 | def __iter__(self): 72 | try: 73 | while True: 74 | data = self.recv_next(self.buf_size) 75 | yield SADPMessage(response=data) 76 | except socket.timeout: 77 | pass 78 | 79 | 80 | def hik_code(serial: str, timestamp: tuple) -> str: 81 | """Generates the old Hikvision reset code. 82 | 83 | Arguments: 84 | :param serial: The device's serial number. 85 | :type serial: str 86 | :param timestamp: A timestamp of the following format: (day, month, year) 87 | :type timestamp: str 88 | 89 | :returns: The generated reset code (can be used within a reset packet). 90 | :rtype: str 91 | """ 92 | magic = 0 93 | result = "" 94 | day = int(timestamp[0]) 95 | month = int(timestamp[1]) 96 | year = int(timestamp[2]) 97 | 98 | composed = serial + year + month + day 99 | for i, val in enumerate(composed): 100 | magic += (ord(val) * (i + 1)) ^ (i + 1) 101 | 102 | magic = str((magic * 0x686B7773) & 0xFFFFFFFF) 103 | for i, c0 in enumerate(magic): 104 | if c0 < 51: 105 | result += chr(c0 + 33) 106 | elif c0 < 53: 107 | result += chr(c0 + 62) 108 | elif c0 < 55: 109 | result += chr(c0 + 47) 110 | elif c0 < 57: 111 | result += chr(c0 + 66) 112 | else: 113 | result += chr(c0) 114 | 115 | return result 116 | -------------------------------------------------------------------------------- /hiktools/sadp/message.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2023 MatrixEditor 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | """ 23 | Small module that contains message declarations for UDP communication. 24 | """ 25 | 26 | __all__ = [ 27 | "SADPMessage", 28 | "fromdict", 29 | "BasicDictObject", 30 | "DiscoveryPacket", 31 | "SafeCode", 32 | "DeviceSafeCodePacket", 33 | "unmarshal", 34 | "ActionResponse", 35 | ] 36 | 37 | import xml.etree.ElementTree as xmltree 38 | import ctypes 39 | import base64 40 | 41 | from typing import Iterator, overload 42 | 43 | __types__ = {} 44 | """A dict object storing all defined message types. 45 | """ 46 | 47 | 48 | def message_type(name: str): 49 | """Registers a new message type. 50 | 51 | :param name: the name of the type to register 52 | :type name: str 53 | """ 54 | 55 | def wrapper(clazz): 56 | if name not in __types__: 57 | __types__[name] = clazz 58 | return clazz 59 | 60 | return wrapper 61 | 62 | 63 | class SADPMessage: 64 | """A simple exchange class. 65 | 66 | For exchanging data between the client and the device this object wrapper 67 | is used. It stores the message string which is sent over broadcast to the 68 | device and the response as bytes. 69 | 70 | :param response: A response buffer storing the received bytes. 71 | :type response: bytes 72 | """ 73 | 74 | def __init__( 75 | self, message: str = None, response=None, sender: tuple = None 76 | ) -> None: 77 | self._message = message 78 | self._response = response 79 | self._sender = (None, 0) if not sender else sender 80 | 81 | def toxml(self) -> xmltree.Element: 82 | """Transforms the received bytes into an XML element.""" 83 | if self._response is not None: 84 | return xmltree.fromstring(self._response) 85 | else: 86 | raise ValueError("Response value is null") 87 | 88 | def set_response(self, response: bytes): 89 | """The message's response setter. 90 | 91 | :param response: the raw response onbject 92 | :type response: bytes 93 | """ 94 | if response is not None: 95 | self._response = response 96 | 97 | @property 98 | def address(self) -> str: 99 | """The sender ip address. (DO NOT USE)""" 100 | return self._sender[0] 101 | 102 | @property 103 | def message(self) -> str: 104 | """The initial message formatted as an XML string.""" 105 | return self._message 106 | 107 | @property 108 | def sender(self) -> tuple: 109 | """The sender ip address and port. (DO NOT USE)""" 110 | return self._sender 111 | 112 | def __bytes__(self) -> bytes: 113 | return self.message.encode("utf-8") 114 | 115 | def __repr__(self) -> str: 116 | return self.message 117 | 118 | def __str__(self) -> str: 119 | return self.message 120 | 121 | 122 | def fromdict(value: dict) -> SADPMessage: 123 | """Converts a dict object into an SADPMessage object. 124 | 125 | :param value: The input dict containing the XML nodes. 126 | :type value: dict 127 | 128 | :returns: An message object containing all nodes of the given 129 | dict object converted into an XML string. 130 | :rtype: SADPMessage 131 | """ 132 | root = xmltree.Element("Probe") 133 | for key in value: 134 | elem = xmltree.Element(key) 135 | elem.text = value[key] 136 | root.append(elem) 137 | return SADPMessage( 138 | message=xmltree.tostring(root, xml_declaration=True).decode("utf-8") 139 | ) 140 | 141 | 142 | def pack(buffer: bytes, fmt: type, index=0) -> int: 143 | """Puts the bytes in the given buffer into one integer. 144 | 145 | :param buffer: the input buffer 146 | :type buffer: bytes 147 | :param fmt: the integer type 148 | :type fmt: type 149 | :param index: the current buffer position, defaults to 0 150 | :type index: int, optional 151 | :return: the packed integer variable 152 | :rtype: int 153 | """ 154 | if fmt == ctypes.c_uint32: 155 | return ( 156 | pack(buffer, ctypes.c_uint16, index) 157 | | pack(buffer, ctypes.c_uint16, index + 2) << 16 158 | ) 159 | if fmt == ctypes.c_uint8: 160 | return fmt(buffer[index]) 161 | if fmt == ctypes.c_uint16: 162 | return ( 163 | pack(buffer, ctypes.c_uint8, index) 164 | | pack(buffer, ctypes.c_uint8, index + 1) << 8 165 | ) 166 | raise TypeError(f'Invalid type "{str(fmt)}" - not implemented!') 167 | 168 | 169 | class BasicDictObject: 170 | """The base class for response objects. 171 | 172 | A small wrapper class implementing basic dict behaviour. 173 | 174 | Example usage: 175 | >>> base = BaseDictObject() 176 | >>> base['hello'] = "World" 177 | >>> print(str(base)) 178 | World 179 | >>> print(repr(base)) 180 | 181 | """ 182 | 183 | def __init__(self, root: xmltree.Element = None, exclude: list = None) -> None: 184 | self._capabilities = {} 185 | if root is not None: 186 | for capability in root: 187 | if not exclude or capability not in exclude: 188 | self[capability.tag] = capability.text 189 | 190 | def __setitem__(self, key, value): 191 | self._capabilities[key] = value 192 | 193 | def __getitem__(self, key): 194 | return self._capabilities[key] 195 | 196 | def __contains__(self, item): 197 | return str(item) in self._capabilities 198 | 199 | def __iter__(self) -> Iterator[str]: 200 | return iter(self._capabilities) 201 | 202 | def __str__(self) -> str: 203 | text = [f"<{self.__class__.__name__}>"] 204 | for cap_name in self: 205 | text.append(f"\t<{cap_name}>{self[cap_name]}") 206 | text.append(f"") 207 | return "\n".join(text) 208 | 209 | def __repr__(self) -> str: 210 | return f"<{self.__class__.__name__} object>" 211 | 212 | 213 | @message_type("inquiry") 214 | class DiscoveryPacket(BasicDictObject): 215 | """A simple discovery packet response wrapper. 216 | 217 | Contains all information related to the inquiry response packet. Possible 218 | nodes are: 219 | 220 | `DeviceType`, `DeviceDescription`, `CommandPort`, `HttpPort`, `MAC`, `Ipv4Address`, 221 | `Ipv4SubnetMask`, `Ipv4Gateway`, `Ipv6Address`, `Ipv6Masklen`, `Ipv6Gateway`, `DHCP`, 222 | `AnalogChannelNum`, `DigitalChannelNum`, `DSPVersion`, `Activated` and 223 | `PasswordResetAbility`. 224 | """ 225 | 226 | 227 | class SafeCode: 228 | """The device's safe code wrapper class. 229 | 230 | A safe code is requested by the SADP Tool to export it and send it to the 231 | customer service. An unlock code should be returned which can reset the device. 232 | """ 233 | 234 | SAFECODE_FLAG = "03000000" 235 | """Safe code header value""" 236 | 237 | @overload 238 | def __init__(self, code: bytes) -> None: 239 | ... 240 | 241 | def __init__(self, code: str) -> None: 242 | if not code: 243 | raise ValueError("Code argument has to be non null!") 244 | 245 | self._b64_encoded = code.encode("utf-8") if isinstance(code, str) else code 246 | self._b64_decoded = base64.decodebytes(self._b64_encoded) 247 | 248 | if self._b64_decoded[:4].hex() != self.SAFECODE_FLAG: 249 | raise ValueError( 250 | "Invalid SafeCode: expected %#x as flag" % self.SAFECODE_FLAG 251 | ) 252 | 253 | self._code = str(self._b64_decoded[4:-4], "utf-8") 254 | self._cksum = pack(self._b64_decoded[:-4], ctypes.c_uint32) 255 | 256 | @property 257 | def checksum(self) -> ctypes.c_uint32: 258 | """The checksum stored in the safe code.""" 259 | return self._cksum 260 | 261 | @property 262 | def code(self) -> str: 263 | """The full safe code as a string""" 264 | return self._code 265 | 266 | def __repr__(self) -> str: 267 | return self.code 268 | 269 | 270 | @message_type("getcode") 271 | class DeviceSafeCodePacket(BasicDictObject): 272 | """A response packet for the 'getcode' message. 273 | 274 | This class can contain the following nodes: 275 | 276 | `MAC`, `Uuid`, `Code`, `Types` 277 | """ 278 | 279 | @property 280 | def code(self) -> str: 281 | """The base64 safecode string.""" 282 | return self["Code"] 283 | 284 | def get_safecode(self) -> SafeCode: 285 | """Returns the converted safecode as a SafeCode object.""" 286 | return SafeCode(self.code) 287 | 288 | 289 | @message_type("reset") 290 | class ActionResponse(BasicDictObject): 291 | """A 'reset' message response packet. 292 | 293 | The response packet just contains one field of interest named 'Result'. It 294 | applies to the following values: failure, success, denied 295 | """ 296 | 297 | def __init__(self, root: xmltree.Element = None) -> None: 298 | super().__init__(root) 299 | 300 | @property 301 | def success(self) -> bool: 302 | """Returns whether the action invocation was successful.""" 303 | return self["Result"] == "success" 304 | 305 | 306 | def unmarshal(root: xmltree.Element): 307 | """Tries to de-serialize an XML-String. 308 | 309 | Possible object types are: DiscoveryPacket, DeviceSafeCodePacket, ActionResponse, 310 | and BasicDictObject for messages that are not implemented yet. 311 | 312 | :param root: The XML root element (non null). 313 | :type root: xmltree.Element 314 | :return: A qualified object instance or None if the given argument was None or 315 | the input could not be converted. 316 | :rtype: ? extends BasicDictObject, None 317 | """ 318 | if root is None: 319 | raise ValueError("Input argument is null") 320 | 321 | req_type = root.find("Types") 322 | if req_type is None: 323 | return None 324 | 325 | for type_name, cls in __types__.items(): 326 | if type_name == req_type.text.lower(): 327 | return cls(root=root) 328 | return BasicDictObject(root=root) 329 | -------------------------------------------------------------------------------- /lua/sadp.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2023 MatrixEditor 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | do 26 | local sadp_eth_type = 0x8033 27 | local sadp_header_length = 0x26 28 | 29 | local sadp_proto = Proto("SADP", "Search Active Device Protocol") 30 | 31 | local packet_type = { 32 | [1] = "Response Packet", 33 | [2] = "Request Packet" 34 | } 35 | 36 | local query_type = { 37 | [0x02] = "DeviceOnlineRequest", 38 | [0x03] = "Inquiry", 39 | [0x04] = "InquiryResponse", 40 | [0x06] = "UpdateIP", 41 | [0x07] = "UpdateIPResponse", 42 | [0x0a] = "ResetPassword", 43 | [0x0b] = "ResetPasswordResponse", 44 | [0x0c] = "CMSInfo", 45 | [0x0d] = "CMSInfoResponse", 46 | [0x10] = "ModifyNetParam", 47 | [0x11] = "ModifyNetParamResponse" 48 | } 49 | 50 | local origin_type = { 51 | [0xF] = "Packet was sent by Device", 52 | [0x4] = "Packet was sent by SADP Tool", 53 | } 54 | 55 | local F_header_marker = ProtoField.uint8("sadp.header.marker", "Marker", base.HEX) 56 | local F_header_type = ProtoField.uint8("sadp.header.type", "Type", base.HEX, packet_type, 0xF) 57 | local F_header_origin = ProtoField.uint16("sadp.header.origin", "Origin", base.HEX, origin_type, 0xF0) 58 | local F_header_counter = ProtoField.uint32("sadp.header.counter", "Counter", base.DEC) 59 | local F_header_marker2 = ProtoField.uint16("sadp.header.marker2", "Marker2", base.HEX) 60 | local F_header_checksum = ProtoField.uint16("sadp.header.checksum", "Checksum", base.HEX) 61 | local F_header_qry_type = ProtoField.uint8("sadp.header.qry.type", "Query Type", base.HEX, query_type, 0xF) 62 | local F_header_qry_params = ProtoField.uint8("sadp.header.qry.params", "Query Params", base.HEX) 63 | local F_header_cur_mac = ProtoField.new("Current MAC","sadp.header.cur.mac", ftypes.ETHER) 64 | local F_header_dst_mac = ProtoField.new("Destination MAC", "sadp.header.dst.mac", ftypes.ETHER) 65 | local F_header_cur_ip = ProtoField.new("Current IP", "sadp.header.cur.ip", ftypes.IPv4) 66 | local F_header_dst_ip = ProtoField.new("Destination IP", "sadp.header.dst.ip", ftypes.IPv4) 67 | local F_header_add_ip = ProtoField.new("Subnet Mask", "sadp.header.subnet", ftypes.IPv4) 68 | 69 | local F_payload_data = ProtoField.new("Data", "sadp.payload.data", ftypes.BYTES) 70 | local F_payload_ipv6 = ProtoField.new("Current IPv6", "sadp.payload.ipv6", ftypes.IPv6) 71 | 72 | -- add the fields to the protocol 73 | sadp_proto.fields = { 74 | -- header 75 | F_header_marker, F_header_type, F_header_origin, 76 | F_header_counter, F_header_marker2, F_header_qry_type, F_header_qry_params, 77 | F_header_cur_mac, F_header_dst_mac, F_header_cur_ip, F_header_dst_ip, 78 | F_header_add_ip, F_header_checksum, 79 | 80 | -- payload 81 | F_payload_data, F_payload_ipv6 82 | } 83 | 84 | 85 | local function sadp_dissector(buffer, pinfo, tree) 86 | buf_len = buffer:len() 87 | if buf_len == 0 then return end 88 | 89 | pinfo.cols.protocol = sadp_proto.name 90 | 91 | local subtree = tree:add(sadp_proto, buffer(), "Search Active Device Protocol") 92 | local header = subtree:add(sadp_proto, buffer(0, sadp_header_length), "Header") 93 | 94 | local marker = buffer(0, 1):le_int() 95 | if marker == 0x21 then 96 | header:add_le(F_header_marker, buffer(0, 1)):append_text(" (default)") 97 | else 98 | header:add_le(F_header_marker, buffer(0, 1)) 99 | end 100 | 101 | local _type = buffer(1, 1):le_int() 102 | if _type == nil then 103 | _type = 0x1 104 | end 105 | 106 | type_subtree = header:add(sadp_proto, buffer(1, 1), "Type: " .. packet_type[_type]) 107 | type_subtree:add(F_header_type, buffer(1, 1)) 108 | 109 | _origin = buffer(2, 2):le_int() 110 | 111 | if _origin == 0x4201 then 112 | origin_subtree = header:add(sadp_proto, buffer(2, 2), "Origin: SADP Tool") 113 | else 114 | origin_subtree = header:add(sadp_proto, buffer(2, 2), "Origin: Device") 115 | end 116 | origin_subtree:add(F_header_origin, buffer(3, 1)) 117 | 118 | header:add(F_header_counter, buffer(4, 4)) 119 | marker = buffer(8, 2):le_int() 120 | if marker == 0x0406 then 121 | header:add_le(F_header_marker2, buffer(8, 2)):append_text(" (default)") 122 | else 123 | header:add_le(F_header_marker2, buffer(8, 2)) 124 | end 125 | 126 | if _type == 1 then 127 | _ptype = buffer(10, 1):le_int() - 1 128 | end 129 | 130 | _type = buffer(10, 1):le_int() 131 | 132 | 133 | local query_subtree = header:add(sadp_proto, buffer(10, 2), "Query Type: " .. query_type[_ptype]) 134 | query_subtree:add(F_header_qry_type, buffer(10, 1)) 135 | query_subtree:add(F_header_qry_params, buffer(11, 1)) 136 | 137 | header:add(F_header_checksum, buffer(12, 2)) 138 | 139 | header:add(F_header_cur_mac, buffer(14, 6)) 140 | header:add(F_header_cur_ip, buffer(20, 4)) 141 | header:add(F_header_dst_mac, buffer(24, 6)) 142 | header:add(F_header_dst_ip, buffer(30, 4)) 143 | header:add(F_header_add_ip, buffer(34, 4)) 144 | 145 | local payload_tree = subtree:add(sadp_proto, buffer(sadp_header_length, buf_len - sadp_header_length), "Payload") 146 | 147 | if _type == 0x03 then 148 | payload_tree:add(F_payload_ipv6, buffer(sadp_header_length, sadp_header_length + 16)) 149 | else 150 | payload_tree:add(F_payload_data, buffer(sadp_header_length, buf_len - sadp_header_length)) 151 | end 152 | 153 | end 154 | 155 | -- declare the fields we need to read 156 | function sadp_proto.dissector(tvbuffer, pinfo, treeitem) 157 | return sadp_dissector(tvbuffer, pinfo, treeitem) 158 | end 159 | 160 | local eth_Type = DissectorTable.get("ethertype") 161 | eth_Type:add(0x8033, sadp_proto) 162 | end 163 | 164 | 165 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "hiktools" 3 | version = "1.2.2" 4 | description="Hikvision utility tools" 5 | authors = [ 6 | { name="MatrixEditor", email="not@supported.com" }, 7 | ] 8 | readme = "README.md" 9 | classifiers = [ 10 | 'Development Status :: 5 - Production/Stable', 11 | 'Intended Audience :: Science/Research', 12 | 'License :: OSI Approved :: MIT License', 13 | 'Programming Language :: Python :: 3.6', 14 | 'Programming Language :: Python :: 3.7', 15 | 'Programming Language :: Python :: 3.8', 16 | 'Programming Language :: Python :: 3.9', 17 | 'Programming Language :: Python :: 3.10', 18 | ] 19 | 20 | [project.urls] 21 | "Homepage" = "https://github.com/MatrixEditor/hiktools" 22 | "API-Docs" = "https://hiktools.readthedocs.io" 23 | "Native-API-Docs" = "https://matrixeditor.github.io/hiktools/docs/html/d3/dcc/md__r_e_a_d_m_e.html" 24 | 25 | [tool.setuptools.packages.find] 26 | where = ["."] 27 | include = ["hiktools*"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography --------------------------------------------------------------------------------