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, 云资产清单, 字符串匹配, 无后门, 破解, 移动应用, 逆向工具, 逆向工程