某短视频sig3算法详解

sig3是某个很火的短视频的核心加密参数,48位,主要介绍深度ollvm混淆的so层算法如何还原,除此之外,此app还有大量的花指令需要处理,这块看龙哥的就好了,非常清晰.

https://www.yuque.com/lilac-2hqvv/zfho3g/issny5?#%20%E3%80%8A%E8%8A%B1%E6%8C%87%E4%BB%A4%E5%A4%84%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89%E3%80%8B

前提准备:

一份去花过后的so,so和apk放123云盘了,在文章末尾.
熟悉crc32,WhiteBoxaes,sha256以及hmac算法,了解越多你能还原的可能性就越大,了解的程度不限于算法细节,特征值,以及算法的魔改方向.本章除了白盒AES不过多介绍,因为写过很多篇了,需要的翻我之前的文章,crc32和sha256以及hmac都会详细介绍.如果你只知道一个md5也没关系,看完你也能收货很多逆向技巧.

因为写文章的时候没办法完全还原我最初的思路,所以我尽可能按照第一次分析这个so的思路来写,所以如果某个地方你觉得很神奇作者tm是怎么想到的,不要奇怪,因为他踩了很多坑,但是坑有很多,没办法完全展现出来,我只能确保你跟着我的思路算法一定可以搞出来,毕竟花几天分析一个so和你一个小时看完这篇文章是截然不同的.
我创建了一个逆向技术交流群,有需要的加我w lyaoyao__i(两个_)

unidbg辅助算法分析

此so有初始化校验,需要先初始化目标函数,否则不会返回正确结果.初始化这块也不是文章的重点,所以这块不详细介绍,一切与算法还原关系不大的我都会淡化,重点介绍上面的几个算法以及unidbg辅助分析算法的技巧.

package com.ks;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
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.api.AssetManager;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.github.unidbg.virtualmodule.android.JniGraphics;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;

