SO逆向入门实战教程四:mfw

一、前言

这是SO逆向入门实战教程的第四篇,上篇的重心是Unidbg的补环境以及哈希算法的简单魔改,本篇的重点是使用Unidbg中对魔改程度较深的加密算法进行分析。

  • 侧重新工具、新思路、新方法的使用,算法分析的常见路子是Frida Hook + IDA ,在本系列中,会淡化Frida 的作用,采用Unidbg Hook + IDA 的路线。
  • 主打入门,但并不限于入门,你会在样本里看到有浅有深的魔改加密算法、以及OLLVM、SO对抗等内容。
  • 对样本的分析仅限于学习和研究,坚决抵制黑灰产。
  • 一共十三篇,1-2天更新一篇。每篇的资料放在文末的百度网盘中。

二、准备

在这里插入图片描述

xPreAuthencode是目标方法,它接收三个参数

参数1是一个context,参数2是输入的明文,参数3是app的包名,返回值是40位的十六进制数。

Frida主动调用测试样本,参数2设为"r0ysue",参数3设为"com.mfw.roadbook",输出:

57c043fe945355a64cb9c3d75db4bd767d1bbccb

三、Unidbg模拟执行

老规矩,先搭一下架子

package com.lession4;

import com.github.unidbg.linux.android.dvm.AbstractJni;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

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

    mfw() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // 创建Android虚拟机
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // 加载so到虚拟内存
        module = dm.getModule(); //获取本SO模块的句柄

        vm.setJni(this);
        vm.setVerbose(true);
        dm.callJNI_OnLoad(emulator);
    };

    public static void main(String[] args) throws Exception {
        mfw test = new mfw();
    }
}

运行

在这里插入图片描述

RegisterNative(com/mfw/tnative/AuthorizeHelper, xPreAuthencode(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;, RX@0x4002e301[libmfw.so]0x2e301)

JNI OnLoad运行成功,我们的目标方法其地址是0x2e301,传入的三个参数是String或者context,都是前文讲过的类型,不做赘述。

package com.lession4;

import com.github.unidbg.linux.android.dvm.AbstractJni;
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.util.ArrayList;
import java.util.List;

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

    mfw() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // 创建Android虚拟机
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // 加载so到虚拟内存
        module = dm.getModule(); //获取本SO模块的句柄

        vm.setJni(this);
        vm.setVerbose(true);
        dm.callJNI_OnLoad(emulator);
    };

    public String xPreAuthencode(){
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
        Object custom = null;
        DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context
        list.add(vm.addLocalObject(context));
        list.add(vm.addLocalObject(new StringObject(vm, "r0ysue")));
        list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook")));

        Number number = module.callFunction(emulator, 0x2e301, list.toArray())[0];
        String result = vm.getObject(number.intValue()).getValue().toString();
        return result;
    }

    public static void main(String[] args) throws Exception {
        mfw test = new mfw();
        System.out.println(test.xPreAuthencode());
    }
}

运行

在这里插入图片描述

我们惊喜的发现,由于该样本中没有太多和JAVA层的交互,直接就顺利跑出了结果!但是呢,跑出算法并不是本篇的重点,让我们继续往下看。

四、算法还原

测试发现,不论输入明文多长,都输出固定长度结果,所以疑似哈希算法,又因为输出恒为40位,所以又疑似哈希算法中的SHA1算法。

首先静态分析一下,根据地址,IDA中跳到0x2e301

在这里插入图片描述

对入参做一下重命名和调整

在这里插入图片描述

sub_30548是一个签名校验函数,做出这种判断的原因有很多点

  • 参数是context上下文和包名
  • 返回值为false时,整个JNI函数返回”illegal signature“(非法签名)。

但我们在用Unidbg模拟执行时,并没有感受到native调用JAVA签名校验的烦恼,这是因为我们传入了APK,Unidbg替我们处理了这部分签名校验,但Unidbg并不能处理所有情况下的签名校验,所以在之前的一些例子里,我们会patch掉签名校验函数。

对JNI函数做进一步的注释

在这里插入图片描述

加密逻辑一定在sub_312E0或者sub_2e1f4中,自上而下先看sub_2E1F4,它参数1是输入的明文,参数3是明文长度,那参数2呢?和上一篇的样本一样,是buffer,v13的定义也可以看出,v13[20],什么都没做,直接放函数中。

