某马拉雅signature参数逆向分析

样本地址: aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzMwNjY3Mw==

抓包分析

当抓到 login 这个包的时候,会发现在请求体中有一个 signature 的参数,今天我们的目标就是它

84c7dd50e8192db401cfbc4b2d6b0565f41fd2e0 这个格式的字符串,长度 40 位,感觉有很大可能是 sha1 算法,不知道魔改没 打开 jadx,搜索一下 signature 这个字段,但是发现搜索出来了很多关键词,那就拿其他相关的关键词进行搜索
fdsOtp 这个字段挺特殊的,直接在 jadx 中搜索,发现了 3 个地方 进去之后发现就找到了 signature 这个字段,它调用了 LoginRequest 这个类的 b 方法
跟进去,最后就直接跟到了是 WTWEctUfLf 这个方法返回的结果

frida,启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function main() {
    Java.perform(function () {
        var LoginEncryptUtil = Java.use("com.ximalaya.ting.android.loginservice.LoginEncryptUtil");
        LoginEncryptUtil["WTWEctUfLf"].implementation = function (context, z, str) {
            console.log(`LoginEncryptUtil.WTWEctUfLf is called: context=${context}, z=${z}, str=${str}`);
            let result = this["WTWEctUfLf"](context, z, str);
            console.log(`LoginEncryptUtil.WTWEctUfLf result=${result}`);
            return result;
        };
    })
}

setImmediate(main);

通过 hook,得到入参是一下这些参数:

1
2
3
4
5
6
7
8
LoginEncryptUtil.WTWEctUfLf is called: context=com.ximalaya.ting.android.xmloader.XMApplication@975842c, z=false, str=account=aqsgOa6SYFGEVnn8XH6KkcTVjfGFaVjlYYUHRIuf2/lypnQQ4NrBStm9weVzMdg8yaYGgyX4dP/F
dQ39TwXVXkE9ZnbeZCgbsQSPYycZ9tzBKs4e7+xw9yRmv7qZaQpPPK880mK408nf/wbLDvj6rb5F
sN0rVP9cOj0pC5lhLgQ=
&fdsOtp={"captcha_id":"3723312ce42a04b5c0b40e605a882037","lot_number":"fd6f39fe63c241c8b7a89e980dec5db6","pass_token":"da453db61a283377f65ccaa7ab1554c58b7090e564d03ad7b4d4fb6a25397401","gen_time":"1770081297","captcha_output":"2Z76G2CapbcJxb3z0NRv6NWndrlIytoIjX7gLdfkJsQrgtBAoCtFZbq7qWs7QDkcQd2bgFHjUC0fpWFUpuIwd_k_JWPqHe9TBEOkJ1jqLFicEpmmAmdU2MEBYwncCxHVi33DvIRKYrwyoEByBsAa1SZ5X7mzDpdEWSMD5CpT3e4WcDXV-vnaQm-p5EaL0prpMKLRAaAfULuMBJJfkl3i6OAmTRc4uvulOTHp9dyw7BwQydb-aPAdat8c1xd9PMd9LO_PeIAdjPJ5oaVZSC5yzEtwgUg7CfIu90p6YDIwkUiBLfVCZVGT3a-y2eS9dcyLlGkAw1A898P__HBQHc8A4-D4lY5hm680D2jZwVLqqWr3GCeKyzOcR1fejMMH-RZIL-ocLdVl60cmTSXbRqNC-xH3CmQbFIBidbSXYUDDxXRKo7BuHaukYsTTuFmKfOzSV6L2kymVNIdw58k6eTRMbfvuL6bcwYaWmZ4_Ss02ZNt4UJeMcoQt733FkNET4fRPiAEwqzNxtesz-GAQ6Uyta3F84KBaFfY3Ix6tJVQVbQQ="}&nonce=0-22881C771F2437b725b395e8aa833f7a42e858ebd51c82129ebbb05eb53be0&password=EzHwdxwNPpKmd0jSSJO79ZQZa/Fu1lYTMGB2QBuRFNWTdtaxv2pIMECdzIdQHmfQkbaSQWjuSqCm
w1Q3ca6foQ6TpfucYKV5sQNZodaIjhF6EIpua/SffV7gWgO2g+mzBk5j3muCSKZCuEduWgtCOaqk
jVZvN+1LgaZSXqHrVdE=
&
LoginEncryptUtil.WTWEctUfLf result=560f3bc0e1556d8399375ae3a831fb2daf771421

