APP逆向分析-以某music软件逆向为例
Sherlockhuman 发表于 山东 移动安全 2579浏览 · 2024-01-09 08:08

** music?定叫他有来无回!

前置准备

  • 你的脑子

  • 安卓9的夜神模拟器(照顾下没有工作机的家人们,同时又因为我的工作机也很烂,索性模拟器了)

  • Pixel1(后续so层需要)

  • Frida

  • Jadx

  • Charles(Burp也行)

  • SocksDroid(Postern也行)

  • **音乐APP(截止到8月17日凌晨4:30分,最新版本的11.7.0)

  • 如果侵犯到贵公司利益,请迅速与我联系删除,本文只为学习,请遵循法律

佐证:

抓包

俗话说,不会抓包,三天之内*了你,*灰给你扬咯。

抓包是辅助我们迅速确定逆向目标的必备技能,后期会专门出一篇文章进行详细教学。别急,着急容易短命。

  • 搞到自己的IP地址,cmd里面ipconfig可以,用Charles帮助里面查看IP也行

  • 打开SocksDroid,输入自己的ip和配置好的端口号

  • 打开,抓包即可,本APP并没有采用诸如SSL Pinning或者双向验证之类的防抓包,整体还是比较顺利的。

  • 我们将对登录下手,选择账号密码登录,随便输入即可,接着登录抓到包。

:::
中途会有图片验证码验证,不必理会,后续你会发现,就算不验证也不影响抓包
:::

  • 得到我们想要的结果,signature等所需就在其中。

解决root提醒

细心的家人们会发现,在我们进入APP后,会显示“当前处于root环境,请注意账号安全”

虽然只是提醒,对后期的工作没有影响,但还是满烦人的,root那么好,我不允许凶root捏!

我们打开jadx,反编译APP,搜索“当前处于root环境,请注意账号安全”,找到代码逻辑。

逻辑很清楚,如果br.bu,那么弹窗“当前处于root环境,请注意账号安全”,主要看bu的返回值咯。

摁住“ctrl(啃臭)”键,单击bu,即可跳转到对应的函数

逻辑或运算,两个判断条件其中有一个成立最终的结果就是true,同时要注意,只要满足第一个条件,后面的条件就不再判断,可巧妙判断设备没有root。

两种方法:

  • hook bu函数,令其返回所需的定值

  • hook bF函数和bG函数,令其返回所需的定值

代码如下:

function kugousign(){

Java.perform(function (){
    //检测root,虽然这个检测对咱们后续工作没什么影响
    let br = Java.use("com.kugou.common.utils.br");
    br["bu"].implementation = function () {
        console.log('bu is called');
        let ret = this.bu();
        console.log('bu ret value is ' + ret);
        return false;
    };

    br["bF"].implementation = function () {
        console.log('bF is called');
        let ret = this.bF();
        console.log('bF ret value is ' + ret);
        return false;
    };



    br["bG"].implementation = function () {
        console.log('bG is called');
        let ret = this.bG();
        console.log('bG ret value is ' + ret);
        return false;
    };
})
}
setImmediate(kugousign);

兴高采烈注入,出事儿了。

Frida检测绕过

本文主要使用的调试工具是frida,好④不④这APP非常叛逆,居然擅自检测了frida,每次进行代码注入都会闪退。

于是简单分析了下,发现是ptrace占坑检测frida(后续单独出文章讲),可以简单理解为:frida附加进程需要占个坑,而APP自己生出了个子进程提前附加在父进程上,把坑跟先一步占了。

绕过不难。

先启动frida,按兵不动。

接着打出如下命令,用于frida的进程附加。

frida -U -f com.kugou.android  -l D:\new\jdong\sign.js

之后根据提醒输入

%resume

这样就绕过检测了,也不乱叫了。

Signature分析

定位方法后续出文章讲解,本文重点不在此处。

最终定位到"com.kugou.common.useraccount.b.u"

分析可知signature来自于a函数

在此时,为了确定我们寻找的位置是否正确,可以用frida验证。

