首页
论坛
课程
招聘
[原创]某app广告太多,看不下去了,逆一波
2021-6-17 11:07 15535

[原创]某app广告太多,看不下去了,逆一波

2021-6-17 11:07
15535

声明:

​ 本篇只供学习,本人未用作商业用途。他人若未经允许,也不得使用以及参考本篇中提到的源码以及附件进行谋利,违者必究

本篇目的:

​ 由于工作原因,很长时间没搞破解了。几个月前只是简单对某镖游戏dump了il2cpp与meta-data,昨天试玩了一下,竟然要更新,fuck! fuck! fuck!

 

​ 然后随便在小米应用商店下载了个小游戏,广告太多,就以去广告为目标 练一下手吧。

游戏主页的最上方有一个广告栏:

点击礼物盒后启动了一个新的页面:

目标一:去除红色箭头所示的广告栏

目标二:礼物盒点击后去除广告

本篇用到的逆向工具

dex文件查看工具

 

Jeb(windows)

 

mac可参考这里

 

apktool2.5

1
2
3
java -jar ~/env/apktool_2.5.0.jar d app-debug.apk
java -jar ~/env/apktool_2.5.0.jar d -r app-debug.apk //Do not decode resources. 不反编译资源文件
java -jar ~/env/apktool_2.5.0.jar d -s app-debug.apk //Do not decode sources. 不反编译源码文件

Baksmali-2.5.2/smali-2.5.2

1
2
java -jar ~/env/baksmali-2.5.2.jar d classes4.dex //反编译dex文件,输出目录默认位out
java -jar ~/env/smali-2.5.2.jar a out //将out目录下面的smali源文件编译为dex文件,输出默认为out.dex

keytool

 

该cmd是为了生成keystore文件,重签名apk时需要。keytool命令在java安装目录/bin下面

1
keytool -genkey -alias mine.keystore -keyalg RSA -validity 30000 -keystore mine.keystore

jarsigner

 

该cmd是为了将apk进行重签名,重签名需要使用keystore文件。jarsigner命令也在java安装目录/bin下面

1
2
3
4
5
jarsigner -verbose -keystore mine.keystore -signedjar new.apk old.apk mine.keystore
 
-keystore:keystore 的名称
new.apk:签名后的apk
old.apk:签名前的apk

adb shell dumpsys activity activities top

 

打印手机上当前的未销毁的activity列表

 

adb shell dumpsys window windows

 

打印手机上当前的window,默认Z-order

游戏主页的广告栏分析与去除

将app启动,显示游戏主页,查看activity

​ dumpsys看看当前的广告栏属于哪个activity

 

 

activity为com.outfit7.mytalkingtomfree/com.jinke.Main

用jeb打开,Main--activity中看看没有相关的contentView之类的。

分析后,发现Main的父类中含有可疑的布局代码

 

上图中onCreate中调用了initContentLayout函数

1
2
3
4
5
6
7
private void initContentLayout() {
    WeakReference v0 = this.banneContainerReference;
    if(v0 == null || v0.get() == null) {
        this.banneContainerReference = new WeakReference(LayoutInflater.from(this.activityWeakReference.get()).inflate(layout.adjustable_banner_layout, this.findViewById(0x1020002), true));
        Log.d(MaoActivity.TAG, "setAdjustableBannerXY create view");
    }
}

这个函数中有一个Log.d,我们可通过查看程序运行日志确认该函数中的if语句是否执行了。

 

我这边在app冷启动时开始抓log,确实有这个日志

1
06-16 17:44:45.360 27784 27784 D com.outfit7.jinke.MaoActivity: setAdjustableBannerXY create view

或者大家也可以通过打断点调试,确认这里是否执行了。

 

代码拆分说明:

1
LayoutInflater.from(this.activityWeakReference.get()) 获取当前显示的activity,然后基于该activity对应的上下文创建一个LayoutInflater
1
inflate(layout.adjustable_banner_layout, this.findViewById(0x1020002), true));

