某破解版网易云弹窗分析&通杀(清羽弹窗非常规注入)

周末听歌, 结果破解版网易云弹出推广弹窗, 经过逆向后发现是清羽弹窗. 给出两种解决方法

前言

分析过程比较简单, 如只需要去除弹窗可以直接不看下文中的分析部分

(以下是背景)

好好个周末, 打开破解版网易云, 结果给我弹个这个......

NM 关注是不可能关注的, 直接逆向看看吧

分析

拿原始APK

拖入Jadx, 可发现该应用使用LSPatch进行打包

直接去到assets/lspatchorigin.apk (原始APK) 并且查看modules, 可以发现装了com.raincat.dolby_beta (杜比大喇叭β版)

定位弹窗位置

确认弹窗类型

但只安装origin.apk后却依旧有弹窗, 故可以确认是原始APK中被修改, 继续Jadx

这么多类, 一个一个找不现实; 查找弹窗中的字符串也搜不到, 怀疑是从网络中获取弹窗内容

使用Reqable进行抓包(使用方法参见), 发现了这个请求

http://code.xxxxxxx.com/tc/wyyyy.txt
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 21 Feb 2025 12:21:07 GMT
Content-Type: text/plain
Last-Modified: Fri, 21 Feb 2025 09:39:28 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
ETag: W/"67b849d0-837"
Content-Encoding: gzip



〈弹窗配置〉

◎〈总开关〉开〈/总开关〉
◎〈智能弹出〉开〈/智能弹出〉
◎〈版本〉9.0.1〈/版本〉

◎〈强制性〉开〈/强制性〉
◎〈点外部关弹窗〉关〈/点外部关弹窗〉
◎〈弹窗风格〉默认白〈/弹窗风格〉
◎〈外部背景透明〉关〈/外部背景透明〉

◎〈标题〉发现新版本〈/标题〉
◎〈标题颜色〉#000000〈/标题颜色〉

◎〈信息〉<br>
软件已失效,关注公众号〔XXXX〕回复〔XXXX〕获取最新版本〈/信息〉
◎〈信息颜色〉#000000〈/信息颜色〉

◎〈同意按钮〉开〈/同意按钮〉
◎〈同意按钮文本〉立即更新〈/同意按钮文本〉
◎〈同意按钮颜色〉#000000〈/同意按钮颜色〉
◎〈同意按钮事件〉网址http://code.xxxxxxx.com/tysj.png〈/同意按钮事件〉

◎〈拒绝按钮〉开〈/拒绝按钮〉
◎〈拒绝按钮文本〉退出软件〈/拒绝按钮文本〉
◎〈拒绝按钮颜色〉#000000〈/拒绝按钮颜色〉
◎〈拒绝按钮事件〉退出〈/拒绝按钮事件〉

◎〈中立按钮〉关〈/中立按钮〉
◎〈中立按钮文本〉进入软件〈/中立按钮文本〉
◎〈中立按钮颜色〉#000000〈/中立按钮颜色〉
◎〈中立按钮事件〉无〈/中立按钮事件〉

〈/弹窗配置〉

//◎〈拒绝按钮〉关〈/拒绝按钮〉
//◎〈拒绝按钮文本〉暂不更新〈/拒绝按钮文本〉
//◎〈拒绝按钮颜色〉#000000〈/拒绝按钮颜色〉
//◎〈拒绝按钮事件〉智能〈/拒绝按钮事件〉
//弹窗颜色:〔传统〕〔HOLO白〕〔HOLO黑〕〔默认白〕〔默认黑〕
//附加提示:〔事件〕提示〔内容〕,单提示直接写:提示〔内容〕
//智能弹出:修改版本会再次弹出,按钮事件设置为〔智能〕时才能正常使用,反之会永久弹出
//版本号:智能弹出开启时,〔配置版本号〕等于〔应用版本号〕时不会弹出
//强制性:当设置为开时,点按钮和外部弹窗都不会消失,除了事件为〔无〕和〔智能〕的按钮

