声明:
本篇只供学习,本人未用作商业用途。他人若未经允许,也不得使用以及参考本篇中提到的源码以及附件进行谋利,违者必究
本篇目的:
由于工作原因,很长时间没搞破解了。几个月前只是简单对某镖游戏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
Stack
...
...
* Hist
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
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"
.method public constructor <init>()V
.registers 1
.line 10
invoke - direct {p0}, Lcom / outfit7 / jinke / ChinaAdApplication; - ><init>()V
return - void
.end method
.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
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
最后于 2021-6-17 12:32
被whulzz编辑
,原因: