新年的第一篇文章,新的一年继续加油,奥利给!冲冲冲。
今天分析的app是 54ix5bqT5a2YX3Y2LjEuNg==
(base64解码),这次还是使用unidbg分析该样本,加密参数有很多,不过只关注sig
和sign
两个参数。
老规矩,上来先抓个包。
1.抓包
可以看到上面👆🏻的sign,就是本次研究的重点。
2.jadx静态分析
把样本app 拖到jadx里面,直接搜索关键词 "sign"
找了一圈,发现并没有找到可疑的加密点。
好吧,换个搜索关键词,我们继续搜,这次用它: sign=
感觉这里比较像,跟进去看看。
发现"&sign="
是由下面函数生成的,跟进去看看。
String signV3 = MXSecurity.signV3(sb2, substring, valueOf, Z1);
然后来到这里
可以看到这是一个native
方法 ,用到的so是mx
。
public static final native String signV3(@NotNull String str, @NotNull String str2, @NotNull String str3, @NotNull String str4);
注意这里还有一个init
方法,很重要 会儿会用到。
public static final native int init(@NotNull Context context, boolean z);
java层的静态分析到此就差不多结束了,接着要用frida动态分析下。
3. frida动态调试
打开frida,运行命令:
frida -U -f com.aikucun.akapp -l hook_sign.js --no-pause
Java.perform(function () {
var MXSecurity = Java.use("com.mengxiang.arch.security.MXSecurity");
MXSecurity["init"].implementation = function (context, z) {
console.log('init is called' + ', ' + 'context: ' + context + ', ' + 'z: ' + z);
var ret = this.init(context, z);
console.log('init ret value is ' + ret);
return ret
};
MXSecurity["signV1"].implementation = function (str, str2, str3) {
console.log("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
console.log('signV1 is called!');
console.log('str: ' + str);
console.log('str2: ' + str2);
console.log('str3: ' + str3);
var ret = this.signV1(str, str2, str3);
console.log('signV1 ret value is ' + ret);
console.log("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
return ret
};
MXSecurity["signV2"].implementation = function (str) {
console.log('signV2 is called' + ', ' + 'str: ' + str);
var ret = this.signV2(str);
console.log('signV2 ret value is ' + ret);
return ret
};
MXSecurity["signV3"].implementation = function (str, str2, str3, str4) {
console.log("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
console.log('signV3 is called!');
console.log('str: ' + str);
console.log('str2: ' + str2);
console.log('str3: ' + str3);
console.log('str4: ' + str4);
var ret = this.signV3(str, str2, str3, str4);
console.log('signV3 ret value is ' + ret);
console.log("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
return ret
};
});
//com.aikucun.akapp
frida hook结果:
init:
init is called, context: com.aikucun.akapp.AppContext@49e2aa7, z: false
init ret value is 0
signV1 :
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
signV1 is called!
str: https://zuul.aikucun.com/aggregation-center-facade/api/app/search/product/image/switch?appid=38741001&did=f332c6946049c0584a192aa540a61f57&noncestr=f6e9ee&subuserid=52920da9e265d5e2353390b7e9e6989e×tamp=1672303951&token=8dfca42e59f04cdd8879b68a0ab2bb38&userid=52920da9e265d5e2353390b7e9e6989e&zuul=1
str2: f6e9ee
str3: 1672303951
signV1 ret value is ad3ce16e5bb8c2d6642a6e7b9bdda168d1907a52
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
signV3:
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
signV3 is called!
str: https://zuul.aikucun.com/aggregation-center-facade/api/app/product/search/v2.0?appid=38741001&did=f332c6946049c0584a192aa540a61f57&noncestr=401513&subuserid=52920da9e265d5e2353390b7e9e6989e&svs=v3×tamp=1672296424&token=8dfca42e59f04cdd8879b68a0ab2bb38&userId=52920da9e265d5e2353390b7e9e6989e&userid=52920da9e265d5e2353390b7e9e6989e
str2: 401513
str3: 1672296424
str4: b421a226205b0c9b86f018ab3ce56337
signV3 ret value is 6a3d26747e23b15ae8ad97b9871fcb5c3ab91b8caa5eb783a4b83b611b39190a
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
这里我们hook了三个方法,可以看到每个方法的入参和返回值都不一样。
4.so层分析
把libmx.so
拖进ida中,在导出表中看到了对应的函数,这是一个静态注册。
其中signV3和signV1都是本次要关注的重点。
先看signV1
它这里拼接了固定的一些参数appid
, noncestr
,timestamp
,secret
,然后再调用了digest()
方法,加密用的是哈希中的sha1
算法。
跟进去再看看
进来之后就是直接反射调用 java 层的 hash
哈希算法,不过在 so 文件里加了个 secrer
参数。这个目测应该是盐值,只要获取到这个参数就可以直接还原算法,获取的方式有很多 jnitrace, frida hook native
都可以。
再看下signV3
这里也是拼接了一些固定参数,调用的也是digest()
,只不过用的哈希算法中的sha256
。java层的分析到此差不多了,我们这里不去还原算法,直接使用 unidbg 进行黑盒调用。
5.unidbg模拟调用
老规矩,上来先搭建个基本的框架。
package com.aikucun;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.linux.android.dvm.wrapper.DvmBoolean;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
public class AikucunSign extends AbstractJni{
private final AndroidEmulator emulator;
private final DvmClass MXSecurity;
private final VM vm;
private Module module;
public AikucunSign() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName("com.aikucun.akapp")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/aikucun/com.aikucun.akapp_6.1.6_liqucn.com.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/aikucun/libmx.so"), false);
module = dm.getModule();
MXSecurity = vm.resolveClass("com/mengxiang/arch/security/MXSecurity");
dm.callJNI_OnLoad(emulator);
}
public void callSignV1(){
}
public static void main(String[] args) {
AikucunSign aikucun = new AikucunSign();
aikucun.callSignV1();
try {
aikucun.destroy();
} catch (IOException e) {
throw new
}
}
跑一下
没有报错 ,继续往下走。
先调用signV1
方法,该方法入参是三个string,返回值也是string。因为是静态方法,所以直接使用callStaticJniMethodObject()
。
public void callSignV1(){
String str1 = "https://zuul.aikucun.com/api/gquery?appid=38741001&did=f332c6946049c0584a192aa540a61f57&noncestr=d1eaf6&subuserid=52920da9e265d5e2353390b7e9e6989e×tamp=1672380663&token=8dfca42e59f04cdd8879b68a0ab2bb38&userid=52920da9e265d5e2353390b7e9e6989e&zuul=1";
String str2 = "d1eaf6";
String str3 = "1672380663";
String result = MXSecurity.callStaticJniMethodObject(emulator, "signV1(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", str1, str2, str3)
.getValue()
.toString();
System.out.println("result: " + result);
}
再跑一下
成功调用了JNIEnv->NewStringUTF("")
,但是返回值为空,这里我们继续分析之前那个so函数。
可以看到函数进去以后会有一个if判断
,条件成立了会执行,反之则返回空。
再回到之前frida调试,我们成功拦截拿到了入参和返回值。
frida拦截可以成功,但是unidbg却调用失败了,那这是为什么尼?
这里我们也还可以用frida 主动调用下,这里就不写了。
失败 的原因有很多种,不过最常见的还是上下文缺失
,缺少环境
问题,这里我们使用 jnitrace
打印下 libmx.so
的执行流,具体使用就不说了。
通过 jnitrace
的打印,才发现,调用 signV1
函数之前还需要调用 init
函数也就是下图的函数.
那我们就先调用这个函数
public void callInit() {
ArrayList<Object> list = new ArrayList<>();
list.add(vm.getJNIEnv());
list.add(vm.addLocalObject(vm.resolveClass("com/mengxiang/arch/security/MXSecurity").newObject(null)));
list.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
list.add(0);
// list.add(vm.addLocalObject(DvmBoolean.valueOf(vm,false)));
Number numbers = module.callFunction(emulator, 0x7F14 + 1, list.toArray());
System.out.println("callInit返回值:"+ numbers);
System.out.println("callInit返回值:"+ numbers.intValue());
}
跑一下,继续报错。
这里的报错提示是说找不到MessageDigest SHA256
,SHA256
是 android里的,java 里是 SHA-256
,我们直接重写这个函数,处理这个逻辑。
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;":
StringObject type = vaList.getObjectArg(0);
String name = "";
if ("\"SHA256\"".equals(type.toString())) {
name = "SHA-256";
}
else {
name = type.toString();
System.out.println("else name: " + name);
}
try {
return vm.resolveClass("java/security/MessageDigest").newObject(MessageDigest.getInstance(name));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
再跑一下,还是报错。
这次把SHA1
也加上去。
String name = "";
if ("\"SHA256\"".equals(type.toString())) {
name = "SHA-256";
} else if ("\"SHA1\"".equals(type.toString())) {
name = "SHA-1";
} else {
name = type.toString();
System.out.println("else name: " + name);
}
再跑一次,这次就把callSignV1
成功跑出来了。
接着再把SignV3
也补上去。
public void callSignV3(){
String str1 = "https://zuul.aikucun.com/aggregation-center-facade/api/app/shareFlag/isHide?appid=38741001&did=f332c6946049c0584a192aa540a61f57&noncestr=e66eed&subuserid=52920da9e265d5e2353390b7e9e6989e&svs=v3×tamp=1672380663&token=8dfca42e59f04cdd8879b68a0ab2bb38&userId=52920da9e265d5e2353390b7e9e6989e&userid=52920da9e265d5e2353390b7e9e6989e";
String str2 = "e66eed";
String str3 = "1672380663";
String str4 = "";
String res = MXSecurity.callStaticJniMethodObject(emulator, "signV3(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", str1, str2, str3, str4)
.getValue()
.toString();
System.out.println("res: " + res);
}
跑一下,sign_v3
结果也出来了。
最后unidbg 和hook出来的结果对比是一致的,证明调用的没问题。
撤退,告辞。
参考文章:https://www.qinless.com/141