搜了一下配置文件内容, 可以确定这是清羽网络弹窗

查找弹窗位置

在网上搜索该类型弹窗, 发现了这篇文章, 但该APK中并没有mutil包, 怀疑被注入到了别的地方.

使用Activity工具查看顶层Activity可发现, 顶层为android.app.AlertDialog

前置知识:

AlertDialog是一个用于显示对话框的类, AlertDialog不能直接new, 必须通过AlertDialog.Builder进行构造

AlertDialog.Builder 部分References:

.setMessage: 设置对话框内容

.setView: 向对话框添加一个UI组件或者UI容器

.setAdapter: 设置对话框的适配器

.setCancelable(boolean cancelable): 是否能够点击对话框外部区域/取消对话框

.setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener): 为对话框添加确认按钮

.setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener): 为对话框添加取消按钮

.setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener): 为对话框添加一般按钮

.show: 显示对话框

此处选择搜索AlertDialog.Builder

不难发现, 其中有搜索结果符合清羽网络弹窗的特征 (注: 特征通过反编译网上公开的OnlineDialog.dex得到)

跟进发现, 确实如此

正经逆向

弹窗&解密字符串部分代码:

/* loaded from: classes10.dex */
public class S extends AsyncTask<String, Exception, C1298> implements DialogInterface.OnClickListener {
    @Override // android.os.AsyncTask
    protected /* synthetic */ void onPostExecute(C1298 c1298) { // 初始化Builder
        AlertDialog.Builder builder;
        S s;
        C1298 c12982 = c1298;
        super.onPostExecute(c12982);
        if (c12982 != null && !c12982.f210.equals("关")) {
            if (!c12982.f213.startsWith("开") || !C1298.f198.equals(C1298.f199)) {
                String str = c12982.f215;
                Activity activity = f192; // 根据不同的设置弹不同的窗
                if (str.equals(m1523("pXT6My4gTj82NxGYm3VW2w=="))) {
                    builder = new AlertDialog.Builder(activity, 5);
                } else if (str.equals(m1523("qqYqjpPJDuzNkTREGtjyLw=="))) {
                    builder = new AlertDialog.Builder(activity, 4);
                } else if (str.equalsIgnoreCase(m1523("uH67aS+UuGLYpIqLEsFD/g=="))) {
                    builder = new AlertDialog.Builder(activity, 3);
                } else if (str.equalsIgnoreCase(m1523("ExuTjIN13KU3QHCSgVZ5Pw=="))) {
                    builder = new AlertDialog.Builder(activity, 2);
                } else if (str.equals(m1523("cgNoU3WYHfLgHQk8tA2CzQ=="))) {
                    builder = new AlertDialog.Builder(activity, 1);
                } else {
                    builder = new AlertDialog.Builder(activity);
                }
                builder.setTitle(Html.fromHtml(c12982.f205)).setMessage(Html.fromHtml(c12982.f207));
                if (c12982.f202.equals("关")) {
                    s = this;
                } else {
                    builder.setPositiveButton(c12982.f209, this);
                    s = this;
                }
                if (!c12982.f203.equals("关")) {
                    builder.setNegativeButton(c12982.f214, s);
                }
                if (!c12982.f217.equals("关")) {
                    builder.setNeutralButton(c12982.f211, s);
                }
                AlertDialog create = builder.create();
                if (c12982.f208.equals("关")) {
                    create.setCanceledOnTouchOutside(false); // 是否允许点击外部
                }
                if (c12982.f218.equals("开")) {
                    create.getWindow().setDimAmount(0.0f);
                }
                create.show(); // 显示弹窗
                if (c12982.f216.equals("开")) {
                    try {
                        Field declaredField = create.getClass().getSuperclass().getDeclaredField("mShowing");
                        declaredField.setAccessible(true);
                        declaredField.set(create, Boolean.FALSE);
                    } catch (Exception e2) {
                        publishProgress(e2);
                    }
                }
                // ...... 省略
            }
        }
    }