let w = Java.use("com.kugou.common.network.w");
w["a"].overload('java.lang.String', 'java.util.Map', 'java.lang.String').implementation = function (str, map, str2) {
    console.log('a is called' + ', ' + 'str: ' + str + ', ' + 'map: ' + map + ', ' + 'str2: ' + str2);
    let ret = this.a(str, map, str2);
    console.log('a ret value is ' + ret);
    return ret;
};


frida反馈

Charles抓包反馈

可得signature值是一样的,同时获得了入参。

定值arg[0]:OIlwieks28dk2k092lksi2UIkp

arg[1]:map数组

:::
JSON.stringify(map) 看看是什么数组

则用frida打印一眼定真

var HashMap = Java.use('java.util.HashMap');
console.log("map:" + Java.cast(map, HashMap).toString());


:::

arg[2]:老长一大堆

我们多抓包打印两次对比参数是否固定,可得:

arg[2]中的参数固定的有:"support_third","dev"(手机的型号),"plat","support_multi","support_verify","gitversion","t3"

可变的参数为:"params","clienttime_ms"(时间戳),"dfid","pk","t1","t2","key","username"

pk

跳转到ICON_PK一看,是

那么就关心下列算法即可:

先把容易的jSONObject.toString()解决吧。

let h = Java.use("com.kugou.common.useraccount.utils.h");
    h["a"].overload('java.lang.String', 'java.lang.String').implementation = function (str, str2) {
        console.log('a is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2);
        let ret = this.a(str, str2);
        console.log('ax ret value is ' + ret);
        return ret;
    };

由图可见jSONObject内只有两个内容,一个clienttime_ms,一个key。

clienttime_ms来自a2:,很显然是时间戳。

key来自a3:,简单分析:

a3来自a方法,a方法如下(里面很多a方法,可以用ctrl+所要跳转的方法,一般能快速定位,并通过frida验证):

再看a方法中的a方法:

看看OS_UNKNOWN是什么:

综上,第一个a方法调用java中加密的api生成128位aes密钥,再传入第二个a方法作为参数,经过遍历和与'-1'的按位与异或,去掉字节b2高位的符号位信息,保留低8位二进制的数据,再将操作后的数据以Integer.toHexString()方法转换为16进制,若结果长度为1,则前面补0,否则转换为小写,拼接到新建的stringBuffer中,返回字符串。由于先前每个字节都以两位十六进制表示,所以算法结果长度为字节数组的两倍。所得的stringBuffer字符串又回到第一个a方法中,利用subString方法截取前16位为结果。

分析为第一个参数,再看看第二个。

遵循由内向外的原则,看看lq:

看看ConfigKey函数:

一眼定真,输入什么就输出什么''listen.usersdkparam.apprsa''

看看b函数:

应该是个定值,frida代码hook过后确定:

let g = Java.use("com.kugou.common.config.g");
    g["b"].overload('com.kugou.common.config.ConfigKey').implementation = function (configKey) {
        console.log('b is called' + ', ' + 'configKey: ' + configKey);
        let ret = this.b(configKey);
        console.log('b ret value is ' + ret);
        return ret;
    };

最后看看a函数:

如果str2不为空,则跳转到如下方法,获取公钥字节数组并生成公钥,保存到f145061a中:

生成字节数组算法c.b()如下,将字符串解码为字节数组:

之后调用hVar.a函数,获取公钥:

将字符串str经过getBytes方法转换为字节数组传入bArr这个新建的128位的空字节数组中,之后利用公钥和bArr数组经过下列方法获取私钥放置于a2中:

最后创建ba对象并且调用其中的a方法,传入私钥并去空格。

ba方法如下:

先将传入的byte数组检验是否为空,为空返回null,否则将新建初始化长度2倍于byte数组的StringBuilder来用于结果的拼接,之后遍历传入的byte数组,将字节右移四位与240按位于,获取高四位,与15按位与,获取低四位,并且将结果与f145776d查表

获取对应的base64字符,拼接到StringBuilder中转为字符串返回。

所以可以使用java代码还原pk算法,效果如下:

dfid

该参数存在于com.kugou.ktv.android.protocol.c.o类的k方法中。

进入cQ函数中查看:

调用a方法,传入两个字符串(key_device_finger_id_date)和(空字符串),结果存入a2,如果结果为空则返回f198358g否则返回a2。

f198358g貌似是:,未细究。

看看a方法:

经过frida代码分析,可得我们所需要的dfid来自于c方法:

let a = Java.use("com.kugou.common.preferences.a");
    a["c"].overload('int', 'java.lang.String').implementation = function (i, str) {
        console.log('c is called' + ', ' + 'i: ' + i + ', ' + 'str: ' + str);
        let ret = this.c(i, str);
        console.log('c ret value is ' + ret);
        return ret;
    };

传入定值为5,即i=5,跳进函数d:

跳进g函数,是字符串之间用'_'拼接:

分析到此处我发现不太对劲,貌似可以写死,于是作罢。

key

key参数生成在com.kugou.android.app.common.comment.b.i的b方法内。

完整方法为:

a2.a("key", new ba().a(br.a(Long.valueOf(br.as()), com.kugou.common.config.c.a().b(com.kugou.android.app.a.a.lp), Integer.valueOf(br.F(KGCommonApplication.getContext())), Long.valueOf(currentTimeMillis))));

层层分解为:

ba().a()

br.a()

Long.valueOf(br.as()), com.kugou.common.config.c.a().b(com.kugou.android.app.a.a.lp), Integer.valueOf(br.F(KGCommonApplication.getContext())), Long.valueOf(currentTimeMillis)

分析as()函数:

看lo的内容

再看外围的b函数:

使用frida打印b函数的结果:

let g = Java.use("com.kugou.common.config.g");
g["b"].overload('com.kugou.common.config.ConfigKey').implementation = function (configKey) {
    console.log('b is called' + ', ' + 'configKey: ' + configKey);
    let ret = this.b(configKey);
    console.log('b ret value is ' + ret);
    return ret;
};

获得定值:

第二个参数:

lp内容:

b函数和上一个一样内容:

frida打印获得结果:

第三个参数主要内容看F函数:

F函数的内容:

frida打印:

let br = Java.use("com.kugou.common.utils.br");
br["F"].overload('android.content.Context').implementation = function (context) {
    console.log('F is called' + ', ' + 'context: ' + context);
    let ret = this.F(context);
    console.log('F ret value is ' + ret);
    return ret;
};

获得结果,为定值:

最后一个参数为时间戳:

些许不同,该函数运行的结果是毫秒级别的,代码需要转换为秒级:

内部都分析完了,该br.a()函数了。

初步分析是简单的拼接,但出了小问题,在frida的过程中,莫名其妙打印不出来,比较担忧,为了确认,可以复现复现源代码。

package key;
public class keye {
    private static final String[] f145777e = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d","e" ,"f"};
    public static String b(Object... objArr) {
        StringBuffer stringBuffer = new StringBuffer();
        for (Object obj : objArr) {
            stringBuffer.append(obj);
        }
        if (false) {
            String str = "";
            for (Object obj2 : objArr) {
                str = str + obj2;
            }
            if (false) {
                d("gehu-getKeyRaw", "getKeyRaw 0:" + str);
            }
            if (false) {
                d("gehu-getKeyRaw", "getKeyRaw 1:" + stringBuffer.toString());
            }
        }
        return stringBuffer.toString();

    }




    public static void d(String str, String str2) {
        if (false) {
            String str3 = "";
            if (false) {
                if (str2 != null) {
                    str3 = str2 + a(4);
                }
                System.out.println("str ="+str);
                System.out.println("str3 ="+ str3);
                return;
            }
            if (str2 != null) {
                str3 = str2 + a(4);
            }
            System.out.println("str ="+str);
            System.out.println("str3 ="+ str3); 
        }
    }



    private static String a(byte b2) {
        int i = 0;
        if (b2 < 0) {
            i = b2 + 256;
        }
        return f145777e[i / 16] + f145777e[i % 16];
    }


    private static String a(int i2) {
        StackTraceElement[] stackTrace;
        if (false && (stackTrace = Thread.currentThread().getStackTrace()) != null && i2 >= 0 && i2 < stackTrace.length) {
            return "\n==> at " + stackTrace[i2];
        }
        return "";
    }
    public static void main(String[] args) {
        Object[] objArr = new Object[4];
        long currentTimeMillis = System.currentTimeMillis() / 1000;
        keye kk = new keye();
        long as = Long.valueOf(1005);
        String lp = "OIlwieks28dk2k092lksi2UIkp";
        int f = Integer.valueOf(11709);
        long time = Long.valueOf(currentTimeMillis);
        objArr[0] = as;
        objArr[1] = lp;
        objArr[2] = f;
        objArr[3] = time;
        String res = kk.b(objArr);
        System.out.println(res);


    }
}

打印发现:

最外围的a函数,是md5加密:

md5加密后的哈希字节数组传入c函数:

再遍历数组后将每个函数传入a方法内:

a方法将输入的字节表示为十六进制的字符串形式,将字节值拆分为两个十六进制字符,并以字符串形式返回它们拼接的结果。

params

看看a3是什么:

上文分析过,此处不再赘述

看第一个参数,先用frida打印:

let a = Java.use("com.kugou.common.useraccount.utils.a");
a["b"].overload('java.lang.String', 'java.lang.String').implementation = function (str, str2) {
    console.log('b is called' + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2);
    let ret = this.b(str, str2);
    console.log('b ret value is ' + ret);
    return ret;
};

这是未登录状态的参数:

跳入b函数:

看操作str和str2的a函数:

看c函数:

可知是MD5加密:

分别以该结果和该结果的后16个字节作为参数。

跳入a函数:

SecretKeySpec用于创建一个包含密钥的SecretKey对象。通过str3.getBytes(str2)将str3转换为指定字符编码Utf-8的字节数组,然后使用该字节数组生成密钥,再使用IvParameterSpec(str4.getBytes())用于设置初始化向量,最后doFinal加密,将结果传入a函数。

再到a函数内:

该函数上文也提及过,不再赘述。

t1和t2

t1和t2都在com.kugou.common.filemanager.protocol.o中。

分别是getToken和getMachineIdCode。

frida验证如下:

let NativeParams = Java.use("com.kugou.common.player.kugouplayer.NativeParams");
NativeParams["getToken"].implementation = function (obj) {
    console.log('getToken is called' + ', ' + 'obj: ' + obj);
    let ret = this.getToken(obj);
    console.log('getToken ret value is ' + ret);
    return ret;
};
NativeParams["getMachineIdCode"].implementation = function (obj) {
    console.log('getMachineIdCode is called' + ', ' + 'obj: ' + obj);
    let ret = this.getMachineIdCode(obj);
    console.log('getMachineIdCode ret value is ' + ret);
    return ret;
};

深入函数内部查看:

再深入:

再深入:

为了寻找到这两个函数在哪个so中,借用yang神的代码(在此处要换成pixel真机,否则无法跑出结果,后续代码也是以真机为准[同时也考虑到Unidbg只支持ARM架构]。t1的返回值在真机和模拟器是不同的,初步分析可能与uuid的不同有关,之后有时间再作讨论):

function find_RegisterNatives(params) {
    let symbols = Module.enumerateSymbolsSync("libart.so");
    let addrRegisterNatives = null;
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];

        //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
        if (symbol.name.indexOf("art") >= 0 &&
            symbol.name.indexOf("JNI") >= 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0 &&
            symbol.name.indexOf("CheckJNI") < 0) {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives)
        }
    }

}

