Open zcahana opened 2 years ago
So, I examined the HTTP requests sent by the app (using the interception method suggested here).
It appears that the app now rotates the auth token for (almost) every outgoing request. Occasionally, the same token is reused across several adjacent (in time) requests. Attempting to repeat (with curl) a request with the exact same token previously used for it, fails with 401, so these tokens must have some short-lived expiry mechanism.
The token itself is a 46-digits hexadecimal string such as 0100E9405D782800B6E989591EF07612EF823626290892
, whereas the first 12 digits are constant across all tokens. No clue about the rest of the 34 digits. But note that the "k"
parameter exchanged (several times) in the 3 auth flow requests described above, as well as the final "token"
parameter, are all 34 hexa digits too (each, a different one).
Anyway, I'm completely clueless about how these tokens are generated, but hopefully it can ring someone's bells.
So, I examined the HTTP requests sent by the app (using the interception method suggested here).
It appears that the app now rotates the auth token for (almost) every outgoing request. Occasionally, the same token is reused across several adjacent (in time) requests. Attempting to repeat (with curl) a request with the exact same token previously used for it, fails with 401, so these tokens must have some short-lived expiry mechanism.
The token itself is a 46-digits hexadecimal string such as
0100E9405D782800B6E989591EF07612EF823626290892
, whereas the first 12 digits are constant across all tokens. No clue about the rest of the 34 digits. But note that the"k"
parameter exchanged (several times) in the 3 auth flow requests described above, as well as the final"token"
parameter, are all 34 hexa digits too (each, a different one).Anyway, I'm completely clueless about how these tokens are generated, but hopefully it can ring someone's bells.
I had this token for an example 0100E26EE9D9BC31FA8420ECC71C0BB3A540A3F07B8E0C
and in all my requests the first 14 chars are constant across all my tokens (and not 12 like you).
i think the token is built with timestamp in the hex that is being verifeid againt one of the requests.
i'm also clueless regarding how these tokens are generated :(
Any chance someone from Pal will step in to the rescue? 🙏🏼
Any chance someone from Pal will step in to the rescue? 🙏🏼
I think they change it because of this plugin (and others)
I think indeed the token is timestamp based. I don't have any experience in reverse engineering android apps, but a simple decomplie and search for "token" showed the following function in the app's code:
public static String getToken(Context paramContext, Integer paramInteger) {
long l3;
long l1 = System.currentTimeMillis() / 1000L;
long l2 = Preferences.from(paramContext).getLong("timeStampLong").longValue();
String str1 = Preferences.from(paramContext).getString("userId");
String str2 = Preferences.from(paramContext).getString("sessionToken");
a.a a = a.a;
if (!str1.equals("")) {
l3 = Long.parseLong(str1);
} else {
l3 = 0L;
}
return IntToHexString(FaceDetectNative.getInstance().getFacialLandmarks(hexStringToByteArray(str2), l2 + l1, l3, paramInteger.intValue()));
}
public static Integer getTokenType(Context paramContext) {
return Preferences.from(paramContext).getInt("tokenType");
}
public static String getUserSessionToken(Context paramContext, Integer paramInteger) {
Integer integer = paramInteger;
if (paramInteger.intValue() == -1)
integer = getTokenType(paramContext);
return getToken(paramContext, integer);
}
Also, I examined the HTTP flow as well:
first the app issues a GET request to https://api1.pal-es.com/v1/bt/un/ts
and receives a timestamp in the response.
Then a request to /v1/bt/user/checkupdate?v=4
containing an x-bt-token
header, receiving a "no updates exist" msg
and deviceHash
built from 8 characters.
Then there is a request to https://api1.pal-es.com/v1/bt/user/check-token
, with a x-bt-token
, and it receives a "token valid" msg
and a ts
value
finally there is a request to https://api1.pal-es.com/v1/bt/device/<device_key>/open-gate?outputNum=1
including a x-bt-token
(all tokens in this process are different)
I think indeed the token is timestamp based. I don't have any experience in reverse engineering android apps, but a simple decomplie and search for "token" showed the following function in the app's code:
public static String getToken(Context paramContext, Integer paramInteger) { long l3; long l1 = System.currentTimeMillis() / 1000L; long l2 = Preferences.from(paramContext).getLong("timeStampLong").longValue(); String str1 = Preferences.from(paramContext).getString("userId"); String str2 = Preferences.from(paramContext).getString("sessionToken"); a.a a = a.a; if (!str1.equals("")) { l3 = Long.parseLong(str1); } else { l3 = 0L; } return IntToHexString(FaceDetectNative.getInstance().getFacialLandmarks(hexStringToByteArray(str2), l2 + l1, l3, paramInteger.intValue())); } public static Integer getTokenType(Context paramContext) { return Preferences.from(paramContext).getInt("tokenType"); } public static String getUserSessionToken(Context paramContext, Integer paramInteger) { Integer integer = paramInteger; if (paramInteger.intValue() == -1) integer = getTokenType(paramContext); return getToken(paramContext, integer); }
Also, I examined the HTTP flow as well: first the app issues a GET request to
https://api1.pal-es.com/v1/bt/un/ts
and receives a timestamp in the response. Then a request to/v1/bt/user/checkupdate?v=4
containing anx-bt-token
header, receiving a "no updates exist"msg
anddeviceHash
built from 8 characters. Then there is a request tohttps://api1.pal-es.com/v1/bt/user/check-token
, with ax-bt-token
, and it receives a "token valid"msg
and ats
value finally there is a request tohttps://api1.pal-es.com/v1/bt/device/<device_key>/open-gate?outputNum=1
including ax-bt-token
(all tokens in this process are different)
Great! Can you please send me the full decompiled code? I knew this token includes a timestamp, but didn’t knew what else they added to convert it to hex, from that code I can create a flow to generate the token.
Sure palgate decompiled
@bmyonatan awesome, that's very helpful! I too glimpsed into the decompiled code, and it seems that most is there in order to reconstruct and automate the authentication flow (although they definitely didn't make this an easy task 😄)
A missing piece is the implementation of FaceDetectNative.getFacialLandmarks()
. This method is implemented via a native library (see System.loadLibrary("native-lib")
in com/bluegate/app/utils/FaceDetectNative.java
). Did you happen to see any hint of native-lib
or getFacialLandmarks()
in the app files you decompiled? The generic name suggests that this is something bundled with the app...
Found the native library in the app apk's: libnative-lib.zip
And here's its aarch64 disassembly: https://onlinedisassembler.com/odaweb/NJcFdjfX (see Java_com_bluegate_app_utils_FaceDetectNative_getFacialLandmarks
).
Anyone wants to take a shot at deciphering that? I thought so 😄
One option is to try to understand what's the source of this object file. Another option is to try to load and run that code via some arm emulator. I don't know if that's feasible for this homebridge plugin here, but might be feasible for my use (palgate-cli).
Anyway, quite a lot of headache for one little token 😆
https://stackoverflow.com/questions/71255467/what-is-android-facedetectnative-getfaciallandmarks Looks like we're not alone...
so, is there a fix for this? perhaps the plugin could use the same timestamp based method to cycle the token.
perhaps the plugin could use the same timestamp based method to cycle the token.
There's no easy way to do this because some of the token generation code uses a function from a native library loaded at runtime. At first I thought we can load it too using some arm64 emulator (e.g. qemu-user-aarch64) but the library also seem to have dependencies on android OS itself so it's not sufficient to emulate the architecture. Hopefully someone with some Android emulation experience can take a shot at this.
@nitaybz maybe you can assist?
Following up- I think a good step might be creating a proof-of-concept android app/code that successfully opens a gate with the new token system. after that is done, we could move forward with android emulation, which in theory should be possible because the pi is arm64.
Here is a simple workaround for the problem. Take an old android phone. Make sure it is able to open the gate using the palgate application. Install Macdroid (free app) on the phone. Create a macro that opens the gate with the following parameters:
trigger: network capture define an identifier
Actions Open palgate Wait 1 second User interaction. Presses x,y (209, 500)
You will get a url that once called, opens the gate. This requires an old phone, but don't tell me you don't have one.
Here is the macro that was saved. remove the .txt extantion. Macro might be a little different, as it depends on screen size and so on. Yru.macro.txt
I also managed to bypass the palgate app,
unlike @tzachi-dar, instead of having a physical phone with macro,
I created an internal webserver that uses google-asistant-sdk (there are probably better implementations to it), when you get the endpoint, i make a call to google assistant through the sdk to open gate
.
then i connected it to Telegram/Whatsapp and managed to give access to whoever is needed :)
Sounds great! do you mind sharing the source code?
Actually, it's pretty simple using https://github.com/googlesamples/assistant-sdk-nodejs Follow the setup steps and then wrap it with express
const express = require("express");
const url = require("url");
const GoogleAssistant = require("./googleassistant");
const deviceCredentials = require("./devicecredentials.json");
const app = express();
const port = process.env.PORT || 1234;
const CREDENTIALS = {
client_id: deviceCredentials.client_id,
client_secret: deviceCredentials.client_secret,
refresh_token: deviceCredentials.refresh_token,
type: "authorized_user",
};
const assistant = new GoogleAssistant(CREDENTIALS);
app.get("/", async (req, res) => {
const query = url.parse(req.url, true).query;
const response = await assistant.assist(query.text);
res.send(response.text);
});
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
googleassistant.js
file is from the lib mentioned above
So then just query localhost:1234/?text="open gate"
(notice that for some questions you'd get an empty response, you can follow the issue here https://github.com/googlesamples/assistant-sdk-nodejs/issues/13, i recommend asking obama's height
- always responses with text :) )
FYI this approach uses my PERSONAL google assistant
For the whatsapp bot i used https://github.com/pedroslopez/whatsapp-web.js/
import { Client, LocalAuth } from "whatsapp-web.js";
import * as qrcode from "qrcode-terminal";
import { actions } from "./actions"; // this is an object with all actions
/**
* actions = {
* gate: () => // request to the google api service
* }
*/
const client = new Client({
authStrategy: new LocalAuth({
dataPath: "./data",
}),
puppeteer: {
args: [ // args to run in docker
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
],
},
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
});
client.on("qr", (qr) => {
qrcode.generate(qr, { small: true });
});
client.on("ready", () => {
console.log("Client is ready!");
});
client.on("message", async (message) => {
if (!message.body.startsWith("!")) {
return;
}
const commandName = message.body.replace("!", "").toLowerCase();
console.log(`Received: '${message.body}' from ${message.from}`);
if (!actions[commandName]) {
console.log("no action found");
return;
}
try {
console.log(`running action ${commandName}`);
await actions[commandName](message);
} catch (e) {
console.log(e);
message.reply("error");
} finally {
console.log("done");
}
});
try {
console.log("starting...");
client.initialize().catch((_) => {
console.log("error", _);
});
} catch (e) {
console.log(e);
}
@EladBezalel , I understand that you actually bypassed the PalGate app, but I didn't understand what have you done with the token, are you sending the request directly from your local webserver? (so it seems) But since it's a rolling token and the token is only alive for a single use and only for 1 min expiration I didn't understand where's the token reference in the code you publish above, can you elaborate?
I must say, I found some sort of bypass but that stopped working today so again it's a dead-end for me.
@EladBezalel , I understand that you actually bypassed the PalGate app, but I didn't understand what have you done with the token, are you sending the request directly from your local webserver? (so it seems) But since it's a rolling token and the token is only alive for a single use and only for 1 min expiration I didn't understand where's the token reference in the code you publish above, can you elaborate?
The workaround he's using is by leveraging the Palgate <-> Google Assistant integration (which is a Palgate feature). The webserver is calling Google, which in turn calls the Palgate servers. The webserver is just acting as a Google Assistant client and that's it.
As a personal note, while this works, and is a pretty generic solution to anything that has a Google Assistant integration but not a HA one, it has an additional (noticeable) latency - since you're going through Google's API which also needs to parse the command, forward it to whatever service it needs to go to and only then Palgate starts acting.
I think that understanding this part is our main goal (from @BuSHari 's comment):
long l3;
long l1 = System.currentTimeMillis() / 1000L;
long l2 = Preferences.from(paramContext).getLong("timeStampLong").longValue();
String str1 = Preferences.from(paramContext).getString("userId");
String str2 = Preferences.from(paramContext).getString("sessionToken");
@bengry Oh I see, well it's a nice W/A but doesn't fit our needs, our main goal is to understand how the token generation works :/
Was it possible to find out the process of token formation?
I found the AuthToken and refresh token via a rooted Android device in the following path
cat /data/data/com.bluegate.app/files/PersistedInstallation.******.json
here is the Is the content with redacted information
{
"Fid": "redacted-fid",
"Status": 3,
"AuthToken": "redacted-token",
"RefreshToken": "redacted-refresh-token",
"TokenCreationEpochInSecs": 1684852460,
"ExpiresInSecs": 604800
}
not sure if it can help us or not.
Just moved into a new place with a Palgate system. I was looking for ways to create a garage door style opener for the car and came across this.
Seems like no one has been able to reverse engineer the nativelib yet. For more context there is an x86 and x64 versions of the shared object.
I am working on reverse engineering the binary. I have their AES encryption/decryption function with their AES Key and IV working already. Working on creating their hash/check function next.
I have only spent one sleepless night on reversing it so far so give me a little more time and I will give more updates.
Edit: The x86 version of the library is easier to re-implement from my first glance.
I thin the easiest way is to write a program to work with the standard library. And run it on the raspberry pi board.
It import shared libraries from the Android system, so it's not really the easiest. Personally I think the easiest way would be wrapping the library for Android x86 with an HTTP endpoint.
I am currently have the APK running on Android x86 perfectly fine and debugging it remotely with GDB to double check my work.
@codedninja Amazing work, thank you for your assistance. Please do keep up posted. 🍻
I have had another look at the native library. Most of it is a set of functions to perform encryption and decryption. The hard part here will be figuring out the key which seems to be partly from the l3 (userToken converted from Strong to Long) input to the getFacialLandmarks and some predetermined array of bytes (T_C_KEY), where:
T_C_KEY[6] = 0x34 T_C_KEY[7] = 0xf4 T_C_KEY[8] = 0x47 T_C_KEY[9] = 0xc3 T_C_KEY[10] = 0x8d T_C_KEY[11] = 0xe1
There is another function aes_enc_dec does most of the heavy cryptographic lifting. Given this, it may be possible to reverse engineer given some security expertise, but not sure I can get much further.
Tomorrow I am gonna push the code I have done reimplementing everything. I have completed some parts. Though I was having issues reimplementing something that uses the timestamp from their servers. I have already extracted their key and will be inside of the code I push.
@codedninja This is amazing! Thank you!
Hi, @codedninja. Where can we see your changes?
Hi @codedninja, if it's a matter of putting parts of code together, or debug stuff I can help
Any there any updates? It's been more than a month since @codedninja 's last comment
I'm waiting for news too. That's the last part of the "smart home puzzle" for me :)
Hi, have we got any updates?
Do you have a way to get the token without the extractor maybe by sniffing the requests from network when using the app on mac?
Just putting my post here so some ppl might bug me in the near future to check this out... I would probably try to look into this also try to implement in python (I'm a HomeAssistant user, not Homebridge though), or just wrap it in a java class with changes/mocks and throw it into py4j
This looks like it may be firebase authentication. I recommend installing certs on a mobile device and running the app behind something like Postman proxy to decrypt all the packets. It may be possible to use the firebase npm package to spoof the initial token creation from the values you find.
Assuming it is firebase, it explains why the decompiled cryptology is hard to work through.
I am happy to try and implement it if no one has yet
This won't work, see the readme of that repo, it redirects to my repo for sniffing the token hence it will not work.
I thin the easiest way is to write a program to work with the standard library. And run it on the raspberry pi board.
It's not working now...
note that I am now offering a 900 USD bounty if anyone can solve this. my contact info on my profile page
Recently the authentication flow used by the PalGate app has been changed, and the current (old) auth endpoint used results with the following error returned from the server:
I've tried to reverse engineer the new auth flow by looking at the app logs, but couldn't extract a usable token. Here's how it looks like in the app's logs (some token/IDs removed):
As for the last token received in the last response, I've tried using it as the value of the
x-bt-token
/x-bt-user-token
headers in http requests, but this results with{"errId":7101,"status":"401","msg":"Invalid Token or Key"}
.I'm wondering if anyone was able to figure out how to use the new auth flow?