Hagb/decryptBooxUpdateUpx

Tab10CPro Strings

junrenshi opened this issue · 18 comments

I install GetBooxUpxKeys in a Boox Tab 10C Pro, and get outputs:

"MODEL" = "Tab10CPro",
"STRING_SETTINGS" = "D999393AEAA81119AC0F8C8C1EA11089",
"STRING_UPGRADE" = "CD7894A509490BAF2BAA129686A083E4"

fingerprint: Onyx/Tab10CPro/Tab10CPro:11/2023-10-30_17-05_3.5_edb1c449c/4119:user/release-keys

Using the strings to decrypt update.upx results in error:

python3 DeBooxUpx.py Tab10CPro
Traceback (most recent call last):
File "/Users/shi/Downloads/decryptBooxUpdateUpx/DeBooxUpx.py", line 337, in
decrypter = DeBooxUpx(**boox_strings[device_name])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/shi/Downloads/decryptBooxUpdateUpx/DeBooxUpx.py", line 289, in init
self.decryptStr(tmpKey, STRING_SETTINGS))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/shi/Downloads/decryptBooxUpdateUpx/DeBooxUpx.py", line 296, in decryptStr
return cipher.decrypt(b64decode(string)).decode().strip()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-1: invalid continuation byte

It seems GetBooxUpxKeys cannot get correct strings for this new device.

You have (presumably) the Base64 decoded string.
The current setup uses Base64 strings

Post libota_jni.so and I'll check without Java.

Yes, they do look like hexadecimal numbers.

libota_ini.so is here:
libota_jni.tgz