    // 加载配置
    private static String m1525(String str) {
        HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(str).openConnection();
        httpURLConnection.setRequestMethod("GET");
        httpURLConnection.setUseCaches(false);
        httpURLConnection.setReadTimeout(20000);
        httpURLConnection.setConnectTimeout(20000);
        httpURLConnection.setInstanceFollowRedirects(false);
        httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 (KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7");
        if (httpURLConnection.getResponseCode() != 200) {
            return "";
        }
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "UTF-8"));
        StringBuffer stringBuffer = new StringBuffer();
        while (true) {
            String readLine = bufferedReader.readLine();
            if (readLine != null) {
                stringBuffer.append(readLine);
            } else {
                return stringBuffer.toString();
            }
        }
    }

    // 字符串解密
    private static String m1523(String str) {
        byte[] bArr = null;
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec("qyma".toCharArray(), "$9s1{;1H".getBytes("UTF-8"), 5, 256)).getEncoded(), a.f1069b);
            Cipher cipher = Cipher.getInstance(a.f1068a);
            cipher.init(2, secretKeySpec, new IvParameterSpec("F-=5!2]/9G(<(=uY".getBytes("UTF-8")));
            bArr = cipher.doFinal(Base64.decode(str, 0));
        } catch (Exception e2) {
        }
        if (bArr != null) {
            return new String(bArr);
        }
        return str;
    }

}

配置加载:

public C1298 doInBackground(String[] strArr) {
        C1298 c1298;
        String str = strArr[0];
        if (str == null || str.trim().isEmpty() || !str.contains("ttp")) {
            publishProgress(new Exception(m1523("QWJrEn1bge8ZMsWxf66kbs8LhqOASMd/reU+j+OFEKA=") + str));
            return null;
        }
        try {
            String m1525 = m1525(str); // 从网络加载配置, 超时会返回空字符串, 走此分支, 不会弹窗
            if (m1525 == null || m1525.trim().isEmpty()) { // 
                publishProgress(new Exception(m1523("yoV9BydWjHbXaVgn5XC9yNn20iDOtUExz7IiOH/nddytCanTZRKWLHsaELlaaC0X")));
                return null;
            }
            try {
            } catch (Exception e2) {
                publishProgress(e2);
                c1298 = null;
            }
            if (!m1525.contains(m1523("fQjidHAOPJqFHfngYLI3v1wPHRz8NgNqEUnEVN1Tho0=")) || !m1525.contains(m1523("bAL5WjbI1JyMNkbXljC1ijfLK/FERzZ8zXkzDIwMdR4="))) {
                throw new Exception(m1523("WYOdLZ4Bd86/i8DZ3K9TW7zYIw3v2j+01nfCPWcPEmb/UazFNCTQqu3/v/emt38v"));
            }
            c1298 = new C1298();
            int lastIndexOf = m1525.lastIndexOf(m1523("fQjidHAOPJqFHfngYLI3v1wPHRz8NgNqEUnEVN1Tho0="));
            int lastIndexOf2 = m1525.lastIndexOf(m1523("bAL5WjbI1JyMNkbXljC1ijfLK/FERzZ8zXkzDIwMdR4="), lastIndexOf);
            if (lastIndexOf2 == -1) {
                throw new Exception("糟糕 配置不完整");
            }
            // ......省略
            return c1298;
        } catch (Exception e3) {
            publishProgress(e3);
            return null;
        }
    }

解决方案

  1. 抓包屏蔽对应域名 (当配置加载失败时会抛出错误, 不会弹窗)

  2. 在APK中查找AlertDialog.Builder 定位到弹窗类(此类继承AsyncTask), 找到onPostExecute , 将其函数体直接改为一个return即可 (Smali: return-void)

LICENSED UNDER CC BY-NC-SA 4.0
Comment