public class ks2 extends AbstractJni implements IOResolver{
    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("file open:"+pathname);
        return null;
    }
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    ks2(){
        emulator = AndroidEmulatorBuilder.for64Bit().build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/ks/ks11.420.30984.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        new JniGraphics(emulator, vm).register(memory);
        new AndroidModule(emulator, vm).register(memory);
        emulator.getSyscallHandler().addIOResolver(this);   //重定向io
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary("kwsgmain", true);
//        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/ks/libkwsgmain.so"), true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
    public void callByAddress(){
        List<Object> list = new ArrayList<>(4);
        list.add(vm.getJNIEnv()); // 第⼀个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填0,⼀般⽤不到。
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        list.add(10412); //参数1
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17"); // SO⽂件有校验
        vm.addLocalObject(appkey);
        DvmInteger intergetobj = DvmInteger.valueOf(vm, 0);
        vm.addLocalObject(intergetobj);
        list.add(vm.addLocalObject(new ArrayObject(intergetobj, appkey, intergetobj, intergetobj, context, intergetobj, intergetobj)));
        // 直接通过地址调⽤
        Number numbers = module.callFunction(emulator, 0x41680, list.toArray());
        System.out.println("numbers:" + numbers);
        DvmObject<?> object = vm.getObject(numbers.intValue());
        String result = (String) object.getValue();
        System.out.println("result:" + result);
    };

    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;": {
                return new StringObject(vm, "/data/app/com.smile.gifmaker-q14Fo0PSb77vTIOM1-iEqQ==/base.apk");
            }
            case "com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;": {
                return new AssetManager(vm, signature);
            }
            case "com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;": {
                return new StringObject(vm, "com.smile.gifmaker");
            }
            case "com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;": {
                DvmClass clazz = vm.resolveClass("android/content/pm/PackageManager");
                return clazz.newObject(signature);
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }
    @Override
    public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "java/lang/Boolean->booleanValue()Z":
                DvmBoolean dvmBoolean = (DvmBoolean) dvmObject;
                return dvmBoolean.getValue();
        }
        return super.callBooleanMethodV(vm, dvmObject, signature, vaList);
    }
    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature) {
            case "com/kuaishou/android/security/internal/common/ExceptionProxy->getProcessName(Landroid/content/Context;)Ljava/lang/String;":
                return new StringObject(vm, "com.smile.gifmaker");
            case "com/meituan/android/common/mtguard/NBridge->getSecName()Ljava/lang/String;":
                return new StringObject(vm, "ppd_com.sankuai.meituan.xbt");
            case "com/meituan/android/common/mtguard/NBridge->getAppContext()Landroid/content/Context;":
                return vm.resolveClass("android/content/Context").newObject(null);
            case "com/meituan/android/common/mtguard/NBridge->getMtgVN()Ljava/lang/String;":
                return new StringObject(vm, "4.4.7.3");
            case "com/meituan/android/common/mtguard/NBridge->getDfpId()Ljava/lang/String;":
                return new StringObject(vm, "");
        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }
    @Override
    public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature){
            case "com/kuaishou/android/security/internal/common/ExceptionProxy->nativeReport(ILjava/lang/String;)V":{
                return;
            }
        }
        super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
    }

    public String get_NS_sig3() throws FileNotFoundException {
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // 第⼀个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第⼆个参数,实例⽅法是jobject,静态⽅法是jclass,直接填0,⼀般⽤不到。
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        list.add(10418); //参数1
        StringObject urlObj = new StringObject(vm, "yangruhua");
        vm.addLocalObject(urlObj);
        ArrayObject arrayObject = new ArrayObject(urlObj);
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
        vm.addLocalObject(appkey);
        DvmInteger intergetobj = DvmInteger.valueOf(vm, -1);
        vm.addLocalObject(intergetobj);
        DvmBoolean boolobj = DvmBoolean.valueOf(vm, false);
        vm.addLocalObject(boolobj);
        StringObject appkey2 = new StringObject(vm, "7e46b28a-8c93-4940-8238-4c60e64e3c81");
        vm.addLocalObject(appkey2);
        list.add(vm.addLocalObject(new ArrayObject(arrayObject, appkey, intergetobj, boolobj, context, null, boolobj, appkey2)));
        Number numbers = module.callFunction(emulator, 0x41680, list.toArray());
        System.out.println("numbers:" + numbers);
        DvmObject<?> object = vm.getObject(numbers.intValue());
        String result = (String) object.getValue();
        System.out.println("result:" + result);
        return result;
    }
    public static void main(String[] args) throws FileNotFoundException {
        ks2 ks = new ks2();
        ks.callByAddress();
        ks.get_NS_sig3();
    }
}

确保运行后能出结果再进行下面的操作,每运行一次结果都在变化,输入是固定的yangruhua.
猜测存在时间戳或者随机数,如果能固定住他们对算法还原帮助会很大,so可以有很多方法获取时间戳和随机数,比如jni,库函数,系统调用(最常见)以及文件访问
在这里插入图片描述
经过我的测试,只需要改unidbg-api/src/main/java/com/github/unidbg/unix/UnixSyscallHandler.java下的gettimeofday64函数的System.currentTimeMillis()固定即可,但是如果你的ks版本是9点几左右的可能不行,还需要固定随机数,因为我这个11版本的随机数采用了时间戳作为随机种子导致结果不变,所以只需要改这一处.后面遇到的时候还会介绍为什么会这样.我固定的时间戳是1712760339987,记住这个值,如果会影响最终结果肯定是参与了运算的.
在这里插入图片描述
运行后每次结果都是0110604391b5265849494a4b7429b2a243f7443554585640

算法分析