function hook_RegisterNatives(addrRegisterNatives) {

    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                let java_class = args[1];
                let class_name = Java.vm.tryGetEnv().getClassName(java_class);
                //console.log(class_name);

                let methods_ptr = ptr(args[2]);

                let method_count = parseInt(args[3]);
                for (let i = 0; i < method_count; i++) {
                    let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                    let name = Memory.readCString(name_ptr);
                    let sig = Memory.readCString(sig_ptr);
                    let symbol = DebugSymbol.fromAddress(fnPtr_ptr)
                    console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr,  " fnOffset:", symbol, " callee:", DebugSymbol.fromAddress(this.returnAddress));
                }
            }
        });
    }
}

setImmediate(find_RegisterNatives);

可知是libj.so:

使用unidbg直接通杀:

package com.kugou;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;


public class videos extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final DvmClass NativeApi;  
    // 包名
    private final String packageName = "com.kugou.android";
    // apk 地址
    private final String packagePath = "D:\unidbg\unidbg-master\unidbg-android\src\test\resources\kugou\kugou11.7.0.apk";
    // so 名称, 要去掉 lib 和  .so
    //private final String libraryName = "j";
    // jni 类名
    private final String jniClassName = "com/kugou/common/player/kugouplayer/j";
    // 调试信息
    private final Boolean verbose = true;


    public videos() {

        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验

        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .setProcessName(packageName)
                .build();

        // 获取模拟器的内存操作接口
        Memory memory = emulator.getMemory();
        emulator.getSyscallHandler().setEnableThreadDispatcher(true);
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
         //创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作

        vm = emulator.createDalvikVM(new File(packagePath));
        vm.setJni(this);// 设置JNI
        vm.setVerbose(verbose);// 打印日志
          //加载目标SO
        DalvikModule dm_shared = vm.loadLibrary(new File("D:\unidbg\unidbg-master\unidbg-android\src\test\resources\kugou\libc++_shared.so"),true);
        dm_shared.callJNI_OnLoad(emulator);
        DalvikModule dm_crypto = vm.loadLibrary(new File("D:\unidbg\unidbg-master\unidbg-android\src\test\resources\kugou\libcrypto_kg.so"),true);
        dm_crypto.callJNI_OnLoad(emulator);
        DalvikModule dm = vm.loadLibrary(new File("D:\unidbg\unidbg-master\unidbg-android\src\test\resources\kugou\libj.so"), true);
        NativeApi = vm.resolveClass(jniClassName);
        dm.callJNI_OnLoad(emulator);

    }
    public String calls(){
        Object arg1 = null ;
        String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "_d(Ljava/lang/Object;)Ljava/lang/String;", arg1).getValue().toString();
        return ret;
    }
    public String callss(){
        Object arg2 = null ;
        String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "_e(Ljava/lang/Object;)Ljava/lang/String;", arg2).getValue().toString();
        return ret;
    }
    public static void main(String[] args) {
        videos vs = new videos();
        String result1 = vs.calls();
        String result2 = vs.callss();
        System.out.println("call _d result:"+result1);
        System.out.println("call _e result:"+result2);
    }

}

