【安卓逆向】一款ai软件的会员功能的解锁与逆向分析

软件名:QUnlhpnkvZznjL8= 😃

先来看一下这个软件的情况

这四个功能都是要vip的,终身会员要168!!!😂,可以,有搞头

用mt管理器看看情况

梆梆加固,脱壳修复不仅麻烦,还有签名校验等问题,这里没有仔细研究,这里使用frida进行hook

第一步肯定是要拿到dex文件,这里使用dexdump.js hook进行脱壳

'use strict';

/**
 * Author: guoqiangck
 * Create: 2019/6/11
 * Dump dex file for packaged apk
 * Hook art/runtime/dex_file.cc OpenMemory or OpenCommon
 * Support Version: Android 4.4 and later versions
 */

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds()

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;

    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    console.log("[" + time + "] " + log);
}

function getAndroidVersion(){
    var version = 0;

    if(Java.available){
        var versionStr = Java.androidVersion;
        version = versionStr.slice(0,1);
    }else{
        LogPrint("Error: cannot get android version");
    }
    LogPrint("Android Version: " + version);
    return version;
}

function getFunctionName(){
    var i = 0;
    var functionName = "";

    // Android 4: hook dvmDexFileOpenPartial
    // Android 5: hook OpenMemory
    // after Android 5: hook OpenCommon
    if(getAndroidVersion() > 4){ // android 5 and later version
        var artExports =  Module.enumerateExportsSync("libart.so");
        for(i = 0; i< artExports.length; i++){
            if(artExports[i].name.indexOf("OpenMemory") !== -1){
                functionName = artExports[i].name;
                LogPrint("index " + i + " function name: "+ functionName);
                break;
            }else if(artExports[i].name.indexOf("OpenCommon") !== -1){
                functionName = artExports[i].name;
                LogPrint("index " + i + " function name: "+ functionName);
                break;
            }
        }
    }else{ //android 4
        var dvmExports =  Module.enumerateExportsSync("libdvm.so");
        if(dvmExports.length !== 0){  // check libdvm.so first
            for(i = 0; i< dvmExports.length; i++){
                if(dvmExports[i].name.indexOf("dexFileParse") !== -1){
                    functionName = dvmExports[i].name;
                    LogPrint("index " + i + " function name: "+ functionName);
                    break;
                }
            }
        }else{ // if not load libdvm.so, check libart.so
            dvmExports = Module.enumerateExportsSync("libart.so");
            for(i = 0; i< dvmExports.length; i++){
                if(dvmExports[i].name.indexOf("OpenMemory") !== -1){
                    functionName = dvmExports[i].name;
                    LogPrint("index " + i + " function name: "+ functionName);
                    break;
                }
            }
        }
    }
    return functionName;
}

function getProcessName(){
    var processName = "";

    var fopenPtr = Module.findExportByName("libc.so", "fopen");
    var fopenFunc = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);
    var fgetsPtr = Module.findExportByName("libc.so", "fgets");
    var fgetsFunc = new NativeFunction(fgetsPtr, 'int', ['pointer', 'int', 'pointer']);
    var fclosePtr = Module.findExportByName("libc.so", "fclose");
    var fcloseFunc = new NativeFunction(fclosePtr, 'int', ['pointer']);

    var pathPtr = Memory.allocUtf8String("/proc/self/cmdline");
    var openFlagsPtr = Memory.allocUtf8String("r");

    var fp = fopenFunc(pathPtr, openFlagsPtr);
    if(fp.isNull() === false){
        var buffData = Memory.alloc(128);
        var ret = fgetsFunc(buffData, 128, fp);
        if(ret !== 0){
            processName = Memory.readCString(buffData);
            LogPrint("processName " + processName);
        }
        fcloseFunc(fp);
    }
    return processName;
}

function arraybuffer2hexstr(buffer)
{
    var hexArr = Array.prototype.map.call(
        new Uint8Array(buffer),
        function (bit) {
            return ('00' + bit.toString(16)).slice(-2)
        }
    );
    return hexArr.join(' ');
}

function checkDexMagic(dataAddr){
    var magicMatch = true;
    var magicFlagHex = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00];

    for(var i = 0; i < 8; i++){
        if(Memory.readU8(ptr(dataAddr).add(i)) !== magicFlagHex[i]){
            magicMatch = false;
            break;
        }
    }

    return magicMatch;
}

