文章目录

  • 一、前言
  • 二、准备
  • 三、Unidbg模拟执行
  • 四、算法还原
  • 五、尾声

一、前言

这是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()); // 第一个参数是envlist.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。Object custom = null;DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// contextlist.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 hookhookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无// hook MDStringOldhookZz.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 32def chunks(messageLength, chunkSize):chunkValues = []for i in range(0, len(messageLength), chunkSize):chunkValues.append(messageLength[i:i + chunkSize])return chunkValuesdef leftRotate(chunk, rotateLength):return ((chunk << rotateLength) | (chunk >> (32 - rotateLength))) & 0xffffffffdef sha1Function(message):# initial hash valuesh0 = 0x67452301h1 = 0xEFCDAB89h2 = 0x98BADCFEh3 = 0x5E4A1F7Ch4 = 0x10325476messageLength = ""# preprocessingfor char in range(len(message)):messageLength += '{0:08b}'.format(ord(message[char]))temp = messageLengthmessageLength += '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] * 80for 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)# sha0w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16])# Initialize hash value for this chunk:a = h0b = h1c = h2d = h3e = h4# main loop:for i in range(0, 80):if 0 <= i <= 19:f = (b & c) | ((~b) & d)k = 0x5A827999elif 20 <= i <= 39:f = b ^ c ^ dk = 0x6ED9EBA1elif 40 <= i <= 59:f = (b & c) | (b & d) | (c & d)k = 0x8F1BBCDCelif 60 <= i <= 79:f = b ^ c ^ dk = 0xCA62C1D6a, b, c, d, e = ((leftRotate(a, 5) + f + e + k + w[i]) & 0xffffffff, a, leftRotate(b, 30), c, d)h0 = h0 + a & 0xffffffffh1 = h1 + b & 0xffffffffh2 = h2 + c & 0xffffffffh3 = h3 + d & 0xffffffffh4 = h4 + e & 0xffffffffreturn '%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; // r0int v5; // r4unsigned int i; // r1int v7; // r0int v8; // r3int v9; // r1unsigned int j; // r2unsigned int v11; // r0unsigned int v12; // r4int v13; // r3int v14; // r0int v15; // r2unsigned int v16; // r5int v17; // r2int v18; // r0int v19; // r4int v20; // r0int v21; // r0int v22; // r4int v23; // r3int k; // r0int v27; // [sp+20h] [bp-84h]char v28; // [sp+28h] [bp-7Ch] BYREFchar v29[12]; // [sp+2Ch] [bp-78h] BYREFint v30[5]; // [sp+38h] [bp-6Ch] BYREFunsigned int v31; // [sp+4Ch] [bp-58h]int v32; // [sp+50h] [bp-54h]unsigned __int8 v33[63]; // [sp+54h] [bp-50h] BYREFchar 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 hookhookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无// hook MDStringOldhookZz.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>() {@Overridepublic 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 = 0x5a827999f = (b & c) ^ (~b & d)elif t <= 39:K = 0x6ed9eba1f = b ^ c ^ delif t <= 59:K = 0x8f1bbcdcf = (b & c) ^ (b & d) ^ (c & d)else:K = 0xca62c1d6f = b ^ c ^ d

样本的80步运算

for t in range(80):if t <= 15:K = 0x5a827999f = (b & c) ^ (~b & d)elif t <= 19:K = 0x6ed9eba1f = b ^ c ^ delif t <= 39:K = 0x8f1bbcdcf = (b & c) ^ (b & d) ^ (c & d)elif t <= 59:K = 0x5a827999f = (b & c) ^ (~b & d)else:K = 0xca62c1d6f = b ^ c ^ d

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

验证结果,大功告成。

五、尾声

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

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

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

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