补环境

先用 unidbg 搭个架子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.ximalaya;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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;

//必须继承AbstractJni
public class EncryptUtil extends AbstractJni {
    public static AndroidEmulator emulator;  // 静态属性,以后对象和类都可以直接使用
    public static Memory memory;
    public static VM vm;
    public static Module module;
    public final DvmClass LoginEncryptUtil;
    // 1 构造方法--》用来初始化
    public EncryptUtil(){
        // 1.创建设备(32位或64位模拟器), 具体看so文件在哪个目录。 在armeabi-v7a就选择32位
        // 传进设备时,如果是32位,后面so文件就要用32位,同理需要用64位的
        // 这个名字可以随便写,一般写成app的包名    以后可能会动
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.ximalaya.ting.android").build();

        // 2.获取内存对象(可以操作内存)
        memory = emulator.getMemory();

        //3.设置安卓sdk版本(只支持19、23)
        memory.setLibraryResolver(new AndroidResolver(23));

        // 4.创建虚拟机(运行安卓代码需要虚拟机,就想运行py代码需要python解释器一样)    以后会动
        vm = emulator.createDalvikVM(new File("apks/ximalaya/xmly.apk"));
        vm.setJni(this); // 后期补环境会用,把要补的环境,写在当前这个类中,执行这个代码即可,但是必须继承AbstractJni
        vm.setVerbose(true); //是否展示调用过程的细节

        // 5.加载so文件
        DalvikModule dm = vm.loadLibrary("login_encrypt", false);   // 以后会动
        dm.callJNI_OnLoad(emulator); // jni开发动态注册,会执行JNI_OnLoad,如果是动态注册,需要执行一下这个,如果静态注册,这个不需要执行,车智赢案例是静态注册
        LoginEncryptUtil = vm.resolveClass("com.ximalaya.ting.android.loginservice.LoginEncryptUtil");
        // 6.dm代表so文件,dm.getModule()得到module对象,基于module对象可以访问so中的成员。
        module = dm.getModule(); // 把so文件加载到内存后,后期可以获取基地址,偏移量等,该变量代指so文件
    }

    //2 sign 成员方法--》主要用来解密
    public void WTWEctUfLf(){
        DvmObject<?> context = vm.resolveClass("android/app/Application", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
        String param_str = "account=aqsgOa6SYFGEVnn8XH6KkcTVjfGFaVjlYYUHRIuf2/lypnQQ4NrBStm9weVzMdg8yaYGgyX4dP/F\n" +
                "dQ39TwXVXkE9ZnbeZCgbsQSPYycZ9tzBKs4e7+xw9yRmv7qZaQpPPK880mK408nf/wbLDvj6rb5F\n" +
                "sN0rVP9cOj0pC5lhLgQ=\n" +
                "&fdsOtp={\"captcha_id\":\"3723312ce42a04b5c0b40e605a882037\",\"lot_number\":\"fd6f39fe63c241c8b7a89e980dec5db6\",\"pass_token\":\"da453db61a283377f65ccaa7ab1554c58b7090e564d03ad7b4d4fb6a25397401\",\"gen_time\":\"1770081297\",\"captcha_output\":\"2Z76G2CapbcJxb3z0NRv6NWndrlIytoIjX7gLdfkJsQrgtBAoCtFZbq7qWs7QDkcQd2bgFHjUC0fpWFUpuIwd_k_JWPqHe9TBEOkJ1jqLFicEpmmAmdU2MEBYwncCxHVi33DvIRKYrwyoEByBsAa1SZ5X7mzDpdEWSMD5CpT3e4WcDXV-vnaQm-p5EaL0prpMKLRAaAfULuMBJJfkl3i6OAmTRc4uvulOTHp9dyw7BwQydb-aPAdat8c1xd9PMd9LO_PeIAdjPJ5oaVZSC5yzEtwgUg7CfIu90p6YDIwkUiBLfVCZVGT3a-y2eS9dcyLlGkAw1A898P__HBQHc8A4-D4lY5hm680D2jZwVLqqWr3GCeKyzOcR1fejMMH-RZIL-ocLdVl60cmTSXbRqNC-xH3CmQbFIBidbSXYUDDxXRKo7BuHaukYsTTuFmKfOzSV6L2kymVNIdw58k6eTRMbfvuL6bcwYaWmZ4_Ss02ZNt4UJeMcoQt733FkNET4fRPiAEwqzNxtesz-GAQ6Uyta3F84KBaFfY3Ix6tJVQVbQQ=\"}&nonce=0-22881C771F2437b725b395e8aa833f7a42e858ebd51c82129ebbb05eb53be0&password=EzHwdxwNPpKmd0jSSJO79ZQZa/Fu1lYTMGB2QBuRFNWTdtaxv2pIMECdzIdQHmfQkbaSQWjuSqCm\n" +
                "w1Q3ca6foQ6TpfucYKV5sQNZodaIjhF6EIpua/SffV7gWgO2g+mzBk5j3muCSKZCuEduWgtCOaqk\n" +
                "jVZvN+1LgaZSXqHrVdE=\n" +
                "&";
        String ret = LoginEncryptUtil.newObject(null).callJniMethodObject(emulator,"WTWEctUfLf(Landroid/content/Context;ZLjava/lang/String;)Ljava/lang/String;",context,false,param_str).getValue().toString();
        System.out.println(ret);
    }

    // 3 main方法---》右键直接运行
    public static void main(String[] args) {
        EncryptUtil loginEncryptUtil=new EncryptUtil();
        loginEncryptUtil.WTWEctUfLf();
    }
}

运行,报错

1
2
3
4
java.lang.UnsupportedOperationException: java/lang/String->toUpperCase()Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)