function checkOdexMagic(dataAddr){
    var magicMatch = true;
    var magicFlagHex = [0x64, 0x65, 0x79, 0x0a, 0x30, 0x33, 0x36, 0x00];

    for(var i = 0; i < 8; i++){
        if(Memory.readU8(ptr(dataAddr).add(i)) !== magicFlagHex[i]){
            magicMatch = false;
            break;
        }
    }

    return magicMatch;
}

function dumpDex(moduleFuncName, processName){
    if(moduleFuncName !== ""){
        var hookFunction;
        if(getAndroidVersion() > 4){ // android 5 and later version
            hookFunction = Module.findExportByName("libart.so", moduleFuncName);
        }else{ // android 4
            hookFunction = Module.findExportByName("libdvm.so", moduleFuncName);  // check libdvm.so first
            if(hookFunction == null) {
                hookFunction = Module.findExportByName("libart.so", moduleFuncName); //// if not load libdvm.so, check libart.so
            }
        }
        Interceptor.attach(hookFunction,{
            onEnter: function(args){
                var begin = 0;
                var dexMagicMatch = false;
                var odexMagicMatch = false;

                dexMagicMatch = checkDexMagic(args[0]);
                if(dexMagicMatch === true){
                    begin = args[0];
                }else{
                    odexMagicMatch = checkOdexMagic(args[0]);
                    if(odexMagicMatch === true){
                        begin = args[0];
                    }
                }

                if(begin === 0){
                    dexMagicMatch = checkDexMagic(args[1]);
                    if(dexMagicMatch === true){
                        begin = args[1];
                    }else{
                      odexMagicMatch = checkOdexMagic(args[1]);
                      if(odexMagicMatch === true){
                          begin = args[1];
                      }
                    }
                }

                if(dexMagicMatch === true){
                    LogPrint("magic : " + Memory.readUtf8String(begin));
                    //console.log(hexdump(begin, { offset: 0, header: false, length: 64, ansi: false }));
                    var address = parseInt(begin,16) + 0x20;
                    var dex_size = Memory.readInt(ptr(address));
                    LogPrint("dex_size :" + dex_size);
                    var dex_path = "/data/data/" + processName + "/" + dex_size + ".dex";
                    var dex_file = new File(dex_path, "wb");
                    dex_file.write(Memory.readByteArray(begin, dex_size));
                    dex_file.flush();
                    dex_file.close();
                    LogPrint("dump dex success, saved path: " + dex_path + "\n");
                }else if(odexMagicMatch === true){
                    LogPrint("magic : " + Memory.readUtf8String(begin));
                    //console.log(hexdump(begin, { offset: 0, header: false, length: 64, ansi: false }));
                    var address = parseInt(begin,16) + 0x0C;
                    var odex_size = Memory.readInt(ptr(address));
                    LogPrint("odex_size :" + odex_size);
                    var odex_path = "/data/data/" + processName + "/" + odex_size + ".odex";
                    var odex_file = new File(odex_path, "wb");
                    odex_file.write(Memory.readByteArray(begin, odex_size));
                    odex_file.flush();
                    odex_file.close();
                    LogPrint("dump odex success, saved path: " + odex_path + "\n");
                }
            },
            onLeave: function(retval){
            }
        });
    }else{
	    LogPrint("Error: cannot find correct module function.");
    }
}

//start dump dex file
var moduleFucntionName = getFunctionName();
var processName = getProcessName();
if(moduleFucntionName !== "" && processName !== ""){
    dumpDex(moduleFucntionName, processName);
}

这里用小黑盒脱壳也行,脱出来的dex文件拖进jadx进行下一步的分析,搜索getvip关键字,锁定到该代码块

