首页
社区
课程
招聘
[原创]某电商网站unidbg模拟执行sign && unidbg辅助算法还原
发表于: 2021-11-1 19:30 14983

[原创]某电商网站unidbg模拟执行sign && unidbg辅助算法还原

2021-11-1 19:30
14983

某电商网站unidbg模拟执行sign && unidbg辅助算法还原

一、前言

免责声明:本文仅作为学习逆向技术分析、研究所用,一切由于其他非法用途而引起的法律纠纷与本作者无关。

 

本篇文章旨在分享如何用unidbg模拟执行so以及如何用unidbg辅助算法还原(v10.2.0

  • unidbg模拟执行,
  • unidbg辅助分析so算法

二、定位受害者:sign签名

 

 

定位到目标,在下方加载了libjdbitmapkit.so。那我们frida跑一下看看入参和返回值,frida开起来,打开app搜索,app直接闪退了,那应该是检测frida的端口,做下frida端口转发,重新hook,成功获取到入参和返回值

 

 

分析下入参:

  • arg1: Context
  • arg2: 接口中functionId
  • arg3: 查询参数
  • arg4: uid
  • arg5-arg6:apk版本
  • 返回值:时间戳+sign+算法版本

hook完后,我们接下来进行模拟执行!

二、unidbg模拟执行

  1. 先把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
public class JdSign extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    public PKCS7 pkcs7;
 
 
    JdSign() {
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\jd\\1.apk"));
        vm.setJni(this); // 设置JNI
        vm.setVerbose(true); // 打印日志
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\jd\\libjdbitmapkit.so"), true); // 加载so到虚拟内存
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
    }
 
    public static void main(String[] args) {
        JdSign jdSign = new JdSign();
    }
}

开始运行,果不其然报错了

 

 

那就在这个类下补一下这个

1
2
3
4
5
6
7
8
9
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature) {
        case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {
            return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(signature);
        }
    }
    return super.getStaticObjectField(vm, dvmClass, signature);
}

以此类推,报什么类型的错误就对应补响应的返回就可以了

 

有个坑强调下:

 

在补com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B时我们可以看到调用unZip方法时传入的参数正是apk,然后进行解压,所以要把apk传进去,不然后续就会一直异常

 

  1. 调用函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public void callSign(){
            List<Object> list = new ArrayList<>(10);
            list.add(vm.getJNIEnv());
            list.add(0);
            DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
            list.add(vm.addLocalObject(context));
            StringObject str = new StringObject(vm,"search");
            StringObject str2 = new StringObject(vm,"{\"addrFilter\":\"1\",\"addressId\":\"0\",\"articleEssay\":\"1\",\"deviceidTail\":\"49\",\"exposedCount\":\"40\",\"frontExpids\":\"F_0_0\",\"gcAreaId\":\"12,904,3024,0\",\"gcLat\":\"0.0\",\"gcLng\":\"0.0\",\"imagesize\":{\"gridImg\":\"532x532\",\"listImg\":\"341x341\",\"longImg\":\"532x681\"},\"insertArticle\":\"1\",\"insertScene\":\"1\",\"insertedCount\":\"2\",\"isCorrect\":\"1\",\"keyword\":\"第一行代码\",\"newMiddleTag\":\"1\",\"newVersion\":\"3\",\"oneBoxMod\":\"1\",\"orignalSearch\":\"1\",\"orignalSelect\":\"1\",\"page\":\"4\",\"pageEntrance\":\"1\",\"pagesize\":\"10\",\"pvid\":\"8cb41ee45f4145d8b5b97d4b9a81ae78\",\"searchVersionCode\":\"9418\",\"secondInsedCount\":\"0\",\"showShopTab\":\"yes\",\"showStoreTab\":\"1\",\"stock\":\"1\",\"ver\":\"108\"}");
            StringObject str3 = new StringObject(vm,"4b9655ae0e4a755a");
            StringObject str4 = new StringObject(vm,"android");
            StringObject str5 = new StringObject(vm,"10.2.0");
            list.add(vm.addLocalObject(str));
            list.add(vm.addLocalObject(str2));
            list.add(vm.addLocalObject(str3));
            list.add(vm.addLocalObject(str4));
            list.add(vm.addLocalObject(str5));
            Number number = module.callFunction(emulator,0x28b4+1,list.toArray())[0];
            String result = vm.getObject(number.intValue()).getValue().toString();
            System.out.println("result: "+ result);
        }

    此时运行还会报一些简单的环境错误,对应的补一下就可以了,此处就不累述了。

    小tips:要是对unidbg模拟执行和补环境不是特别了解,可以加龙哥的unidbg星球详细学习一下,这男人真就不眠不休的更。。。

    现在可以正确执行出来sign的结果了,unidbg调用成功,接下来分析算法

