** 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加密:
验证确实如此:
尾声:诸位能看到此,诚惶诚恐。小生愚钝,斗胆将自己练手的过程写作文章,恐贻笑大方,但也望能在佬们的批评中汲取教训,为后续在逆向路上有所小成寻个因果。