【安卓逆向】unidbg案例-某库存 sig sign分析

新年的第一篇文章,新的一年继续加油,奥利给!冲冲冲。

今天分析的app是 54ix5bqT5a2YX3Y2LjEuNg== (base64解码),这次还是使用unidbg分析该样本,加密参数有很多,不过只关注sigsign两个参数。

老规矩,上来先抓个包。

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&timestamp=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&timestamp=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

在这里插入图片描述
它这里拼接了固定的一些参数appidnoncestrtimestampsecret,然后再调用了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&timestamp=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 SHA256SHA256 是 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&timestamp=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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值