首页
社区
课程
招聘
某短视频sig3算法分析
2024-4-13 19:35 1597

某短视频sig3算法分析

2024-4-13 19:35
1597

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

1
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辅助分析算法的技巧.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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的时机呢?最好是在调用初始化函数后,否则初始化的那个执行可能会干扰,不过关系不大.

1
2
3
4
5
6
7
8
9
10
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中下断看下,先看下这个位置走了多少次

1
2
3
4
5
6
7
8
9
10
11
12
13
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次调用的时候断下

1
2
3
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为特征纯算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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为特征值的查表法实现

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
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个字节,那个位置离生成位置肯定是非常近的.

1
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处入参是否改变.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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一下.

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
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


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞4
打赏
分享
最新回复 (2)
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
蜡笔晓辉 2024-4-15 00:05
2
0
网页已保存本地
雪    币: 30
活跃值: (360)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
linkin5epk 2024-4-15 00:31
3
0
666
游客
登录 | 注册 方可回帖
返回