补就是了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
   switch (signature){
	   case "java/lang/String->toUpperCase()Ljava/lang/String;":{
		   String str = (String) dvmObject.getValue();
		   System.out.println("输入参数==>"+str);
		   return new StringObject(vm,str.toUpperCase());
	   }
   }
   return super.callObjectMethod(vm, dvmObject, signature, varArg);
}

接着,又报错,StringBuilder 没有初始化

1
2
3
4
java.lang.UnsupportedOperationException: java/lang/StringBuilder-><init>()V
	at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)
	at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:733)
	at com.github.unidbg.linux.android.dvm.DvmMethod.newObject(DvmMethod.java:224)

继续补

1
2
3
4
5
6
7
8
9
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
	switch (signature){
		case "java/lang/StringBuilder-><init>()V":{
			return vm.resolveClass("java/lang/StringBuilder").newObject(new StringBuilder());
		}
	}
	return super.newObject(vm, dvmClass, signature, varArg);
}

接着报错

1
2
3
4
java.lang.UnsupportedOperationException: java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:433)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:422)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethod(DvmMethod.java:54)

接着补

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
   switch (signature){
	   case "java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;":{
		   String algorithm = varArg.getObjectArg(0).getValue().toString();
		   try {
			   MessageDigest md = MessageDigest.getInstance(algorithm);
			   return vm.resolveClass("java/security/MessageDigest").newObject(md);
		   } catch (NoSuchAlgorithmException e) {
			   throw new RuntimeException(e);
		   }
	   }
   }
   return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}

接着就应该补 update 了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Override
public void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
	switch (signature){
		case "java/security/MessageDigest->update([B)V":{
			MessageDigest md = (MessageDigest)dvmObject.getValue();
			byte[] data = (byte[]) varArg.getObjectArg(0).getValue();
			md.update(data);
			return;
		}
	}
	super.callVoidMethod(vm, dvmObject, signature, varArg);
}

补 digest

1
2
3
4
case "java/security/MessageDigest->digest()[B":{
	MessageDigest md = (MessageDigest)dvmObject.getValue();
	return new ByteArray(vm,md.digest());
}

接着又报错

1
2
3
4
java.lang.UnsupportedOperationException: java/lang/Integer->toHexString(I)Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:433)
	at com.ximalaya.EncryptUtil.callStaticObjectMethod(EncryptUtil.java:100)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:422)

补上

1
2
3
4
5
case "java/lang/Integer->toHexString(I)Ljava/lang/String;":{
	int value = varArg.getIntArg(0);
	String hex = Integer.toHexString(value);
	return new StringObject(vm,hex);
}

然后,又报错

1
2
3
4
java.lang.UnsupportedOperationException: java/lang/String->length()I
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:965)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:938)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethod(DvmMethod.java:129)