LayoutInflater.inflate函数,代码加载布局,其函数声明为:

1
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

所以布局文件为layout.adjustable_banner_layout,这个布局的名字看起来就像是广告位。

看看广告位布局文件layout.adjustable_banner_layout

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:orientation="vertical" android:id="@id/mao_adjustable_container_root" android:layout_width="fill_parent" android:layout_height="fill_parent"
  xmlns:android="http://schemas.android.com/apk/res/android">
    <FrameLayout android:id="@id/mao_adjustable_ad" android:layout_width="wrap_content" android:layout_height="wrap_content" />
</RelativeLayout>

xml中整个view的layout_height为fill_parent,这个高度值可能太大了,看起来应该不是主页上方广告的布局。

通过调试查找可疑layout, 在MaoActivity的onResume处打个端点

 

直接查看当前Activity的DecorView,查看其children,发现0,0 1080,206这个boundary就是广告显示的位置

在jeb中看看com.miui.zeus.mimo.sdk.ad.banner.b{75aee83 VFE...C.. ......ID 0,0-1080,206}这玩意

b继承了FrameLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class b extends FrameLayout {
    public class com.miui.zeus.mimo.sdk.ad.banner.b$1 implements View$OnClickListener {
        public com.miui.zeus.mimo.sdk.ad.banner.b$1(b arg1) {
            this.a = arg1;
            super();
        }
 
        public void onClick(View arg2) {
            b v2 = this.a;
            a v0 = v2.e;
            if(v0 != null) {
                v0.a(b.a(v2));
            }
        }
    }
    ...
    ...
    ...
}

继续查看这个类中有关初始化的函数,发现有一个函数调用了setOnClickListener。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//com.miui.zeus.mimo.sdk.ad.banner.b
private void a(View arg3) {
     if(this.d()) {
         this.b = arg3.findViewById(l.c("mimo_banner_view_summary"));
         this.f = arg3.findViewById(l.c("mimo_banner_border"));
         Glide.with(this.i).load(Integer.valueOf(l.b("mimo_banner_border"))).into(this.f);
         this.k = arg3.findViewById(l.c("mimo_banner_view_flipper"));
     }
     else {
         this.a = arg3.findViewById(l.c("mimo_banner_view_image"));
     }
 
     this.c = arg3.findViewById(l.c("mimo_banner_view_ad_mark"));
     this.d = arg3.findViewById(l.c("mimo_banner_view_close"));
     this.h = new d();
     this.d.setOnClickListener(new View$OnClickListener() {
         public void onClick(View arg1) {
             a v1 = this.a.e;
             if(v1 != null) {
                 v1.b();
             }
         }
     });
     ((FrameLayout)this).setOnClickListener(this.g);
 }

看看void a(View arg3)这个函数有哪些地方调用了

jeb中查看交叉引用非常方便,选中函数a,然后按下快捷键x,发现只有一个地方调用了a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.miui.zeus.mimo.sdk.ad.banner.b
public void a(c arg3) {
      this.m = arg3;
      int v0 = com.miui.zeus.mimo.sdk.utils.a.a(arg3.Y());
      this.j = v0;
      if(v0 == 0) {
          this.j = l.a("mimo_banner_view_layout");
      }
 
      this.a(LayoutInflater.from(this.i).inflate(this.j, ((ViewGroup)this)));
      String v3 = arg3.M();
      if(TextUtils.isEmpty(((CharSequence)v3))) {
          this.b();
          return;
      }
 
      this.a(v3);
  }

这个函数中使用了LayoutInflator。 YES!!!!!无疑了,这个调用链肯定初始化了主页上方的广告位。

 

