Anonymous 发表于 2023-3-14 21:03:19

Android逆向-脱壳APP

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版本的,当前下载为Android-arm64版本的根据自己的手机去选择下载好之后使用adb推送到手机上




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




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




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




继续安装x音漫客官网自行下载 adb install *.apk
4.猜测逻辑



可以看到有的是带这种有锁的,也就是开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(" hooking " + mtdName + ": " + listener_name);
    target.overloads.forEach(function (overload) {
      overload.implementation = function () {
            //send(" " + mtdName + ": " + getObjClassName(this));
            console.log(" " + mtdName + ": " + getObjClassName(this))
            return this.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版本



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

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




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




这里可以看出来是百度加固(碰到的多了而已),那么我们先去脱下壳了
6.脱壳
这里对于壳的概念就不多做解释了,我们直接上frida-dexdump即可,frida-dexdump链接(https://github.com/hluwa/frida-dexdump)

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




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




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




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



这里我们就可以看到了,双击过去看看他干了什么
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();
                }
            }



可以看到不管什么情况都会走到这个函数我们进去看看
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 关键函数


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 运行看看




可以看到原始返回是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)





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






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

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去排除




页: [1]
查看完整版本: Android逆向-脱壳APP