结果有了也固定住了,接下来该还原算法了.
看下目标函数,动态注册的,符号什么的去的很干净,几乎没办法根据字符串猜函数功能(几乎所有的).
在这里插入图片描述
接近3000行的伪c代码,并且只是其中一个主函数,并且左边的流程图有严重的ollvm混淆,怎么看是否有ollvm混淆,看流程图或者导出函数
在这里插入图片描述
这种x开头并且一大串的就是,流程图就是上面所展示的,还可以看伪c代码,有很多while循环或者分支的就是.
怎么分析?
思路1:从前往后看,跟着入参走
思路2:从后往前推,由结果倒推算法
思路1只适合比较简单的so,如果so很复杂,你压根跟踪不了入参,因为入参很多地方都会出现,而且大部分时候入参不止一个.所以思路2是比较好的,当然这也是我个人的习惯,如果你非要跟入参也不是不行,根据个人习惯就好了.

trace分析

接近3000多行代码,0110604391b5265849494a4b7429b2a243f7443554585640第一次生成的位置在哪?如果只是找到了它最终返回的位置没办法定位到最初生成的位置,因为3000多行这个变量赋值来赋值去很混乱,静态分析几乎找不到,当然你时间够多也可以试试.
解决方法呢?unidbg trace.trace的时机呢?最好是在调用初始化函数后,否则初始化的那个执行可能会干扰,不过关系不大.

String traceFile = "unidbg-android/src/test/java/com/ks/trace.txt";
PrintStream traceStream = null;
try{
    traceStream = new PrintStream(new FileOutputStream(traceFile), true);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
 //核心 trace 开启代码,也可以自己指定函数地址和偏移量
emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream);
System.out.println("_NS_sig3 start");

在这里插入图片描述
就在函数最开始的地方trace一下,大概一两分钟的时间就好了.
在这里插入图片描述
最终trace的结果10万行.上面我们说了从结果往前推,结果是0110604391b5265849494a4b7429b2a243f7443554585640,这个时候搜索的技巧显得很重要了,你无法确定结果是大端续还是小端续,是一个字节拼接还是4个字节拼接,所以你都得尝试一下.
在这里插入图片描述
比如你可以先搜0x0110,没结果,搜0x4360(按4个字节倒过来的)
在这里插入图片描述
都不行,试一下搜一个字节的,估计会比较多,0110604391b5265849494a4b7429b2a243f7443554585640
你不要直接搜0x01或者0x10,这个太普遍了,0x43出现的概率就很小,出现的话是我们的目标位置可能性更大.
在这里插入图片描述
注意啊,从后往前搜,287个也挺多,找赋值的地方,这个位置显然不太对,ldp x20, x19, [sp, #0x50]这条指令从ldr演化过来,p是Pair,中文就是一双的意思,也就是两个,x20存高位数据,x19存低位数据,从sp偏移0x50处取,这个位置显然不对,这个0x43会被覆盖掉,往上找.
在这里插入图片描述
这个位置感觉很合适,赋值操作,搜一下[libkwsgmain.so 0x012340] [e903142a] 0x40012340: “mov w9, w20”,注意不要搜到后面的,只搜这条指令
在这里插入图片描述
最后一个0x40在这里插入图片描述
倒数第二个0x56
在这里插入图片描述
不就是0110604391b5265849494a4b7429b2a243f7443554585640从后往前的字节吗?so中的地址是12340

在这里插入图片描述
进来后显示在这个位置,c代码看不懂在干什么,看下汇编
在这里插入图片描述
w20给w9,w20没出现过,unidbg中下断看下,先看下这个位置走了多少次

public void HookByConsoleDebugger() {
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x12340, new BreakPointCallback() {
        int num = 0;
        RegisterContext context = emulator.getContext();
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            num+=1;
            System.out.println("num次:"+num);
        return true;
        }
    });
    }

在这里插入图片描述
和trace的结果一样,接下来修改下代码在第81次调用的时候断下

if(num>80){
   return false;
}