哦豁,关键代码应该是这里了,看主要代码

 UserBizInfo.m68548();
            cryptographicUtils.setUserId(UserBizInfo.f35141, requireActivity());
            this.mVipTimeTv.setVisibility(0);
            String str = UserBizInfo.m68548().f35148;
            if (str != null && !str.isEmpty() && !str.equals("null")) {
                GlideLoader.m33806(getActivity()).m33809().m33813(str).m33814(this.mHeadIv);
            }
            String vipStatus = CommonSetting.instance().getVipStatus();
            this.f10222 = vipStatus;
            if (StringUtil.isEmpty(vipStatus)) {
                this.mVipTimeTv.setText("开通会员尊享高级特权");
                if (this.f34783.contains(this.f34787)) {
                    return;
                }
                this.f34783.add(0, this.f34787);
                this.f10223.notifyDataSetChanged();
                return;
            } else if (this.f10222.equals("1")) {
                String vipEndTime = CommonSetting.instance().getVipEndTime();
                if (StringUtil.isEmpty(vipEndTime) || vipEndTime.length() <= 9) {
                    return;
                }
                int parseInt = Integer.parseInt(vipEndTime.substring(0, 4));
                if (parseInt - Integer.parseInt(C5753.m33868((System.currentTimeMillis() / 1000) + "", "yyyy")) > 30) {
                    this.mVipTimeTv.setText("终身会员");
                } else {
                    TextView textView2 = this.mVipTimeTv;
                    textView2.setText("会员到期时间:" + vipEndTime.substring(0, 10));
                }
                if (this.f34783.contains(this.f34787)) {
                    this.f34783.remove(this.f34787);
                    this.f10223.notifyDataSetChanged();
                    return;
                }
                return;
            } else {
                this.mVipTimeTv.setText("开通会员尊享高级特权");
                if (this.f34783.contains(this.f34787)) {
                    return;
                }
                this.f34783.add(0, this.f34787);
                this.f10223.notifyDataSetChanged();
                return;
            }

这里逻辑就是通过

String vipStatus = CommonSetting.instance().getVipStatus();

获取vip状态,我们点进去getVipStatus方法进行查看

这里返回值根据上述代码可以确定,返回值如果是字符串"1"的话,根据

lse if (this.f10222.equals("1")) {

                String vipEndTime = CommonSetting.instance().getVipEndTime();

                if (StringUtil.isEmpty(vipEndTime) || vipEndTime.length() <= 9) {

                    return;

                }

                int parseInt = Integer.parseInt(vipEndTime.substring(0, 4));

                if (parseInt - Integer.parseInt(C5753.m33868((System.currentTimeMillis() / 1000) + "", "yyyy")) > 30) {

                    this.mVipTimeTv.setText("终身会员");

                } else {

                    TextView textView2 = this.mVipTimeTv;

                    textView2.setText("会员到期时间:" + vipEndTime.substring(0, 10));

                }

可以判断是会员,那我们可以hook getVipStatus

hook代码如下:

function hook(){
    let CommonSetting = Java.use("com.shututek.original.utils.CommonSetting");
    CommonSetting["getVipStatus"].implementation = function () {
        console.log(`CommonSetting.getVipStatus is called`);
        let result = this["getVipStatus"]();
        console.log(`CommonSetting.getVipStatus result=${result}`);
        //result = "1"
        //console.log(`CommonSetting.getVipStatus result=${result}`);
        return result;
    };
}

function main(){
    Java.perform(function(){
        hook()
    })
}

setImmediate(main)

process terminated 有frida检测,我们来看看检测点是在哪里

可以发现是加载了libSecShell.so之后,进程被杀掉,拖进ida分析一下,发现程序没有新开辟一个进程进行检测

那就先试试attach模式下可不可以注入吧,先启动程序,然后Frida -UF,发现

成功注入,程序应该就是程序启动的时候有检测点😂

那就frida -UF -l hook.js attach模式注入脚本

点击需要会员功能的时候发现返回值是-1,这里返回值直接改成1试试,再次点击会员功能,发现可以成功进入!!!😍

至此,会员功能解锁完毕,hook脚本如下:

function hook(){
    let CommonSetting = Java.use("com.shututek.original.utils.CommonSetting");
    CommonSetting["getVipStatus"].implementation = function () {
        console.log(`CommonSetting.getVipStatus is called`);
        let result = this["getVipStatus"]();
        console.log(`CommonSetting.getVipStatus result=${result}`);
        result = "1"
        console.log(`CommonSetting.getVipStatus result=${result}`);
        return result;
    };
}


function main(){
    Java.perform(function(){
        hook()
    })
}


setImmediate(main)

如何想要方便的话,可以把frida脚本改写成xp代码进行hook😍,后面再补充

 

posted @ 2024-04-15 23:03  GGBomb  阅读(422)  评论(1编辑  收藏  举报