【安卓逆向】unidbg入门级案例-某航空app_hnairSign分析

今天要分析的是某航空app,版本号是8.19.0,分析的样本在文章底部会提供,这次我们要借用unidbg 来辅助进行算法还原。

有关unidbg的介绍笔者就不做过多的描述,大家可自行百度查询。
该样本的so比较简单,但重点是记录分析的思路和过程。这里万分感谢鹏佬的指点。

老规矩,上来先抓个包。

1.抓包

在这里插入图片描述
经分析该app没有壳,里面有一个hnairSign参数加密,hnairSign 就是本次样本研究的重点,我们不在意如何拿到数据,而是研究如何构造加密的。

2.jadx静态分析

把样本app 拖到jadx里面,直接搜索关键词 "hnairSign",可以看到只有一条搜索结果,跟进去看看。

在这里插入图片描述

然后来到了这个,可以看到i6.b("hnairSign", signRequest);,其中hnairSign 是该String signRequest = signRequest(aVar);方法返回来的。

在这里插入图片描述

紧接着跟进 signRequest(aVar) 函数看一看,来到这里

在这里插入图片描述

可以看到 signRequest 函数的入参是v.a aVar ,返回值是String,并且是由该(String) i.p(HNASignature.getHNASignature(headersForSign, queryForSign, requestBodyForSign, str, a9), new String[]{">>"}).get(0); 返回来的。

在这里我们看到了HNASignature.getHNASignature(),根据函数命名规范,大胆的猜测下,这里就是最核心的加密代码。

getHNASignature()函数的入参是headersForSign, queryForSign, requestBodyForSign, str, a9 几个连续的String,具体是什么,目前不知道,不过先放置在这里。

继续跟进去,来到这里。

在这里插入图片描述

package com.rytong.hnair;

public class HNASignature {
    public static native String getHNASignature(String str, String str2, String str3, String str4, String str5);
}

关键字native,看来本次样本不是在java层那么简单了,本着学习的态度,进入so层看看代码是如何写的。

通常情况下,会有如下的代码:

System.loadLibrary("xxxxx")

会告诉我们,样本加密在那个so文件,但是这里并没有。

不过我们也可以用yang神的frida脚本 hook_RegisterNativeshttps://github.com/lasting-yang/frida_hook_libart) 来打印注册的native函数和so文件。

那它的脚本到底是如何做到的尼,点进去源码看看。

在这里插入图片描述

java层的静态分析到此就差不多结束了,接着要用frida动态分析下。

3. frida动态调试

打开frida服务,运行命令:

frida -U com.rytong.hnair -l hair_hook.js

hair_hook代码:


Java.perform(function () {
    var HNASignature = Java.use("com.rytong.hnair.HNASignature");
    HNASignature.getHNASignature.implementation = function (str1, str2, str3, str4, str5) {
        console.log("---------------------------");
        console.log('getHNASignature is called');
        console.log("str1:" + str1);
        console.log("str2:" + str2);
        console.log("str3:" + str3);
        console.log("str4:" + str4);
        console.log("str5:" + str5);
        var ret = this.getHNASignature(str, str2, str3, str4, str5);
        console.log('getHNASignature加密值:' + ret);
        return ret;
    };
})

frida hook结果:

在这里插入图片描述

getHNASignature is called
str1:{}
str2:{}
str3:{"akey":"184C5F04D8BE43DCBD2EE3ABC928F616","aname":"com.rytong.hnair","atarget":"standard","aver":"8.19.0","did":"9908e1587f76bbd6","dname":"OnePlus_ONEPLUS A5010","gtcid":"0629cf96a1a94543d3b1db7625b97e44","mchannel":"official","schannel":"AD","slang":"zh-CN","sname":"OnePlus\/OnePlus5T\/OnePlus5T:8.1.0\/OPM1.171019.011\/1812111113:user\/release-keys","stime":"1671674896594","sver":"8.1.0","system":"AD","szone":"+0800","abuild":"63403","hver":"8.19.0.30995.c079e3182.standard","cms":[{"name":"cdnConfig"}],"h5Version":"8.19.0.30995.c079e3182.standard"}
str4:21047C596EAD45209346AE29F0350491
str5:F6B15ABD66F91951036C955CB25B069F
getHNASignature加密值:F3DCBDD8A2350604A7487FF97E3A709B331F3E51>>63403184C5F04D8BE43DCBD2EE3ABC928F616com.rytong.hnairstandard8.19.09908e1587f76bbd6OnePlus_ONEPLUS A50100629cf96a1a94543d3b1db7625b97e448.19.0.30995.c079e3182.standard8.19.0.30995.c079e3182.standardofficialADzh-CNOnePlus/OnePlus5T/OnePlus5T:8.1.0/OPM1.171019.011/1812111113:user/release-keys16716748965948.1.0AD+0800>>F6B15ABD66F91951036C955CB25B069F