在这里插入图片描述
可以看到此时x20就是0x01了就是第一个字节,我的本意是想监控谁往一块内存地址写入了0110604391b5265849494a4b7429b2a243f7443554585640,但是这个位置都是赋值操作,不是最开始的地方,所以这个位置不是很理想,再去trace的位置搜一下有没有更好的位置,没有再回过头来分析.
这次我在0x43后面加了一个空格
在这里插入图片描述
结果有54个,这个位置似乎挺好的ldrb w1, [x21], #1,还是ldr演变过来的,b就是byte,加载一个字节给w1,那么x21会不会存的就是那24个字节呢?试试看,还是和上面一样的操作流程.3d720处下断,也是走104次,在第81处断下.
在这里插入图片描述
结果出来了,就是这个位置了,先声明一下,后面的一些操作也不一定是我最开始的流程,也有可能是写文章时突发奇想出来的,因为我也记不清最开始是怎么操作的.
接着这控制台输入bt(back trace),也就是看堆栈,和你js操作差不多
在这里插入图片描述
每个栈都看一下,最后锁定在0x04561c
在这里插入图片描述
ida中就是这个位置,并且这个位置就是在最开始的那个大函数内部,接着下断看下3D5F4处调了几次
在这里插入图片描述
3次,额,不确定是有其他地方调用了还是本来在这行汇编就跑了3次,所以hook下这行汇编看看
还是之前的hook代码,改下地址就好了,结果是一次,把返回的true改成false,就可以断下来
在这里插入图片描述
这个时候参数已经组装好了,根据ATPCS调用约定,arm64下参数1到参数8 分别保存到 X0~X7 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 X0 中,所以这行汇编处参数已经组装好了
在这里插入图片描述
mx0看一下,结果在函数调用前就已经生成了.
在这里插入图片描述
你可以尝试监控一下谁往x0的地址也就是0xbffff5f0写入了数据,不过这里不需要了,上面的代码就能看明白,v371来自上面的一个do while循环,直接监控也是这个位置,所以每个字节来自v371^(v287异或v290)
注意,两次异或,我们在^=汇编处下断看下,稍微懂点汇编就行.
在这里插入图片描述
0x45598处
在这里插入图片描述
调用了23下,但是结果是24字节,把ture改成false断下来分析下

在这里插入图片描述
x11异或x12再给x11,这个x12是0xfffff940,x11是0x41,这个x12不是地址,直接m会报错的
在这里插入图片描述
看下一条指令strb w11, [x8, x10],拆解下就是 str b(store byte),将w11存到x8偏移x10处,b0x4004559c,在这行汇编下个临时断点看下结果
在这里插入图片描述
x11变成了0xfffff901,后面两位不就是0110604391b5265849494a4b7429b2a243f7443554585640的开头吗,x12是0xfffff940,x11是0x41,他两异或就是0x40异或0x41得到0x01,后续都是这样操作
在这里插入图片描述
这里真的很清晰了,概况一下就是v371处的地址先计算所有字节之和得到v287,取v287的最后一个字节进行异或,一共23轮,至于最后一个字节,一开始就已经生成了.
所以重点就是来自mx8的前23个字节,这里也可以看到第24个字节0x40已经生成了,对应结果的最后一个字节
在这里插入图片描述
这23个字节你有没有感觉很奇怪,中间很多00,正常来说如果是经过了什么处理肯定不会出现那么多00,应该和结果一样是16个字符比较均匀的排列,所以我怀疑这个位置离参数生成结果比较近,当然你也可以和上面的步骤一样接着跟这23个字符,去trace的文件中找.
能影响入参的就两个,一共时间戳1712760339987,另一个明文yangruhua.
接下来有3种情况
1只改时间戳
2只改明文
3两个都改
3种情况的输出我都打印一下,时间戳改成1712760339986,字符串改成yangruhua1
第一种
在这里插入图片描述
第二种
在这里插入图片描述
第三种
在这里插入图片描述
总结一下,这样改动只会影响13-16字节,对比23两种情况发现,这4个字节的产生是由明文决定的,但是就算明文不变,时间戳也会影响结果,但是从1712760339987改到1712760339986结果不变,什么原因?
会不会是需要秒级别以上时间戳改动才会影响结果?
把1712760339987改成1712760338987
在这里插入图片描述
运行一下,第5-8和17-20字节都改变了,并且17-20字节由13 A6 16 66变成了12 A6 16 66,我只是把1712760339987改成了1712760338987,也是只改了一秒,会不会这4个字节和这个秒数有关
在这里插入图片描述
很明显了17-20字节数秒级时间戳的16进制的小端序,正常的就是大端序,按字节反转就是小端序.至于5-8字节也变化了,开头我们说了随机数,9版本的我试过是纯随机的,11版本的好像用了时间戳作为随机种子,时间戳固定的话,随机数也是相对固定的,还和其他因素有关,没办法由时间戳来推出这4个字节,至于是怎么发现的,本来打算trace分析一波,会走到jnionload里面去,执行时机非常早,需要在模拟器刚刚创建的时候就trace上,但是jnionload还没执行
在这里插入图片描述
但是篇幅太长了,所以我这里就不继续了!!!
所以只需要分析第13-16字节的数据就好了,已知只和明文有关.

