Android逆向-脱壳APP

admin 2023年3月14日18:09:09评论150 views字数 8127阅读27分5秒阅读模式

1.需要环境

kali环境 / 其他Linux / windows / macOSpython3.8.0frida12.8.0     / Android 8以下用这个版本 高版本直接最新frida server 12.8.0frida-tools 5.3.0objection 1.8.4Redmi5A ROM:PixelExperience / 其他root的均可x音漫客 app 最新版

2.kali frida 环境搭建

  • 首先安装python (python3.8.0)

  • 安装frida 12.8.0

    • pip install frida==12.8.0

  • frida-tools 5.3.0

    • pip install frida-tools==5.3.0

  • objection 1.8.4

    • pip install objection==1.8.4

3.手机安装kali frida server 12.8.0

打开 frida12.8.0

Android逆向-脱壳APP

这里我们要下载Android版本的,当前下载为Android-arm64版本的根据自己的手机去选择下载好之后使用adb推送到手机上

Android逆向-脱壳APP

我们使用adb进入手机运行frida server

Android逆向-脱壳APP

解压之后改个名字然后需要赋予执行权限 chmod 777 fr12.8.0

Android逆向-脱壳APP

./fr12.8.0 -D 后台运行frida server

Android逆向-脱壳APP

继续安装x音漫客官网自行下载 adb install *.apk

4.猜测逻辑

Android逆向-脱壳APP

可以看到有的是带这种有锁的,也就是开VIP后可以看的,上面的那些则可以免费观看,可以猜测一下这里有个分支判断是否是vip然后走向不同的地方,那么我们hook所有的onClick函数看看是什么逻辑

5.验证逻辑

var jclazz = null;
var jobj = null;

function getObjClassName(obj) {
   if (!jclazz) {
       var jclazz = Java.use("java.lang.Class");
  }
   if (!jobj) {
       var jobj = Java.use("java.lang.Object");
  }
   return jclazz.getName.call(jobj.getClass.call(obj));
}

function watch(obj, mtdName) {
   var listener_name = getObjClassName(obj);
   var target = Java.use(listener_name);
   if (!target || !mtdName in target) {
       return;
  }
   // send("[WatchEvent] hooking " + mtdName + ": " + listener_name);
   target[mtdName].overloads.forEach(function (overload) {
       overload.implementation = function () {
           //send("[WatchEvent] " + mtdName + ": " + getObjClassName(this));
           console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this))
           return this[mtdName].apply(this, arguments);
      };
  })
}

function OnClickListener() {
   Java.perform(function () {

       //以spawn启动进程的模式来attach的话
       Java.use("android.view.View").setOnClickListener.implementation = function (listener) {
           if (listener != null) {
               watch(listener, 'onClick');
          }
           return this.setOnClickListener(listener);
      };

       //如果frida以attach的模式进行attch的话
       Java.choose("android.view.View$ListenerInfo", {
           onMatch: function (instance) {
               instance = instance.mOnClickListener.value;
               if (instance) {
                   console.log("mOnClickListener name is :" + getObjClassName(instance));
                   watch(instance, 'onClick');
              }
          },
           onComplete: function () {
          }
      })
  })
}
setImmediate(OnClickListener);
使用frida来运行一下 如果错误请换frida版本

Android逆向-脱壳APP

运行之后点击带锁的按钮会出现cn.zymk.comic.ui.adapter.DirectoryPictureAdapter.1

类名:cn.zymk.comic.ui.adapter.DirectoryPictureAdapter匿名函数:$1我们用jadx反编译来看下

Android逆向-脱壳APP

发现并没有搜到我们想要的匿名函数或者这个类

Android逆向-脱壳APP

这里可以看出来是百度加固(碰到的多了而已),那么我们先去脱下壳了

6.脱壳