连续hook多次发现,参数 str1 和 str2 固定为{},参数str3是一个json字符串,参数str4为固定值21047C596EAD45209346AE29F0350491,参数str5为固定值F6B15ABD66F91951036C955CB25B069F,可以把它暂且理解为盐值吧!

4.so层分析

把样本app后缀修改成zip结尾,然后解压缩,在lib下的armeabi-v7a 32位目录下找到加密样本libsignature.so

把该so样本直接拖到ida里,全程选择默认,一路点击ok。

在这里插入图片描述

然后又是熟悉的页面,熟悉的晦涩难懂。。。硬着头皮上。。。。。

进入到这里,我们首先要确定两件事。

第一加密的native函数到底是静态注册还是动态注册的。

第二样本so用的是Thumb 指令 还是Arm指令。

那么有问题的小明同学就想问一句,你说的到底是个啥,能简单的介绍下吗?

一般在Exports导出表能搜索到以"java_"开头的函数就是静态注册,反之则为动态注册。

静态注册的so函数是这样命名的:

Java_包名_类名_函数名

动态注册通常你会看到JNI_OnLoad

示例如图:

在这里插入图片描述

那如何判断 Thumb 和 Arm 指令集尼?

ida里依次找到Options ----> General

在这里插入图片描述

然后 Number of opcode bytes(non-graph) 设置为4 ,点击ok.

在这里插入图片描述

然后在IDA View中查看opcode 的长度, 如果出现 2 个字节和 4 个字节的, 说明为 thumb 指令集。

如果都是 4 个字节的, 说明是 arm 指令集。

在 Thumb 指令集下, inline hook 的需要进行偏移地址 +1 操作;

示例如图:

在这里插入图片描述

继续之前的,刚才我们找到了"java_" 开头的函数 Java_com_rytong_hnair_HNASignature_getHNASignature ,点进去。

在这里插入图片描述

使用空格键 可以实现 文字图 和 竖形流程图 的切换(ps:这里可能表述不准确)。

在这里插入图片描述

然后再按 Fn5健 就能看到 加密的 c/c++代码了。

在这里插入图片描述

这里就能看到加密的c函数HNASignature(&v19, &v25, &v24, &v23, &v22, &v21)
几个入参应该分别对应着java层的入参(形参)。

public static native String getHNASignature(String str, String str2, String str3, String str4, String str5);
}

同时我们在导出函数窗口还看到了CHMAC_SHA1::HMAC_SHA1()

在这里插入图片描述

根据经验,大胆的猜测下这里用的是hmac_sha1算法。

好了,so层的分析到一段落。

5.unidbg分析

unidbg 最重要的就是补环境

环境搭建 可参考文章:https://blog.csdn.net/qq_41179280/article/details/121771586

第一步,先搭建基本框架

