initial version of a very hacky websocket tool

This commit is contained in:
Trammell Hudson
2022-01-30 19:01:31 +01:00
commit afe099672c
3 changed files with 272 additions and 0 deletions

57
README.md Normal file
View 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
View 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
View 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)