SO逆向入门实战教程四:mfw相关推荐

  1. SO逆向入门实战教程九——blackbox

    文章目录 一.前言 二.准备 三.Unidbg模拟执行 四.Unidbg算法还原 五.尾声 一.前言 上篇中,我们借AB之口,讨论了这样一个问题--Unidbg是否适合做算法分析的主力工具,这个问题没 ...

  2. SO逆向入门实战教程八:文件读写

    文章目录 一.前言 二.demo1设计 三.Unidbg模拟执行demo1 四.demo2设计 五.Unidbg模拟执行demo2 六.尾声 一.前言 本篇分析的是自写demo,帮助大家熟悉Unidb ...

  3. SO逆向入门实战教程十:SimpleSign

    一.前言 这是系列的第十篇,通过该样本可以充分学习如何在Unidbg中补充环境.朋友zh3nu11和我共同完成了这篇内容,感谢. 二.准备 首先我们发现了init函数,它应该就是SO的初始化函数,其余 ...

  4. SO逆向入门实战教程一:OASIS

    文章目录 一.前言 二.准备 三.Unidbg模拟执行 四.ExAndroidNativeEmu 模拟执行 五.算法分析 六.尾声 一.前言 这是SO逆向入门实战教程的第一篇,总共会有十三篇,十三个实 ...

  5. Spring Boot 入门实战教程

    Spring Boot 2.0 入门实战教程 开发环境:JDK1.8或以上 源码下载:https://pan.baidu.com/s/1Z771VDiuabDBJJV445xLeA 欢迎访问我的个人博 ...

  6. Python之Numpy入门实战教程(2):进阶篇之线性代数

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  7. Python之Numpy入门实战教程(1):基础篇

    Numpy.Pandas.Matplotlib是Python的三个重要科学计算库,今天整理了Numpy的入门实战教程.NumPy是使用Python进行科学计算的基础库. NumPy以强大的N维数组对象 ...

  8. MVC5+EF6 入门完整教程四

    MVC5+EF6 入门完整教程四 原文:MVC5+EF6 入门完整教程四 上篇文章主要讲了如何配置EF, 我们回顾下主要过程: 创建Data Model à 创建Database Context à创 ...

  9. 视频教程-深度学习与PyTorch入门实战教程-深度学习

    深度学习与PyTorch入门实战教程 新加坡国立大学研究员 龙良曲 ¥399.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP订阅课程,领取优惠,最少立减5元 ↓ ...

  10. 昆仑通态人机界面与单片机通信实战教程四:单片机程序的设计

    大家好,我是『芯知识学堂』的SingleYork,前面给大家介绍了"昆仑通态人机界面与单片机通信实战教程三:脚本驱动与HDMI工程的关联",今天笔者就要来给大家介绍这个教程的最后一 ...

最新文章

  1. ajax更改dom,javascript – 用Ajax响应替换DOM节点
  2. iOS获取最上层控制器
  3. Go的runtime.GOMAXPROCS
  4. 使命召唤 战区:战术竞技新思路,卷入RPG元素的激烈战斗
  5. Linux下配置安装PHP环境
  6. maven工程错误汇总
  7. 白盒测试和黑盒测试_黑盒测试与白盒测试的比较
  8. 使用Eclipse在Amazon Ec2中部署Java Web应用程序的完整指南
  9. matlab std函数_如何利用Matlab进行小波分析
  10. ElasticSearch概述(一)——简介
  11. docker pytorch
  12. Python入门学习笔记(8)
  13. 从ETL工具到企业云数据管理,在大数据风口的Informatica完成蜕变
  14. 初学者的回归测试,都该注意哪几点?
  15. 《C专家编程》笔记——第一章
  16. Foxmail的创建
  17. 基于QT实现的旅游路线查询系统
  18. matlab在c盘有缓存文件夹吗,win10如何清除C盘缓存文件-win10清除C盘缓存的方法 - 河东软件园...
  19. Python123-练习题
  20. Python生成器及send用法讲解

热门文章

  1. 制造业OEER语言数据挖掘之相关性分析
  2. Pytorch构建Transformer实现英文翻译
  3. CPU 是怎么认识代码的?
  4. 树莓派CM4基于emmc安装Ubuntu系统及初始配置
  5. android 中角度计算
  6. malloc、calloc、realloc函数
  7. javascript eval 函数作用
  8. 2021-04-15 三级管npn和pnp的区别
  9. oracle 19c jdbc之Reactive Streams Ingestion (RSI) Library
  10. ⭐⭐⭐【DFS+理解题意】找出直系亲属