使用HookZz 在函数进入前Hook参数1和参数3,函数出去后Hook 参数2。

    public void hook_312E0(){
        // 获取HookZz对象
        IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
        // enable hook
        hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
        // hook MDStringOld
        hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
            @Override
            // 方法执行前
            public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer input = ctx.getPointerArg(0);
                byte[] inputhex = input.getByteArray(0, ctx.getR2Int());
                Inspector.inspect(inputhex, "input");

                Pointer out = ctx.getPointerArg(1);
                ctx.push(out);
            };

            @Override
            // 方法执行后
            public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer output = ctx.pop();
                byte[] outputhex = output.getByteArray(0, 20);
                Inspector.inspect(outputhex, "output");
            }
        });
        hookZz.disable_arm_arm64_b_branch();
    };

在这里插入图片描述

参数正是我们输入的明文,返回值就是最终结果,所以我们只用关注这个函数即可。

在这里插入图片描述

数值上按H转成十六进制

在这里插入图片描述

疑似SHA1算法,看一下标准的魔数

在这里插入图片描述

可以发现,IV的第四个和第五个被改变了。

接下来我们依照样本中的IV,对标准算法进行修改和验证

# 0xffffffff is used to make sure numbers dont go over 32

def chunks(messageLength, chunkSize):
    chunkValues = []
    for i in range(0, len(messageLength), chunkSize):
        chunkValues.append(messageLength[i:i + chunkSize])

    return chunkValues


def leftRotate(chunk, rotateLength):
    return ((chunk << rotateLength) | (chunk >> (32 - rotateLength))) & 0xffffffff


def sha1Function(message):
    # initial hash values
    h0 = 0x67452301
    h1 = 0xEFCDAB89
    h2 = 0x98BADCFE
    h3 = 0x5E4A1F7C
    h4 = 0x10325476

    messageLength = ""

    # preprocessing
    for char in range(len(message)):
        messageLength += '{0:08b}'.format(ord(message[char]))

    temp = messageLength
    messageLength += '1'

    while (len(messageLength) % 512 != 448):
        messageLength += '0'

    messageLength += '{0:064b}'.format(len(temp))
    chunk = chunks(messageLength, 512)

    for eachChunk in chunk:
        words = chunks(eachChunk, 32)
        w = [0] * 80
        for n in range(0, 16):
            w[n] = int(words[n], 2)

        for i in range(16, 80):
            # sha1
            # w[i] = leftRotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1)
            # sha0
            w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16])

            # Initialize hash value for this chunk:
        a = h0
        b = h1
        c = h2
        d = h3
        e = h4

        # main loop:
        for i in range(0, 80):
            if 0 <= i <= 19:
                f = (b & c) | ((~b) & d)
                k = 0x5A827999

            elif 20 <= i <= 39:
                f = b ^ c ^ d
                k = 0x6ED9EBA1

            elif 40 <= i <= 59:
                f = (b & c) | (b & d) | (c & d)
                k = 0x8F1BBCDC

            elif 60 <= i <= 79:
                f = b ^ c ^ d
                k = 0xCA62C1D6

            a, b, c, d, e = ((leftRotate(a, 5) + f + e + k + w[i]) & 0xffffffff, a, leftRotate(b, 30), c, d)

        h0 = h0 + a & 0xffffffff
        h1 = h1 + b & 0xffffffff
        h2 = h2 + c & 0xffffffff
        h3 = h3 + d & 0xffffffff
        h4 = h4 + e & 0xffffffff

    return '%08x%08x%08x%08x%08x' % (h0, h1, h2, h3, h4)


plainText = "r0ysue"
sha1Hash = sha1Function(plainText)
print(sha1Hash)

在这里插入图片描述

遗憾的是,这次结果并没有一致!换而言之,我们遇到了对算法的真正魔改,而非只是上节课那般,修改一下IV!

我们该如何在几百行代码中,找到魔改在何方呢?重命名一下入参,看一下此处的代码。

