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]