x-zse-96安卓端纯算,魔改AES还原

两天前发了一个x-zse-96的文章,当时遇到了点问题,只分析到了最后一个白盒AES函数里面,并且当时用dfa攻击还原出了秘钥,IV也确定了,但是加密结果不对,本来打算把下文鸽掉的,因为当时unidbg没跑起来,用frida去hook白盒AES中的每一行汇编有点麻烦,没有unidbg方便.后来小白大佬说unidbg可以跑通,并把还原算法的任务交给了我.我拿着小白提供的unidbg代码,开始了算法还原之旅.
还原算法,篇幅有点长,会介绍AES的10轮加密以及如何从汇编代码和伪c代码中还原出魔改的AES,由于so是从内存中dump下来的,反编译出来的伪代码准确性很差,主要看的就是汇编.
严格来说,这个AES魔改的已经完全不能算是AES了,但是他c中的函数名称还是叫wb_laes_encrypt,
白盒也不算.

unidbg代码

package com.zh2;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
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.array.ByteArray;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.linux.file.SimpleFileIO;
import com.github.unidbg.memory.Memory;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class zh extends AbstractJni implements IOResolver {private final AndroidEmulator emulator;private final DvmClass CryptoTool;private final VM vm;private final Module module;public zh() throws IOException {emulator = AndroidEmulatorBuilder.for32Bit().addBackendFactory(new Unicorn2Factory(true)).setProcessName("com.zhihu.android").build();Memory memory = emulator.getMemory();memory.setLibraryResolver(new AndroidResolver(23));vm = emulator.createDalvikVM(new File("unidbg-android/apks/zh2/zh9.29.0.apk"));vm.setJni(this);vm.setVerbose(true);emulator.getSyscallHandler().addIOResolver(this);DalvikModule dm2 = vm.loadLibrary("bangcle_crypto_tool", true);module = dm2.getModule();CryptoTool = vm.resolveClass("com.bangcle.CryptoTool");dm2.callJNI_OnLoad(emulator);}public static String bytesToHex(byte[] bytes) {StringBuilder sb = new StringBuilder();for (byte b : bytes) {int unsignedInt = b & 0xff;String hex = Integer.toHexString(unsignedInt);if (hex.length() == 1) {sb.append('0');}sb.append(hex);}return sb.toString();}public void x_zse_96_encrypt() throws IOException {List<Object> list = new ArrayList<>(5);list.add(vm.getJNIEnv());list.add(0);byte[] byy1 = {-118,-125,-125,41,41,34,-113,-115,42,35,34,42,-125,-128,40,-125,-125,33,-126,41,40,-118,-117,35,-125,-128,-117,35,42,-115,35,-113};byte[] byy2 = {-103,48,58,58,50,52,58,57,-110,-110,58,59,58,-103,-110,-110};ByteArray arr1 = new ByteArray(vm,byy1);list.add(vm.addLocalObject(arr1));list.add(vm.addLocalObject(new StringObject(vm, "541a3a5896fbefd351917c8251328a236a7efbf27d0fad8283ef59ef07aa386dbb2b1fcbba167135d575877ba0205a02f0aac2d31957bc7f028ed5888d4bbe69ed6768efc15ab703dc0f406b301845a0a64cf3c427c82870053bd7ba6721649c3a9aca8c3c31710a6be5ce71e4686842732d9314d6898cc3fdca075db46d1ccf3a7f9b20615f4a303c5235bd02c5cdc791eb123b9d9f7e72e954de3bcbf7d314064a1eced78d13679d040dd4080640d18c37bbde")));ByteArray arr2 = new ByteArray(vm,byy2);list.add(vm.addLocalObject(arr2));Number number = module.callFunction(emulator, 0xa708, list.toArray());ByteArray resultArr = vm.getObject(number.intValue());System.out.println("result:"+ bytesToHex(resultArr.getValue()));}public static void main(String[] args) throws IOException {zh zh = new zh();zh.patchInstruction();zh.x_zse_96_encrypt();}private void patchInstruction() {try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {KeystoneEncoded assemble = keystone.assemble("nop;nop");byte[] machineCode = assemble.getMachineCode();emulator.getMemory().pointer(module.base + 0x8EBC).write(0,machineCode,0,machineCode.length);KeystoneEncoded encoded = keystone.assemble("mov r3, 1");byte[] patchCode = encoded.getMachineCode();emulator.getMemory().pointer(module.base + 0x4e70).write(0, patchCode, 0, patchCode.length);KeystoneEncoded encoded1 = keystone.assemble("mov r3, 1");byte[] patchCode1 = encoded1.getMachineCode();emulator.getMemory().pointer(module.base + 0x4e84).write(0, patchCode1, 0, patchCode1.length);} catch (Exception e) {e.printStackTrace();}}@Overridepublic DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {switch (signature) {case "com/secneo/apkwrapper/H->PKGNAME:Ljava/lang/String;": {String packageName = vm.getPackageName();if (packageName != null) {return new StringObject(vm, packageName);}break;}case "com/secneo/apkwrapper/H->ISMPASS:Ljava/lang/String;": {return new StringObject(vm, "###MPASS###");}}return super.getStaticObjectField(vm, dvmClass, signature);}@Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": {return vm.resolveClass("android/app/ContextImpl").newObject(signature);}case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;": {return vm.resolveClass("android/content/pm/PackageManager").newObject(signature);}case "java/io/File->getPath()Ljava/lang/String;": {System.out.println("PATH:" + dvmObject.getValue());if (dvmObject.getValue().equals("android/os/Environment->getExternalStorageDirectory()Ljava/io/File;")) {return new StringObject(vm, "/mnt/sdcard");}}case "java/lang/String->intern()Ljava/lang/String;": {String string = dvmObject.getValue().toString();return new StringObject(vm, string.intern());}case "java/lang/Class->getDeclaredFields()[Ljava/lang/reflect/Field;": {DvmClass c = (DvmClass) dvmObject;System.out.println(c.getClassName());}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);}@Overridepublic DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {switch (signature) {case "android/content/pm/PackageInfo->applicationInfo:Landroid/content/pm/ApplicationInfo;": {return vm.resolveClass("android/content/pm/ApplicationInfo").newObject(signature);}case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": {return new StringObject(vm, "/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/base.apk");}case "android/content/pm/ApplicationInfo->dataDir:Ljava/lang/String;": {return new StringObject(vm, "/data/data/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==");}case "android/content/pm/ApplicationInfo->nativeLibraryDir:Ljava/lang/String;": {return new StringObject(vm, "/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/lib/arm");}}return super.getObjectField(vm, dvmObject, signature);}@Overridepublic DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "android/os/Environment->getExternalStorageDirectory()Ljava/io/File;": {return vm.resolveClass("java/io/File").newObject(signature);}}return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);}@Overridepublic FileResult resolve(Emulator emulator, String pathname, int oflags) {if (pathname.equals("/proc/self/cmdline")) {return FileResult.success(new ByteArrayFileIO(oflags, pathname, "com.zhihu.android".getBytes()));}if (pathname.equals("/data/app/com.zhihu.android-XnTWbKh_JrM9gUKdEn_Wug==/base.apk")) {return FileResult.success(new SimpleFileIO(oflags, new File("D:\\code\\me\\app\\unidbg-master\\unidbg-android\\src\\test\\resources\\zhihu\\zhihu.apk"), pathname));}return null;}
}

unidbg跑不起来的原因是有几个初始化的函数无法执行起来,当时我也是打了几个patch下去,最终没能跑起来,也许是版本有差异,我弄的是最新版的,这个unidbg中的版本是9.29.0.

还原算法

在这里插入图片描述

这个就是目标函数了,其实它有5个参数,ida识别出错了,参数一和参数2都是入参,结果通过a2返回回去
在这里插入图片描述
来看下函数最开始的地方,v3被赋值成了入参,v53 = *a3,v52 = a3[3] / 32 + 6
这里ida反汇编出来的有些问题,不过根据aes的算法特征也可以大概猜出这里是在做什么,v52 = a3[3] / 32 + 6,我们知道AES有10轮加密,第10轮没有mixcolumns(列混淆),并且aes的分组长度16字节128位,128/32+6=10,下面的从1开始到10结束正好是9轮.
v52 = a3[3] / 32 + 6

初始轮秘钥加

这样的话框中的应该就是第一个轮秘钥加了.说是加其实是异或,比如秘钥k0是0x11111111111111111111111111111111,明文是0x22222222222222222222222222222222,异或就是他两直接异或就好了.
在这里插入图片描述
因为c语言只能一个字节一个字节的异或,所以16个字节他这里有16轮.result是第一个入参,和第二个入参是同一个地址,这里我hook过了result经过9轮加密始终没变,所以上面result赋值的结果不用管,只需要看*(&v36 + i) = (byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4) ^ (16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * ((v3 + i) >> 4))] >> 4));这行就可以了,正常来说两个字节异或直接0x11^0x22就可以了,他这里那么长肯定是改了AES的,v3是入参,v53来自上面的*a3,接下来unidbg中看下这个v53是多少,鼠标选中这个星号,按tab转文本汇编
在这里插入图片描述
STR R3, [R11,#-16] (Store Register),存储指令,结合着c代码,就是把寄存器r3的值存到R11偏移-16的位置,对应着v53=*a3,unidbg中下断看下R3是多少

public void HookByConsoleDebugger() {Debugger debugger = emulator.attach();debugger.addBreakPoint(module.base+0x6444);}
需要encrypt函数调用前执行

在这里插入图片描述
m某个寄存器可以直接看内存中的数据,默认是112字节,可以在后面加数组指定dump多少字节,好像后面还有在这里插入图片描述

长度是1611,这个11好像是aes的11个轮秘钥,但是他的11个轮秘钥是没有联系的,标准的aes是第一轮可以推出后面的轮秘钥的.
(&v36 + i) = (byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4) ^ (16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * (
(v3 + i) >> 4))] >> 4)); 16轮循环下来,v53只消耗第一个轮秘钥,v3是入参,v36是结果,byte_D914是一个数组.这里看上去写的有点复杂,其实不然,就是byte_D914[(v53 + i) & 0xF ^ (16 * (v3 + i))] >> 4异或(16 * (byte_D914[((v53 + i) >> 4) & 0xF ^ (16 * ((v3 + i) >> 4))] >> 4)),想要获得结果就需要知道byte_D914是多少
在这里插入图片描述
点过去只有100字节,但是*(v53 + i) & 0xF ^ (16 * (v3 + i))或者((v53 + i) >> 4) & 0xF ^ (16 * (*(v3 + i) >> 4))的结果在0x0到0xff之间有256个,所以一直带下面的256个应该都是byte_D914的.但是当你把这256个字节扣出来并且加密一遍后你会发现结果有几个字节是正确的,有几个字节是错误的,应该是因为dump下来的so有些问题.这里ida中的静态byte_D914是不准确的,真正准确的是内存中的,所以可以从unidbg中把这256个字节dump下来.在这里插入图片描述
鼠标选中byte_D914按tab转到文本汇编,后面的一些操作都需要汇编的基础,因为c的代码不准确,想要搞so的算法还原必须要懂汇编
在这里插入图片描述
LDRB R3, [R2, R3](Load Register Byte)字面意思,从R2+R3的地址加载一个字节到R3中,对应着c代码就是从byte_D914中取数据呗.