int __fastcall sub_312E0(char *input, int output, int Length)
{
  int v4; // r0
  int v5; // r4
  unsigned int i; // r1
  int v7; // r0
  int v8; // r3
  int v9; // r1
  unsigned int j; // r2
  unsigned int v11; // r0
  unsigned int v12; // r4
  int v13; // r3
  int v14; // r0
  int v15; // r2
  unsigned int v16; // r5
  int v17; // r2
  int v18; // r0
  int v19; // r4
  int v20; // r0
  int v21; // r0
  int v22; // r4
  int v23; // r3
  int k; // r0
  int v27; // [sp+20h] [bp-84h]
  char v28; // [sp+28h] [bp-7Ch] BYREF
  char v29[12]; // [sp+2Ch] [bp-78h] BYREF
  int v30[5]; // [sp+38h] [bp-6Ch] BYREF
  unsigned int v31; // [sp+4Ch] [bp-58h]
  int v32; // [sp+50h] [bp-54h]
  unsigned __int8 v33[63]; // [sp+54h] [bp-50h] BYREF
  char v34; // [sp+93h] [bp-11h]
  int v35; // [sp+94h] [bp-10h]

  v30[1] = 0xEFCDAB89;
  v30[0] = 0x67452301;
  v30[2] = 0x98BADCFE;
  v30[3] = 0x5E4A1F7C;
  v30[4] = 0x10325476;
  v4 = 0;
  v32 = 0;
  v31 = 0;
  if ( Length )
  {
    v5 = Length - 1;
    for ( i = 0; ; i = v31 )
    {
      v31 = i + 8;
      if ( i >= 0xFFFFFFF8 )
        v32 = ++v4;
      v32 = v4;
      v7 = (i >> 3) & 0x3F;
      v8 = 0;
      if ( v7 == 63 )
      {
        v34 = *input;
        sub_3151C(v30, v33);
        v7 = 0;
        v8 = 1;
      }
      qmemcpy(&v33[v7], &input[v8], v8 ^ 1);
      if ( !v5 )
        break;
      --v5;
      ++input;
      v4 = v32;
    }
  }
  v9 = 0;
  for ( j = 0; j != 8; ++j )
  {
    v29[j] = (unsigned int)v30[(j < 4) + 5] >> (~(_BYTE)v9 & 0x18);
    v9 += 8;
  }
  v28 = 0x80;
  v11 = v31;
  v12 = v31 + 8;
  v31 += 8;
  v13 = v32;
  if ( v11 >= 0xFFFFFFF8 )
    v13 = ++v32;
  v32 = v13;
  v14 = (v11 >> 3) & 0x3F;
  v15 = 0;
  if ( v14 == 63 )
  {
    v34 = 0x80;
    sub_3151C(v30, v33);
    v14 = 0;
    v15 = 1;
    v12 = v31;
  }
  qmemcpy(&v33[v14], (const void *)((unsigned int)&v28 | v15), v15 ^ 1);
  if ( (v12 & 0x1F8) == 448 )
  {
    v16 = v12;
  }
  else
  {
    v16 = v12;
    do
    {
      v17 = 0;
      v28 = 0;
      v16 += 8;
      v31 = v16;
      v18 = v32;
      if ( v12 >= 0xFFFFFFF8 )
        v18 = ++v32;
      v32 = v18;
      v19 = (v12 >> 3) & 0x3F;
      if ( v19 == 63 )
      {
        v19 = 0;
        v34 = 0;
        sub_3151C(v30, v33);
        v16 = v31;
        v17 = 1;
      }
      qmemcpy(&v33[v19], (const void *)((unsigned int)&v28 | v17), v17 ^ 1);
      v12 = v16;
    }
    while ( (v16 & 0x1F8) != 448 );
  }
  v31 = v16 + 64;
  v20 = v32;
  if ( v16 >= 0xFFFFFFC0 )
    v20 = ++v32;
  v32 = v20;
  v21 = (v16 >> 3) & 0x3F;
  v22 = 0;
  if ( (unsigned int)(v21 + 8) < 0x40 )
  {
    v23 = 0;
  }
  else
  {
    v27 = 64 - v21;
    qmemcpy(&v33[v21], v29, 64 - v21);
    sub_3151C(v30, v33);
    v23 = v27;
    v21 = 0;
  }
  qmemcpy(&v33[v21], &v29[v23], 8 - v23);
  for ( k = 0; k != 20; ++k )
  {
    *(_BYTE *)(output + k) = *(unsigned int *)((char *)v30 + (k & 0xFFFFFFFC)) >> (~(_BYTE)v22 & 0x18);
    v22 += 8;
  }
  return _stack_chk_guard - v35;
}

在代码中多次出现sub_3151C,点进去看一下

在这里插入图片描述

代码量五六百行,应该就是函数的运算部分。加上前面的部分,总共七八百行代码,那么我们该如何找魔改发生在何处呢?甚至?它是不是没魔改算法流程,而是在标准运算结束后,和某个KEY做了异或?

这个问题的解决办法依赖于对哈希算法流程的深度理解,感兴趣的可以看一下SO基础课中关于密码学原理与实现的部分或者康康Unidbg系列内容的视频课,文字想讲清楚不是一件容易的事。