这里对于壳的概念就不多做解释了,我们直接上frida-dexdump即可,frida-dexdump链接(https://github.com/hluwa/frida-dexdump)

pip install frida-dexdumpfrida-dexdump -FU 使用方式跟frida一样

Android逆向-脱壳APP

这里注意一点 -FU 是前置窗口也就是手机当前打开的窗口 如果不是当前窗口请用frida-dexdump -u -f 包名

Android逆向-脱壳APP

生成了很多的dex 我们过滤下带有MainActivity

Android逆向-脱壳APP

可以看出来08带有MainActivity且是最大的我们直接 jadx classes08.dex 然后在在里面搜一下我们刚刚的那个类

7.继续分析原理

Android逆向-脱壳APP

这里我们就可以看到了,双击过去看看他干了什么

public void onClick(final View view3) {
               Utils.noMultiClick(view3);
               if (chapterListItemBean.level > 0 && chapterListItemBean.is_release == 0) {
                   PriorityCouponUseDialog.Builder builder = new PriorityCouponUseDialog.Builder(DirectoryPictureAdapter.this.mContext, PriorityCouponUseDialog.DialogType.CLOSE);
                   int i2 = chapterListItemBean.level;
                   PriorityCouponUseDialog show = builder.setChapterData(i2, chapterListItemBean.chapter_name + " " + chapterListItemBean.chapter_title, chapterListItemBean.chapter_id, DirectoryPictureAdapter.this.comicBean.comic_id, DirectoryPictureAdapter.this.comicBean.comic_name, chapterListItemBean.create_time).setOnActionListener(new PriorityCouponUseDialog.OnActionListener() { // from class: cn.zymk.comic.ui.adapter.DirectoryPictureAdapter.1.1
                       @Override // cn.zymk.comic.view.dialog.PriorityCouponUseDialog.OnActionListener
                       public void onClickBack() {
                      }

                       @Override // cn.zymk.comic.view.dialog.PriorityCouponUseDialog.OnActionListener
                       public void onUseCouponFailed() {
                      }

                       @Override // cn.zymk.comic.view.dialog.PriorityCouponUseDialog.OnActionListener
                       public void onUseCouponSuccess() {
                           chapterListItemBean.is_release = 1;
                           DirectoryPictureAdapter.this.notifyDataSetChanged();
                      }
                  }).show();
                   if (DirectoryPictureAdapter.this.mContext instanceof BaseActivity) {
                       DirectoryPictureAdapter.this.mContext.setPriorityCouponUseDialog(show);
                       return;
                  }
                   return;
              }
               int netType = PhoneHelper.getInstance().getNetType();
               if (netType <= 1 || netType > 4) {
                   DirectoryPictureAdapter.this.jump2ReadPage(view3, chapterListItemBean);
              } else {
                   new CustomDialog.Builder(DirectoryPictureAdapter.this.mActivity).setMessage(R.string.no_wifi).setPositiveButton((CharSequence) DirectoryPictureAdapter.this.mActivity.getString(R.string.kown_no_wifi), true, new CanDialogInterface.OnClickListener() { // from class: cn.zymk.comic.ui.adapter.DirectoryPictureAdapter.1.2
                       public void onClick(CanBaseDialog canBaseDialog, int i3, CharSequence charSequence, boolean[] zArr) {
                           DirectoryPictureAdapter.this.jump2ReadPage(view3, chapterListItemBean);
                      }
                  }).setNegativeButton(R.string.cancel, true, (CanDialogInterface.OnClickListener) null).show();
              }
          }

Android逆向-脱壳APP

可以看到不管什么情况都会走到这个函数我们进去看看

public void jump2ReadPage(View view, ChapterListItemBean chapterListItemBean) {
       ComicBean comicBean;
       UserBean userBean = App.getInstance().getUserBean();
       if (this.comicBean != null && userBean != null && chapterListItemBean.is_vip == 1 && !Utils.isVip(userBean.isvip)) {
           PayChapterActivity.startActivity((Activity) this.mActivity, this.comicBean.comic_name, this.comicBean.comic_id, chapterListItemBean, Constants.ACTION_BOUGHT_SUCCESS_FROM_DETAIL_2_READ);
      } else if (chapterListItemBean.isRecharge || chapterListItemBean.price <= 0 || (comicBean = this.comicBean) == null) {
           gotoReadPage(view, chapterListItemBean);
      } else if (userBean != null) {
           a.e("userBean.isvip" + userBean.isvip);
           if (Utils.isVip(userBean.isvip) && Utils.chapterChargeVip(chapterListItemBean)) {
               gotoReadPage(view, chapterListItemBean);
          } else if (SetConfigBean.getAutoBuy(this.mActivity)) {
               this.mActivity.buyThisChapter(chapterListItemBean);
          } else {
               PayChapterActivity.startActivity((Activity) this.mActivity, this.comicBean.comic_name, this.comicBean.comic_id, chapterListItemBean, Constants.ACTION_BOUGHT_SUCCESS_FROM_DETAIL_2_READ);
          }
      } else {
           PayChapterActivity.startActivity((Activity) this.mActivity, comicBean.comic_name, this.comicBean.comic_id, chapterListItemBean, Constants.ACTION_BOUGHT_SUCCESS_FROM_DETAIL_2_READ);
      }
  }

这里的代码就比较清晰了,可以看到很多的PayChapterActivity.startActivity,应该就是跳转到付款窗口那么我们就让Utils.isVip(userBean.isvip)这个函数返回true看看是否会走到gotoReadPage,这里我们需要写js的frida代码,需要安装nodenode链接

8.frida hook 关键函数

Android逆向-脱壳APP

function HookVip() {
   Java.use("cn.zymk.comic.utils.Utils").isVip.implementation = function (number) {
       // 先看看他原始返回了什么
       var result = this.isVip(number);
       console.log("result = ", result);
       return result;
  }
}

function main() {
   // 主要hook和java相关的函数都需要包含在Java.perform中
   Java.perform(() => {
       HookVip();
  })
}

setImmediate(main)

frida -UF -l zymk.js 运行看看

Android逆向-脱壳APP

可以看到原始返回是false 那么我们把他的返回值改了会怎么样呢

function HookVip() {
   Java.use("cn.zymk.comic.utils.Utils").isVip.implementation = function (number) {
       // 先看看他原始返回了什么
       // var result = this.isVip(number);
       // console.log("result = ", result);
       // return result;
       return true;
  }
}

function main() {
   Java.perform(() => {
       HookVip();
  })
}

setImmediate(main)

Android逆向-脱壳APP

Android逆向-脱壳APP

可以发现我们先跳到了内容界面然后里面,过了几秒又跳到了充值界面,那么这里试试直接把这个充值界面ret掉看看他能不能过去这个验证

Android逆向-脱壳APP

Android逆向-脱壳APP

可以发现已经可以正常的观看了

function HookVip() {
   Java.use("cn.zymk.comic.utils.Utils").isVip.implementation = function (number) {
       // 先看看他原始返回了什么
       // var result = this.isVip(number);
       // console.log("result = ", result);
       // return result;
       return true;
  }
}

function HookPayChapterActivity(){
   Java.use("cn.zymk.comic.ui.mine.PayChapterActivity").startActivity.overload('android.app.Activity', 'java.lang.String', 'java.lang.String', 'cn.zymk.comic.model.ChapterListItemBean', 'java.lang.String').implementation =
   function(activity, str, str2, chapterListItemBean, str3, z){
       return;
       
  }
}

function main() {
   Java.perform(() => {
       HookVip();
       HookPayChapterActivity();
  })
}

setImmediate(main)

9.总结

逆向的过程中要找到自己需要的关键点,然后跟着关键点去往下寻找找到突破口,在遇到加固或者混淆的时候尝试去dump下dex,如果遇到了函数抽取就要去主动调用下让他走一个过程再去dump,hook所有的onClick可以让我们更快的去定位到关键位置,如何去分辨dump的dex中哪个是我们想要的,可以看相关的Activity去排除


Android逆向-脱壳APP

丈八网安蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案以及靶场仿真复现等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。


Android逆向-脱壳APP

原文始发于微信公众号(蛇矛实验室):Android逆向-脱壳APP

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月14日18:09:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Android逆向-脱壳APPhttps://cn-sec.com/archives/1604452.html

发表评论

匿名网友 填写信息