继续查找a(c arg3)的调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//com.miui.zeus.mimo.sdk.ad.banner
public void a(com.miui.zeus.mimo.sdk.server.api.c arg3, ViewGroup arg4, BannerInteractionListener arg5) {
        this.l = System.currentTimeMillis();
        String v0 = "BannerUIController";
        j.a(v0, "showBanner");
        this.e = arg3;
        this.h = arg5;
        if(arg3 == null) {
            this.a(com.miui.zeus.mimo.sdk.utils.error.a.e.au, com.miui.zeus.mimo.sdk.utils.error.a.e.av);
            j.b(v0, "Empty splash ad info view arguments");
            return;
        }
 
        this.f = arg4;
        this.c.post(new Runnable(arg3) {
            public void run() {
                try {
                    c.a(this.b, new b(c.a(this.b)));
                    c.b(this.b).a(this.b);
                    c.b(this.b).a(this.a);
                }
                catch(Exception v0) {
                    j.b("BannerUIController", "Failed to create view", ((Throwable)v0));
                    this.b.a();
                }
            }
        });
    }

这个函数中的字符很给力,showBanner, Failed to create view等

 

a(com.miui.zeus.mimo.sdk.server.api.c arg3, ViewGroup arg4, BannerInteractionListener arg5)的调用者

1
2
3
4
5
6
7
//com.miui.zeus.mimo.sdk.ad.banner
public void a(ViewGroup arg3, BannerInteractionListener arg4) {
    this.f = arg3;
    b v3 = new b(this, arg4);
    this.q = v3;
    this.g.a(this.d, this.f, ((BannerInteractionListener)v3));
}

a(ViewGroup arg3, BannerInteractionListener arg4)的调用者

1
2
3
4
5
6
7
8
//com.miui.zeus.mimo.sdk.BannerAd
public void showAd(ViewGroup arg3, BannerInteractionListener arg4) {
        if(arg3 == null) {
            j.b("BannerAd", "showAd failed, container can not be null");
        }
 
        this.mAdImpl.a(arg3, arg4);
    }

showAd(ViewGroup arg3, BannerInteractionListener arg4)的调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onBannerAdLoadSuccess() {
    String v0 = "MiMoAdBannerAdapter";
    MLog.w(v0, "load banner ad success ,start render");
    if(MiMoAdBannerAdapter.access$100(this.a) != null) {
        MLog.w(v0, "start show banner ad");
        MiMoAdBannerAdapter.access$100(this.a).showAd(MiMoAdBannerAdapter.access$200(this.a), MiMoAdBannerAdapter.access$300(this.a));
    }
    else {
        MLog.w(v0, "load banner ad success,but banner ad is null");
        this.a.notifyLoadError(new MMAdError(-2000));
        this.a.trackDspLoad(String.valueOf(-2000), null);
    }
}

​ 上述调用栈关系也可以使用动态调试的方式,一步到位。我没有使用调试的方法,因为这个广告在我的手机上出现的概率太低,想调试一次太难了,大家理解一下哈。

基于上述分析,去掉主页上方广告位的思路:

源头去除: onBannerAdLoadSuccess函数中,showAd调用移除。缺点:可能影响了app原本逻辑

 

尾部去除:设置view的visibility。优点:基本不影响app原本逻辑

 

昨天调试的时候保存了广告栏显示的代码源头。大家可以自行分析一下,看是否跟我找到的一样。

1
2
3
4
5
//com.xiaomi.ad.mediation.mimo.MiMoAdBannerAdapter
public void loadAndShow(AdInternalConfig arg1, AdLoadAndShowListener arg2, AdLoadAndShowInteractionListener arg3) {
        super.loadAndShow(arg1, arg2, arg3);
        AndroidUtils.runOnMainThread(this.mMainHandler, new MiMoAdBannerAdapter$a(this, arg1));
    }
1
2
3
4
5
6
7
8
9
10
11
public class MiMoAdBannerAdapter$a implements Runnable {
    public MiMoAdBannerAdapter$a(MiMoAdBannerAdapter arg1, AdInternalConfig arg2) {
        this.b = arg1;
        this.a = arg2;
        super();
    }
 
    public void run() {
        this.b.loadBannerAd(this.a);
    }
}

直接将run函数return,实测有效

1
2
3
4
5
6
7
java -jar ~/env/baksmali-2.5.2.jar d classes2.dex
 
cd out/com/miui/zeus/mimo/sdk/ad/banner
 
vim a\$3.smali
 
//去掉invoke-virtual。相当于run()函数什么都没有做

还有一种方案:模拟广告位close的方法,对app逻辑无任何影响,大家自行尝试

点击礼物盒跳转至广告activity,如何disable该功能

老办法,先用dumpsys看看activity是哪个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
admin@C02D7132MD6R bin % adb shell dumpsys activity activities
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
  Stack #1:
     ...
     ...
      * Hist #1: ActivityRecord{1f34e49 u0 com.outfit7.mytalkingtomfree/com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity t3}
          packageName=com.outfit7.mytalkingtomfree processName=com.outfit7.mytalkingtomfree
          launchedFromUid=10139 launchedFromPackage=com.outfit7.mytalkingtomfree userId=0
          app=ProcessRecord{382ce36 4930:com.outfit7.mytalkingtomfree/u0a139}
          Intent { cmp=com.outfit7.mytalkingtomfree/com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity (has extras) }
          ...
          ...
          resizeMode=RESIZE_MODE_RESIZEABLE
      * Hist #0: ActivityRecord{64ced92 u0 com.outfit7.mytalkingtomfree/com.jinke.Main t3}
          packageName=com.outfit7.mytalkingtomfree processName=com.outfit7.mytalkingtomfree
          launchedFromUid=10139 launchedFromPackage=com.outfit7.mytalkingtomfree userId=0
          app=ProcessRecord{382ce36 4930:com.outfit7.mytalkingtomfree/u0a139}
          Intent { flg=0x10000000 cmp=com.outfit7.mytalkingtomfree/com.jinke.Main (has extras) }

RewardVideoAdActivity与主页(Main)不一个activity,暴力拦截startActivity可行?

先挂断点查看startActivity调用栈, 我断在了instrumentation->execSartActivity方法

 

很可惜,尝试了头部去除以及尾部去除,重打包运行会有两个问题:

1.activity确实被拦截掉了,但是页面会卡在一个view中,显示加载中...

 

2.按返回键尝试将卡view的界面关掉,礼物盒没有正常开启奖励,肯定是影响了游戏逻辑。

 

奖励没拿到。。。。毛用都没有

继续硬头皮找关键点吧

广告activity显示有一个特点,close按钮隐藏后显示

广告加载完之后,右上角有一个X按钮,会显示出来。未加载完的时候隐藏。所以就在Activity中找找有没有相关的close或者onClick关键字

 

jeb中直接找到RewardVideoAdActivity, 确实搜索到了close与onclick关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity
private void k() {
    int v0 = com.miui.zeus.mimo.sdk.utils.a.c(this.i.Y());
    if(v0 == 0) {
        v0 = l.a("mimo_reward_view_end_page_landscape");
    }
 
    this.h = LayoutInflater.from(((Context)this)).inflate(v0, this.g, true);
    String v0_1 = this.i.M();
    String v1 = this.i.L();
    Bitmap v0_2 = BitmapFactory.decodeFile(v0_1, this.o);
    Bitmap v1_1 = BitmapFactory.decodeFile(v1, this.o);
    this.h.findViewById(l.c("mimo_reward_flv_video")).setImageBitmap(v0_2);
    this.h.findViewById(l.c("mimo_reward_icon")).setImageBitmap(v1_1);
    this.h.findViewById(l.c("mimo_reward_title")).setText(this.i.h());
    this.h.findViewById(l.c("mimo_reward_summary")).setText(this.i.g());
    this.h.findViewById(l.c("mimo_reward_dsp")).setText(this.i.i());
    View v0_3 = this.h.findViewById(l.c("mimo_reward_jump_btn"));
    ((TextView)v0_3).setText(this.i.O());
    b v1_2 = new b();
    this.y = v1_2;
    ((com.miui.zeus.mimo.sdk.anim.a)v1_2).b(v0_3).a(1200).a(-1).b(1).a(new AccelerateDecelerateInterpolator()).c();
    this.h.findViewById(l.c("mimo_reward_close_img")).setOnClickListener(((View$OnClickListener)this));
    this.g.setOnClickListener(((View$OnClickListener)this));
}