crc32+aes+sha256+hmac

算法就是上面这几个,接下来具体分析下.先说明下sha256+hmac这两个其实是组合hmacSha256,也可以拆开来计算.接着上面追踪38 64 FC ED,输入是yangruhua
在这里插入图片描述搜了下没有结果,看看是不是小端序
在这里插入图片描述
是的,就3处.下面那条str是存储指令,看上面那条121d4处
在这里插入图片描述
进来就是return的位置,就是他两异或
看下这个函数的入参,因为返回值就是结果,函数是120C4,这个函数处下断,先看下调用了几次,确认是1次.
看下入参
在这里插入图片描述
3个参数 入参2是入参1的长度0x30
在这里插入图片描述
在这里插入图片描述

crc32

这里是小端序,0x04c11db7,什么时候是大端什么时候是小端,凭感觉,0x04c11db7这个是啥?传过去一般来说不可能没用吧
在这里插入图片描述
crc32特征值,其实我第一次做的时候并不是直接去看这个函数的,而是跟着trace的代码往上分析,它不是异或得来的吗,一步步往上看,会发现用到了一个0xEDB88320,并且很频繁
在这里插入图片描述
搜一下
在这里插入图片描述
也是crc32的特征值,介绍一下crc32吧
CRC32 是一种流行的校验和算法,用于检测数据损坏。该算法存在多种具有相似数学特性的变体。所以这个特征值有很多,常见的有这两个
以0xEDB88320为特征纯算

CRC32_POLYNOMIAL = 0xEDB88320
def calculate_crc32(data):
    crc = 0xFFFFFFFF # 初始值
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ CRC32_POLYNOMIAL
            else:
                crc >>= 1

    return crc ^ 0xFFFFFFFF # 取反
# 示例数据
data = bytes.fromhex('452585a574619ae3eee5403180b854b7188cfc945093a20d1e0a1441ed806cfbe0ed8ea4aab0c1d5f4519f8d19c4948f')
byte_array = bytearray(data)
crc32 = calculate_crc32(byte_array)
# 打印结果
print("CRC32:", format(crc32, '08X'))

以0x04c11db7为特征值的查表法实现

def generate_crc32_table(_poly):
    custom_crc32_table = []
    for i in range(256):
        c = i << 24
        for j in range(8):
            if (c & 0x80000000):
                c = (c << 1) ^ _poly
            else:
                c = c << 1
        custom_crc32_table.append(c & 0xffffffff)
    return custom_crc32_table
origin_crc32_table = generate_crc32_table(0x04c11db7)
def getCrc32(bytes_arr):
    length = len(bytes_arr)
    if bytes_arr != None:
        crc = 0xffffffff
        for i in range(0, length):
            crc = (crc << 8) ^ origin_crc32_table[(getReverse(bytes_arr[i], 8) ^ (crc >> 24)) & 0xff]
    else:
        crc = 0xffffffff
    crc = getReverse(crc ^ 0xffffffff, 32)
    return crc
