SO reverse entry practical tutorial 4: mfw

1, Foreword

This is the fourth part of the SO reverse entry practical tutorial. The focus of the first part is the supplement environment of Unidbg and the simple magic modification of hash algorithm. The focus of this article is to analyze the encryption algorithm with deep magic modification in Unidbg.

  • The common way of algorithm analysis is Frida Hook + IDA. In this series, the role of Frida will be weakened and the route of Unidbg Hook + IDA will be adopted.
  • The main introduction, but not limited to the introduction, you will see the shallow and deep magic change encryption algorithm, as well as the OLLVM, SO confrontation and other contents in the sample.
  • The analysis of samples is limited to study and research, and resolutely resist black ash production.
  • There are 13 articles in total, and one article will be updated in 1-2 days. The information of each article is put in Baidu online disk at the end of the article.

2, Prepare

Xpreauthenencode is the target method that receives three parameters

Parameter 1 is a context, parameter 2 is the input plaintext, parameter 3 is the package name of app, and the return value is a 40 digit hexadecimal number.

Frida actively calls the test sample, parameter 2 is set to "r0ysue", parameter 3 is set to "com.mfw.roadbook", output:

57c043fe945355a64cb9c3d75db4bd767d1bbccb

3, Unidbg simulation execution

The old rule is to put up a shelf first

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(); // Create simulator instance
        final Memory memory = emulator.getMemory(); // Memory operation interface of simulator
        memory.setLibraryResolver(new AndroidResolver(23)); // Set system class library resolution
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // Create Android virtual machine
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // Load so into virtual memory
        module = dm.getModule(); //Get the handle of this SO module

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

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

function

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 runs successfully. The address of our target method is 0x2e301. The three parameters passed in are String or context, which are all of the types mentioned above. We won't repeat them.

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(); // Create simulator instance
        final Memory memory = emulator.getMemory(); // Memory operation interface of simulator
        memory.setLibraryResolver(new AndroidResolver(23)); // Set system class library resolution
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // Create Android virtual machine
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // Load so into virtual memory
        module = dm.getModule(); //Get the handle of this SO module

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

    public String xPreAuthencode(){
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // The first parameter is env
        list.add(0); // The second parameter, the instance method is jobject, and the static method is jclazz. Fill in 0 directly, which is generally unavailable.
        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());
    }
}

function

We were pleasantly surprised to find that there was not much interaction with the JAVA layer in the sample, so we ran out of the results smoothly! However, the run out algorithm is not the focus of this article. Let's continue to look at it.

4, Algorithm restore

The test found that no matter how long the input plaintext is, the fixed length result is output, so it is suspected of the hash algorithm. Because the output is always 40 bits, it is also suspected of the SHA1 algorithm in the hash algorithm.

First, make a static analysis. According to the address, jump to 0x2e301 in IDA

Rename and adjust the input parameters

sub_30548 is a signature verification function. There are many reasons for making this judgment

  • The parameters are context and package name
  • When the return value is false, the entire JNI function returns "illegal signature".

However, when using Unidbg to simulate the execution, we did not feel the trouble of calling JAVA signature verification by native. This is because we passed in APK and Unidbg handled this part of signature verification for us, but Unidbg can not handle signature verification in all cases. Therefore, in some previous examples, we will patch out the signature verification function.

Make further comments on JNI function

The encryption logic must be in sub_312E0 or sub_ In 2e1f4, look at sub from top to bottom_ 2e1f4, its parameter 1 is the input plaintext, and parameter 3 is the plaintext length. What about parameter 2? Like the sample in the previous article, it is buffer. As can be seen from the definition of v13, v13[20] does nothing and directly puts it into the function.

Use HookZz to Hook parameter 1 and parameter 3 before the function enters and Hook parameter 2 after the function goes out.

    public void hook_312E0(){
        // Get HookZz object
        IHookZz hookZz = HookZz.getInstance(emulator); // Load hookzz, support inline hook, see the document https://github.com/jmpews/HookZz
        // enable hook
        hookZz.enable_arm_arm64_b_branch(); // Test enable_arm_arm64_b_branch, optional
        // hook MDStringOld
        hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap export function
            @Override
            // Before method execution
            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
            // After method execution
            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();
    };