void k()这个函数中使用了LayoutInflater,“mimo_reward_close_img”, setOnClickListener(this)。太明显了,毫无压力。

再考虑一个问题,何时广告播放完显示X按钮呢

直接搜关键字setVisibility,该函数会将一个view显示出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity
private void p() {
    int v0;
    if(this.i.c()) {
        this.n();
        if(!TextUtils.isEmpty(this.i.u())) {
            v0 = 1;
        }
        else {
            goto label_11;
        }
    }
    else {
    label_11:
        v0 = 0;
    }
 
    if(v0 != 0) {
        this.m();
    }
    else {
        this.f.setVisibility(8);
        this.k.setVisibility(8);
        this.g.setVisibility(0);
        b v0_1 = this.y;
        if(v0_1 != null) {
            ((com.miui.zeus.mimo.sdk.anim.a)v0_1).c();
        }
 
        ViewFlipper v0_2 = this.u;
        if(v0_2 != null) {
            v0_2.stopFlipping();
        }
 
        com.miui.zeus.mimo.sdk.utils.analytics.b.a(this.i.m(), this.i, "END_PAGE_VIEW", "load_success", 0, "");
    }
}

搜到了,就在RewardVideoAdActivity代码中,代码还是那么的明显,毫无压力。

 

this.g.setVisibility(0),这里的0是一个flag, 就是将view显示出来。

在RewardVideoAdActivity.p()函数打断点,调试一把,看看调用栈

 

源头是通过onCompletion调过来的, com.miui.zeus.mimo.sdk.video.TextureVideoView$3实现了MediaPlayer.OnCompletionListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class com.miui.zeus.mimo.sdk.video.TextureVideoView$3 implements MediaPlayer$OnCompletionListener {
    public com.miui.zeus.mimo.sdk.video.TextureVideoView$3(TextureVideoView arg1) {
        this.a = arg1;
        super();
    }
 
    public void onCompletion(MediaPlayer arg2) {
        TextureVideoView.c(this.a, 5);
        TextureVideoView.d(this.a, 5);
        if(TextureVideoView.e(this.a) != null) {
            TextureVideoView.e(this.a).hide();
        }
 
        if(TextureVideoView.h(this.a) != null) {
            TextureVideoView.h(this.a).onCompletion(TextureVideoView.d(this.a));
        }
    }
}

原来广告是通过MediaPlayer播放的,然后播放是否完成 是 通过MediaPlayer的回掉接口掉过来的。

模拟MediaPlayer播放完成,模拟点击close按钮,实现插件

首先要拿到MediaPlayer实例

手有点疼了。。。。

 

主要是通过jeb,查看交叉引用关系

 

MediaPlayer是TextureVideoView的成员

1
2
3
4
5
6
//com/miui/zeus/mimo/sdk/video/TextureVideoView
public Map m;
public int n;
public int o;
public Surface p;
public MediaPlayer q;

TextureVideoView是VideoAdView的成员

1
2
3
4
5
6
//com.miui.zeus.mimo.sdk.video.VideoAdView
public TextureVideoView a;
public ImageView b;
public FrameLayout c;
public boolean d;
public static final String e = "VideoAdView";

VideoAdView是RewardVideoActivity的成员