过程并非一帆风顺,自然要补些环境。

使用frida代码查看结果:

let SecretAccess = Java.use("com.kugou.common.utils.SecretAccess");
    SecretAccess["getAndroidId"].implementation = function () {
        console.log('getAndroidId is called');
        let ret = this.getAndroidId();
        console.log('getAndroidId ret value is ' + ret);
        return ret;
    };

在unidbg提供的报错示例代码中:

依葫芦画瓢即可:

又报错:

老方法:

let SecretAccess = Java.use("com.kugou.common.utils.SecretAccess");
    SecretAccess["getSafeDeviceId"].implementation = function () {
        console.log('getSafeDeviceId is called');
        let ret = this.getSafeDeviceId();
        console.log('getSafeDeviceId ret value is ' + ret);
        return ret;
    };

继续报错:

是系统函数,作用是查看设备是什么玩意,我们随便给定义一个得了:

跑出了结果:
但这样肯定不行直接修改AbstractJNI的代码会“污染”环境。
我们选取继承:

package com.kugou;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;



public class videos extends AbstractJni {


private final AndroidEmulator emulator;
private final VM vm;
private final DvmClass NativeApi;

// 包名
private final String packageName = "com.kugou.android";
// apk 地址
private final String packagePath = "D:\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\resources\\kugou\\kugou11.7.0.apk";
// so 名称, 要去掉 lib 和  .so
//private final String libraryName = "j";
// jni 类名
private final String jniClassName = "com/kugou/common/player/kugouplayer/j";
// 调试信息
private final Boolean verbose = true;



public videos() {

    // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验

    emulator = AndroidEmulatorBuilder
            .for32Bit()
            .addBackendFactory(new Unicorn2Factory(true))
            .setProcessName(packageName)
            .build();

    // 获取模拟器的内存操作接口
    Memory memory = emulator.getMemory();
    emulator.getSyscallHandler().setEnableThreadDispatcher(true);
    // 设置系统类库解析
    memory.setLibraryResolver(new AndroidResolver(23));
     //创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作


    vm = emulator.createDalvikVM(new File(packagePath));
    vm.setJni(this);// 设置JNI
    vm.setVerbose(verbose);// 打印日志
      //加载目标SO
    DalvikModule dm_shared = vm.loadLibrary(new File("D:\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\resources\\kugou\\libc++_shared.so"),true);
    dm_shared.callJNI_OnLoad(emulator);
    DalvikModule dm_crypto = vm.loadLibrary(new File("D:\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\resources\\kugou\\libcrypto_kg.so"),true);
    dm_crypto.callJNI_OnLoad(emulator);
    DalvikModule dm = vm.loadLibrary(new File("D:\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\resources\\kugou\\libj.so"), true);
    NativeApi = vm.resolveClass(jniClassName);
    dm.callJNI_OnLoad(emulator);

}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    switch (signature){
        case "com/kugou/common/utils/SecretAccess->getAndroidId()Ljava/lang/String;": {

            return new StringObject(vm,"5c083d1045985cf2");

        }
        case "com/kugou/common/utils/SecretAccess->getSafeDeviceId()Ljava/lang/String;":{
            return new StringObject(vm,"null");
        }

    }
    return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature){
        case "android/os/Build->MODEL:Ljava/lang/String;":
            return new StringObject(vm,"Xiaomi");

    }
    throw new UnsupportedOperationException(signature);
}




