Linkmail16/HappyMod-Reverse-Engineering

GitHub: Linkmail16/HappyMod-Reverse-Engineering

Stars: 3 | Forks: 0

# ApkOmega / HappyMod API — Reverse Engineering Documentation La documentacion la obtuve realizando ingenieria inversa al apk de HappyMod ## Tabla de contenidos 1. [Generacion del UID](#1-generacion-del-uid) 2. [Generacion del Stamp](#2-generacion-del-stamp) 3. [Decodificacion de la respuesta](#3-decodificacion-de-la-respuesta) 4. [Una implementacion mia en Python](#4-implementacion-completa-en-python) 5. [Endpoints basicos de la app](#5-endpoints-documentados-de-la-aplicacion) - 5.1 [Sincronizacion de tiempo del servidor](#51-sincronizacion-de-tiempo-del-servidor) - 5.2 [Busqueda de aplicaciones](#52-busqueda-de-aplicaciones) - 5.3 [Generacion del Hash de descarga](#53-generacion-del-hash-de-descarga) - 5.4 [Descarga del APK](#54-descarga-de-apk) - 5.5 [Detalle de aplicacion](#55-detalle-de-aplicacion) ## 1. Generacion del UID ### Algoritmo uid = MD5( prefijo + identificador ) #### Prefijo (siempre igual para el mismo dispositivo) prefijo = "35" prefijo += str(len(Build.BOARD) % 10) prefijo += str(len(Build.BRAND) % 10) prefijo += str(len(Build.DEVICE) % 10) prefijo += str(len(Build.DISPLAY) % 10) prefijo += str(len(Build.HOST) % 10) prefijo += str(len(Build.ID) % 10) prefijo += str(len(Build.MANUFACTURER) % 10) prefijo += str(len(Build.MODEL) % 10) prefijo += str(len(Build.PRODUCT) % 10) prefijo += str(len(Build.TAGS) % 10) prefijo += str(len(Build.TYPE) % 10) prefijo += str(len(Build.USER) % 10) #### Identificador (por orden de prioridad) 1. Google Advertising ID (GAID) -- preferido en dispositivos modernos si GAID es null o contiene "00000000" -- siguiente 2. Build.SERIAL + ANDROID_ID -- Android < 8.0 (SDK < 26) si vacio > siguiente 3. UUID aleatorio persistido en: Android >= 13 → SharedPreferences Android 6-12 → Downloads/HappyMod/device_id.txt (si hay permisos) o sharedPreferences si no hay permisos Android >= 10 → MediaStore (device_id.png en Downloads/HappyMod/) #### Implementacion Python import hashlib def generate_uid(board, brand, device, display, host, build_id, manufacturer, model, product, tags, type_, user, identifier: str) -> str: prefix = "35" for field in [board, brand, device, display, host, build_id, manufacturer, model, product, tags, type_, user]: prefix += str(len(field) % 10) return hashlib.md5((prefix + identifier).encode()).hexdigest() ## 2. Generacion del Stamp ### El `time_str` // C0060b.m256b() time_str = (System.currentTimeMillis() / 1000) - offset_servidor El `offset_servidor` es la diferencia entre el reloj local y el timestamp devuelto por `server_time.php`, entonces, `time_str` es el timestamp unix actual en segundos sincronizado con el servidor ### Logica nativa (`libCSTAMP.so`) Funcion: `Java_com_happymod_apk_utils_NativeHelper_getStamp`: // argumentos: a3 = time_str, a4 = uid v11 = Jstring2CStr(a1, a4); // uid v10 = Jstring2CStr(a1, a3); // time_str ptr = malloc(len(v11) + len(v10) + len(KEY) + 1); strcpy(ptr, v11); // uid strcat(ptr, v10); // + time_str strcat(ptr, KEY); // + "this_is_happymod" MD5Init(); MD5Update(ctx, ptr, len(ptr)); MD5Final(digest, ctx); // formatea los 16 bytes como hex lowercase for i in 0..15: sprintf(result, "%s%02x", result, digest[i]) return result; key extraida del segmento `.rodata` de `libCSTAMP.so` en offset `0x989`: "this_is_happymod" ### Quedaria asi stamp = MD5( uid + time_str + "this_is_happymod" ) #### Aca entonces en Python import hashlib, time, requests def get_server_offset(uid: str) -> int: r = requests.post( "https://app.apkomega.com/202010/api/server_time.php", data={"version": "3.2.6", "uid": uid, "country": "CO"} ) body = decode_response(r.text) data = json.loads(body) if data.get("status") == 1: return int(time.time()) - data["timestamp"] return 0 def get_stamp(uid: str, offset: int = 0) -> str: time_str = str(int(time.time()) - offset) key = "this_is_happymod" raw = uid + time_str + key return hashlib.md5(raw.encode()).hexdigest() ## 3. Decodificacion de la respuesta La respuesta http no es json directo, pasa por tres capas en orden: respuesta http raw | v [1] sustitucion posicional (vigenere numerico inverso) | v [2] base64 decode (alphabet estandar A-Za-z0-9+/) | v [3] gzip decompress (si magic bytes == 0x1f 0x8b) | v json valido ### 1 — Sustitucion posicional Para cada caracter en posicion `i`: - Si es alfanumerico `[0-9 A-Z a-z]`: - `shifted = ASCII(char) - (i % 10)` - Wrapping entre los tres rangos `[48-57]`, `[65-90]`, `[97-122]` def vigenere_decode(raw: str) -> str: result = [] for i, ch in enumerate(raw): code = ord(ch) if (48 <= code <= 57) or (65 <= code <= 90) or (97 <= code <= 122): shifted = code - (i % 10) if shifted < 48: code = 122 - (48 - shifted) + 1 elif code < 65 or shifted >= 65: if code < 97 or shifted >= 97: code = shifted else: code = (90 - (97 - shifted)) + 1 else: code = (57 - (65 - shifted)) + 1 result.append(chr(code)) return ''.join(result) ### 2 — Base64 decode Alphabet estandar (`f20716a` / `f20717b`, flags=0): A-Z a-z 0-9 + / import base64 def b64_decode(s: str) -> bytes: return base64.b64decode(s + '==') # padding por si falta ### 3 — Gzip decompress (`m22190e`) El metodo detecta automaticamente el magic header `0x1f 0x8b`: import gzip def maybe_gunzip(data: bytes) -> bytes: if data[:2] == b'\x1f\x8b': return gzip.decompress(data) return data ### Funcion completa def decode_response(raw: str) -> str: step1 = vigenere_decode(raw) step2 = b64_decode(step1) step3 = maybe_gunzip(step2) return step3.decode('utf-8') ## 4. Mi implementacion en Python import hashlib, base64, gzip, time, json import requests # los datos que requiere el endpoint UID = "68920e5674b1d3ec969e4637d31e0345" VERSION = "3.2.6" LANG = "es" BASE = "https://app.apkomega.com" # decodificamos def vigenere_decode(raw: str) -> str: result = [] for i, ch in enumerate(raw): code = ord(ch) if (48 <= code <= 57) or (65 <= code <= 90) or (97 <= code <= 122): shifted = code - (i % 10) if shifted < 48: code = 122 - (48 - shifted) + 1 elif code < 65 or shifted >= 65: code = shifted if (code >= 97 and shifted >= 97) else (90 - (97 - shifted)) + 1 else: code = (57 - (65 - shifted)) + 1 result.append(chr(code)) return ''.join(result) def decode_response(raw: str) -> str: step1 = vigenere_decode(raw) step2 = base64.b64decode(step1 + '==') if step2[:2] == b'\x1f\x8b': step2 = gzip.decompress(step2) return step2.decode('utf-8') # stamp def get_server_offset(uid: str) -> int: try: r = requests.post( BASE + "/202010/api/server_time.php", data={"version": VERSION, "uid": uid, "country": "CO"} ) data = json.loads(decode_response(r.text)) if data.get("status") == 1: return int(time.time()) - data["timestamp"] except Exception as e: print("Error obteniendo server time:", e) return 0 def get_stamp(uid: str, offset: int = 0) -> str: time_str = str(int(time.time()) - offset) return hashlib.md5((uid + time_str + "this_is_happymod").encode()).hexdigest() # busqueda def search(keyword: str, page: int = 1, is_new_user: bool = True) -> dict: offset = get_server_offset(UID) stamp = get_stamp(UID, offset) payload = { "version": VERSION, "uid": UID, "stamp": stamp, "page": str(page), "keywords": keyword, "lang": LANG, "is_new_user": "1" if is_new_user else "2", "is_input": "2", "input_word": keyword[:3], } r = requests.post(BASE + "/202010/api/search_list.php", data=payload) return json.loads(decode_response(r.text)) # ejemplo if __name__ == "__main__": result = search("whatsapp") for app in result.get("list", []): print(app.get("title"), "-", app.get("url_id")) ## 5. Endpoints documentados de la aplicacion Todos los endpoints que usan `stamp` requieren que este sea generado en el momento de la solicitud, la respuesta de todos los endpoints de `apkomega.com` usa el mismo esquema de cifrado descrito en la seccion 4 ### 5.1 Sincronizacion de tiempo del servidor **URL:** POST https://app.apkomega.com/202010/api/server_time.php **Payload:** | Campo | Descripcion | Ejemplo | |-----------|----------------------|---------| | `version` | Version del APK | `3.2.6` | | `uid` | UID del dispositivo | `68920e5674b1d3ec969e4637d31e0345` | | `country` | Codigo de pais ISO | `CO` | **Respuesta decodificada** { "status": 1, "timestamp": 1716000000 } | Campo | Descripcion | |-------------|--------------------------------------------------| | `status` | `1` = exito, `-20` = error de fecha | | `timestamp` | Timestamp Unix actual del servidor (segundos) | El cliente calcula `offset = tiempo_local - timestamp_servidor` y lo resta a cada `time_str` para sincronizar el stamp con el servidor Ojo: El resultado se cachea en memoria (`_server_time_offset`) para no repetir la llamada en cada stamp ### 5.2 Busqueda de aplicaciones **URL:** POST https://app.apkomega.com/202010/api/search_list.php **Payload:** | Campo | Tipo | Descripcion | Ejemplo | |--------------|-------|-------------------------------------------------------|-------------| | `version` | str | Version del APK | `3.2.6` | | `uid` | str | UID del dispositivo | `68920e...` | | `stamp` | str | Token MD5 (ver seccion 3) | `aded81...` | | `page` | int | Numero de pagina (empieza en 1) | `1` | | `keywords` | str | Termino de busqueda | `whatsapp` | | `lang` | str | Idioma ISO 639-1 | `es` | | `is_new_user`| int | `1` si el APK se instalo hoy, `2` si no | `1` | | `is_input` | int | `2` si el usuario escribio el termino, `1` si no | `2` | | `input_word` | str | Primeros 3 chars escritos antes de buscar | `wha` | **Respuesta decodificada:** { "status": 1, "has_next_page": 1, "list": [ { "mod_info": "Mod descripcion", "title": "WhatsApp", "icon": "https://...", "url_id": "com.whatsapp", "star": "4.5", "size": "50M", "author": "WhatsApp Inc.", "update_flag_image": "", "has_faq": "0", "is_ad": 0, "data_type": 0 } ] } | Campo | Descripcion | |------------------|----------------------------------------------------------| | `status` | `1` = hay resultados, `-20` = error de sesion | | `has_next_page` | `1` si hay mas paginas disponibles | | `url_id` | Package name del APK — se usa en otras llamadas | | `data_type` | `1` = tiene lista de mods, `0` = APK simple | | `is_ad` | `1` = es un resultado patrocinado | | `update_flag_image` | URL de imagen de badge (ej. "Updated"), vacio si no | ### 5.3 Generacion del hash de descarga El `hash` es un hash es un token de autorizacion derivado del package name del mod, construido cortando y reordenando 4 fragmentos de 4 caracteres del md5 **Codigo java original:** String strM23357b = C8854j.m23357b(this.f2163b + "android_require_apk"); String str27 = strM23357b.substring(10, 14) + strM23357b.substring(25, 29) + strM23357b.substring(18, 22) + strM23357b.substring(5, 9); Donde `this.f2163b` es el `url_id` del mod y `C8854j.m23357b()` es md5 **Entonces:** full_md5 = MD5( url_id + "android_require_apk" ) hash = full_md5[10:14] + full_md5[25:29] + full_md5[18:22] + full_md5[5:9] **Ya en python:** import hashlib def generate_hash(url_id: str) -> str: full_md5 = hashlib.md5((url_id + "android_require_apk").encode()).hexdigest() return full_md5[10:14] + full_md5[25:29] + full_md5[18:22] + full_md5[5:9] **Verificacion:** url_id = "com.mod.tiktok-videos-shop-livemod-apk-43-9-16" full_md5 = MD5("com.mod.tiktok-videos-shop-livemod-apk-43-9-16android_require_apk") = "...2496...81fc...2664...746c..." (posiciones 5,10,18,25) hash = "24962664746c81fc" 16 chars hex ### 5.4 Descarga de APK **URL:** POST https://d.apkomega.com/202101/api/get_apk_download_v2.php Usa subdominio `d.` (en lugar de `app.`) y ruta `202101` (en lugar de `202010`). **Payload:** | Campo | Tipo | Descripcion | Ejemplo / Valor fijo | |------------|-------|----------------------------------------------------|------------------------------| | `version` | str | Version del APK | `3.2.6` | | `uid` | str | UID del dispositivo | `68920e...` | | `stamp` | str | Token MD5 (ver seccion 3) | generado en el momento | | `country` | str | Codigo de pais ISO | `US` | | `lang` | str | Idioma ISO 639-1 | `es` | | `hash` | str | Token de autorizacion de 16 chars (ver seccion 6.3)| `24962664746c81fc` | | `url_id` | str | Package name del mod (no de la app base) | `com.mod.tiktok-...` | | `refer` | str | Titulo del mod concatenado con `\|1` | `TikTok Mod 43.9.16\|1` | | `aid` | str | ID fijo extraido de `libCSTAMP.so` offset `0x940` | `98pyooirb6mad326` | | `get_hpt` | int | Flag desconocido, siempre `0` | `0` | | `channel` | str | Canal de distribucion | `happymod` | | `username` | str | Usuario logueado, vacio si no hay sesion | `""` | **Sobre `aid`:** Es el string `STRAID` extraido del segmento `.rodata` de `libCSTAMP.so`, generado por `NativeHelper.getAid()` **Sobre `url_id`:** Debe ser el `url_id` del mod especifico (Ejemplo: `com.mod.tiktok-videos-shop-livemod-apk-43-9-16`), no el package name de la app base (`com.zhiliaoapp.musically`), el servidor devuelve `status: -10` si se usa el package base **Respuesta decodificada:** { "status": 1, "url_id": "com.mod.tiktok-videos-shop-livemod-apk-43-9-16", "apk_path": "http://s4-hot-2-c.happymodio.com/downloadfile/mod//=", "static_path": "http://s4-hot-2-c.happymodio.com/download_file/mod/.swf", "stamp": "bbddd820f5af2ebbd76c9d822e9a02cf", "path": "=", "cache_time": 59, "full_size": "481246481", "what_level": "lv5", "verify": "f1b5c559cc1acfe67f6a37dac0ffba7a", "no_cdn": 0, "is_boundle": 0, "is_force": 0, "is_vip": 0, "vip_award_time": 0 } | Campo | Descripcion | |---------------|--------------------------------------------------------------------| | `status` | `1` = exito, `-10` = hash invalido o url_id incorrecto | | `apk_path` | URL de descarga con path codificado internamente — no usar directamente | | `static_path` | **URL directa de descarga del APK** — esta es la que se usa | | `stamp` | MD5 del servidor para validar la descarga | | `full_size` | Tamano del APK en bytes | | `what_level` | Nivel de CDN asignado (`lv5` = CDN caliente) | | `verify` | MD5 del archivo APK para verificar integridad post-descarga | | `is_vip` | `1` si el mod requiere cuenta VIP | ### 5.5 Detalle de aplicacion **URL:** GET https://app.happymodapp.com/clist/{version},{lang},{country},{page},{url_id},{sort},{page},{template} Usa un dominio diferente (`happymodapp.com`) y no requiere `uid` ni `stamp`. La respuesta tampoco usa el esquema de cifrado de las otras apis **Parametros de ruta:** | Parametro | Descripcion | Ejemplo / Default | |------------|------------------------------------------|--------------------------------| | `version` | Version del APK | `3.2.6` | | `lang` | Idioma ISO 639-1 | `es` | | `country` | Codigo de pais ISO | `US` | | `page` | Numero de pagina (aparece dos veces) | `1` | | `url_id` | Package name de la app | `com.whatsapp` | | `sort` | Criterio de ordenamiento de mods | `rating` | | `template` | Template HTML del servidor | `pdt_mod_list_v3.html` | **Ejemplo de url construida:** GET https://app.happymodapp.com/clist/3.2.6,es,US,1,com.whatsapp,rating,1,pdt_mod_list_v3.html **Respuesta:** html o json crudo, no cifrado, me dio pereza documentarlo y ya ## 6. Analice: | Clase Java (nombres de jadx) | Nombre original | Funcion | |-------------------------|--------------------------|---------------------------------------------| | `p410y5.C9106c` | `SearchManager.java` | Construye y ejecuta la peticion de busqueda | | `p007a5.C0060b` | `TimestampManager.java` | Genera `time_str` sincronizado al servidor | | `p376v6.C8861q` | `Util.java` | Utilidades: uid, version, stamp, pais | | `p212h7.C7774c` | `DeviceIdUtil.java` | Genera el uid del dispositivo | | `p320q7.C8474a` | `HappyKobe24.java` | Decodificacion capa 1 (Vigenere) | | `p320q7.C8475b` | `MyKobe.java` | Decodificacion capa 2/3 (Base64 + GZIP) | | `p309p7.C8416q` | `IcOld.java` | Constantes de endpoints de la API | | `libCSTAMP.so` | Nativa (C/C++) | Genera el stamp con MD5 | ### Strings clave extraidas de `libCSTAMP.so` | Offset | Valor | Uso | |---------|----------------------|------------------------------| | `0x989` | `this_is_happymod` | Salt del stamp (KEY) | | `0x940` | `98pyooirb6mad326` | STRAID — usado en `getAid()` |