def getReverse(tempData, byte_length):
    reverseData = 0
    for i in range(0, byte_length):
        reverseData += ((tempData>>i)&1)<<(byte_length-1-i)
    return reverseData
data = bytes.fromhex('452585a574619ae3eee5403180b854b7188cfc945093a20d1e0a1441ed806cfbe0ed8ea4aab0c1d5f4519f8d19c4948f')
byte_array = bytearray(data)
crc32 = getCrc32(byte_array)
print("CRC32:", format(crc32, '08X'))

这两个计算的结果都是EDFC6438,转小端序就是38 64 FC ED,除此之外binascii.crc32就有crc算法,不需要上面的算法,这里贴上只是为了介绍这个算法,
在这里插入图片描述
有帖子说0x04C11DB7 是正式,0xEDB88320 是反式,这里不需要管,因为它没有魔改,结果也是出来了

还不懂可以参考这篇文章https://github.com/Michaelangel007/crc32

wbAes

在这里插入图片描述
追踪这48字节的由来,方法有很多,比如可以这此处查看下调用栈或者直接跟上一个函数.
我这里选择一个比较快的方式,跟踪0x404e4e40这块内存内存,谁往这里写入了这48个字节,那个位置离生成位置肯定是非常近的.

emulator.traceWrite(0x404e4e40,0x404e4e40+0x30);

在这里插入图片描述

有结果,看下pc寄存器指向的位置0x1c17c,这个pc寄存器指向了当前将要执行的指令的地址,LR寄存器指向的是结束的地址.
在这里插入图片描述
这个位置视乎不太好往上找,看下lr寄存器指向的位置0x1ea10的上一条,memcpy,src拷贝到v28,长度是v41.
在这里插入图片描述
如果是拷贝的话,就不是最初生成的位置,需要追踪scr的赋值位置,还是一样的道理,直接找很慢,还不一定能找到.
在这里插入图片描述

0x1ea10的上一条汇编,bl会跳到memcpy,估计会跳到libc.so里面,但是在此之前参数一样组装好了,
根据ATPCS调用约定,arm64下参数1到参数8 分别保存到 X0~X7 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 X0 中,所以这行汇编处参数已经组装好了,我们要的数据就在x1中.
在这里插入图片描述
地址是0x404d3240,改下trace代码 emulator.traceWrite(0x404d3240,0x404d3240+0x30);
在这里插入图片描述
跳到pc寄存器指向的位置0x265fc,可以看到结果是在这里一个字节一个字节添上去的,我这里改了入参名,所以是output和input,你那里不是.
在这里插入图片描述
hook下2636C函数
在这里插入图片描述
会调用3次,应该是一次16个字节,一共48个字节
在这里插入图片描述
在这里插入图片描述
看下入参,这个后面有16个10,这个似乎是pkcs7填充过的,应该不会有其他的巧合,这种填充一般用于对称加密比如des和aes,但是des分组长度8字节,这个16字节,大概率是aes了.我需要验证下,但是这个位置不好验证,因为假设它真的是aes,这个位置已经填充过了,我需要在最开始调用的地方验证.
监控下填充过的数据0x404d3300的赋值位置
在这里插入图片描述
pc寄存器是libc指向的,看lr寄存器0x25ab8
在这里插入图片描述
进入了259a0函数处,hook看一下.只调用一次,看下入参
在这里插入图片描述
在这里插入图片描述
入参1是未填充过的,入参2是长度0x20.
接下来验证下是都是真的pkcs7填充过的,在这个位置hook把入参改了,同时监控0x2636C处入参是否改变.

debugger.addBreakPoint(module.base+0x2636C);
debugger.addBreakPoint(module.base+0x259a0,new BreakPointCallback() {
RegisterContext context = emulator.getContext();
@Override
public boolean onHit(Emulator<?> emulator, long address) {
    String hexString = "79616e677275687561"; // 十六进制字符串
    int length = hexString.length()/2;
    MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true);
    byte[] byteArray = DatatypeConverter.parseHexBinary(hexString);
    fakeInputBlock.getPointer().write(byteArray);
    // 修改X1为指向新字符串的新指针
    emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,fakeInputBlock.getPointer().peer);
    emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X1, 0x9);
    emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            return true;
        }
    });
    return true;
}
    });

