La documentacion la obtuve realizando ingenieria inversa al apk de HappyMod
uid = MD5( prefijo + identificador )
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)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/)
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()// C0060b.m256b()
time_str = (System.currentTimeMillis() / 1000) - offset_servidorEl 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
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"
stamp = MD5( uid + time_str + "this_is_happymod" )
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()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
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)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 faltaEl 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 datadef decode_response(raw: str) -> str:
step1 = vigenere_decode(raw)
step2 = b64_decode(step1)
step3 = maybe_gunzip(step2)
return step3.decode('utf-8')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"))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
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
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 |
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
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/<stamp>/<path_encoded>=",
"static_path": "http://s4-hot-2-c.happymodio.com/download_file/mod/<md5>.swf",
"stamp": "bbddd820f5af2ebbd76c9d822e9a02cf",
"path": "<base64_encoded_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 |
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
| 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 |
| Offset | Valor | Uso |
|---|---|---|
0x989 |
this_is_happymod |
Salt del stamp (KEY) |
0x940 |
98pyooirb6mad326 |
STRAID — usado en getAid() |
La documentacion la hice apoyandome de JADX e IDA pro, la app no esta tan ofuscada asi que fue facil reversear todo Lo hice con fines educativos y de investigacion