initial version of a very hacky websocket tool
This commit is contained in:
57
README.md
Normal file
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Interface with Home Connect appliances in Python
|
||||||
|
|
||||||
|
This is a very, very beta interface for Bosch-Siemens Home Connect
|
||||||
|
devices through their local network connection. It has some tools
|
||||||
|
to find the TLS PSK (Pre-shared Key) that is used to allow local
|
||||||
|
access, and a Python script that can construct the proper Websocket
|
||||||
|
interface to subscribe to events.
|
||||||
|
|
||||||
|
*WARNING: This is not ready for prime time!*
|
||||||
|
|
||||||
|
|
||||||
|
## Finding the PSK
|
||||||
|
|
||||||
|
You'll need to find the PSK for your devices with a rooted
|
||||||
|
Android phone and the `find-psk.frida` script for Frida.
|
||||||
|
|
||||||
|
```
|
||||||
|
frida --no-pause -f com.bshg.homeconnect.android.release -U -l find-psk.frida
|
||||||
|
```
|
||||||
|
|
||||||
|
It should print a message like:
|
||||||
|
|
||||||
|
```
|
||||||
|
psk callback hint 'HCCOM_Local_App'
|
||||||
|
psk 32 0x6ee63fb2f0
|
||||||
|
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
|
||||||
|
00000000 0e c8 1f d8 c6 49 fa d8 bc e7 fd 34 33 54 13 d4 .....I.....43T..
|
||||||
|
00000010 73 f9 2e 01 fc d8 26 80 49 89 4c 19 d7 2e cd cb s.....&.I.L.....
|
||||||
|
```
|
||||||
|
|
||||||
|
Which gives you the 32-byte PSK value.
|
||||||
|
|
||||||
|
## SSL logging
|
||||||
|
|
||||||
|
The Frida script will also dump all of the SSL traffic so that you can
|
||||||
|
see different endpoints and things. Not much is documented yet.
|
||||||
|
|
||||||
|
Note that the TX from the phone on the websocket is "masked" with an
|
||||||
|
repeating 4-byte XOR that is sent in the first part of each messages.
|
||||||
|
The script could be augmented to decode those as well.
|
||||||
|
The replies from the device are not masked so they can be read in the clear.
|
||||||
|
|
||||||
|
## hcpy
|
||||||
|
|
||||||
|
The `hcpy` tool can contact your device, and if the PSK is correct, it will
|
||||||
|
register for notification of events.
|
||||||
|
|
||||||
|
```
|
||||||
|
RX: {'sID': 2354590730, 'msgID': 3734589701, 'resource': '/ei/initialValues', 'version': 2, 'action': 'POST', 'data': [{'edMsgID': 3182729968}]}
|
||||||
|
TX: {"sID":2354590730,"msgID":3734589701,"resource":"/ei/initialValues","version":2,"action":"RESPONSE","data":[{"deviceType":"Application","deviceName":"py-hca","deviceID":"1234"}]}
|
||||||
|
TX: {"sID":2354590730,"msgID":3182729968,"resource":"/ci/services","version":1,"action":"GET"}
|
||||||
|
TX: {"sID":2354590730,"msgID":3182729969,"resource":"/iz/info","version":1,"action":"GET"}
|
||||||
|
TX: {"sID":2354590730,"msgID":3182729970,"resource":"/ei/deviceReady","version":2,"action":"NOTIFY"}
|
||||||
|
RX: {'sID': 2354590730, 'msgID': 3182729968, 'resource': '/ci/services', 'version': 1, 'action': 'RESPONSE', 'data': [{'service': 'ci', 'version': 3}, {'service': 'ei', 'version': 2}, {'service': 'iz', 'version': 1}, {'service': 'ni', 'version': 1}, {'service': 'ro', 'version': 1}]}
|
||||||
|
RX: {'sID': 2354590730, 'msgID': 3182729969, 'resource': '/iz/info', 'version': 1, 'action': 'RESPONSE', 'data': [{'deviceID': '....', 'eNumber': 'SX65EX56CN/11', 'brand': 'SIEMENS', 'vib': 'SX65EX56CN', 'mac': '....', 'haVersion': '1.4', 'swVersion': '3.2.10.20200911163726', 'hwVersion': '2.0.0.2', 'deviceType': 'Dishwasher', 'deviceInfo': '', 'customerIndex': '11', 'serialNumber': '....', 'fdString': '0201', 'shipSki': '....'}]}
|
||||||
|
```
|
||||||
|
|
||||||
86
find-psk.frida
Normal file
86
find-psk.frida
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Locate the TLS PSK used by the dishwasher as the password that
|
||||||
|
* protects access over the network.
|
||||||
|
*
|
||||||
|
* Launch it with:
|
||||||
|
* frida --no-pause -f com.bshg.homeconnect.android.release -U -l find-psk.frida
|
||||||
|
*
|
||||||
|
* This will also dump all of the cleartext SSL traffic before encryption
|
||||||
|
* and after decryption. For websocket traffic from the server, this
|
||||||
|
* will be in the clear, although to the server is masked with an XOR
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* Note that it has to delay the Interceptor attach calls until the
|
||||||
|
* library has been dlopen()'ed by the application, which is why there
|
||||||
|
* is a setTimeout().
|
||||||
|
*
|
||||||
|
* TODO: Is the PSK deterministic or is it randomly generated and stored
|
||||||
|
* in the cloud?
|
||||||
|
*
|
||||||
|
* TODO: XOR unmask the websock TX traffic
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("looking for libHCPService.so");
|
||||||
|
|
||||||
|
const SSL_get_servername = new NativeFunction(
|
||||||
|
Module.getExportByName("libHCPService.so", "SSL_get_servername"),
|
||||||
|
"pointer",
|
||||||
|
["pointer","int"]
|
||||||
|
);
|
||||||
|
|
||||||
|
Interceptor.attach(Module.getExportByName("libHCPService.so", "SSL_read"),
|
||||||
|
{
|
||||||
|
onEnter(args) {
|
||||||
|
this.ssl = args[0];
|
||||||
|
this.buf = args[1];
|
||||||
|
},
|
||||||
|
onLeave(retval) {
|
||||||
|
const server_ptr = SSL_get_servername(this.ssl, 0);
|
||||||
|
const server = server_ptr.readUtf8String();
|
||||||
|
|
||||||
|
retval |= 0;
|
||||||
|
if (retval <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
console.log("RX", server);
|
||||||
|
console.log(Memory.readByteArray(this.buf, retval));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Interceptor.attach(Module.getExportByName("libHCPService.so", "SSL_write"),
|
||||||
|
{
|
||||||
|
onEnter(args) {
|
||||||
|
this.ssl = args[0];
|
||||||
|
const len = Number(args[2]);
|
||||||
|
const server_ptr = SSL_get_servername(this.ssl, 0);
|
||||||
|
const server = server_ptr.readUtf8String();
|
||||||
|
|
||||||
|
console.log("TX", server);
|
||||||
|
console.log(Memory.readByteArray(args[1], len));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* hcp::client_psk_callback is called when OpenSSL has made a connection and
|
||||||
|
* the server has offered a client hint.
|
||||||
|
*/
|
||||||
|
Interceptor.attach(Module.getExportByName("libHCPService.so", "_ZN3hcp19client_psk_callbackEP6ssl_stPKcPcjPhj"),
|
||||||
|
{
|
||||||
|
onEnter(args) {
|
||||||
|
this.ssl = args[0];
|
||||||
|
this.identity = args[2];
|
||||||
|
this.psk_buf = args[4];
|
||||||
|
const hint = Memory.readUtf8String(args[1]);
|
||||||
|
console.log("psk callback hint '" + hint + "'");
|
||||||
|
},
|
||||||
|
onLeave(len) {
|
||||||
|
len |= 0;
|
||||||
|
console.log("psk", len, this.psk_buf);
|
||||||
|
const buf = Memory.readByteArray(this.psk_buf, len);
|
||||||
|
console.log(buf);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}, 1000)
|
||||||
129
hcpy
Executable file
129
hcpy
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Contact a Bosh-Siemens Home Connect device
|
||||||
|
# todo: document how to extract the psk
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sslpsk
|
||||||
|
import websocket
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
# Monkey patch for sslpsk in pip using the old _sslobj
|
||||||
|
def _sslobj(sock):
|
||||||
|
if (3, 5) <= sys.version_info <= (3, 7):
|
||||||
|
return sock._sslobj._sslobj
|
||||||
|
else:
|
||||||
|
return sock._sslobj
|
||||||
|
sslpsk.sslpsk._sslobj = _sslobj
|
||||||
|
|
||||||
|
|
||||||
|
debug = False
|
||||||
|
host = '10.1.0.133'
|
||||||
|
port = 443;
|
||||||
|
uri = "wss://"+host+":"+str(port)+ "/homeconnect"
|
||||||
|
psk = b'\x0e\xc8\x1f\xd8\xc6\x49\xfa\xd8\xbc\xe7\xfd\x34\x33\x54\x13\xd4\x73\xf9\x2e\x01\xfc\xd8\x26\x80\x49\x89\x4c\x19\xd7\x2e\xcd\xcb';
|
||||||
|
session_id = None
|
||||||
|
msg_id = None
|
||||||
|
|
||||||
|
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
tcp_sock.connect((host,port))
|
||||||
|
ssl_sock = sslpsk.wrap_socket(
|
||||||
|
tcp_sock,
|
||||||
|
ssl_version = ssl.PROTOCOL_TLSv1_2,
|
||||||
|
ciphers = 'ECDHE-PSK-CHACHA20-POLY1305',
|
||||||
|
psk = psk, #(psk, b'py-hca'),
|
||||||
|
)
|
||||||
|
|
||||||
|
ws = None
|
||||||
|
def tx(msg):
|
||||||
|
buf = json.dumps(msg, separators=(',', ':') )
|
||||||
|
# swap " for '
|
||||||
|
buf = re.sub("'", '"', buf)
|
||||||
|
print("TX:", buf)
|
||||||
|
ws.send(buf)
|
||||||
|
|
||||||
|
def send_initial_messages():
|
||||||
|
global session_id, msg_id
|
||||||
|
# subscribe to stuff
|
||||||
|
tx({
|
||||||
|
"sID": session_id,
|
||||||
|
"msgID": msg_id,
|
||||||
|
"resource": "/ci/services",
|
||||||
|
"version": 1,
|
||||||
|
"action": "GET",
|
||||||
|
})
|
||||||
|
|
||||||
|
msg_id += 1
|
||||||
|
|
||||||
|
tx({
|
||||||
|
"sID": session_id,
|
||||||
|
"msgID": msg_id,
|
||||||
|
"resource": "/iz/info",
|
||||||
|
"version": 1,
|
||||||
|
"action": "GET",
|
||||||
|
})
|
||||||
|
|
||||||
|
msg_id += 1
|
||||||
|
|
||||||
|
tx({
|
||||||
|
"sID": session_id,
|
||||||
|
"msgID": msg_id,
|
||||||
|
"resource": "/ei/deviceReady",
|
||||||
|
"version": 2,
|
||||||
|
"action": "NOTIFY",
|
||||||
|
})
|
||||||
|
|
||||||
|
msg_id += 1
|
||||||
|
|
||||||
|
def handle_message(buf):
|
||||||
|
global session_id, msg_id
|
||||||
|
msg = json.loads(buf)
|
||||||
|
print("RX:", msg)
|
||||||
|
|
||||||
|
# first message from the device establishes the session etc
|
||||||
|
# {'sID': 926468163, 'msgID': 3785595876, 'resource': '/ei/initialValues', 'version': 2, 'action': 'POST', 'data': [{'edMsgID': 2569124008}]}
|
||||||
|
if session_id == None:
|
||||||
|
session_id = msg["sID"]
|
||||||
|
msg_id = msg["data"][0]["edMsgID"]
|
||||||
|
|
||||||
|
# reply with a bogus message
|
||||||
|
tx({
|
||||||
|
'sID': session_id,
|
||||||
|
'msgID': msg["msgID"],
|
||||||
|
'resource': msg["resource"],
|
||||||
|
'version': msg["version"],
|
||||||
|
'action': 'RESPONSE',
|
||||||
|
'data': [{
|
||||||
|
"deviceType": "Application",
|
||||||
|
"deviceName": "py-hca",
|
||||||
|
"deviceID": "1234",
|
||||||
|
#"deviceName": "Pixel",
|
||||||
|
#"deviceID": "d304ee06571f0d09",
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
|
send_initial_messages()
|
||||||
|
|
||||||
|
# do other stuff?
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
websocket.enableTrace(True)
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
ws.connect(uri,
|
||||||
|
socket=ssl_sock,
|
||||||
|
origin = "", #"https://" + host,
|
||||||
|
)
|
||||||
|
# on_message = handle_message,
|
||||||
|
# on_error = exit,
|
||||||
|
#)
|
||||||
|
|
||||||
|
#ws.run_forever()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buf = ws.recv()
|
||||||
|
if buf is None or buf == "":
|
||||||
|
continue
|
||||||
|
handle_message(buf)
|
||||||
Reference in New Issue
Block a user