这里需要注意需要把入参的长度一并改了,否则可能会发生意想不到的错误
在这里插入图片描述
0x2636C处入参长度也变了,还记得之前是0x30对吧.
在这里插入图片描述
看吧,pkcs7,差7个字节补7个07.其实就算这样也不能确定是aes,肯定有其他对称算法也是16字节分组,不过比较少.暂时就认为他是了,你也可以用ida的插件识别下用到了哪些算法.
在这里插入图片描述
我是本着学习的时候能不用就不用,不到万不得已不用这个插件,有RijnDael_AES字眼,大概率就是aes了,有时候你会见到RijnDael,不要奇怪,最初就是叫RijnDael,后来评高级des(aes),Advanced Encryption Standard,des是Data Encryption Standard,也就变成了aes,几乎很少见到RijnDael.
那首先确定下是什么模式,填充方式已经知道了,pkcs7,常见的就是cbc和ecb呗.cbc需要一个iv,这些模式其实很多对称加密算法都是通用的.
怎么验证呢?传两个一样的参数就好了,看看结果是否一样.
把16进制改成79616e6772756875616c6f7665796f7579616e6772756875616c6f7665796f75
长度改回0x20.
入参
在这里插入图片描述
加密两轮后结果
在这里插入图片描述
结果一样是ecb,现在差最后一个不知道了,也是最重要的秘钥.
看了下入参,没有秘钥,大概率是白盒了,接下来找state块,入参是个很好的选择.

在这里插入图片描述
比如25938函数,还是要把之前的入参改了,这样可以减少加密的轮数,确保只有一个分组.
在这里插入图片描述
确实是10轮,看下第一轮入参是什么,注意这个入参后面返回也是在这块地址,很典型的参数当返回值.
入参
在这里插入图片描述
blr下临时断点到函数执行完的位置,直接查看之前那块内存就好了,注意是m0x404e3000,不是mx0,虽然这里可以,但是大部分都不行.
在这里插入图片描述
这样看不明显,排成矩阵看一下
在这里插入图片描述
在这里插入图片描述
这不就是循环左移了,还是之前的那个问题,第一次第一次竟然是入参,我在某幸咖啡谈到过,这里不再说原因了.那篇也是白盒aes.那就很好办了,这个位置刚好10轮,直接dfa一下.

 debugger.addBreakPoint(module.base+0x26cb8,new BreakPointCallback() {
 RegisterContext context = emulator.getContext();
 @Override
 public boolean onHit(Emulator<?> emulator, long address) {
     byte[] bytes = emulator.getBackend().mem_read(0x404e3000,0x10);
     StringBuilder hexString = new StringBuilder();
             for (byte b : bytes) {
                 hexString.append(String.format("%02X", b & 0xFF));
             }
     System.out.println("aesResult:"+hexString);
     String filename = "unidbg-android/src/test/java/com/ks/dfaAes.txt"; // 文件名
     try {
         FileWriter writer = new FileWriter(filename,true);
         writer.write(hexString.toString()+"\n"); // 写入字符串
         writer.close();
     } catch (IOException e) {
         System.err.println("写入文件时出现错误:" + e.getMessage());
     }

     return true;
 }
});    

public void callDfa(){
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x25938,new BreakPointCallback() {
            UnidbgPointer pointer;
            RegisterContext context = emulator.getContext();

            int num = 1;
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                pointer = context.getPointerArg(0);
                if(num%9==0){
                    pointer.setByte(randint(0,15),(byte) randint(0,0xff));
                }
                num+=1;
                return true;
            }

        });
    }
    public static int randint(int min,int max){
        Random rand = new Random();
        return rand.nextInt((max-min)+1)+min;
    }