I get the same results as you.
(The libjni_ota.so does not have those values as either binary or string directly in it. It's generated.)
But that is 32 hex digits, which is 16 bytes or 128 bits.

The strings we've been using have been 44 characters which can be evenly base64 converted to 33 bytes or 264 bits.
(Note that none of the strings end in '=', which means it's an even 4 char to 3 byte conversion.)

This new libota_jni.so has a third JNI function, Java_com_onyx_android_onyxotaservice_RsaUtil_nativeDecryptFile
So maybe things have changed radically. @Hagb

Hagb commented

@junrenshi Could you please upload your /system/app/OnyxOtaService/OnyxOtaService.apk? It might be needed to decompile this apk to try to find how it works.

The file is now moved to /sys/priv-app/OnyxOtaService/.

Here is the file:
OnyxOtaService.apk.tgz

Would you have a link for that update.upx?
I can't seem to find it.

Works nicely for me. :)

Poke3:/data/local/assets # unzip -l update.zip
Archive:  update.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      155  2009-01-01 00:00   payload_properties.txt
        0  2009-01-01 00:00   apex_info.pb
      678  2009-01-01 00:00   care_map.pb
      615  2009-01-01 00:00   META-INF/com/android/metadata
     1030  2009-01-01 00:00   META-INF/com/android/metadata.pb
     1513  2009-01-01 00:00   META-INF/com/android/otacert
1556748068  2009-01-01 00:00   payload.bin
---------                     -------
1556752059                     7 files

Great! You did that. Can I do that as well?

That was my proof of concept, extracting without Python or Java.
I don't pretend to understand any of this encryption junk.

You can mod the stock DeBooxUpd to only work for the Tab10CPro.
@Hagb will have to figure out what to do going forward.

Just insert these two lines in DeBooxUpd, line 287, right after self.path: str =

        self.key = b'\xd9\x99\x39\x3a\xea\xa8\x11\x19\xac\x0f\x8c\x8c\x1e\xa1\x10\x89'
        self.iv = b'\xcd\x78\x94\xa5\x09\x49\x0b\xaf\x2b\xaa\x12\x96\x86\xa0\x83\xe4'

Great! It also works for me. Thanks a lot!

Hey guys,

Recently got my Note air 3C, after looking at both libota_jni.so and OnyxOtaService.apk, Boox seems to have changed how the decryption of the Upx files work.

Previously there was a function which returns the AES secret key (Java_com_onyx_android_onyxotaservice_RsaUtil_nativeGetSecretKey) and AES IV (Java_com_onyx_android_onyxotaservice_RsaUtil_nativeGetIvParameter), these were encrypted using DES (With the model name as the key, lol) and encoded using Base64.

Now for my new device with fingerprint: "Onyx/NoteAir3C/NoteAir3C:11/2023-10-17_09-55_3.5_a37b266eb/2482:user/test-keys", this is no longer the case.

Now there are two functions exported from the libota_jni.so dynamic library that return the direct AES secret key and AES IV.
These functions have mangled C++ names but are getKeyString and getInitVectorString

This means that the fancy DES decryption is no longer needed but rather the parameters can be feed straight into AES to perform the decryption.

Here is a modified version of the script to extract the new private key/IV
@Hagb Feel free to use this code


from ExAndroidNativeEmu.androidemu.const import emu_const
from ExAndroidNativeEmu.androidemu.emulator import Emulator
from ExAndroidNativeEmu.androidemu.internal import elf_reader
from ExAndroidNativeEmu.androidemu.java.classes.types import *
from ExAndroidNativeEmu.androidemu.utils.memory_helpers import read_utf8


AES_SECRET_KEY_FUNCTION = 'getKeyString'
AES_IV_FUNCTION = 'getInitVectorString'


def call_function(so_path, function_name, *args):
    # Initialize emulator
    reader = elf_reader.ELFReader(so_path)
    if reader.is_elf32():
        emulator = Emulator(
            vfs_root="ExAndroidNativeEmu/vfs",
            arch=emu_const.ARCH_ARM32,
            config_path="ExAndroidNativeEmu/emu_cfg/default.json"
        )
    else:
        emulator = Emulator(
            vfs_root="ExAndroidNativeEmu/vfs",
            arch=emu_const.ARCH_ARM64,
            config_path="ExAndroidNativeEmu/emu_cfg/default.json"
        )

    module = emulator.load_library(so_path)
    function_symbol = [module.find_symbol(export) for export in module.symbols if function_name in export]
    result = emulator.call_native(function_symbol[-1], *args)
    return read_utf8(emulator.mu, result)


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("python %s /path/to/libota_jni.so" % __file__)
        exit(0)

    secret_key = call_function(sys.argv[1], AES_SECRET_KEY_FUNCTION)
    iv = call_function(sys.argv[1], AES_IV_FUNCTION)

    print(f"AES Secret key: {secret_key}")
    print(f"AES Initialization vector: {iv}")

Below I have attached the corresponding binaries and screenshots of the new apk and library functions.

image
image
image

Binaries.zip

For now we can just backtrack to the strings:

    'Tab10CPro': {
        'MODEL': 'Tab10CPro',
        'STRING_SETTINGS': 'on8l4AwDwIgVNi6AgrAqLzkc94XMx9O37CCELz23Fm+J',
        'STRING_UPGRADE': 'pQIr4QYOsvxLqxXik9cIIzca+QzC2pa9U/hY8p/EAkur'
    }

Update: continue in #70 for Note Air 3C, instead of polluting this issue.

from ExAndroidNativeEmu.androidemu.const import emu_const
from ExAndroidNativeEmu.androidemu.emulator import Emulator
from ExAndroidNativeEmu.androidemu.internal import elf_reader
from ExAndroidNativeEmu.androidemu.java.classes.types import *
from ExAndroidNativeEmu.androidemu.utils.memory_helpers import read_utf8


AES_SECRET_KEY_FUNCTION = 'getKeyString'
AES_IV_FUNCTION = 'getInitVectorString'


def call_function(so_path, function_name, *args):
    # Initialize emulator
    reader = elf_reader.ELFReader(so_path)
    if reader.is_elf32():
        emulator = Emulator(
            vfs_root="ExAndroidNativeEmu/vfs",
            arch=emu_const.ARCH_ARM32,
            config_path="ExAndroidNativeEmu/emu_cfg/default.json"
        )
    else:
        emulator = Emulator(
            vfs_root="ExAndroidNativeEmu/vfs",
            arch=emu_const.ARCH_ARM64,
            config_path="ExAndroidNativeEmu/emu_cfg/default.json"
        )

    module = emulator.load_library(so_path)
    function_symbol = [module.find_symbol(export) for export in module.symbols if function_name in export]
    result = emulator.call_native(function_symbol[-1], *args)
    return read_utf8(emulator.mu, result)


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("python %s /path/to/libota_jni.so" % __file__)
        exit(0)

    secret_key = call_function(sys.argv[1], AES_SECRET_KEY_FUNCTION)
    iv = call_function(sys.argv[1], AES_IV_FUNCTION)

    print(f"AES Secret key: {secret_key}")
    print(f"AES Initialization vector: {iv}")

This works perfectly on Note Air 3C, and the key I got is

AES Secret key: 4DEAED7F843CDCEDB384E6FC468C540E
AES Initialization vector: 964E8856F922178810AEC06E0D363512

I modified the sample code to

#!/usr/bin/env python3

from DeBooxUpx import DeBooxUpx

MODEL = "NovaPro" 
STRING_SETTINGS = "j857wYAQcWZgvIEQ/tcQqzxreUJgFHwJl6D2TN9BuSkQ" 
STRING_UPGRADE = "+soGw/YVdGIRJiAs5SMmv1ihW37H1Fa9+/1w2Vgt14Ag" 
STRING_LOCAL = "lpsj9NJ8Kzv8jHb+OO8A5lxC+9Zhl243bFmDZYaF" 

import sys

# Run with
# $ ./decrypt.py INPUT OUTPUT

updateUpxPath = sys.argv[1]
decryptedPath = sys.argv[2]

decrypter = DeBooxUpx(MODEL, STRING_SETTINGS, STRING_UPGRADE, STRING_LOCAL)
decrypter.key = bytes.fromhex("4DEAED7F843CDCEDB384E6FC468C540E")
decrypter.iv = bytes.fromhex("964E8856F922178810AEC06E0D363512")
print('When updating, the device decrypt update package into', decrypter.path)
decrypter.deUpx(updateUpxPath, decryptedPath)

and it decrypts the UPX file successfully.

Hagb commented

Thank @RenateUSB and @hexomethyl ! I'm thinking that we could store these strings in boox_strings dict of DeBooxUpx like the older ones, and determine whether it is the AES keys or the DES-encrypted AES keys based on the length of the string?

@Hagb I think that there are a number of ways to do this:

  1. Use 32 character hex strings for everything. The old strings can be decrypted into new source
  2. Use 44 character base64 string for everything. The new strings can be encrypted into new source
  3. Use a mixture using the same slot names
  4. Use a mixture using new slot names (key, iv) for new style strings

I think that (1) is the best solution. It's less text. It's the way forward.
In the short term I think that (2) is fine for now.
See #62 for four new model strings of which two are "back encrypted".

Hagb commented

@RenateUSB Thank you! The solution (1) and the code looks good to me.
Could you please submit a PR? Btw, Steam in deUpxSteam is a typo I made, which should be Stream. You could fix it together.

Close this issue by d48b8cb