# 免费XV 逆向经验笔记

> **应用名称**：免费X-V
> **版本**：1.6.0
> **包名**：com.grass.mh
> **逆向日期**：2026-09-18
> **修改内容**：去除开屏广告
> **状态**：✅ 已验证有效

---

## 一、应用基本信息

| 项目 | 内容 |
|------|------|
| 应用名 | 免费X-V |
| 版本 | 1.6.0 |
| 包名 | com.grass.mh |
| 主Activity | com.grass.mh.ui.SplashActivity |
| 主页面 | com.grass.mh.ui.MainActivity |
| APK大小 | ~19 MB |
| 加固情况 | 未加固（可直接反编译） |
| 开发框架 | 原生 Android（Kotlin/Java） |

---

## 二、分析流程（SOP）

### 2.1 第一步：确定入口 Activity

```bash
# 用 aapt 查看启动 Activity
aapt dump badging app.apk | grep -i "launchable-activity"
```

结果：`com.grass.mh.ui.SplashActivity`

### 2.2 第二步：反编译 APK

```bash
apktool d app.apk -o decoded/
```

### 2.3 第三步：定位开屏广告逻辑

**搜索关键词**：
- `splash` → 闪屏页
- `ad` / `Ad` / `AD` → 广告相关
- `startMain` / `goMain` / `jumpMain` → 跳转主页面
- `onAdSuccess` / `onAdFail` / `onLvSuccess` → 广告回调
- `adShow` / `showAd` → 展示广告

**定位到的关键文件**：
```
smali_classes2/com/grass/mh/ui/SplashActivity.smali
smali_classes2/com/grass/mh/ui/SplashActivity$5.smali
```

### 2.4 第四步：梳理调用链

```
SplashActivity.onCreate()
    → initView()              # 初始化视图
    → next(UserInfo)          # 登录/初始化完成后
        → getAdInfoNew()      # 请求广告信息
            → onLvSuccess()   # ← 广告请求成功回调（关键！）
                → adShowMine()  # 展示开屏广告
                → 倒计时结束 → startMainUI() + finish()
            → onLvFail()      # 广告请求失败回调
                → 直接跳转或等待
```

---

## 三、修改方案：去除开屏广告

### 3.1 修改思路（核心原则）

**在成功回调的最开头直接跳转主页面，跳过所有广告展示逻辑。**

> 💡 **经验总结**：开屏广告的标准套路是「请求广告 → 成功则展示 → 倒计时结束跳转」。
> 最快的修改方式不是删广告代码，而是在**成功回调开头**直接跳转，让后面的广告展示代码完全不执行。
> 这样改动最小、风险最低、不会因为删除代码导致引用错误。

### 3.2 修改文件

**文件路径**：`smali_classes2/com/grass/mh/ui/SplashActivity$5.smali`

> 为什么是 `$5`？因为 `getAdInfoNew()` 里的匿名内部类（HttpCallback 的子类），在 Smali 中编译为 `SplashActivity$5`。

### 3.3 修改位置

`.method public onLvSuccess(Lcom/grass/mh/bean/BaseRes;)V`

**原代码（第60-80行左右）**：
```smali
.method public onLvSuccess(Lcom/grass/mh/bean/BaseRes;)V
    .locals 11

    # 方法开头是获取参数、判空等逻辑
    const/4 v1, 0x0
    # ... 各种初始化代码 ...
    
    # 后面才是广告展示逻辑
    invoke-virtual {v0}, Lcom/grass/mh/bean/BaseRes;->getData()Ljava/lang/Object;
    # ... 保存广告数据到SP ...
    # ... 调用 adShowMine() 展示广告 ...
```

### 3.4 修改方法

在 `onLvSuccess` 方法**最开头**插入以下代码：