The parameter is the plaintext we entered, and the return value is the final result, so we only need to focus on this function.

Press H to convert the value to hexadecimal

Suspected SHA1 algorithm, look at the standard magic number

It can be found that the fourth and fifth of IV have been changed.

Next, we modify and verify the standard algorithm according to the IV in the sample

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

Unfortunately, the results are not consistent this time! In other words, we have encountered a real magic change to the algorithm, rather than just modifying IV like last class!

How can we find the magic change in hundreds of lines of code? Rename the input parameter and take a look at the code here.

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 appears more than once in the code_ 3151c, click in and have a look

The amount of code is five or six hundred lines, which should be the operation part of the function. Plus the previous part, a total of seven or eight hundred lines of code, so how can we find out where the magic change takes place? even to the extent that? Does it have no magic algorithm process, but XOR with a KEY after the standard operation?

The solution to this problem depends on the in-depth understanding of the hash algorithm process. Those interested can take a look at the part about the principle and implementation of cryptography in the SO basic course or the video course of Kangkang Unidbg series. It is not easy to explain the text clearly.

A hash algorithm can be simply divided into two parts: filling and encryption. Directly Hook the encryption function, look at its input parameters, and judge whether the filling part has changed.

    public void hook_3151C(){
        // Get HookZz object
        IHookZz hookZz = HookZz.getInstance(emulator); // Load hookzz, support inline hook, see the document https://github.com/jmpews/HookZz
        // enable hook
        hookZz.enable_arm_arm64_b_branch(); // Test enable_arm_arm64_b_branch, optional
        // hook MDStringOld
        hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap export function
            @Override
            // Before method execution
            public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                // Similar to 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
            // After method execution
            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();
    };

function

[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 results reflect such a problem

The magic change is the algorithm itself. Because the input parameters of the operation function are normal and filled in plaintext, there is no possibility of custom filling or transformation of plaintext. The output parameters are the output results. Therefore, the algorithm does not make some custom steps after the standard process. What it modifies is the algorithm itself.

At this time, we should consider what constitutes the operation part of SHA1 algorithm? SHA1 and MD5 adopt the same structure. Each 512 bit packet requires one round of operation. Our input length does not exceed the length of one packet, so only one round of operation is considered. One round of operation is 80 steps, and every 20 steps is a mode.

First, record the results that should be obtained under normal conditions in each of the 80 steps

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

Next, verify the results of 80 steps in the sample through inline Hook. This process requires a deep understanding of the principle and programming implementation of the encryption algorithm

You can use HookZz to implement Inline hook in the following way

    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) { // Through the base+offset inline wrap internal function, it can be seen as sub in IDA_ XXX those
                System.out.println("R2:"+ctx.getR2Long());
            }
        });

    }

But on the whole, we need to do more than ten or even dozens of inline hook s. In this case, it's a little inconvenient to use HookZz. You might as well try Unidbg's console debugger.


In such a repetitive work, we found that in the first 16 steps, the encryption process of the sample is consistent with that of the standard algorithm, and we parted ways from step 17. We use Python code to represent

80 step operation of standard process

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 step operation of samples

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

In the standard process, K and nonlinear function are switched in 20 steps. There are four modes in total. In the sample, K and nonlinear function are switched every 16 steps. There are five modes, but they are still four modes in the standard process in essence, because one mode is used twice.

Verify the results and you're done.

5, Epilogue

The writing process of this article is very awkward. Encryption algorithm is a difficult thing. I want to explain the sample of deep magic change encryption algorithm in a limited space. The content described in this article is not enough to really understand this magic change sample. Readers can get a real understanding of the magic modification samples in the samples through one of the following paths.

  • Read SHA1 WIKI + official English document + handwritten C implementation
  • Sign up for my class, watch the live broadcast and laugh together

In addition, there is also a magic change Hmac in the sample. You can learn and study it.

Data link: https://pan.baidu.com/s/1MHe0Oen6KKsdWru0YWTUzQ
Extraction code: ro1x

Added by pernest on Wed, 02 Feb 2022 09:35:18 +0200