一个哈希算法,可以简单划分成填充和加密两部分,直接Hook加密函数,看它的入参,依此判定填充部分是否发生过改变。

    public void hook_3151C(){
        // 获取HookZz对象
        IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
        // enable hook
        hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
        // hook MDStringOld
        hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
            @Override
            // 方法执行前
            public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                // 类似于Frida args[0]
                Pointer input = ctx.getPointerArg(0);
                byte[] inputhex = input.getByteArray(0, 20);
                Inspector.inspect(inputhex, "IV");

                Pointer text = ctx.getPointerArg(1);
                byte[] texthex = text.getByteArray(0, 64);
                Inspector.inspect(texthex, "block");
                ctx.push(input);
                ctx.push(text);
            };

            @Override
            // 方法执行后
            public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer text = ctx.pop();
                Pointer IV = ctx.pop();

                byte[] IVhex = IV.getByteArray(0, 20);
                Inspector.inspect(IVhex, "IV");

                byte[] outputhex = text.getByteArray(0, 64);
                Inspector.inspect(outputhex, "block out");

            }
        });
        hookZz.disable_arm_arm64_b_branch();
    };

运行

[08:53:06 577]IV, md5=b70ca24521f790e6bf3c4a16ba868a03, hex=0123456789abcdeffedcba987c1f4a5e76543210
size: 20
0000: 01 23 45 67 89 AB CD EF FE DC BA 98 7C 1F 4A 5E    .#Eg........|.J^
0010: 76 54 32 10                                        vT2.
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[08:53:06 578]block, md5=c8e3dfac5d04ac7fb62160cd976bb01c, hex=72307973756580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030
size: 64
0000: 72 30 79 73 75 65 80 00 00 00 00 00 00 00 00 00    r0ysue..........
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30    ...............0
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[08:53:06 592]IV, md5=eb8ea7f8f507f692ef0778f13a59a330, hex=fe43c057a6555394d7c3b94c76bdb45dcbbc1b7d
size: 20
0000: FE 43 C0 57 A6 55 53 94 D7 C3 B9 4C 76 BD B4 5D    .C.W.US....Lv..]
0010: CB BC 1B 7D                                        ...}
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[08:53:06 592]block out, md5=c8e3dfac5d04ac7fb62160cd976bb01c, hex=72307973756580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030
size: 64
0000: 72 30 79 73 75 65 80 00 00 00 00 00 00 00 00 00    r0ysue..........
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30    ...............0
^-----------------------------------------------------------------------------^

Hook结果反映了这样一个问题

魔改的是算法本身,因为运算函数的入参是正常的、填充后的明文,所以不存在自定义填充、或者对明文做变换的可能,出参即是输出的结果,所以算法并不是在标准流程之后做了一些自定义步骤,它修改的——就是算法本身。

那么这个时候就该考虑,SHA1算法的运算部分是由什么组成?SHA1和MD5采用了相同的结构,每512比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是80步,每隔20步是一种模式。

首先记录80步中,每一步正常情况下应该得出的结果

0x5e1444aa
0xecb6ad5e
0x4066d34
0xed08cc85
0xe8f28c34
0x237ebcb7
0xeecacf3d
0xaf1a9fa8
0x921750fc
0x4380efc5
0xff26c559
0xe3d49cd6
0x517dcdd6
0x22a2bc19
0x3eaf6dc2
0x4891169b
0x20c32ce1
0x8556c446
0xdd2c894f
0x5420ba17
0x6ec4e797
0x91e5d34b
0xba26ad8
0xef34ad50
0xd1126575
0x7dd310e7
0x6b52d1f9
0xe7768a2
0xac273146
0x694146b8
0xebe5e627
0xfa712f50
0x10bfabc0
0x4cb1379b
0x665c4398
0xb2b46868
0x2ac8a949
0xb65eae61
0x3524a2e5
0x72ac7756
0x7f0e6c94
0x2928a555
0x7ed33fde
0x46a8f7fc
0x66ff0f01
0x52cfa822
0x4b18fa72
0xe39f852e
0xe0a3043a
0x9729af47
0xc142ad63
0x77c7096f
0x94602ecb
0x3e7202e5
0x89c7a8f2
0xbd2782bb
0xe6f058a3
0x8ca5906
0xe5cb4077
0x4a238672
0xe93aa2e
0xcf4dd760
0x111f600f
0x3853e9bf
0x7e375ab5
0xe4ba4774
0x9e39f23
0x4041ea20
0x82265213
0x9f37f728
0x3adf0819
0x586ac5e9
0xe5675b10
0xfb192c0e
0xc885ea1b
0x30628c48
0x833f6da5
0x5d958b47
0x2b11a368
0xc5611c9d

