From 47e8c5a3a4b4d7d7228204dd7aa73bf4dbab66dd Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 17:51:46 +0100
Subject: [PATCH 1/7] Remove existing data on re-run and fix issue on latest
Anker app version (3.21.1)
---
SolixBLE/device.py | 73 ++++++++++++++++++++++++++++++++--------
SolixBLE/prime_device.py | 71 +++++++++++++++++++++++++++++++-------
scripts/patch.sh | 19 ++++++-----
3 files changed, 127 insertions(+), 36 deletions(-)
diff --git a/SolixBLE/device.py b/SolixBLE/device.py
index 226afc3..6b92b52 100644
--- a/SolixBLE/device.py
+++ b/SolixBLE/device.py
@@ -377,7 +377,7 @@ def _split_packet(self, packet: bytes) -> tuple[bytes, bytes, bytes]:
return packet_pattern, packet_cmd, packet_payload
- def _parse_payload(self, payload: bytearray | bytes) -> dict[str, bytes]:
+ def _parse_payload(self, payload: bytearray | bytes, quiet: bool = True) -> dict[str, bytes]:
"""
Parse payload bytes into parameters.
@@ -389,7 +389,9 @@ def _parse_payload(self, payload: bytearray | bytes) -> dict[str, bytes]:
but the successfully parsed parameters (if any) will be returned.
:param payload: Payload to parse into parameters.
+ :param quiet: If parsing errors should raise an exception.
:returns: Dictionary mapping parameter ids (a1, a2, ...) to data.
+ :raises IndexError: If error and quiet is false.
"""
def _verbose_pop(data: bytearray, length: int, name: str) -> bytes:
@@ -454,12 +456,14 @@ def _verbose_pop(data: bytearray, length: int, name: str) -> bytes:
)
parsed_data[param_id] = param_data
- except IndexError:
+ except IndexError as e:
_LOGGER.exception(
f"Unexpected end of packet! Data may be missing or invalid!"
f" Extracted so far: '{self._parameters_to_str(parsed_data)}'."
f" Payload: '{payload.hex()}'"
)
+ if not quiet:
+ raise e
return parsed_data
@@ -518,15 +522,22 @@ def _encrypt_payload(self, payload: bytes) -> bytes:
)
return cipher.encrypt(padded_data)
- async def _process_telemetry_packet(
- self, payload: bytes, cmd: bytes = None
- ) -> None:
- """Process a telemetry packet from the device.
+ def _reassemble_payload(
+ self, payload: bytes, cmd: bytes | None = None,
+ ) -> bytes | None:
+ """Reassemble and return the raw payload.
- This performs the default processing of telemetry packets in which
- telemetry payloads are spread across multiple packets. This is
- overridden for devices which do not use multi-packet payloads for
- telemetry.
+ This reassembles the payload if it is spread across multiple packets
+ and will return the completed payload or original payload in the case
+ of packets which contain the full payload. Note that this does not
+ perform any additional processing (e.g encryption, parsing into parameters).
+
+ If the payload is not ready (i.e not the last packet in a series of packets
+ where the payload is spread across multiple packets) then None is returned.
+
+ :param payload: The payload including layout information.
+ :param cmd: The command value of the packet.
+ :returns: Reassembled payload bytes or None if payload is not complete.
"""
# First byte encodes fragment info (high nibble = index, low = total)
@@ -551,7 +562,7 @@ async def _process_telemetry_packet(
# Wait until all fragments have arrived
if len(self._fragment_buffers[cmd_key]) < fragment_total:
_LOGGER.debug("Waiting for remaining fragments...")
- return
+ return None
# Reassemble in order
payload = b"".join(
@@ -564,11 +575,40 @@ async def _process_telemetry_packet(
else:
# Strip fragment info
+ _LOGGER.debug("Not multi-packet payload!")
payload = payload[1:]
+ return payload
+
+ async def _process_telemetry_packet(
+ self, payload: bytes, cmd: bytes = None
+ ) -> None:
+ """Process a telemetry packet from the device.
+
+ This performs the default processing of telemetry packets in which
+ telemetry payloads are spread across multiple packets. This is
+ overridden for devices which do not use multi-packet payloads for
+ telemetry.
+ """
+
+ # Sometimes the payload does not include fragment information so if
+ # we decrypt it and it works and it looks like a normal packet then we
+ # skip reassembly
+ try:
+ decrypted_payload = self._decrypt_payload(payload)
+ parameters = self._parse_payload(decrypted_payload, quiet=False)
+ return await self._process_telemetry(parameters)
+
+ except Exception as e:
+ _LOGGER.debug(f"Failed to parse payload as single-fragment payload, trying multi-fragment parsing next: {e}")
+
+ payload = self._reassemble_payload(payload, cmd)
+ if payload is None:
+ return None
+
decrypted_payload = self._decrypt_payload(payload)
- _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
parameters = self._parse_payload(decrypted_payload)
+ _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
return await self._process_telemetry(parameters)
async def _process_telemetry(self, parameters: dict[str, bytes]) -> None:
@@ -624,8 +664,13 @@ async def _process_notification(
pattern, cmd, payload = self._split_packet(data)
_LOGGER.debug(f"Pattern: {pattern.hex()}")
_LOGGER.debug(f"CMD: {cmd.hex()}")
- _LOGGER.debug(f"Payload: {payload.hex()}")
- _LOGGER.debug(f"Payload length: {len(payload)}")
+ _LOGGER.debug(f"Payload (raw): {payload.hex()}")
+ _LOGGER.debug(f"Payload length (raw): {len(payload)}")
+
+
+
+ _LOGGER.debug(f"Payload (raw): {payload.hex()}")
+ _LOGGER.debug(f"Payload length (raw): {len(payload)}")
# If the packet has a future registered then we just trigger that
# future instead of processing it here
diff --git a/SolixBLE/prime_device.py b/SolixBLE/prime_device.py
index 7064ebe..778b83c 100644
--- a/SolixBLE/prime_device.py
+++ b/SolixBLE/prime_device.py
@@ -487,19 +487,64 @@ async def _process_negotiation(self, cmd: bytes, payload: bytes) -> None:
# Packet processing #
#####################
- async def _process_telemetry_packet(
- self, payload: bytes, cmd: bytes = None
- ) -> None:
- """
- Process a telemetry packet from an Anker Prime device.
-
- Anker Prime devices pack all telemetry data into a single packet
- requiring no special logic to handle.
- """
- decrypted_payload = self._decrypt_payload(payload)
- _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
- parameters = self._parse_payload(decrypted_payload)
- return await self._process_telemetry(parameters)
+ # async def _process_telemetry_packet(
+ # self, payload: bytes, cmd: bytes | None = None,
+ # ) -> None:
+ # """Process a telemetry packet from the device.
+
+ # This performs the default processing of telemetry packets in which
+ # telemetry payloads are spread across multiple packets. This is
+ # overridden for devices which do not use multi-packet payloads for
+ # telemetry.
+ # """
+
+ # payload = self._decrypt_payload(payload)
+ # _LOGGER.debug(f"Decrypted payload: {payload.hex()}")
+
+ # # First byte encodes fragment info (high nibble = index, low = total)
+ # fragment_index = (payload[0] >> 4) & 0x0F
+ # fragment_total = payload[0] & 0x0F
+
+ # # Multi-part message
+ # if fragment_total > 1:
+ # fragment_data = payload[1:]
+ # cmd_key = bytes(cmd)
+ # _LOGGER.debug(
+ # f"Fragment {fragment_index}/{fragment_total} for cmd {cmd.hex()}, {len(fragment_data)} bytes"
+ # )
+
+ # # Store fragment
+ # if cmd_key not in self._fragment_buffers or fragment_index == 1:
+ # self._fragment_buffers[cmd_key] = {}
+ # self._fragment_totals[cmd_key] = fragment_total
+
+ # self._fragment_buffers[cmd_key][fragment_index] = fragment_data
+
+ # # Wait until all fragments have arrived
+ # if len(self._fragment_buffers[cmd_key]) < fragment_total:
+ # _LOGGER.debug("Waiting for remaining fragments...")
+ # return
+
+ # # Reassemble in order
+ # payload = b"".join(
+ # self._fragment_buffers[cmd_key][i]
+ # for i in sorted(self._fragment_buffers[cmd_key])
+ # )
+ # del self._fragment_buffers[cmd_key]
+ # del self._fragment_totals[cmd_key]
+ # _LOGGER.debug(f"Reassembled payload: {len(payload)} bytes")
+
+ # else:
+ # # Strip fragment info
+ # _LOGGER.debug("Not multi-packet payload!")
+
+ # if payload[0] not in bytearray.fromhex(
+ # "a0a1a2a3a4a5a6a7a8a9"
+ # ):
+ # payload = payload[1:]
+
+ # parameters = self._parse_payload(payload)
+ # return await self._process_telemetry(parameters)
async def _send_command(self, cmd: bytes, payload: bytes) -> None:
"""Send a command to the device.
diff --git a/scripts/patch.sh b/scripts/patch.sh
index 9432d83..a34dabe 100755
--- a/scripts/patch.sh
+++ b/scripts/patch.sh
@@ -3,11 +3,11 @@
# Script name : patch.sh
# Description : Script for injecting Frida gadget into Anker Android app
# Author : Harvey Lelliott (@flip-dots)
-# Date : 23/03/26
+# Date : 16/07/26
# Usage : ./patch.sh [ADB device (e.g 192.168.1.1:1234)]
#
# License : MIT
-# Revision : 1.0.0
+# Revision : 1.1.0
#
set -euxo pipefail
@@ -16,7 +16,7 @@ set -euxo pipefail
# Constants #
#############
-FRIDA_VERSION="17.8.2"
+FRIDA_VERSION="17.8.3"
UBER_APK_SIGNER_VERSION="1.3.0"
@@ -53,7 +53,7 @@ WORKING_FOLDER=$(pwd)
# Folder to put all data in
DATA_FOLDER="${WORKING_FOLDER}/data"
-mkdir -p $DATA_FOLDER
+rm -rf $DATA_FOLDER && mkdir -p $DATA_FOLDER
# Folder to put source APK inside
APK_SOURCE_FOLDER="${DATA_FOLDER}/source_apks"
@@ -73,7 +73,7 @@ mkdir -p $APK_SIGNED_FOLDER
# Folder to put tools in
TOOLS_FOLDER="${WORKING_FOLDER}/tools"
-mkdir -p $TOOLS_FOLDER
+rm -rf $TOOLS_FOLDER && mkdir -p $TOOLS_FOLDER
# Folder to put Frida gadgets in
FRIDA_FOLDER="${TOOLS_FOLDER}/frida"
@@ -124,10 +124,11 @@ cp "${WORKING_FOLDER}/frida_config.json" "${APK_DECOMPILED_FOLDER}/lib/armeabi-v
cp "${WORKING_FOLDER}/frida_config.json" "${APK_DECOMPILED_FOLDER}/lib/arm64-v8a/libnative-utils.config.so"
# Add Frida gadget to app startup
-sed -i '' '34c \
-const-string v0, "native-utils"\
-invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\
-' "${APK_DECOMPILED_FOLDER}/smali/s/h/e/l/l/A.smali"
+sed -i '' 's/\.locals 5/\.locals 6/g' "${APK_DECOMPILED_FOLDER}/smali/s/h/e/l/l/S.smali"
+sed -i '' '2386c \
+const-string v5, "native-utils"\
+invoke-static {v5}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\
+' "${APK_DECOMPILED_FOLDER}/smali/s/h/e/l/l/S.smali"
# Modify manifest to enable Frida loading
sed -i '' '// s/android:allowBackup="false"/android:allowBackup="true"/' "${APK_DECOMPILED_FOLDER}/AndroidManifest.xml"
From 0bbbe26127306cc483b0e23067720354ec584250 Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 17:57:14 +0100
Subject: [PATCH 2/7] Bypass improved anti-tamper detection
---
scripts/frida.js | 103 ++++++++++++++++++++++++++++++++++++---------
scripts/frida_2.js | 60 ++++++++++++++++++++++++--
2 files changed, 139 insertions(+), 24 deletions(-)
diff --git a/scripts/frida.js b/scripts/frida.js
index c8bac48..56387f2 100644
--- a/scripts/frida.js
+++ b/scripts/frida.js
@@ -2,33 +2,15 @@
* Script name : frida.js
* Description : Frida script for preventing exit and extracting shared preferences
* Author : Harvey Lelliott (@flip-dots)
- * Date : 23/03/26
+ * Date : 16/07/26
*
* License : MIT
- * Revision : 1.0.0
+ * Revision : 1.1.0
*
* This script is based off https://codeshare.frida.re/@ninjadiary/frinja---sharedpreferences/
* but with additional code to prevent the anti-tamper mechanism from killing the app.
*/
-// Prevent anti-tamper mechanism from fully killing app
-setImmediate(function() {
- Java.perform(function() {
- var System = Java.use('java.lang.System');
- var Process = Java.use('android.os.Process');
-
- // Stop System.exit()
- System.exit.implementation = function(code) {
- console.log("[!] Intercepted exit (" + code + ")");
- };
-
- // Stop Process.killProcess()
- Process.killProcess.implementation = function(pid) {
- console.log("[!] Intercepted killProcess for PID: " + pid);
- };
- });
-});
-
// Log any reads/writes to shared preferences
// From: https://codeshare.frida.re/@ninjadiary/frinja---sharedpreferences/
setImmediate(function() {
@@ -181,3 +163,84 @@ Java.perform(function () {
};
});
});
+
+
+/**********************
+ * *
+ * Anti-Tamper Bypass *
+ * *
+ **********************/
+
+
+// Prevent anti-tamper mechanism from killing the app in Java
+setImmediate(function() {
+ Java.perform(function() {
+ console.log("Tampering with anti-tamper protection using Java API...");
+
+ var System = Java.use('java.lang.System');
+ var Process = Java.use('android.os.Process');
+
+ // Stop System.exit()
+ System.exit.implementation = function(code) {
+ console.log("[!] Intercepted exit (" + code + ")");
+ };
+
+ // Stop Process.killProcess()
+ Process.killProcess.implementation = function(pid) {
+ console.log("[!] Intercepted killProcess for PID: " + pid);
+ };
+ });
+});
+
+
+// Prevent anti-tamper mechanism from killing the app in C
+function blockNativeExit() {
+ console.log("Tampering with anti-tamper protection using C API...");
+
+ let usleepAddr = null;
+ try {
+ usleepAddr = Module.findGlobalExportByName("usleep") || Module.findExportByName("usleep");
+ } catch (e) {
+ console.log("[-] Could not find usleep globally: " + e.message);
+ }
+
+ let nativeUsleep = null;
+ if (usleepAddr) {
+ nativeUsleep = new NativeFunction(usleepAddr, 'int', ['uint32']);
+ }
+
+ // Standard libc functions used by packers to terminate processes
+ const exitSymbols = ["exit", "_exit", "abort", "kill", "pthread_exit"];
+
+ exitSymbols.forEach(function (symbol) {
+ let address = null;
+ try {
+ // Find global exports using Frida 17 APIs
+ address = Module.findGlobalExportByName(symbol) || Module.findExportByName(symbol);
+ } catch (e) {
+ // Ignore search errors
+ }
+
+ if (address) {
+ try {
+ Interceptor.attach(address, {
+ onEnter: function (args) {
+ console.log("[!] Native anti-debug triggered " + symbol + "(). Freezing thread to prevent crash...");
+
+ // Infinite loop using the native OS sleep to halt the calling thread
+ while (true) {
+ if (nativeUsleep) {
+ nativeUsleep(100000); // Sleep for 100ms per iteration safely
+ }
+ }
+ }
+ });
+ console.log("[+] Hooked native function: " + symbol);
+ } catch (e) {
+ console.log("[-] Failed to hook: " + symbol + " (" + e.message + ")");
+ }
+ }
+ });
+}
+
+blockNativeExit();
diff --git a/scripts/frida_2.js b/scripts/frida_2.js
index ee27c25..7b7b47e 100644
--- a/scripts/frida_2.js
+++ b/scripts/frida_2.js
@@ -1,10 +1,10 @@
/*
- * Script name : frida.js
+ * Script name : frida_2.js
* Description : Frida script for preventing exit, extracting shared preferences, and tracing crypto/BLE with timestamps
* Author : Harvey Lelliott (@flip-dots) / Enhanced
- * Date : 23/03/26
+ * Date : 16/07/26
* * License : MIT
- * Revision : 1.1.1
+ * Revision : 1.2.1
*/
// --- UTILITY FUNCTIONS ---
@@ -293,4 +293,56 @@ setImmediate(function() {
log("[-] Error hooking Bluetooth: " + e);
}
});
-});
\ No newline at end of file
+});
+
+// Prevent anti-tamper mechanism from killing the app in C
+function blockNativeExit() {
+ console.log("Tampering with anti-tamper protection using C API...");
+
+ let usleepAddr = null;
+ try {
+ usleepAddr = Module.findGlobalExportByName("usleep") || Module.findExportByName("usleep");
+ } catch (e) {
+ console.log("[-] Could not find usleep globally: " + e.message);
+ }
+
+ let nativeUsleep = null;
+ if (usleepAddr) {
+ nativeUsleep = new NativeFunction(usleepAddr, 'int', ['uint32']);
+ }
+
+ // Standard libc functions used by packers to terminate processes
+ const exitSymbols = ["exit", "_exit", "abort", "kill", "pthread_exit"];
+
+ exitSymbols.forEach(function (symbol) {
+ let address = null;
+ try {
+ // Find global exports using Frida 17 APIs
+ address = Module.findGlobalExportByName(symbol) || Module.findExportByName(symbol);
+ } catch (e) {
+ // Ignore search errors
+ }
+
+ if (address) {
+ try {
+ Interceptor.attach(address, {
+ onEnter: function (args) {
+ console.log("[!] Native anti-debug triggered " + symbol + "(). Freezing thread to prevent crash...");
+
+ // Infinite loop using the native OS sleep to halt the calling thread
+ while (true) {
+ if (nativeUsleep) {
+ nativeUsleep(100000); // Sleep for 100ms per iteration safely
+ }
+ }
+ }
+ });
+ console.log("[+] Hooked native function: " + symbol);
+ } catch (e) {
+ console.log("[-] Failed to hook: " + symbol + " (" + e.message + ")");
+ }
+ }
+ });
+}
+
+blockNativeExit();
From 757d4bd0ebc5c95febb6fe1f8d80a01c120f88fd Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 17:58:45 +0100
Subject: [PATCH 3/7] Do not restart app after 5 seconds (only needed for old
frida scripts)
---
scripts/run.sh | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/scripts/run.sh b/scripts/run.sh
index 3dcd6f5..35746cb 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -55,12 +55,14 @@ echo "Starting execution..."
# Port forward the port used by Frida gadget
adb -s $DEVICE forward tcp:49152 tcp:49152
-# In 5 seconds open the app (non-blocking)
-(sleep 5 && adb -s $DEVICE shell monkey -p com.anker.charging 1 && echo "Restarted app!") &
+# Open app
+adb -s $DEVICE shell monkey -p com.anker.charging 1
-# Open the app and execute the Frida script
-adb -s $DEVICE shell monkey -p com.anker.charging 1 \
- && frida -H 127.0.0.1:49152 -n Gadget -l frida.js \
+# Wait 1 second
+sleep 1
+
+# Execute Frida script
+frida -H 127.0.0.1:49152 -n Gadget -l frida.js \
2>&1 | tee -a "${LOG_FOLDER}/${TIMESTAMP}.log"
echo "Done!"
From c1ba6f8762c04f07fa5c27f087222461bd19ac1c Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:00:53 +0100
Subject: [PATCH 4/7] Add alternate version of patch script for patching the
Flutter APK directly
---
scripts/patch_2.sh | 182 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 182 insertions(+)
create mode 100755 scripts/patch_2.sh
diff --git a/scripts/patch_2.sh b/scripts/patch_2.sh
new file mode 100755
index 0000000..b1d873e
--- /dev/null
+++ b/scripts/patch_2.sh
@@ -0,0 +1,182 @@
+#!/bin/bash
+#
+# Script name : patch_2.sh
+# Description : Script for injecting Frida gadget into Anker Android app
+# Author : Harvey Lelliott (@flip-dots)
+# Date : 16/07/26
+# Usage : ./patch_2.sh [ADB device (e.g 192.168.1.1:1234)]
+#
+# License : MIT
+# Revision : 1.0.0
+#
+set -euxo pipefail
+
+
+#############
+# Constants #
+#############
+
+FRIDA_VERSION="17.8.3"
+
+UBER_APK_SIGNER_VERSION="1.3.0"
+
+
+##################################
+# Environment and arg validation #
+##################################
+echo "Checking environment/tools..."
+
+# Validate arguments
+if [ $# -lt 1 ]; then
+ echo "Missing device argument (e.g 192.168.1.1:1234)!"
+ exit 2
+fi
+
+# The device for use with ADB
+DEVICE=$1
+
+# Check tools
+command -v wget >/dev/null 2>&1 || { echo >&2 "wget is required!"; exit 1; }
+command -v adb >/dev/null 2>&1 || { echo >&2 "adb is required!"; exit 1; }
+command -v apktool >/dev/null 2>&1 || { echo >&2 "apktool is required!"; exit 1; }
+command -v java >/dev/null 2>&1 || { echo >&2 "java is required!"; exit 1; }
+command -v frida >/dev/null 2>&1 || { echo >&2 "frida is required!"; exit 1; }
+
+
+################
+# Folder setup #
+################
+echo "Setting up folders..."
+
+# The current folder
+WORKING_FOLDER=$(pwd)
+
+# Folder to put all data in
+DATA_FOLDER="${WORKING_FOLDER}/data"
+mkdir -p $DATA_FOLDER
+
+# Folder to put source APK inside
+APK_SOURCE_FOLDER="${DATA_FOLDER}/source_apks"
+mkdir -p $APK_SOURCE_FOLDER
+
+# Folder to put the main decompiled APK inside
+APK_MAIN_DECOMPILED_FOLDER="${DATA_FOLDER}/base_apk_decompiled"
+mkdir -p $APK_MAIN_DECOMPILED_FOLDER
+
+# Folder to put the flutter decompiled APK inside
+APK_FLUTTER_DECOMPILED_FOLDER="${DATA_FOLDER}/flutter_apk_decompiled"
+mkdir -p $APK_FLUTTER_DECOMPILED_FOLDER
+
+# Folder to put patched APKs inside
+APK_PATCHED_FOLDER="${DATA_FOLDER}/patched"
+mkdir -p $APK_PATCHED_FOLDER
+
+# Folder to put signed APKs inside
+APK_SIGNED_FOLDER="${DATA_FOLDER}/signed"
+mkdir -p $APK_SIGNED_FOLDER
+
+# Folder to put tools in
+TOOLS_FOLDER="${WORKING_FOLDER}/tools"
+mkdir -p $TOOLS_FOLDER
+
+# Folder to put Frida gadgets in
+FRIDA_FOLDER="${TOOLS_FOLDER}/frida"
+mkdir -p $FRIDA_FOLDER
+
+
+
+#######################
+# Download deps/tools #
+#######################
+echo "Downloading dependencies/tools"
+
+cd $FRIDA_FOLDER && wget "https://github.com/zer0def/undetected-frida/releases/download/${FRIDA_VERSION}/undetected-frida-gadget-${FRIDA_VERSION}-android-arm.so.xz"
+cd $FRIDA_FOLDER && wget "https://github.com/zer0def/undetected-frida/releases/download/${FRIDA_VERSION}/undetected-frida-gadget-${FRIDA_VERSION}-android-arm64.so.xz"
+cd $FRIDA_FOLDER && unxz -f "undetected-frida-gadget-${FRIDA_VERSION}-android-arm.so.xz"
+cd $FRIDA_FOLDER && unxz -f "undetected-frida-gadget-${FRIDA_VERSION}-android-arm64.so.xz"
+
+cd $TOOLS_FOLDER && wget "https://github.com/patrickfav/uber-apk-signer/releases/download/v${UBER_APK_SIGNER_VERSION}/uber-apk-signer-${UBER_APK_SIGNER_VERSION}.jar"
+
+
+########################
+# Pull and extract APK #
+########################
+
+# Pull Anker APKs from Phone
+echo "Extracting original APKs from phone..."
+adb -s $DEVICE shell pm path com.anker.charging | sed 's/^package://' | tr -d '\r' | xargs -I {} adb -s $DEVICE pull {} $APK_SOURCE_FOLDER
+
+
+################
+# Inject Frida #
+################
+echo "Injecting Frida gadget into flutter APK..."
+
+# Extract Flutter library from Flutter APK
+mkdir -p "${APK_FLUTTER_DECOMPILED_FOLDER}/lib/arm64-v8a"
+unzip -p "${APK_SOURCE_FOLDER}/split_config.arm64_v8a.apk" "lib/arm64-v8a/libflutter.so" > "${APK_FLUTTER_DECOMPILED_FOLDER}/lib/arm64-v8a/libflutter.so"
+
+# Use LIEF to inject the Frida gadget as a dependency of Flutter
+python3 -c "
+import lief
+import sys
+
+target_path = '${APK_FLUTTER_DECOMPILED_FOLDER}/lib/arm64-v8a/libflutter.so'
+
+try:
+ # Parse the ELF binary
+ binary = lief.parse(target_path)
+
+ # Inject the dependency
+ binary.add_library('libnative-utils.so')
+ binary.write(target_path)
+ print(f'Successfully injected Frida into Flutter')
+except Exception as e:
+ print(f'Failed to inject Frida into Flutter: {e}')
+ sys.exit(1)
+"
+
+# Copy Frida gadget binaries into Flutter APK
+cp "${FRIDA_FOLDER}/undetected-frida-gadget-${FRIDA_VERSION}-android-arm64.so" "${APK_FLUTTER_DECOMPILED_FOLDER}/lib/arm64-v8a/libnative-utils.so"
+cp "${WORKING_FOLDER}/frida_config.json" "${APK_FLUTTER_DECOMPILED_FOLDER}/lib/arm64-v8a/libnative-utils.config.so"
+
+
+# Decompile the main APK
+echo "Decompiling main APK..."
+apktool d "${APK_SOURCE_FOLDER}/base.apk" -o $APK_MAIN_DECOMPILED_FOLDER -f
+
+# Modify manifest of main APK to enable Frida loading
+sed -i '' '// s/android:allowBackup="false"/android:allowBackup="true"/' "${APK_MAIN_DECOMPILED_FOLDER}/AndroidManifest.xml"
+sed -i '' '// s/android:extractNativeLibs="false"/android:extractNativeLibs="true" android:debuggable="true"/' "${APK_MAIN_DECOMPILED_FOLDER}/AndroidManifest.xml"
+
+
+
+##########################
+# Re-package and re-sign #
+##########################
+echo "Re-packaging and re-signing APKs..."
+
+# Re-package APKs
+apktool b -o "${APK_PATCHED_FOLDER}/base.apk" ${APK_MAIN_DECOMPILED_FOLDER}
+
+cp "${APK_SOURCE_FOLDER}/split_config.arm64_v8a.apk" "${APK_PATCHED_FOLDER}/split_config.arm64_v8a.apk"
+cd "${APK_FLUTTER_DECOMPILED_FOLDER}" && zip -ur "${APK_PATCHED_FOLDER}/split_config.arm64_v8a.apk" lib/
+
+# Re-sign APKs
+java -jar "${TOOLS_FOLDER}/uber-apk-signer-${UBER_APK_SIGNER_VERSION}.jar" -o $APK_SIGNED_FOLDER --allowResign --apks \
+ "${APK_PATCHED_FOLDER}/base.apk" \
+ "${APK_PATCHED_FOLDER}/split_config.arm64_v8a.apk" \
+ "${APK_SOURCE_FOLDER}/split_config.en.apk" \
+ "${APK_SOURCE_FOLDER}/split_config.xxhdpi.apk" \
+ "${APK_SOURCE_FOLDER}/split_flutter_assets_pack.apk"
+
+# Uninstall existing Anker app
+adb -s $DEVICE uninstall com.anker.charging
+
+# Install patched APKs
+adb -s $DEVICE install-multiple \
+ "${APK_SIGNED_FOLDER}/base-aligned-debugSigned.apk" \
+ "${APK_SIGNED_FOLDER}/split_config.arm64_v8a-aligned-debugSigned.apk" \
+ "${APK_SIGNED_FOLDER}/split_config.en-aligned-debugSigned.apk" \
+ "${APK_SIGNED_FOLDER}/split_config.xxhdpi-aligned-debugSigned.apk" \
+ "${APK_SIGNED_FOLDER}/split_flutter_assets_pack-aligned-debugSigned.apk"
From 8aec4b90dbfc77b8c09e59bb12746a44b08fdbaa Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:05:50 +0100
Subject: [PATCH 5/7] Improve warnings/notes
---
docs/source/app_decoding.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/source/app_decoding.rst b/docs/source/app_decoding.rst
index a5261d2..39db2d2 100644
--- a/docs/source/app_decoding.rst
+++ b/docs/source/app_decoding.rst
@@ -100,7 +100,7 @@ To manually capture the logs follow these steps:
6. Perform the actions required to get the packets needed for analysis (add device, send commands, etc).
.. note::
- This process is incredibly time sensitive, it may take multiple attempts to get it right.
+ This process is incredibly time sensitive, it may take multiple attempts (10+) to get it right.
run.sh
@@ -111,7 +111,7 @@ and in addition writes the logs to a file as well as the console. This is
the recommend approach.
.. note::
- The app must not be running when you start the script.
+ The app must **not** be running when you start the script.
This script performs the following actions:
From 12b0a9b84fd5cb30bb1b6235a80bcb378fcf08ec Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:20:54 +0100
Subject: [PATCH 6/7] Kill app if running on start
---
scripts/run.sh | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/scripts/run.sh b/scripts/run.sh
index 35746cb..90f86e0 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -3,11 +3,11 @@
# Script name : run.sh
# Description : Script for executing patched Anker app and starting Frida.
# Author : Harvey Lelliott (@flip-dots)
-# Date : 23/03/26
+# Date : 16/07/26
# Usage : ./run.sh [ADB device (e.g 192.168.1.1:1234)]
#
# License : MIT
-# Revision : 1.0.0
+# Revision : 1.1.0
#
set -euxo pipefail
@@ -55,10 +55,12 @@ echo "Starting execution..."
# Port forward the port used by Frida gadget
adb -s $DEVICE forward tcp:49152 tcp:49152
+# Kill app
+adb -s "$DEVICE" shell am force-stop com.anker.charging
+sleep 1
+
# Open app
adb -s $DEVICE shell monkey -p com.anker.charging 1
-
-# Wait 1 second
sleep 1
# Execute Frida script
From 81270f4d2727f1a3c01d70f81ced37d9f71ac72d Mon Sep 17 00:00:00 2001
From: Harvey <42912136+flip-dots@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:29:50 +0100
Subject: [PATCH 7/7] Revert changes inherited from experimental branch
---
SolixBLE/device.py | 73 ++++++++--------------------------------
SolixBLE/prime_device.py | 71 +++++++-------------------------------
2 files changed, 27 insertions(+), 117 deletions(-)
diff --git a/SolixBLE/device.py b/SolixBLE/device.py
index 6b92b52..226afc3 100644
--- a/SolixBLE/device.py
+++ b/SolixBLE/device.py
@@ -377,7 +377,7 @@ def _split_packet(self, packet: bytes) -> tuple[bytes, bytes, bytes]:
return packet_pattern, packet_cmd, packet_payload
- def _parse_payload(self, payload: bytearray | bytes, quiet: bool = True) -> dict[str, bytes]:
+ def _parse_payload(self, payload: bytearray | bytes) -> dict[str, bytes]:
"""
Parse payload bytes into parameters.
@@ -389,9 +389,7 @@ def _parse_payload(self, payload: bytearray | bytes, quiet: bool = True) -> dict
but the successfully parsed parameters (if any) will be returned.
:param payload: Payload to parse into parameters.
- :param quiet: If parsing errors should raise an exception.
:returns: Dictionary mapping parameter ids (a1, a2, ...) to data.
- :raises IndexError: If error and quiet is false.
"""
def _verbose_pop(data: bytearray, length: int, name: str) -> bytes:
@@ -456,14 +454,12 @@ def _verbose_pop(data: bytearray, length: int, name: str) -> bytes:
)
parsed_data[param_id] = param_data
- except IndexError as e:
+ except IndexError:
_LOGGER.exception(
f"Unexpected end of packet! Data may be missing or invalid!"
f" Extracted so far: '{self._parameters_to_str(parsed_data)}'."
f" Payload: '{payload.hex()}'"
)
- if not quiet:
- raise e
return parsed_data
@@ -522,22 +518,15 @@ def _encrypt_payload(self, payload: bytes) -> bytes:
)
return cipher.encrypt(padded_data)
- def _reassemble_payload(
- self, payload: bytes, cmd: bytes | None = None,
- ) -> bytes | None:
- """Reassemble and return the raw payload.
-
- This reassembles the payload if it is spread across multiple packets
- and will return the completed payload or original payload in the case
- of packets which contain the full payload. Note that this does not
- perform any additional processing (e.g encryption, parsing into parameters).
-
- If the payload is not ready (i.e not the last packet in a series of packets
- where the payload is spread across multiple packets) then None is returned.
+ async def _process_telemetry_packet(
+ self, payload: bytes, cmd: bytes = None
+ ) -> None:
+ """Process a telemetry packet from the device.
- :param payload: The payload including layout information.
- :param cmd: The command value of the packet.
- :returns: Reassembled payload bytes or None if payload is not complete.
+ This performs the default processing of telemetry packets in which
+ telemetry payloads are spread across multiple packets. This is
+ overridden for devices which do not use multi-packet payloads for
+ telemetry.
"""
# First byte encodes fragment info (high nibble = index, low = total)
@@ -562,7 +551,7 @@ def _reassemble_payload(
# Wait until all fragments have arrived
if len(self._fragment_buffers[cmd_key]) < fragment_total:
_LOGGER.debug("Waiting for remaining fragments...")
- return None
+ return
# Reassemble in order
payload = b"".join(
@@ -575,40 +564,11 @@ def _reassemble_payload(
else:
# Strip fragment info
- _LOGGER.debug("Not multi-packet payload!")
payload = payload[1:]
- return payload
-
- async def _process_telemetry_packet(
- self, payload: bytes, cmd: bytes = None
- ) -> None:
- """Process a telemetry packet from the device.
-
- This performs the default processing of telemetry packets in which
- telemetry payloads are spread across multiple packets. This is
- overridden for devices which do not use multi-packet payloads for
- telemetry.
- """
-
- # Sometimes the payload does not include fragment information so if
- # we decrypt it and it works and it looks like a normal packet then we
- # skip reassembly
- try:
- decrypted_payload = self._decrypt_payload(payload)
- parameters = self._parse_payload(decrypted_payload, quiet=False)
- return await self._process_telemetry(parameters)
-
- except Exception as e:
- _LOGGER.debug(f"Failed to parse payload as single-fragment payload, trying multi-fragment parsing next: {e}")
-
- payload = self._reassemble_payload(payload, cmd)
- if payload is None:
- return None
-
decrypted_payload = self._decrypt_payload(payload)
- parameters = self._parse_payload(decrypted_payload)
_LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
+ parameters = self._parse_payload(decrypted_payload)
return await self._process_telemetry(parameters)
async def _process_telemetry(self, parameters: dict[str, bytes]) -> None:
@@ -664,13 +624,8 @@ async def _process_notification(
pattern, cmd, payload = self._split_packet(data)
_LOGGER.debug(f"Pattern: {pattern.hex()}")
_LOGGER.debug(f"CMD: {cmd.hex()}")
- _LOGGER.debug(f"Payload (raw): {payload.hex()}")
- _LOGGER.debug(f"Payload length (raw): {len(payload)}")
-
-
-
- _LOGGER.debug(f"Payload (raw): {payload.hex()}")
- _LOGGER.debug(f"Payload length (raw): {len(payload)}")
+ _LOGGER.debug(f"Payload: {payload.hex()}")
+ _LOGGER.debug(f"Payload length: {len(payload)}")
# If the packet has a future registered then we just trigger that
# future instead of processing it here
diff --git a/SolixBLE/prime_device.py b/SolixBLE/prime_device.py
index 778b83c..7064ebe 100644
--- a/SolixBLE/prime_device.py
+++ b/SolixBLE/prime_device.py
@@ -487,64 +487,19 @@ async def _process_negotiation(self, cmd: bytes, payload: bytes) -> None:
# Packet processing #
#####################
- # async def _process_telemetry_packet(
- # self, payload: bytes, cmd: bytes | None = None,
- # ) -> None:
- # """Process a telemetry packet from the device.
-
- # This performs the default processing of telemetry packets in which
- # telemetry payloads are spread across multiple packets. This is
- # overridden for devices which do not use multi-packet payloads for
- # telemetry.
- # """
-
- # payload = self._decrypt_payload(payload)
- # _LOGGER.debug(f"Decrypted payload: {payload.hex()}")
-
- # # First byte encodes fragment info (high nibble = index, low = total)
- # fragment_index = (payload[0] >> 4) & 0x0F
- # fragment_total = payload[0] & 0x0F
-
- # # Multi-part message
- # if fragment_total > 1:
- # fragment_data = payload[1:]
- # cmd_key = bytes(cmd)
- # _LOGGER.debug(
- # f"Fragment {fragment_index}/{fragment_total} for cmd {cmd.hex()}, {len(fragment_data)} bytes"
- # )
-
- # # Store fragment
- # if cmd_key not in self._fragment_buffers or fragment_index == 1:
- # self._fragment_buffers[cmd_key] = {}
- # self._fragment_totals[cmd_key] = fragment_total
-
- # self._fragment_buffers[cmd_key][fragment_index] = fragment_data
-
- # # Wait until all fragments have arrived
- # if len(self._fragment_buffers[cmd_key]) < fragment_total:
- # _LOGGER.debug("Waiting for remaining fragments...")
- # return
-
- # # Reassemble in order
- # payload = b"".join(
- # self._fragment_buffers[cmd_key][i]
- # for i in sorted(self._fragment_buffers[cmd_key])
- # )
- # del self._fragment_buffers[cmd_key]
- # del self._fragment_totals[cmd_key]
- # _LOGGER.debug(f"Reassembled payload: {len(payload)} bytes")
-
- # else:
- # # Strip fragment info
- # _LOGGER.debug("Not multi-packet payload!")
-
- # if payload[0] not in bytearray.fromhex(
- # "a0a1a2a3a4a5a6a7a8a9"
- # ):
- # payload = payload[1:]
-
- # parameters = self._parse_payload(payload)
- # return await self._process_telemetry(parameters)
+ async def _process_telemetry_packet(
+ self, payload: bytes, cmd: bytes = None
+ ) -> None:
+ """
+ Process a telemetry packet from an Anker Prime device.
+
+ Anker Prime devices pack all telemetry data into a single packet
+ requiring no special logic to handle.
+ """
+ decrypted_payload = self._decrypt_payload(payload)
+ _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
+ parameters = self._parse_payload(decrypted_payload)
+ return await self._process_telemetry(parameters)
async def _send_command(self, cmd: bytes, payload: bytes) -> None:
"""Send a command to the device.