package com.rytong.hnair;

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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class HNASignatureTest extends AbstractJni {
    private final AndroidEmulator emulator;
    private final Module module;
    private final VM vm;

    public HNASignatureTest() {
        // 创建模拟器实例,进程名依照实际进程名填写,要模拟32位或者64位,在这里区分
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.rytong.hnair").build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析,有19和23 两个版本选择
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/hair/hair_8.19.0.apk"));
        // 设置是否打印Jni调用细节
        vm.setVerbose(false);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/hair/armeabi_v7a/libsignature.so"), false);
        //获取SO模块的句柄
        module = dm.getModule();
        System.out.println("baseAddr:"+  module.base);
        // 设置JNI
        vm.setJni(this);
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    }

初始化的大致逻辑就是先创建个模拟器对象emulator,然后去内存申请接口,再传入apk和so文件,最后再执行JNI OnLoad。

unidbg 是模拟执行so 的,因为是模拟而并不是真正的,所以有很多东西没有实现,所以会报各种各种的错误。

接下来,我们逐行的详细解释下,代码中的注释已经很清楚。

new AndroidResolver() 指定系统类库解析,那为什么只有2个版本可供选择尼?

点进源码,我们发现unidbg作者只实现了 19和23 两个版本。

在这里插入图片描述

emulator.createDalvikVM()是创建Android虚拟机,为什么要传入apk文件尼?

点进源码看看

在这里插入图片描述

可以看到这里 Apk接口会自动获取版本号,版本名称,ManifestXml,签名,包名等(建议写上,会帮我们做部分签名校验的工作)

vm.setJni(this); 是设置jni,那什么是jni?

带着疑问,百度下?

在这里插入图片描述

详细可参考文章:https://blog.csdn.net/yaojingqingcheng/article/details/123497697

简单的说就是jni 是java世界和c/c++世界沟通的桥梁,java可以通过jni来调用c/c++封装好的函数,反之也可以,其中第⼀个参数JNIEnv *env,这个参数就是java环境。

执行so文件以后,紧接着执行JNI_OnLoad,那JNI_OnLoad是个啥子尼?

JNI_OnLoad()

Java调用System.loadLibrary()加载一个库的时候,会首先在库中搜索JNI_OnLoad()函数,如果该函数存在,则执行它;

JNI_OnLoad()的作用主要有几点:

  1. 告诉JVM,这个库需要要求使用的JNI版本是什么
  2. 执行初始化操作
  3. 将JavaVM参数保存为全局对象,方便以后在任何地方获取JNIEnv对象

如果一个库不存在JNI_OnLoad()函数,那么JVM默认会使用最老版本的JNI,即1.1。
JNI_OnLoad方法在每一个库中只能存在一个。

也就是说我们加载了libsignature.so库以后,就应该需要执行下JNI_OnLoad,当然如果库没有JNI_OnLoad,那自然不用执行,但是不管有没有,执行就是了,反正不会错。

运行一下:

在这里插入图片描述
然后补call方法

 public void callSign(String encryptData){
        List<Object> list = new ArrayList<>(7);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
        String str1 = "{}";
        list.add(vm.addLocalObject(new StringObject(vm,str1)));
        String str2 = "{}";
        list.add(vm.addLocalObject(new StringObject(vm,str2)));
        list.add(vm.addLocalObject(new StringObject(vm,encryptData)));
        String str4 = "21047C596EAD45209346AE29F0350491";
        list.add(vm.addLocalObject(new StringObject(vm,str4)));
        String str5 = "F6B15ABD66F91951036C955CB25B069F";
        list.add(vm.addLocalObject(new StringObject(vm,str5)));

        Number number = module.callFunction(emulator,0xA49C+1, list.toArray());

        DvmObject result = vm.getObject(number.intValue());
        String value = (String) result.getValue();
        System.out.println("result ->"  + value);
        System.out.println("result ------>"  + value.split(">>")[0]);

    }

需要注意几个点:

  • 第一个参数是JNIEnv,
  • 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0或者为空,这样做有小风险,一般用不到。
  • 传入Native的JAVA参数,除了八个基本类型外(byte、char、short、int、long、float、double、boolean),都必须vm.addLocalObject添加到局部引用中去。

后面几个参数就是前面我们frida hook出 必要的传参。

运行一下:

在这里插入图片描述

对比下,发现unidbg跑出来的结果和抓包拿到的一样。

在这里插入图片描述

全部代码如下:

package com.rytong.hnair;

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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class HNASignatureTest extends AbstractJni {
    private final AndroidEmulator emulator;
    private final Module module;
    private final VM vm;
    //
    public HNASignatureTest() {
        // 创建模拟器实例,进程名依照实际进程名填写,要模拟32位或者64位,在这里区分
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.rytong.hnair").build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析,有19和23 两个版本选择
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/hair/hair_8.19.0.apk"));
        // 设置是否打印Jni调用细节
        vm.setVerbose(false);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/hair/armeabi_v7a/libsignature.so"), false);
        //获取SO模块的句柄
        module = dm.getModule();
        System.out.println("baseAddr:"+  module.base);
        // 设置JNI
        vm.setJni(this);
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    public void callSign(String encryptData){
        List<Object> list = new ArrayList<>(7);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
        String str1 = "{}";
        list.add(vm.addLocalObject(new StringObject(vm,str1)));
        String str2 = "{}";
        list.add(vm.addLocalObject(new StringObject(vm,str2)));
        list.add(vm.addLocalObject(new StringObject(vm,encryptData)));
        String str4 = "21047C596EAD45209346AE29F0350491";
        list.add(vm.addLocalObject(new StringObject(vm,str4)));
        String str5 = "F6B15ABD66F91951036C955CB25B069F";
        list.add(vm.addLocalObject(new StringObject(vm,str5)));

        Number number = module.callFunction(emulator,0xA49C+1, list.toArray());

        DvmObject result = vm.getObject(number.intValue());
        String value = (String) result.getValue();
        System.out.println("result ->"  + value);
        System.out.println("result ------>"  + value.split(">>")[0]);

    }



    public static void main(String[] args) throws IOException {
        HNASignatureTest hnsign = new HNASignatureTest();

        String encryptData1 = "{\"akey\":\"184C5F04D8BE43DCBD2EE3ABC928F616\",\"aname\":\"com.rytong.hnair\",\"atarget\":\"standard\",\"aver\":\"8.19.0\",\"did\":\"9908e1587f76bbd6\",\"dname\":\"OnePlus_ONEPLUS A5010\",\"gtcid\":\"0629cf96a1a94543d3b1db7625b97e44\",\"mchannel\":\"official\",\"schannel\":\"AD\",\"slang\":\"zh-CN\",\"sname\":\"OnePlus\\/OnePlus5T\\/OnePlus5T:8.1.0\\/OPM1.171019.011\\/1812111113:user\\/release-keys\",\"stime\":\"1671674896594\",\"sver\":\"8.1.0\",\"system\":\"AD\",\"szone\":\"+0800\",\"abuild\":\"63403\",\"hver\":\"8.19.0.30995.c079e3182.standard\",\"cms\":[{\"name\":\"cdnConfig\"}],\"h5Version\":\"8.19.0.30995.c079e3182.standard\"}";
        System.out.println(encryptData1);
        hnsign.callSign(encryptData1);

    }


}

6.算法还原

经过对比发现,参数str3其实就是把请求的body里所有的key/values取出来

在这里插入图片描述

再看下frida返回值可以发现,其实就是把字典排个序,然后把value拼接到一起,尾部追加个固定盐值F6B15ABD66F91951036C955CB25B069F,用的是hmac_sha1标准算法。

python代码就不贴了。

在这里插入图片描述
可以看到,unidbg,frida和抓包三者拿到的值都是一样的。

参考文章:

https://blog.csdn.net/qq_38851536/article/details/117418582
https://blog.csdn.net/qq_38851536/article/details/118115569
https://www.cnblogs.com/zhujiabin/p/10605745.html
https://blog.csdn.net/Qiled/article/details/124348705

apk样本:http://www.2265.com/soft/55340.html

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
某flutter-app逆向分析是指对于一个使用flutter框架开发的应用进行逆向工程分析。逆向工程是通过分析应用的代码、二进制文件等来了解其内部实现细节。 首先,我们需要获取该应用的安装包文件(APK或IPA文件),然后进行解包操作,将其转换为可读取的文件目录结构。 接下来,我们可以使用一些工具来提取应用的资源文件、代码文件等。对于flutter-app来说,可以提取出dart文件,这是flutter的主要代码文件,其中包含了应用的逻辑实现。 通过阅读dart文件,我们可以了解应用的代码结构、数据模型、界面设计等。可以分析应用的逻辑实现方法,包括各种函数、类、方法的调用关系。 同时,还可以通过分析相关配置文件、资源文件等来了解应用的各种设置、资源加载方式等。 在逆向过程中,还可以使用一些调试工具来进一步了解应用的运行机制。例如,hook工具可以拦截应用的函数调用,并捕获输入输出数据,用于进一步分析。 逆向分析的目的可以有很多,比如了解应用的工作原理、发现潜在的漏洞或安全问题、提供参考用于自己的开发等。 需要注意的是,逆向分析需要遵守法律规定。未经授权的逆向分析可能侵犯他人的知识产权,涉及到隐私等方面的问题。因此,在进行逆向分析之前,应该了解并遵守当地相关法律法规,避免产生法律纠纷。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值