Linkmail16/HappyMod-Reverse-Engineering

GitHub: Linkmail16/HappyMod-Reverse-Engineering

该项目记录了对 HappyMod 安卓应用进行逆向工程的完整过程,并提供了复现其 API 请求与响应解码的 Python 实现。

Stars: 4 | Forks: 0

# ApkOmega / HappyMod API — 逆向工程文档 本文档是通过对 HappyMod 的 APK 进行逆向工程获取的 ## 目录 1. [UID 生成](#1-generacion-del-uid) 2. [Stamp 生成](#2-generacion-del-stamp) 3. [响应解码](#3-decodificacion-de-la-respuesta) 4. [我的 Python 实现](#4-implementacion-completa-en-python) 5. [应用基础 Endpoints](#5-endpoints-documentados-de-la-aplicacion) - 5.1 [服务器时间同步](#51-sincronizacion-de-tiempo-del-servidor) - 5.2 [应用搜索](#52-busqueda-de-aplicaciones) - 5.3 [下载 Hash 生成](#53-generacion-del-hash-de-descarga) - 5.4 [APK 下载](#54-descarga-de-apk) - 5.5 [应用详情](#55-detalle-de-aplicacion) ## 1. UID 生成 ### 算法 ``` 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/) ``` #### 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. Stamp 生成 ### `time_str` ``` // C0060b.m256b() time_str = (System.currentTimeMillis() / 1000) - offset_servidor ``` `offset_servidor` 是本地时钟与 `server_time.php` 返回的 timestamp 之间的差值,因此,`time_str` 就是与服务器同步的当前 Unix timestamp(以秒为单位) ### 原生逻辑(`libCSTAMP.so`) 函数: `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; ``` 从 `libCSTAMP.so` 的 `.rodata` 段中偏移量 `0x989` 处提取的 key: ``` "this_is_happymod" ``` ### 逻辑如下 ``` stamp = MD5( uid + time_str + "this_is_happymod" ) ``` #### 对应的 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. 响应解码 HTTP 响应不是直接的 JSON,而是按顺序经过三层处理: ``` 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 — 位置替换 对于位置 `i` 处的每个字符: - 如果是字母或数字 `[0-9 A-Z a-z]`: - `shifted = ASCII(char) - (i % 10)` - 在三个范围 `[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 解码 标准字母表(`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 解压缩(`m22190e`) 该方法会自动检测 magic header `0x1f 0x8b`: ``` import gzip def maybe_gunzip(data: bytes) -> bytes: if data[:2] == b'\x1f\x8b': return gzip.decompress(data) return data ``` ### 完整函数 ``` def decode_response(raw: str) -> str: step1 = vigenere_decode(raw) step2 = b64_decode(step1) step3 = maybe_gunzip(step2) return step3.decode('utf-8') ``` ## 4. 我的 Python 实现 ``` import hashlib, base64, gzip, time, json import requests # endpoint 所需的数据 UID = "68920e5674b1d3ec969e4637d31e0345" VERSION = "3.2.6" LANG = "es" BASE = "https://app.apkomega.com" # 解码 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() # 搜索 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)) # 示例 if __name__ == "__main__": result = search("whatsapp") for app in result.get("list", []): print(app.get("title"), "-", app.get("url_id")) ``` ## 5. 应用的已文档化 Endpoints 所有使用 `stamp` 的 endpoints 都要求在发起请求时实时生成它。`apkomega.com` 所有 endpoints 的响应都使用第 4 节中描述的加密方案 ### 5.1 服务器时间同步 **URL:** ``` POST https://app.apkomega.com/202010/api/server_time.php ``` **Payload:** | 字段 | 描述 | 示例 | |------------|---------------------|-----------------------------| | `version` | APK 版本 | `3.2.6` | | `uid` | 设备 UID | `68920e5674b1d3ec969e4637d31e0345` | | `country` | ISO 国家代码 | `CO` | **解码后的响应** ``` { "status": 1, "timestamp": 1716000000 } ``` | 字段 | 描述 | |-------------|----------------------------------------------------| | `status` | `1` = 成功,`-20` = 日期错误 | | `timestamp` | 服务器当前的 Unix timestamp(秒) | 客户端会计算 `offset = 本地时间 - 服务器_timestamp`,并将其从每个 `time_str` 中减去,以实现 stamp 与服务器的同步 注意:结果会被缓存在内存(`_server_time_offset`)中,以免在每次生成 stamp 时重复调用 ### 5.2 应用搜索 **URL:** ``` POST https://app.apkomega.com/202010/api/search_list.php ``` **Payload:** | 字段 | 类型 | 描述 | 示例 | |--------------|-------|-----------------------------------------------------|-------------| | `version` | str | APK 版本 | `3.2.6` | | `uid` | str | 设备 UID | `68920e...` | | `stamp` | str | MD5 token(见第 3 节) | `aded81...` | | `page` | int | 页码(从 1 开始) | `1` | | `keywords` | str | 搜索词 | `whatsapp` | | `lang` | str | ISO 639-1 语言代码 | `es` | | `is_new_user`| int | 如果 APK 是今天安装的则为 `1`,否则为 `2` | `1` | | `is_input` | int | 如果用户输入了搜索词则为 `2`,否则为 `1` | `2` | | `input_word` | str | 搜索前输入的前 3 个字符 | `wha` | **解码后的响应:** ``` { "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 } ] } ``` | 字段 | 描述 | |------------------|--------------------------------------------------------| | `status` | `1` = 有结果,`-20` = 会话错误 | | `has_next_page` | 如果有更多可用页则为 `1` | | `url_id` | APK 的 Package name — 用于其他调用 | | `data_type` | `1` = 包含 mod 列表,`0` = 普通 APK | | `is_ad` | `1` = 这是赞助商结果 | | `update_flag_image` | 徽章图片 URL(例如 "Updated"),如果没有则为空 | ### 5.3 下载 hash 生成 `hash` 是一个由 mod 的 package name 派生出的授权 token,通过截取并重新排列 md5 的 4 个长度为 4 的字符片段构建而成 **原始 Java 代码:** ``` 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); ``` 其中 `this.f2163b` 是该 mod 的 `url_id`,而 `C8854j.m23357b()` 用于计算 md5 **即:** ``` 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] ``` **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] ``` **验证:** ``` 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 APK 下载 **URL:** ``` POST https://d.apkomega.com/202101/api/get_apk_download_v2.php ``` 使用子域名 `d.`(代替 `app.`)和路径 `202101`(代替 `202010`)。 **Payload:** | 字段 | 类型 | 描述 | 示例 / 固定值 | |------------|-------|---------------------------------------------------|-------------------------------| | `version` | str | APK 版本 | `3.2.6` | | `uid` | str | 设备 UID | `68920e...` | | `stamp` | str | MD5 token(见第 3 节) | 实时生成 | | `country` | str | ISO 国家代码 | `US` | | `lang` | str | ISO 639-1 语言代码 | `es` | | `hash` | str | 16 字符授权 token(见第 6.3 节) | `24962664746c81fc` | | `url_id` | str | mod 的 Package name(非基础应用) | `com.mod.tiktok-...` | | `refer` | str | mod 标题拼接 `\|1` | `TikTok Mod 43.9.16\|1` | | `aid` | str | 从 `libCSTAMP.so` 偏移量 `0x940` 提取的固定 ID | `98pyooirb6mad326` | | `get_hpt` | int | 未知 flag,始终为 `0` | `0` | | `channel` | str | 分发渠道 | `happymod` | | `username` | str | 登录用户,无会话时为空 | `""` | **关于 `aid`:** 它是从 `libCSTAMP.so` 的 `.rodata` 段提取的 `STRAID` 字符串,由 `NativeHelper.getAid()` 生成 **关于 `url_id`:** 必须是特定 mod 的 `url_id`(示例:`com.mod.tiktok-videos-shop-livemod-apk-43-9-16`),而不是基础应用的 Package name(`com.zhiliaoapp.musically`),如果使用了基础的 package,服务器将返回 `status: -10` **解码后的响应:** ``` { "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 } ``` | 字段 | 描述 | |--------------|---------------------------------------------------------------------| | `status` | `1` = 成功,`-10` = hash 无效或 url_id 错误 | | `apk_path` | 内部编码了路径的下载 URL — 请勿直接使用 | | `static_path`| **直接的 APK 下载 URL** — 使用这个 | | `stamp` | 服务器用于验证下载的 MD5 | | `full_size` | APK 的大小(字节) | | `what_level` | 分配的 CDN 级别(`lv5` = 热门 CDN) | | `verify` | APK 文件的 MD5,用于在下载后验证完整性 | | `is_vip` | 如果该 mod 需要 VIP 账户则为 `1` | ### 5.5 应用详情 **URL:** ``` GET https://app.happymodapp.com/clist/{version},{lang},{country},{page},{url_id},{sort},{page},{template} ``` 使用不同的域名(`happymodapp.com`),且不需要 `uid` 或 `stamp`。 其响应也不使用其他 API 的加密方案 **路径参数:** | 参数 | 描述 | 示例 / 默认值 | |------------|-----------------------------------|---------------------------------| | `version` | APK 版本 | `3.2.6` | | `lang` | ISO 639-1 语言代码 | `es` | | `country` | ISO 国家代码 | `US` | | `page` | 页码(出现两次) | `1` | | `url_id` | 应用的 Package name | `com.whatsapp` | | `sort` | mod 的排序规则 | `rating` | | `template` | 服务器的 HTML 模板 | `pdt_mod_list_v3.html` | **构建的 URL 示例:** ``` GET https://app.happymodapp.com/clist/3.2.6,es,US,1,com.whatsapp,rating,1,pdt_mod_list_v3.html ``` **响应:** 原始 HTML 或 JSON,未加密,我懒得去记录了,就这样吧 ## 6. 分析: | Java 类(jadx 中的名称) | 原始名称 | 功能 | |--------------------------|--------------------------|----------------------------------------------| | `p410y5.C9106c` | `SearchManager.java` | 构建并执行搜索请求 | | `p007a5.C0060b` | `TimestampManager.java` | 生成与服务器同步的 `time_str` | | `p376v6.C8861q` | `Util.java` | 工具类:uid、版本、stamp、国家 | | `p212h7.C7774c` | `DeviceIdUtil.java` | 生成设备 uid | | `p320q7.C8474a` | `HappyKobe24.java` | 第 1 层解码(Vigenere) | | `p320q7.C8475b` | `MyKobe.java` | 第 2/3 层解码(Base64 + GZIP) | | `p309p7.C8416q` | `IcOld.java` | API endpoints 常量 | | `libCSTAMP.so` | 原生(C/C++) | 使用 MD5 生成 stamp | ### 从 `libCSTAMP.so` 提取的关键字符串 | 偏移量 | 值 | 用途 | |--------|---------------------|-------------------------------| | `0x989`| `this_is_happymod` | stamp 的 Salt(KEY) | | `0x940`| `98pyooirb6mad326` | STRAID — 用于 `getAid()` |
标签:Android, API文档, DSL, Python, 云资产清单, 字符串匹配, 无后门, 破解, 移动应用, 逆向工具, 逆向工程