接下来通过inline Hook的方式,验证样本中80步的结果,这个过程需要对加密算法的原理和编程实现都有非常深的了解才能完成

在这里插入图片描述

可以使用如下的方式用HookZz实现Inline hook

    public void hook_315B0(){
        IHookZz hookZz = HookZz.getInstance(emulator);
        hookZz.enable_arm_arm64_b_branch();

        hookZz.instrument(module.base + 0x315B0 + 1, new InstrumentCallback<Arm32RegisterContext>() {
            @Override
            public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
                System.out.println("R2:"+ctx.getR2Long());
            }
        });

    }

但我们整体上需要进行十数次甚至数十次的inline hook,在这种情况下,用HookZz就略有些不方便,不妨试试Unidbg的console debugger。

请添加图片描述
在这里插入图片描述

在这样一种重复的工作中我们发现,前16步,样本与标准算法的加密流程是一致的,从第17步开始分道扬镳。我们用Python代码来表示

标准流程的80步运算

for t in range(80):
    if t <= 19:
        K = 0x5a827999
        f = (b & c) ^ (~b & d)
    elif t <= 39:
        K = 0x6ed9eba1
        f = b ^ c ^ d
    elif t <= 59:
        K = 0x8f1bbcdc
        f = (b & c) ^ (b & d) ^ (c & d)
    else:
        K = 0xca62c1d6
        f = b ^ c ^ d

样本的80步运算

for t in range(80):
    if t <= 15:
        K = 0x5a827999
        f = (b & c) ^ (~b & d)
    elif t <= 19:
        K = 0x6ed9eba1
        f = b ^ c ^ d
    elif t <= 39:
        K = 0x8f1bbcdc
        f = (b & c) ^ (b & d) ^ (c & d)
    elif t <= 59:
        K = 0x5a827999
        f = (b & c) ^ (~b & d)
    else:
        K = 0xca62c1d6
        f = b ^ c ^ d

在标准流程中,20步切换一下K和非线性函数,一共4种模式,在样本中,每16步切换一下K和非线性函数,一种五种模式,但本质上依然是标准流程里的四个模式,因为一个模式用了两次。

验证结果,大功告成。

在这里插入图片描述

五、尾声

本篇的写作过程是很别扭的,加密算法是一个艰深的东西,想在有限的篇幅里讲解深度魔改加密算法的样本更是,这篇文章中所阐述的内容并不足以真正理解这个魔改样本。读者可以自行通过如下路径之一,获得对样本中魔改样本的真正的理解。

  • 阅读SHA1 WIKI + 官方英文文档 + 手写一遍C实现
  • 报我的课,看直播一起嘻嘻哈哈

除此之外,样本中还有一个魔改Hmac,嘤,大家可以学习和研究一下。

资料链接:https://pan.baidu.com/s/1MHe0Oen6KKsdWru0YWTUzQ
提取码:ro1x

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
度为2的有序树与二叉树有一些区别。首先,度为2的有序树至少有3个结点,而二叉树可以为空。其次,度为2的有序树中,孩子结点的左右次序是相对于另一个孩子结点而言的。如果某个结点只有一个孩子,则无需区分其左右次序。而对于二叉树,无论其孩子数是否为2,都需要确定其左右次序,即二叉树的结点次序是确定的,不是相对于另一个结点而言的。此外,含有n个结点的二叉树的高度为log2n下取整。 虽然度为2的有序树和二叉树在一些方面有差异,但可以说度为2的有序树就是二叉树。因为对于任何一棵二叉树,我们都可以认为它是一个度为2的有序树,只是其中一些结点的孩子数可能为0或者1。而完全二叉树是一种特殊的二叉树,它满足每个结点都与高度为h的满二叉树中编号为1—n的结点一一对应的条件。在完全二叉树中,如果一个结点没有左孩子,则它必定没有右孩子,那它就是叶子结点。 综上所述,度为2的有序树可以看作是一种特殊的二叉树。它们的区别在于度为2的有序树中孩子结点的左右次序相对于另一个孩子而言,而二叉树的结点次序是确定的。然而,我们可以将任何一棵二叉树视为度为2的有序树,只需将孩子数为0或1的结点视为度为2的结点即可。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [数据结构---第五章树与二叉树---二叉树的概念---选择题](https://blog.csdn.net/programmer9/article/details/125015325)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [第六章 树和二叉树习题1](https://download.csdn.net/download/MFW333/87596042)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值