1
2
3
4
5
6
7
public class RewardVideoAdActivity extends Activity implements View$OnClickListener, a {
    public static final String a = "key_baseadinfo";
    public static final String b = "RewardVideoAdActivity";
    public static final String c = "key_exposure";
    public static final long d = 60000;
    public EventRecordFrameLayout e;
    public VideoAdView f;

结论: 可以通过拿到RewardVideoActivity实例,然后反射一步步拿到MediaPlayer实例

如何拿到RewardVideoActivity实例

这可能是个老生常谈的问题。方式可能有好几种吧, 下面是我能想到的

 

1: ActivityThread中保存了当前进程的activity列表

1
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

2.拦截ActivityThread.H,这玩意是一个Handler,第一时间通过system_server将activity的生命周期,回掉过来。

 

3.通过frida或者其他hook工具,在activity->onCreate或者onResume这里下手

 

4.通过注入Instrumentation的方式,管控activity生命周期,可以拦截到onCreate以及onResume等

 

我使用了最后一种方案,用java写了点代码,最后编译为smali文件后,通过静态代码注入重打包了apk。

贴代码

instrumentation注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void initInstrumentation() {
    final String CLASS_ACTIVITYTHREAD = "android.app.ActivityThread";
 
    RefUtils.MethodRef<Object> currentActivityThreadRef =
            new RefUtils.MethodRef<Object>(CLASS_ACTIVITYTHREAD, true,
                    "currentActivityThread", new Class[0]);
    Object activityThread = currentActivityThreadRef.invoke(null, new Object[0]);
 
    RefUtils.FieldRef<Instrumentation> instrumentationFieldRef =
            new RefUtils.FieldRef<Instrumentation>(CLASS_ACTIVITYTHREAD, false, "mInstrumentation");
    Instrumentation appInstru = instrumentationFieldRef.get(activityThread);
 
    HackerInstrumentation.INSTANCE().setOriginInstru(appInstru);
 
    instrumentationFieldRef.set(activityThread, HackerInstrumentation.INSTANCE());
}

Instrumentation实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class HackerInstrumentation extends Instrumentation {
 
    private Instrumentation mOrigin = null;
 
    public static HackerInstrumentation INSTANCE() {
        return DefaultHackerInstru.get();
    }
 
    private HackerInstrumentation(){}
 
 
    private static class DefaultHackerInstru {
        private static HackerInstrumentation sInstancce = new HackerInstrumentation();
        static HackerInstrumentation get() {
            return sInstancce;
        }
    }
 
    public void setOriginInstru(Instrumentation instru) {
        mOrigin = instru;
    }
 
    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        mOrigin.callActivityOnCreate(activity, icicle);
 
        TalkingTomHacker.afterCreateActivity(activity);
    }
 
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
        mOrigin.callActivityOnCreate(activity, icicle, persistentState);
 
        TalkingTomHacker.afterCreateActivity(activity);
    }
 
    @Override
    public void callActivityOnDestroy(Activity activity) {
        mOrigin.callActivityOnDestroy(activity);
 
        TalkingTomHacker.afterCreateActivity(activity);
    }
 
    @Override
    public void callActivityOnResume(Activity activity) {
        mOrigin.callActivityOnResume(activity);
 
        TalkingTomHacker.afterResumeActivity(activity);
    }
}

