前言
分析过程比较简单, 如只需要去除弹窗可以直接不看下文中的分析部分
(以下是背景)
好好个周末, 打开破解版网易云, 结果给我弹个这个......
NM 关注是不可能关注的, 直接逆向看看吧
分析
拿原始APK
拖入Jadx, 可发现该应用使用LSPatch进行打包
直接去到assets/lspatch
找origin.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;
}
}
解决方案
抓包屏蔽对应域名 (当配置加载失败时会抛出错误, 不会弹窗)
在APK中查找
AlertDialog.Builder
定位到弹窗类(此类继承AsyncTask), 找到onPostExecute
, 将其函数体直接改为一个return即可 (Smali:return-void
)