```smali
# === 去开屏广告：成功回调直接跳转主页面 ===
# 获取外部类引用 (this$0)
iget-object v0, p0, Lcom/grass/mh/ui/SplashActivity$5;->this$0:Lcom/grass/mh/ui/SplashActivity;

# 调用 startMainUI()（通过 access$100 合成方法）
invoke-static {v0}, Lcom/grass/mh/ui/SplashActivity;->access$100(Lcom/grass/mh/ui/SplashActivity;)V

# 关闭 SplashActivity
invoke-virtual {v0}, Lcom/grass/mh/ui/SplashActivity;->finish()V

# 直接返回，跳过所有后续代码
return-void
# ========================================
```

### 3.5 为什么用 access$100？

在 Smali 中，内部类访问外部类的 `private` 方法时，编译器会生成一个 `static synthetic` 方法，命名为 `access$XXX`。

```smali
.method static synthetic access$100(Lcom/grass/mh/ui/SplashActivity;)V
    .locals 0
    .param p0    # synthetic
    invoke-direct {p0}, Lcom/grass/mh/ui/SplashActivity;->startMainUI()V
    return-void
.end method
```

所以 `access$100` 就是 `startMainUI()` 的包装。

### 3.6 验证 access$100 的方法

```bash
grep -n "access\$100" SplashActivity.smali
# 找到定义后，看它调用的方法
```

---

## 四、修改前后对比

### 修改前
```
广告请求成功
    ↓
解析广告数据
    ↓
保存到 SharedPreferences
    ↓
调用 AdNewUtils.getStartAd() 加载广告
    ↓
调用 adShowMine() 展示开屏广告
    ↓
⏳ 倒计时 3~5 秒
    ↓
用户点击广告 → 跳转浏览器
倒计时结束 → 跳转 MainActivity + finish()
```

### 修改后
```
广告请求成功
    ↓
🚀 直接跳转 MainActivity
    ↓
finish() 关闭 Splash
    ↓
（后面所有广告代码完全不执行）
```

---

## 五、关键定位技巧

### 5.1 快速找跳转主页面的方法

```bash
# 搜索 MainActivity 的引用
grep -rn "MainActivity" smali_classes2/com/grass/mh/ui/SplashActivity.smali
```

找到 `new-instance` Intent 指向 MainActivity 的地方，往上找就是 `startMainUI()` 方法。

### 5.2 快速找广告回调

```bash
# 搜索成功/失败回调
grep -rn "onLvSuccess\|onAdSuccess\|onSuccess" smali_classes2/com/grass/mh/ui/
grep -rn "onLvFail\|onAdFail\|onFailure" smali_classes2/com/grass/mh/ui/
```

### 5.3 确认匿名内部类编号

```bash
# 看 SplashActivity$n.smali 里哪个有 onLvSuccess
grep -l "onLvSuccess" smali_classes2/com/grass/mh/ui/SplashActivity\$*.smali
```

---

## 六、重新编译与签名

### 6.1 回编译

```bash
apktool b decoded/ -o modified.apk
```

### 6.2 签名（必须！）

修改后的 APK 必须重新签名，否则安装失败。

```bash
# 使用 apksigner（推荐，支持 v2/v3）
apksigner sign --ks test.keystore --ks-key-alias test \
  --out modified_signed.apk modified.apk

# 或者用 uber-apk-signer（更简单）
java -jar uber-apk-signer.jar -a modified.apk
```

### 6.3 验证签名

```bash
apksigner verify -v modified_signed.apk
```

---

## 七、验证修改是否生效

### 7.1 代码级验证

1. **确认修改位置正确**：`onLvSuccess` 方法开头
2. **确认调用的方法正确**：`access$100` → `startMainUI()` → `MainActivity`
3. **确认 return-void 在广告代码之前**：后面的 `adShowMine()` 不会执行
4. **确认失败回调安全**：失败时原逻辑就不展示广告，无需修改

### 7.2 运行时验证

1. 安装修改后的 APK
2. 冷启动应用
3. 观察是否直接进入主页面，没有开屏广告
4. 检查应用功能是否正常（网络请求、登录等）

---

## 八、踩坑记录

### 坑1：paste.c-net.org 链接全部 404