三、sign算法还原

本案例采用IDA + unidbg来还原算法

  1. 打开IDA搜索下getSignFromJni这个函数是否为静态注册,运气比较好,正好是静态注册,如果是动态注册,就直接使用yang神的脚本hook下registerNatives,找下函数的偏移地址

  1. 点进去看看这个函数,并修正下JNIEnv,以及参数

    大致看下这个函数

    多次调用函数sub_1261C,将java层传过来的字符串拼接起来,比如function=search&body=省略

  2. 在so层生成了一个时间戳,并拼接上去st=13位时间戳

    原来是拼接了时间戳,我说呢,每次unidbg调用的sign都不一样,从unidbg调用结果可以看出,还生成了st时间戳传到java层,这很好理解,把时间戳也放进去加密了,总要让服务器知道是哪个时间戳吧。

    这个sign的值又是32位16进制,我很难不想到md5加密

  3. 接着向下看

    这里的v48就是&sign=这个字符串,然后又通过sub_1261c把v47拼接上去了,那v47应该就是sign的值了,往上看看前面几个主要函数,分析算法时一定要抓主干分析。

  4. 接下来重点分析sub_18B8sub_227C函数

    掏出龙哥写的IDA插件:findhash跑一下,骚等一会应该就出结果了

    此时有两种思路

    • 直接查看这几个特征,观察是谁调用了它
    • frida写个主动调用,然后用生成的frida脚本,hook下看下调用栈

    我直接使用第一种简单看了下第一个函数,点进去看谁调用了它

    大大的写着sub_227C调用了它,这个函数不就是刚才主流程中的函数吗,这个时候再往下看看这个函数,密密麻麻的算法,像极了md5运算

    插播一下,md5的大致流程(抄来的)

    哈希加密一般分为Init、Update、Final三步

    简单来说:

    1. Init是一个初始化函数,初始化核心变量
    2. Update是主计算过程
    3. Final整理和填写输出结果

    那这个函数不就是md5的计算过程吗,而且sub_227C多处调用了这个函数,入参肯定有参与计算的明文

  5. console debugger可以派上用场了

    可以先逆推,我先看下面的函数结果,再看参数是怎么生成的

    • 先hook下sub_227C以及刚才的update函数
    1
    2
    3
    4
    public void HookByConsoleDebugger() {
            Debugger debugger = emulator.attach();
            debugger.addBreakPoint(module.base + 0x227c+1); // hook sub_227C
        }

    运行后就停在了sub_227C处,看一下参数的三个参数(在r0-r2)

    r0和r2是个地址,r1应该是个明文长度吧,打印试试

    这r1果然是r0的长度,r0应该就是明文了

    这r2看不出来啥,看着像个buffer,可能用来存放结果(我们可以在函数结尾打印下这个地址看看)

    直接在面板输入b0x244a在sub_227C函数结尾下个断点看看,

    按c是到下一个断点,这个时候停了下来,打印刚才r2的地址康康

    胸弟,你果然不简单啊,原来你就是最后的sign值了,那看来分析是没错了。

    • hook下md5update函数看看加密的明文参数是什么

      在0x1988处下个断点,

      我们看下参数

      mr1好像就是刚才sub_227C的一部分啊

      继续按c到下一个断点,还是断在md5update这个函数,这是因为update截取一部分明文update进去

      这样看下来就比较明显了,每次传64字节update进去运算,那我们直接把sub_227C函数传入的明文在逆向之友上验证一下

      有点意思,那我们只要知道入参是什么那不就结束了吗,看这个入参像个base64,还有上面一个函数没看呢

    • hook下sub_18B8

      先打开这个函数整体看一下,

      多处用到了aAbcdefghijklmn,双击看一下是什么,发现就是"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",这像极了base64的码表

      下个断点验证下入参和结果:

      这r0看起来像个buffer,等下函数结尾验证下这个地址,r1一串很长的16进制是不是base64的明文呢?r2应该是r1的长度了,在函数结尾下个断点b0x1976,验证下

      还真是base64的结果了,在线网站验证下,ok没啥问题。那现在流程就很清楚了啊,就只要知道传入的参数r1这一串是怎么生成的就ok了

  6. 梳理下sign加密流程

    • sub_127E4函数传入了java层的字符串,拼接成一个字符串
    • so生成一个时间戳拼接到明文上
    • sub_126AC函数将明文做异或操作,变成一串像乱码一样的字符
    • sub_18B8函数进行base64编码
    • base64编码的结果进行md5加密
  7. 分析sub_126AC对明文做了什么操作

    • 入参分析

      参数1就是JNIEnv,,v65为加密明文字节码,v33为加密长度,v26以及v27为随机数,v25、v26经过sub_12640函数转成整数,控制算法的版本

    • sub_126AC函数下面的switch就是控制走哪个版本的算法,我分析了其中一个v=111走sub_10DE4生成了base64的入参

    hook下看下入参

    参数1:固定的字符串

    参数2:拼接后的明文

    拼接的格式functionId=...&body=查询的参数&uuid=...&client=android&clientVersion=10.2.0&st=13位时间戳&sv=111(算法版本,可以请求时固定)

  8. sub_12ECC对明文做了异或处理

    进入sub_12ECC,分析下入参:

    五个参数,此处讲一下r0-r3存放前4个参数,第5个参数在sp中,可小端序查看一下

    参数1:

    参数2:固定的字符串

    参数3:1

    参数4:明文

    参数5:明文长度

    msp查看sp寄存器,小端序查看为0x02e8,然后打印r3

    经分析,主要的异或在下方的循环中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if ( a3 )                                   // pass
       {
         v15 = 0;
         do
         {
           v16 = &v21[7] + (v15 & 15);             // v21这个数组偏移0x20(&v21[7])
           v17 = v15++ & 7;
           v18 = *(v16 - 20); 取数组往前偏移20字节
           result = ((v18 ^ *plainText ^ *(a2 + v17)) + v18);
           LOBYTE(v18) = v18 ^ result;             // 这里逐位异或,LOBYTE取低位的一个字节
           *plainText++ = v18;
           *(plainText - 1) = v18 ^ *(a2 + v17);
         }
         while ( v15 != plainTextlen );
       }

