# TiviMate 5.3.3 — DexProtector Analysis Reference

**Package**: `ar.tvplayer.tv` | **Developer**: Armobsoft FZE | **Protector**: DexProtector by Arxan

## Detection Patterns

### Application Class (Cert Pinning)
- **File**: `ar.tvplayer.tv.ProtectedTvPlayerApplication extends Application`
- `attachBaseContext` calls `b()` then `System.loadLibrary("dexprotector")`
- `b()` computes SHA-256 of signing cert, compares against `cafa2c5e0affcc243b4df23d1a69720a78a92412afc210a051b88ca68fca2c5b`
- SMALI condition: `if-nez v4, :cond_8e` after `String.equals()` — if certs DON'T match, throw RuntimeException

### JNI Bridge Class
- `LibTvPlayerApplication` — ~290 overloaded native methods (all named `a` or single-letter, dispatched by `int i` first parameter)
- Each method takes varying combinations of `(int, byte, short, Object, ...)` — typical DexProtector JNI dispatch pattern

### R.java Obfuscation
- All resource fields renamed to `h` regardless of original name
- Original names preserved only in JADX INFO comments: `/* JADX INFO: renamed from: h_res_0x7f1301d8 */`
- SMALI references use the obfuscated field name `h` with the resource class type

### SMALI Class Renaming
- All app classes renamed to Unicode filenames (e.g., `ːʤﹶᵷ.smali`, `ˌˆ.smali`, `ᵪᵻᵜᵜ.smali`)
- 6,066 files in `smali_classes3/` under DexProtector for TiviMate
- jadx writes them to `defpackage/AbstractCXXXX.java` — these labels are jadx-invented, not actual class names

### String Encryption
- **No readable app-specific strings exist in SMALI**. `const-string` instructions for app text, URLs, API endpoints are all absent.
- DEX string pool is encrypted; decrypted at runtime by `libdexprotector.so`
- The native `s()` method on `ProtectedTvPlayerApplication` is the decryption entry point
- Resource strings (strings.xml) have readable VALUES but obfuscated NAMEs (`h_res_0x7f13XXXX`)

## Cert Check Bypass

### Locate the check
In `ar/tvplayer/tv/ProtectedTvPlayerApplication.smali`:
```smali
.method private b()V
    .registers 9
    .prologue
    const/4 v7, 0x1
    const-string v4, "SHA-256"
    ...get signature, hash it...
    const-string v0, "cafa2c5e0affcc243b4df23d1a69720a78a92412afc210a051b88ca68fca2c5b"
    invoke-virtual {v0, v1}, String;->equals(Ljava/lang/Object;)Z
    move-result v4
    if-nez v4, :cond_8e
    ...throw RuntimeException on mismatch...
    :cond_8e
    return-void
.end method
```

### Patch
Replace entire `b()` method with no-op:
```smali
.method private b()V
    .registers 1
    return-void
.end method
```

## DEX-Level Patching Workflow

Apktool rebuild fails (200+ duplicate layout resources). Use direct DEX manipulation:

```bash
```bash
unzip original.apk classes.dex classes2.dex classes3.dex -d working/
# Disassemble target DEX (usually classes.dex for the Application class)
java -jar baksmali.jar d working/classes.dex -o working/smali/
# Edit SMALI (cert check, feature flags, etc.)
# Reassemble
java -jar smali.jar a -o working/classes.dex working/smali/
```

**Note**: The APK manifest has `android:extractNativeLibs="false"`. When repacking with Python zipfile, native libs lose their page alignment, causing `INSTALL_FAILED_INVALID_APK`. Two fixes:

**Fix A — Patch manifest to set extractNativeLibs=true** (requires binary AXML patching — see below):
Instead of `false`, set it to `true` so Android extracts .so files instead of mmapping them.

**Fix B — Keep extractNativeLibs=false and use zipalign**:
```bash
zipalign -p -f 4 patched-unaligned.apk patched.apk
```

### Inject back with Python zipfile (strip META-INF, keep native libs STORED)
python3 -c "
import zipfile, os, shutil
with zipfile.ZipFile('original.apk', 'r') as zin, \
     zipfile.ZipFile('patched.apk', 'w', zipfile.ZIP_DEFLATED) as zout:
    for item in zin.infolist():
        if item.filename.startswith('META-INF/'): continue
        data = zin.read(item.filename)
        if item.filename == 'classes.dex':
            with open('working/classes.dex', 'rb') as f:
                data = f.read()
        # ANDROID REQUIRES: native .so files and .dex files must be STORED (uncompressed)
        is_native = item.filename.startswith('lib/') and item.filename.endswith('.so')
        is_dex = item.filename.endswith('.dex')
        if is_native or is_dex:
            new_item = zipfile.ZipInfo(item.filename)
            new_item.compress_type = zipfile.ZIP_STORED
            zout.writestr(new_item, data)
        else:
            zout.writestr(item, data)
"
# Sign with SHA-256 (SHA-1 is blocked by modern Android):
jarsigner -keystore debug.keystore -storepass android -keypass android \
  -sigalg SHA256withRSA -digestalg SHA-256 patched.apk debug
# Verify:
jarsigner -verify patched.apk
```

**Critical**: Native libraries (`lib/*.so`) and DEX files must use `ZIP_STORED` (uncompressed) in the APK. If they're compressed (`ZIP_DEFLATED`), Android's `PackageManager` fails with `INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2`. This is an Android platform requirement, not apktool-specific.