debugger.addBreakPoint(module.base+0x64c0);

在这里插入图片描述

r3 0x18就是偏移,偏移是从0x0到0xff,所以r2中的数据应该就是byte_D914
在这里插入图片描述
这些字节都对的上,后面的就有些偏差了,这里我们只要unidbg中的结果
在这里插入图片描述
还原算法的话就把byte_D914转成数组就好了,用到的时候从里面取值.
经过上面一系列操作的话,入参和第一个秘钥加密的结果就好了.这个结果很重要

9轮循环

标准aes中的9轮循环里的内容是字节替换,循环左移,列混淆,轮秘钥加.轮秘钥加上面我们说过了,字节替换就是以自身数值为索引取出s盒里的内容,s盒256字节,类似上面的byte_D914.循环左移和列混淆这个魔改的aes不涉及,所以这里不说了,稍微有些麻烦,感兴趣的自行网上检索.
对照着这4个步骤来看这9轮循环
因为是魔改的,字节替换,循环左移,列混淆,轮秘钥加这4个步骤都改了,所以就按照标准aes中的叫法来给下面的4个循环取上对应的名字

#字节替换

在这里插入图片描述

循环左移

在这里插入图片描述

列混淆和轮秘钥加

在这里插入图片描述
这4个步骤看上去挺相似的,但是主要是v4,v20.v53这几个值在操作还有中间的数组取值操作,这个数组的我就不说了,和上面的数组获取的方式一样.
v53是11个轮秘钥,步骤4正好也是用的v53中的轮秘钥,v4和v20是主要需要关注的点,中间记录了他们的变化,我截图没截上而已.这两个值的变化说简单也不简单,说难也难不到哪去.
有那么一丝白盒AES查表的意味.
在这里插入图片描述
这是python还原的几个数组还有几张表,有点大,折叠了.
接着上面的分析.我只分析第一个字节替换的过程,后面的3个步骤和这个的分析是一模一样的,所以我尽可能写详细点.
在这里插入图片描述
主要是看v4和v20是怎么生成的.
v36也就是最开始的第一轮轮秘钥加得到的结果,先来看下结果是多少.
鼠标选中v36转到文本汇编
在这里插入图片描述
LDRB R3, [R11,#-36] LDRB指令上面介绍过了,这行指令的意思就是从R11偏移-36(-0x24)的地方取下一个字节给R3,结合c代码,R11处地址-0x24中的数据就是v36
在这里插入图片描述
0xbffff5ac-0x24=0xbffff588
在这里插入图片描述
记住这16个字节,每一轮加密中的4组加密都需要这个值(每一轮不一样),根据这里的索引去查表,一次用4个字节,一共4次刚好16个字节,每次的4个字节从表中一次查4个字节也是16字节,对应的正好就是v4和v20的值,后面会说表怎么拿到. 在这里插入图片描述
接下来先去看下经过68-99行,v20的值变成了多少.
在这里插入图片描述

取地址处看下汇编
在这里插入图片描述

ADD R3, R2, R3 将寄存器 R2 和寄存器 R3 中的值相加,并将结果存储到寄存器 R3中
unidbg中下断看下R2和R3是什么,根据&v4 + i不看断点处大概也能猜到,R3是i(偏移),R2就是v4

debugger.addBreakPoint(module.base+0x69B4);

在这里插入图片描述
结果也正好验证了,此处刚进入循环所有r3是0,来看下r2的值,也就是v20
在这里插入图片描述
ok,结果我们知道了,接下来带着结果去看v20是怎么生成的.6574处
在这里插入图片描述
LDR R3, [R3,R2,LSL#2]将寄存器 R2 中的值左移两位(相当于*4)+R3再赋给R3,unidbg中下断看下

debugger.addBreakPoint(module.base+0x6574);

在这里插入图片描述
r2这个0xc5有没有很眼熟,请看上面这张图
在这里插入图片描述
按照LDR R3, [R3,R2,LSL#2]汇编的操作来执行一下,0xc5*4=0x314,R3=0x4000dc14+0x314=0x4000df28,看下内存中的值是多少
再看下v20的前4个字节
在这里插入图片描述
00 BC BC 03变成 03 BC BC 00怎么变得相信不需要我多说了,后面的12个字节也是同理.
接下来说一下怎么获取这张大表,unidbg中hook即可.
因为LDR R3, [R3,R2,LSL#2],r2在0x0到0xff之间,*4后就是0x0到1020,以间隔4dump下unidbg中的内存即可

debugger.addBreakPoint(module.base+0x6574, new BreakPointCallback() {int num=0;@Overridepublic boolean onHit(Emulator<?> emulator, long address) {int i;Backend backend = emulator.getBackend();if(num==0){for (i=0;i<=1020;i+=4){int x =0x4000dc14+Integer.parseInt(Integer.toHexString(i), 16);byte[] bytes = backend.mem_read(x, 0x04);StringBuilder hexString = new StringBuilder();for (byte b : bytes) {hexString.append(String.format("%02X ", b & 0xFF));}System.out.println(Integer.parseInt(Integer.toHexString(i), 16)+":"+ hexString.toString().trim());};}num+=1;return true;}});

结果就是这样的
在这里插入图片描述
尝试搜索一下0x314也就是788
在这里插入图片描述
可以看到结果也是对的上的,后面的流程都是这样,自此,几个数和表的获取都说完了,接下来的流程就是细心点就没问题了.

第10轮

在这里插入图片描述
第10轮标准AES有字节替换,循环左移,轮秘钥加.这里它只有一个循环.
这个表的获取方式和上面的略有不同,不过我相信你如果把前9轮加密能搞定的话,最后一轮肯定是没问题的了.

总结

这样下来的话,这个白盒AES中的算法就搞定了,这里还有几个注意事项
1传进去的明文是32位的,刚好是两个分组长度,所以会再填充一个分组,这个填充模式也是魔改的,具体可以看下方的图
在这里插入图片描述
因为传进去的md5值32位,所以只需要再填充一组就可以了,也就是9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b,所以总的分组数就是3组.
2因为是cbc模式,每个分组加密的结果会和下一轮的明文进行异或.初始iv是99303a3a32343a3992923a3b3a999292
3python纯算代码太长了,有好几张表,这里我放不下就放到下方知识星球了,感兴趣的可以加一下.

最后

微信公众号
在这里插入图片描述
知识星球
在这里插入图片描述
如果你觉得这篇文章对你有帮助也可请作者喝一杯咖啡
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/765554.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

内网渗透(一)必须了解Windows工作组

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、内网渗透测试简介 内网也叫局域网&#xff0c;是指在…

《自动机理论、语言和计算导论》阅读笔记:p1-p4

《自动机理论、语言和计算导论》学习第1天&#xff0c;p1-p4&#xff0c;总计4页。这只是个人的学习记录&#xff0c;因为很多东西不懂&#xff0c;难免存在理解错误的地方。 一、技术总结 1.有限自动机(finite automata)示例 1.software for checking digital circuits。 …

工作需求,Vue实现登录

加油&#xff0c;新时代打工人&#xff01; vue 2.x Element UI <template><div class"body" :style"{background-image: url(${require(/assets/images/login.png)})}"><el-form :rules"rules" ref"loginForm" :mode…

MySQL 中的索引

MySQL 中的索引 一、索引的创建和删除1.主键会自动添加索引2.unique 约束的字段自动添加索引3.给指定的字段添加索引4.删除指定索引5.查询表上的索引 二、索引的分类三、MySQL索引采用了B树数据结构1.B树的经典面试题 四、其他索引及相关调优1.Hash索引2.聚集索引和非聚集索引3…

【linux线程(四)】初识线程池手撕线程池

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux线程池 1. 前言2. 什么是…

Python 从0开始 一步步基于Django创建项目(3)使用Admin site管理数据模型

本文内容建立在《Python 从0开始 一步步基于Django创建项目&#xff08;2&#xff09;创建应用程序&数据模型》的基础上。 Django提供的admin site&#xff0c;使得网站管理员&#xff0c;能够轻松管理网站的数据模型。 本文首先创建‘管理员账户’&#xff0c;即超级用户…

华为OD机22道试题

华为OD机试题 2.查找小朋友的好朋友位置 在学校中&#xff0c;N 个小朋友站成一队&#xff0c;第 i 个小朋友的身高为 height[i]&#xff0c;第 i 个小朋友可以看到第一个比自己身高更高的小朋友j&#xff0c;那么 j 是 i 的好朋友 (要求&#xff1a;j>i) 。 请重新生成一个…

202305 CSP认证

202305-1 重复局面 第一题直接干 #include<bits/stdc.h> using namespace std; unordered_map<string, int> chess; int main() {ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);string line, str ""; int n;cin >> n;while(n --){str …

数据结构:链式队列

1.设计思想&#xff1a; 我们可以设计出以上五种队列&#xff0c;但是基于时间复杂度&#xff0c;和空间复杂度的最优解&#xff0c;我们选择入队和出队均为O(1)的&#xff0c;也就是第五种 2.结构设计 typedef struct LPNode//数据节点 {int data;//数据struct LPNode* next…

Redis消息队列与thinkphp/queue操作

业务场景 场景一 用户完成注册后需要发送欢迎注册的问候邮件、同时后台要发送实时消息给用户对应的业务员有新的客户注册、最后将用户的注册数据通过接口推送到一个营销用的第三方平台。 遇到两个问题&#xff1a; 由于代码是串行方式&#xff0c;流程大致为&#xff1a;开…

基于STC12C5A60S2系列1T 8051单片机可编程计数阵列CCP/PCA/PWM模块的捕获模式(外部中断)应用

基于STC12C5A60S2系列1T 8051单片机可编程计数阵列CCP/PCA/PWM模块的捕获模式(外部中断)应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍STC12C5A60S2系列1T 805…

【Langchain-Chatchat】部署ChatGLM3-6B-32K教程

介绍 Langchain-Chatchat这个框架可以帮助我们更容易的部署大语言模型&#xff0c;之前也写过ChatGLM传统的部署教程&#xff0c;有兴趣的可以参考 【ChatGLM3】第三代大语言模型多GPU部署指南【ChatGLM2-6B】从0到1部署GPU版本 借助Langchain-Chatchat框架&#xff0c;可以…

32串口学习

基于之前的GPIO等工程&#xff0c;后面的上手难度就简单多了&#xff0c;主要是相关寄存器的设置。 void USART1_Config(void) {GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;/* config USART1 clock */RCC_APB2PeriphClockCmd(RCC_APB2Periph…

计算机网络:信道复用技术概念解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

数据结构从入门到精通——希尔排序

希尔排序 前言一、希尔排序( 缩小增量排序 )二、希尔排序的特性总结三、希尔排序动画演示四、希尔排序具体代码实现test.c 前言 希尔排序是一种基于插入排序的算法&#xff0c;通过比较相距一定间隔的元素来工作&#xff0c;各趟比较所用的距离随着算法的进行而减小&#xff0…

【前端】Web API

1.Web API 简介 JS分为三大部分&#xff1a; ESCMScript&#xff1a;基础语法部分DOM API&#xff1a;操作页面结构BOM API&#xff1a;操作浏览器 Web API包含 DOM BOM 2.DOM基本概念 DOM全称 Document Object Mod…

最短路算法

数据结构、算法总述&#xff1a;数据结构/算法 C/C-CSDN博客 目录 朴素dijkstra算法 堆优化版dijkstra算法 Bellman-Ford算法 spfa 算法&#xff08;队列优化的Bellman-Ford算法&#xff09; spfa判断图中是否存在负环 floyd算法 朴素dijkstra算法 思路&#xff1a; 集合…

Linux相关命令(2)

1、W &#xff1a;主要是查看当前登录的用户 在上面这个截图里面呢&#xff0c; 第一列 user &#xff0c;代表登录的用户&#xff0c; 第二列&#xff0c; tty 代表用户登录的终端号&#xff0c;因为在 linux 中并不是只有一个终端的&#xff0c; pts/2 代表是图形界面的第…

长三角科技盛会“2024南京国际人工智能,机器人,自动驾驶展览会”

2024南京国际人工智能,机器人,自动驾驶展览会 2024 Nanjing International Ai, Robotics, Autonomous Driving Expo 时间:2024年11月22-24日 地点:南京国际博览中心 南京&#xff0c;这座历史悠久的文化名城&#xff0c;如今正站在新一轮科技产业变革的前沿&#xff0c;以人工…

院子摄像头的监控

院子摄像头的监控和禁止区域入侵检测相比&#xff0c;多了2个功能&#xff1a;1&#xff09;如果检测到有人入侵&#xff0c;则把截图保存起来&#xff0c;2&#xff09;如果检测到有人入侵&#xff0c;则向数据库插入一条事件数据。 打开checkingfence.py&#xff0c;添加如下…