在函数结束位置把结果输出出来,调用200下后处结果.再用phoenixAES算出第10轮秘钥E8B900********************36B72,秘钥不公开,用aes_keyschedule算出主秘钥
在这里插入图片描述

684559*******************A5476,验证一下结果是对的.

hmacSha256

剩最后一个,假设我们没通过插件找到sha256痕迹
回到上面的那20个字节加密数据,20个字节你想到了什么,sha256?常见的是这个.

在这里插入图片描述
上面我们说了259a0是比较早的一个入参时机,a2来自26a14,直接下断看下a2的地址,trace这块内存.
在这里插入图片描述

来自0x404d8580
emulator.traceWrite(0x404d8580,0x404d8580+0x20);
在这里插入图片描述
看下这个位置0x1e260
在这里插入图片描述
在这里插入图片描述
参数3是入参,4是长度,2是个地址,存结果.blr后按c
在这里插入图片描述
结束时有结果,试一下是不是标准sha256
在这里插入图片描述
结果不一样,考虑3种情况
1 有盐值
2 hmac算法
3 魔改
先验证1
明文16进制79 61 6e 67 72 75 68 75 61,按照sha256的处理先填充80 00一直带56字节,再添加8字节的附加消息长度(大端序,md5是小端序)
在trace的文件中4个字节一组开始搜,0x61800000
在这里插入图片描述
你要说巧合的话哪会有这么多,不信的话可以改下入参,让他填充到80的时候刚好4个字节.
这样的话加盐的说法就不成立了.
魔改暂时不考虑,只有所有的可能都被排除的情况下才会去考虑魔改,因为这种要是魔改的话肯定就不是简单的给你改改常量那样了.
也就是说hmac?
这个碰到的少,很多人不了解算法细节.
我总结8个字就是两次加盐,两次哈希.hmac可以有很多哈希函数作为载体.md5,sha1,sha256,sha512,等等.
我下面说的对所有的都通用
1秘钥扩展:秘钥转hex后填充0达到分组长度,除了sha512是1024分组,其他都是512分组
2秘钥异或0x36得到扩展秘钥1,为什么是0x36,这是固定的,没那么多为什么.
3异或后的数据与明文(Message)级联:简单点就是扩展后的秘钥1拼接明文
4 3得到的数据哈希
5秘钥异或0x5c得到扩展秘钥2
6 扩展秘钥2级联4哈希的结果
7 对6的结果哈希就是最终结果.
所以hmac的特征值就是0x36和0x5c,不过很疑惑,trace的文件中找不到与0x36和0x5c有关的信息,注意是没有任何,我怀疑秘钥是刚好64字节,否则填充后00异或至少还有一个0x36,我这不是空穴来风,我仔细查找了trace的结果中的可能是结果的位置,没有看到有异或0x36的,以及ida的伪代码中也没有出现0x36,那就是说这个秘钥是提前处理好过的.如果能找到某个函数传了64个字节的话就比较好办了,否则的话需要去分析伪代码,就有点复杂了.
接着上面的伪代码分析
在这里插入图片描述
sub_219FC
在这里插入图片描述
1000多行很可疑,看下入参
在这里插入图片描述
入参5比较可疑
在这里插入图片描述
这里视乎是80个字节,有没有可能是两个异或后的扩展秘钥?
在这里插入图片描述
看懂了吗,这不就是扩展后的秘钥,到此,所有参数分析完毕!!!

总结

这篇如果写的详细一点的话至少可以写5篇,unidbg,trace技巧,crc32,白盒aes,hmacSha256,我写成一篇确实有点一口吃成大胖子的感觉.可能不太适合刚入行的朋友吗,不过这个作为国内数一数二的短视频加密难度还是很可以的,有基础的可以细细品味一下.
链接:https://www.123pan.com/s/4O7Zjv-gM2Bd.html

最后

微信公众号
在这里插入图片描述
知识星球
在这里插入图片描述

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨如画.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值