## Binary AXML Patching (extractNativeLibs)

TiviMate's manifest has `android:extractNativeLibs="false"`, requiring page-aligned native libs. Since zipalign isn't usually available on dev machines, patch it to `true`:

1. Extract the binary manifest: `unzip tivimate.apk AndroidManifest.xml`

2. Parse the AXML string pool to find the `extractNativeLibs` string index:
   - StringPool chunk starts at offset 8 (after 8-byte XML header)
   - String index for `extractNativeLibs` in TiviMate 5.3.3 = 21 (0x15)
   
3. Find the attribute with `name=21` in the application `<StartTag>`. Boolean typed value (type 0x12) with data=0 = false, data=0xFFFFFFFF = true.

4. Python patch:
   ```python
   import struct
   with open('AndroidManifest.xml', 'rb') as f:
       data = bytearray(f.read())
   # string index 21 = extractNativeLibs
   target = struct.pack('<I', 21)
   for i in range(len(data) - 20):
       if data[i:i+4] == target and data[i+11] == 0x12:  # boolean attr
           struct.pack_into('<I', data, i+12, 0xFFFFFFFF)  # true
           break
   with open('AndroidManifest.xml', 'wb') as f:
       f.write(data)
   ```

5. Inject patched manifest back into APK alongside patched DEX, strip META-INF, re-sign.

This avoids the `INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2` error without needing zipalign.

## Premium System Architecture

| Component | Implementation |
|-----------|---------------|
| **Auth** | Parse SDK (email/password) with `ParseLoginActivity` |
| **Payments** | Google Play Billing 9.1.0 (AIDL: `IInAppBillingService`) |
| **Activation** | Server-side via `tivimate.com` or Companion app |
| **Device limit** | 5 devices per account |
| **Premium models** | Annual subscription (~$29.99/yr) + 7-day trial, OR one-time (~$49.99) |
| **User storage** | Parse backend — premium status as Parse object field |

### Key Resource IDs (premium-related)
These are the `h_res_0x7f13XXXX` IDs mapped to their content:

| ID | String Value |
|----|-------------|
| `0x7f1301d8` | "Premium" |
| `0x7f1301e0` | "All Premium features available" |
| `0x7f1301e5` | "All features are available in Premium version" |
| `0x7f130463` | "Unlock Premium" |
| `0x7f1301ee` | "Subscription" |
| `0x7f1301ec` | "One-time payment %s" |
| `0x7f1301f0` | Premium description with trial |
| `0x7f1302d9` | "Expiration date" |
| `0x7f13032a` | "If you have Premium unlocked, it will be locked" |
| `0x7f1301d9` | "Activate existing device (%d/5)" |
| `0x7f1301da` | "Activate new device" |

## Native Libraries

| Library | Purpose | Size |
|---------|---------|------|
| `libdexprotector.so` | Runtime protection, string decryption | 443KB |
| `libffmpegJNI.so` | FFmpeg media decoder (ExoPlayer extension) | varies |
| `libavutil.so` | FFmpeg utility library | varies |
| `libsqliteJni.so` | Native SQLite bridge (Room) | varies |
| `libdatastore_shared_counter.so` | Jetpack DataStore cross-process counter | varies |

`libdexprotector.so` analysis:
- JNI_OnLoad at offset 0x448, 72 bytes (minimal — just returns JNI_VERSION)
- .init_array section: empty (no static constructors)
- Only 2 dynamic symbols: JNI_OnLoad, JNI_OnLoad
- .text section encrypted (no readable disassembly)
- No readable strings beyond "JNI_OnLoad" and "libdexprotector.so"

## Premium Bypass Strategy

Static-only patching reaches limits with server-side Parse activation. Recommended approach:

1. Patch cert check (done above) → produces installable APK
2. Test if native lib accepts modified DEX
3. If it works but premium still locked → use Frida:
   ```javascript
   // Hook ParseUser boolean fields
   Java.use("com.parse.ParseUser")
     .getBoolean.overload("java.lang.String")
     .implementation = function(key) {
       if (key.contains("premium") || key.contains("subscribed")) return true;
       return this.getBoolean(key);
     };
   
   // Hook SharedPreferences premium keys
   Java.use("android.content.SharedPreferences")
     .getBoolean.overload("java.lang.String", "boolean")
     .implementation = function(key, def) {
       var k = key.toLowerCase();
       if (k.contains("premium") || k.contains("pro") || k.contains("subscription")) return true;
       return this.getBoolean(key, def);
     };
   ```
4. Dump decrypted DEX via Frida `Java.enumerateLoadedClasses()` + class loader dump
5. Repack without DexProtector using dumped classes

## Version Info

- **Analyzed version**: 5.3.3 (versionCode: 1000005332)
- **minSdk**: 23, **targetSdk**: 34, **compileSdk**: 37 (Android 14 preview)
- **Permissions**: INTERNET, NETWORK_STATE, STORAGE, RECORD_AUDIO, BOOT_COMPLETED, SCHEDULE_EXACT_ALARM, SYSTEM_ALERT_WINDOW, BILLING
- **DEX count**: 3 (classes.dex: 39KB, classes2.dex: 548KB, classes3.dex: 5.6MB)
- **Total SMALI files**: 6,659