打一个断点在循环的开始b0x12F68,查看下循环

 

 

可以配合汇编一起看下

  1. and r2, r3, #0xf

    将r3 & 0xf并存到r2寄存器中,r3观察循环可知就是v15,

  2. add r0, sp, #0x20

    可以按s单步调试,查看下sp寄存器

    这几个是循环中一直用到的分析下

    1
    2
    3
    4
    v21[2] = 0x68449237;
    v21[3] = 0x7FCC3DA5;
    v21[4] = 0x88D90FBB;
    v21[5] = 0x5AE99AEE;

    其实就是v21的小端序,写死就可以。其它的可以对照汇编指令修改一下,就可以把这个循环改写出来了

  3. 测试下sign,没问题

四、表单body加密

本来想写到这里就结束了,后来发现我换个关键词去搜索,就歇菜了,那肯定是还有其他参数做了关键词和页数校验

 

hook发现,果然这里还有个base64,而且是个自写的仿base64

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_base64() {
    // hook cipher中的body加密
    Java.perform(function () {
        var cls = Java.use("com.jd.phc.d")
        var jstring = Java.use("java.lang.String")
        cls.x.implementation = function (bArr) {
            var result = this.x(bArr)
            console.log("base64 arg1: ",jstring.$new(bArr))
            console.log("base64 result:",result)
            return result
        }
    })
}

逻辑比较简单,重写下java就可以了

五、写在最后

这篇整体来说难度不大,后面可能会遇到算法去符号,md5魔改、aes等,这就需要对算法的原理非常熟悉了。

 

最后还是推荐加入龙哥的星球学习unidbg教程,或者肉师傅之前的so系列课系统的学习密码学实现

 


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 3
支持
分享
最新回复 (4)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
2021-11-1 19:50
0
雪    币: 97
活跃值: (818)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
666
2021-11-1 20:53
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
请问大佬BitmapkitZip是如何导进去的
2022-2-28 10:25
0
雪    币: 102
活跃值: (2055)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
mark
2022-8-19 10:10
0
游客
登录 | 注册 方可回帖
返回
//