补上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
	switch (signature){
		case "java/lang/String->length()I":{
			String str = (String) dvmObject.getValue();
			return str.length();
		}
	}
	return super.callIntMethod(vm, dvmObject, signature, varArg);
}

报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
java.lang.UnsupportedOperationException: java/lang/StringBuilder->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
	at com.ximalaya.EncryptUtil.callObjectMethod(EncryptUtil.java:74)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
	
	
java.lang.UnsupportedOperationException: java/lang/StringBuilder->append(I)Ljava/lang/StringBuilder;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
	at com.ximalaya.EncryptUtil.callObjectMethod(EncryptUtil.java:93)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)

补上

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
case "java/lang/StringBuilder->append(Ljava/lang/String;)Ljava/lang/StringBuilder;": {
	Object value = dvmObject.getValue();
	if (value instanceof StringBuilder){
		StringBuilder sb = (StringBuilder)value;
		sb.append(varArg.getObjectArg(0).getValue().toString());
		return vm.resolveClass("java/lang/StringBuilder");
	}
}
case "java/lang/StringBuilder->append(I)Ljava/lang/StringBuilder;":{
	Object value = dvmObject.getValue();
	if (value instanceof StringBuilder){
		StringBuilder sb = (StringBuilder)value;
		sb.append(varArg.getIntArg(0));
		return vm.resolveClass("java/lang/StringBuilder");
	}
}

继续报错:

1
2
3
4
java.lang.UnsupportedOperationException: java/lang/StringBuilder->toString()Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
	at com.ximalaya.EncryptUtil.callObjectMethod(EncryptUtil.java:93)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)

补上

1
2
3
4
case "java/lang/StringBuilder->toString()Ljava/lang/String;":{
	StringBuilder sb = (StringBuilder)dvmObject.getValue();
	return new StringObject(vm,sb.toString());
}

接着就出结果了

算法分析

根据前面的分析已经知道了算法是在 liblogin_encrypt.so 中,将这个 so 文件拖入到 ida 中进行分析
进入之后,找到 Java_com_ximalaya_ting_android_loginservice_LoginEncryptUtil_WTWEctUfLf 这个导出函数,发现有 ollvm

挺麻烦的,看着,直接用 unidbg 来 trace 一下这个代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void traceCode() {
	String traceFile = "unidbg-android/src/test/java/com/ximalaya/traceCode.log"; // 输出的路径
	PrintStream traceStream = null; // 打印流
	try {
		traceStream = new PrintStream(new FileOutputStream(traceFile), true);
	} catch (FileNotFoundException e) {
		throw new RuntimeException(e);
	}
	// traceCode 对代码进行监控
	emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
}

同时,hook 一下 memcpy,一般都会调用 libc.so 里面的这个工具函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
emulator.attach().addBreakPoint(module.findSymbolByName("memcpy").getAddress(), new BreakPointCallback() {
	@Override
	public boolean onHit(Emulator<?> emulator, long address) {
		RegisterContext registerContext = emulator.getContext();
		UnidbgPointer src = registerContext.getPointerArg(1);
		int length = registerContext.getIntArg(2);
		Inspector.inspect(src.getByteArray(0, length), "memcpy:" + src.toString());
		return true;
	}
});

然后得到了 trace 文件,首先来看一下密文是什么,从结果逆推 560f3bc0e1556d8399375ae3a831fb2daf771421,看着像 sha1 算法,单个字节开始搜索

发现都是从寄存器 r0 中进行取值,得到最终的结果,偏移为: 0x05b1b
到 ida 中跳转过去

hook 一下

1
emulator.attach().addBreakPoint(module.base+0x5B1A);

发现 r0 就是结果值

接着 trace 一下

1
emulator.traceWrite(0x1220d018, 0x1220d018 +0x13);

这儿有点奇怪的就是,没有看到哪里 write 之后,就已经得到了结果的值

然后我往上面看到之前补环境的日志

SHA1 ,他在请求参数后面拼接了一个固定的值: MOBILE-V1-PRODUCT-7D74899B338B4F348E2383970CC09991E8E8D8F2BC744EF0BEE94D76D718C089 然后 sha1 就没了

我了个这也太巧了吧,无所谓了,逆向就是这样,有些时候弄着弄着就出来了,有点怪