public String calls(){
    String args1 = "com.kugou.common.q.b@93e301";
    Object arg1 = (Object) args1 ;
    String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "_d(Ljava/lang/Object;)Ljava/lang/String;", arg1).getValue().toString();
    return ret;
}
public String callss(){
    String args2 = "com.kugou.common.q.b@93e301";
    Object arg2 = (Object) args2;
    System.out.println(arg2.getClass().getSimpleName());
    String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "_e(Ljava/lang/Object;)Ljava/lang/String;", arg2).getValue().toString();
    return ret;
}



public static void main(String[] args) {
    videos vs = new videos();
    String result1 = vs.calls();
    String result2 = vs.callss();
    System.out.println("call _d result:"+result1);
    System.out.println("call _e result:"+result2);
}
}

最后一脚

跳入a方法:

跳入b方法:

使用frida去hook一下a方法就可以知道str4是何内容,以及b方法的作用(推测为排序加拼接---keySet方法的升序排列,可能将map内的键值对位置打乱,将appid首发):

let ba = Java.use("com.kugou.common.utils.ba");
    ba["a"].overload('java.lang.String').implementation = function (str) {
        console.log('ax is called' + ', ' + 'strx: ' + str);
        let ret = this.a(str);
        console.log('ax ret value is ' + ret);
        return ret;
    };

得到结果:

可以验证。

跳入a方法,直接就是没有任何套路的md5加密:

验证确实如此:

尾声:诸位能看到此,诚惶诚恐。小生愚钝,斗胆将自己练手的过程写作文章,恐贻笑大方,但也望能在佬们的批评中汲取教训,为后续在逆向路上有所小成寻个因果。

1 条评论
某人
表情
可输入 255