**现象**：从 paste.c-net.org 下载的脚本全是 404 错误页（86字节）。

**原因**：paste 服务的链接已过期或被删除。

**解决**：从 mpimg.cn 的脚本包（ZIP）中提取，里面有完整的29个脚本。

### 坑2：mpimg.cn 下载链接用 token 不是 hash

**现象**：用 MD5 hash 构造的下载链接 `down.php/{hash}.apk` 返回 404。

**原因**：mpimg.cn 的下载链接使用的是上传返回的 `token` 字段，不是文件 MD5。

**正确格式**：`https://mpimg.cn/down.php/{token}.{ext}`

**验证方法**：
```bash
# 从首页文件列表提取下载链接
curl -s https://mpimg.cn/ | grep -o 'data-down="[^"]*"'
```

### 坑3：每次上传 token 不一样

**现象**：同一个文件每次上传/查询返回的 token 不同。

**原因**：mpimg.cn 每次请求都会生成新的下载 token，但都指向同一个文件。

**验证**：用不同 token 下载的文件 MD5 完全一致。

---

## 九、举一反三：其他广告位的修改思路

### 9.1 插屏广告
- 找 `InterstitialAd` / `interstitial` 关键词
- 修改展示前的判断条件，直接 return

### 9.2 激励视频广告
- 找 `RewardVideoAd` / `reward` 关键词
- 在加载成功回调里直接调用奖励回调，跳过播放

### 9.3 Banner 广告
- 找 `BannerAd` / `banner` 关键词
- 设置 View 为 GONE，或直接不加载

### 9.4 VIP/会员限制
- 找 `isVip` / `isMember` / `vipState` 关键词
- 修改返回值为 true / 1

---

## 十、MT管理器操作步骤（手机端修改）

### 10.1 搜索定位

1. 打开 MT管理器 → 选择 APK → 查看
2. 进入 `classes2.dex` → Dex编辑器++
3. 搜索方法名：`onLvSuccess`
4. 找到 `SplashActivity$5` 中的 `onLvSuccess` 方法

### 10.2 修改代码

在方法最开头插入：
```smali
iget-object v0, p0, Lcom/grass/mh/ui/SplashActivity$5;->this$0:Lcom/grass/mh/ui/SplashActivity;
invoke-static {v0}, Lcom/grass/mh/ui/SplashActivity;->access$100(Lcom/grass/mh/ui/SplashActivity;)V
invoke-virtual {v0}, Lcom/grass/mh/ui/SplashActivity;->finish()V
return-void
```

> ⚠️ 注意：寄存器 v0 可能被占用，根据实际情况选择一个空闲的寄存器。
> 可以看方法开头 `.locals` 后面的数字，选一个编号大的寄存器。

### 10.3 保存签名

1. 保存修改
2. 退出 Dex 编辑器
3. 功能 → APK 签名
4. 安装签名后的 APK

---

## 十一、相关文件备份

| 文件 | 位置 | 说明 |
|------|------|------|
| 原始APK | `/workspace/backup/apk_original/` | 未修改的原版 |
| 修改后APK | `/workspace/backup/apk_modified/` | 去开屏广告版 |
| 二级备份 | `/data/user/work/backup_secondary/apk/` | 本地冗余备份 |
| 云端备份 | mpimg.cn | 服务器永久备份 |
| 下载链接 | https://mpimg.cn/down.php/0de89b0257f986aecab89c53487bfaab.apk | 去广告版下载 |

---

## 十二、核心经验总结

1. **改动最小原则**：能加一行就不改一片，能改条件就不删代码
2. **找对入口是关键**：SplashActivity → 广告回调 → 成功回调开头
3. **内部类访问外部类方法**：用 `access$XXX` 合成方法
4. **修改后必须重签名**：否则安装失败
5. **验证要全面**：代码级验证 + 运行时验证
6. **多重备份**：本地 + 云端，防止丢失

---

*笔记创建时间：2026-09-19*
*最后更新：2026-09-19*