在callActivityOnCreate回掉中拿到activity实例,然后通过反射拿到MediaPlayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public static void afterCreateActivity(Activity activity) {
    Log.d(TAG, "afterCreateActivity " + activity);
    try {
        disableRewardActivityMediaPlayer(activity);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
 
private static void disableRewardActivityMediaPlayer(Activity activity) throws Exception {
    final String REWARD_ACTIVITY_NAME = "com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity";
    if (!TextUtils.equals(activity.getLocalClassName(), REWARD_ACTIVITY_NAME)) {
        return;
    }
 
    final String VIDEO_AD_VIEW_FIELD_TYPE = "com.miui.zeus.mimo.sdk.video.VideoAdView";
 
    Field tmpRef = null;
    Field[] fields = activity.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (TextUtils.equals(field.getType().getName(), VIDEO_AD_VIEW_FIELD_TYPE)) {
            tmpRef = field;
            break;
        }
    }
 
    if (tmpRef == null) {
        return;
    }
 
    tmpRef.setAccessible(true);
 
    Object videoAdView = tmpRef.get(activity);
 
    if (videoAdView == null) {
        Log.e(TAG, "videoAdView is null");
        return;
    }
 
    final String TEXTURE_VIDEO_VIEW_TYPE = "com.miui.zeus.mimo.sdk.video.TextureVideoView";
 
    tmpRef = null;
 
    fields = videoAdView.getClass().getDeclaredFields();
 
    for (Field field : fields) {
        if (TextUtils.equals(field.getType().getName(), TEXTURE_VIDEO_VIEW_TYPE)) {
            tmpRef = field;
            break;
        }
    }
 
    if (tmpRef == null) {
        return;
    }
 
    tmpRef.setAccessible(true);
    TextureView textureVideoView = (TextureView) tmpRef.get(videoAdView);
 
    if (textureVideoView == null) {
        Log.e(TAG, "textureVideoView null");
        return;
    }
 
    final String MEDIA_PLAYER_TYPE = "android.media.MediaPlayer";
    tmpRef = null;
    fields = textureVideoView.getClass().getDeclaredFields();
 
    for (Field field : fields) {
        if (TextUtils.equals(field.getType().getName(), MEDIA_PLAYER_TYPE)) {
            tmpRef = field;
            break;
        }
    }
 
    if (tmpRef == null) {
        return;
    }
 
    TextureView.SurfaceTextureListener originListener = textureVideoView.getSurfaceTextureListener();
 
    if (originListener == null) {
        Log.e(TAG, "origin surface texure Listener null");
        return;
    }
 
    setOldTextureListener(originListener);
 
    textureVideoView.setSurfaceTextureListener(sSurfaceTextureListener);
 
    final Field mediaPlayerRef = tmpRef;
    ...
    ...
}

模拟MediaPlayer播放完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
HandlerUtils.post(new Runnable() {
    @Override
    public void run() {
        sModifyMediaPlayerCondition.block();
        sModifyMediaPlayerCondition.close();
        mediaPlayerRef.setAccessible(true);
        MediaPlayer player = null;
        try {
            player = (MediaPlayer) mediaPlayerRef.get(textureVideoView);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
 
        if (player == null) {
            Log.e(TAG, "MediaPlayer is null");
            return;
        }
        player.pause();
        try {
            //模拟MediaPlayer播放完成
            pretendCompleteMediaPlayer(player);
        } catch (Exception e) {
            e.printStackTrace();
        }
        player.release();
    }
});

模拟点击close按钮,关闭广告activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void disableRewardActivityResume(Activity activity) {
    final String REWARD_ACTIVITY_NAME = "com.miui.zeus.mimo.sdk.ad.reward.RewardVideoAdActivity";
    if (!TextUtils.equals(activity.getLocalClassName(), REWARD_ACTIVITY_NAME)) {
        return;
    }
 
    synchronized (TalkingTomHacker.class) {
        if (activity.isDestroyed() || activity.isFinishing()) {
            return;
        }
        pretendClickCloseActivity(activity, "mimo_reward_close_img");
    }
}
 
 
private static void pretendClickCloseActivity(Activity activity, String resStr) {
    if (activity instanceof View.OnClickListener) {
        int resId = activity.getApplicationContext().getResources().getIdentifier(resStr, "id", activity.getPackageName());
 
        if (resId <= 0) {
            Log.e(TAG, "pretendClickCloseActivity failed for res " + resStr + " not found");
            return;
        }
 
        View closeLikelyView = new View(activity.getApplicationContext());
        closeLikelyView.setId(resId);
        ((View.OnClickListener) activity).onClick(closeLikelyView);
        Log.d(TAG, "pretendClickCloseActivity success");
    } else {
        Log.e(TAG, "pretendClickCloseActivity failed for not OnClickListener instance");
    }
}

代码写完,写一个接口,方便静态代码注入

1
2
3
4
5
public class PluginManager {
    public static void init(Context context) {
        TalkingTomHacker.initInstrumentation();
    }
}

嗯,没看错,接口代码就是如此简单,注入的时候,直接调用PluginManager.init就行了。

如何将插件代码注入到apk中呢?

1.反编译apk,得到smali文件,在Application的onCreate函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.class public Lcom/jinke/XiaomiApplication;
.super Lcom/outfit7/jinke/ChinaAdApplication;
.source "XiaomiApplication.java"
 
 
# direct methods
.method public constructor <init>()V
    .registers 1
 
    .line 10
    invoke-direct {p0}, Lcom/outfit7/jinke/ChinaAdApplication;-><init>()V
 
    return-void
.end method
 
 
# virtual methods
.method public onCreate()V
    .registers 3
 
    .line 14
    invoke-super {p0}, Lcom/outfit7/jinke/ChinaAdApplication;->onCreate()V
 
    .line 19
    new-instance v0, Lcom/xiaomi/gamecenter/sdk/entry/MiAppInfo;
 
    invoke-direct {v0}, Lcom/xiaomi/gamecenter/sdk/entry/MiAppInfo;-><init>()V
 
    const-string v1, "2882303761517147655"
 
    .line 20
    invoke-virtual {v0, v1}, Lcom/xiaomi/gamecenter/sdk/entry/MiAppInfo;->setAppId(Ljava/lang/String;)V
 
    const-string v1, "5811714712655"
 
    .line 21
    invoke-virtual {v0, v1}, Lcom/xiaomi/gamecenter/sdk/entry/MiAppInfo;->setAppKey(Ljava/lang/String;)V
 
    .line 22
    new-instance v1, Lcom/jinke/XiaomiApplication$1;
 
    invoke-direct {v1, p0}, Lcom/jinke/XiaomiApplication$1;-><init>(Lcom/jinke/XiaomiApplication;)V
 
    invoke-static {p0, v0, v1}, Lcom/xiaomi/gamecenter/sdk/MiCommplatform;->Init(Landroid/content/Context;Lcom/xiaomi/gamecenter/sdk/entry/MiAppInfo;Lcom/xiaomi/gamecenter/sdk/OnInitProcessListener;)V
 
    invoke-static {p0}, Lcom/hack/core/PluginManager;->init(Landroid/content/Context;)V
 
    return-void
.end method

这个smali文件中,只加入了最后一句, 目的就是初始化插件,实现Instrumentation注入

1
invoke-static {p0}, Lcom/hack/core/PluginManager;->init(Landroid/content/Context;)V

2.由于classes.dex文件方法数的限制,插件的smali文件无法编进classes.dex中,最后我放入到了classes4.dex可以成功

反编译/回编classes.dex

1
2
java -jar ~/env/baksmali-2.5.2.jar d classes.dex
java -jar ~/env/smali-2.5.2.jar a out

反编译/回编classes4.dex (反编译后将插件的smali放到out下面,然后再回编就行了)

1
2
java -jar ~/env/baksmali-2.5.2.jar d classes4.dex
java -jar ~/env/smali-2.5.2.jar a out

3.将上面面两步得到的dex,替换apk中的classes.dex与classes4.dex

Windows:我是通过zip工具打开的apk,然后直接将dex拖进去就行,比较方便

 

Mac:未尝试

 

 

4.删除apk中原本的签名文件夹(META-INF)重签名

 

不足之处

1.广告页面会先弹出,80ms左右才会消失掉,所以视觉上还能看到广告activity

 

2.可能还有其他地方有广告。这个apk广告太多了。。。。不过目前来看应该好多了

 

带插件的apk如下:(法律原因,已删除)

 

 

原包apk:

 

链接: https://pan.baidu.com/s/1to64frG_x6DtqnrVAUZLfQ 密码: 7fwm


[培训] 优秀毕业生寄语:恭喜id咸鱼炒白菜拿到远超3W月薪的offer,《安卓高级研修班》火热招生!!!

最后于 2021-6-17 12:32 被whulzz编辑 ,原因:
收藏
点赞13
打赏
分享
最新回复 (14)
雪    币: 440
活跃值: 活跃值 (43)
能力值: (RANK:0 )
在线值:
发帖
回帖
粉丝