固定参数-以京东sign逆向为例
前言
在逆向过程中,需要结合frida或unidbg,对整个算法进行一步步的分析,有时候我们分析完某一部分,想要继续往下分析时,需要重新发起请求,这时候的参数往往都已经改变了,这样会打断我们的节奏,影响效率。此外,有些算法除了我们外部传进去的参数外,还有一些其他的参数参与了加密,比如时间戳,随机数,一旦这些参与了算法,那么即使每次的传入参数不变,加密的结果还是会变。
外部输入参数的固定
frida
在京东app的hook中,我们选择编写一个函数,能够固定的调用Java层的native函数。
var bptr = Module.findBaseAddress('libjdbitmapkit.so')
console.log(bptr);function hook_12ECC() {Interceptor.attach(bptr.add(0x12ECC+1), {onEnter: function(args) {this.arg0 = args[0];this.arg3 = args[3];console.log(args[0], args[1], args[2], args[3], args[4]);dump("arg0-ptr0", args[0].readPointer());dump("arg0-ptr1", args[0]);dump("arg0-ptr1", args[0].add(4).readPointer(), 64);console.log("arg1", args[1].readCString());// dump("arg2", args[2]);console.log("arg3", args[3].readCString(parseInt(args[4])));},onLeave: function(){console.log("arg0-ret", this.arg0);dump("ret-arg0-ptr1", this.arg0);// dump("ret-arg0-ptr2", this.arg0.add(8).readPointer());dump("12ecc-ret-arg2", this.arg3)}})
}function callBitmapkitUtils() {var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');var currentApplication = Java.use("android.app.ActivityThread").currentApplication();var context = currentApplication.getApplicationContext();var b = 'clientImage';var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';var d = 'd5a585639f505b18';var e = 'android';var f = '10.2.0';var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);console.log('res: ', res);
}Java.perform(function() {hook_12ECC();
})
然后启动frida之后,可以在shell中输入callBitmapkitUtils()来调用函数。
这样一来不像在手机上滑动页面、点击页面那样,有时会有多个请求发出,会多次调用加密的方法。这样的好处是,再也不用在手机上操作了,而且请求的内容和个数是可控的。不过我们注意到京东的参数里有st和sv这两个参数,这可不是由我们传入的,属于内部输入参数,接下来我们要固定它们。
unidbg
对于unidbg而言,在我们编写代码的时候,一般都是固定输入的
内部输入参数的固定
frida
对于内部输入参数而言,可能有时间戳,随机数,常量(数字或字符),其中前2个是会改变的,这会影响逆向分析,所以需要固定这两个参数。时间戳一般是调用libc.so的gettimeofday函数,随机数则是调用libc.so的lrand48或srand48
function hook_libc(){var ptr_t = Module.findExportByName("libc.so", "gettimeofday");Interceptor.attach(ptr_t, {onEnter: function(args){this.arg0 = args[0];},onLeave: function() {this.arg0.writeU32(1639393559);this.arg0.add(4).writeU32(0);}});Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {onEnter: function(args) {},onLeave:function(retval){retval.replace(1);}});
}
Java.perform(function() {hook_libc();hook_12ECC();
})
这样一来,即使多次运行,st和sv也不会改变,有利于我们的分析。
不过,固定时间戳有个问题,其他函数在获取时间戳的时候发现不对,可能会导致frida环境崩溃,所以我们希望只在调用的时候固定时间戳。所以我们更新如下代码
var logvar = 0;function callBitmapkitUtils() {var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');var currentApplication = Java.use("android.app.ActivityThread").currentApplication();var context = currentApplication.getApplicationContext();var b = 'clientImage';var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';var d = 'd5a585639f505b18';var e = 'android';var f = '10.2.0';logvar = 1;var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);logvar = 0;console.log('res: ', res);
}function hook_libc(){var ptr_t = Module.findExportByName("libc.so", "gettimeofday");Interceptor.attach(ptr_t, {onEnter: function(args){this.arg0 = args[0];},onLeave: function() {if (logvar) {this.arg0.writeU32(1639393559);this.arg0.add(4).writeU32(0);}}});Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {onEnter: function(args) {},onLeave:function(retval){if (logvar){retval.replace(1);}}});
}
这样一来,只有在logvar值为1时才会固定参数。而logvar的默认值为0,只有在调用callBitmapkitUtils方法的时候才会改为1,调用完成后又会改为0。
unidbg
对于unidbg,我们运行多次后会发现,时间戳st会变,sv一直是111,好像和frida上的表现不一样,难道出了什么问题?
其实如果研究了京东libjdbitmapkit.so就会发现sv的后两位都是随机数余3。
而在unidbg对libc.so的随机数生成的实现中,种子是固定的(我猜的,没深究源码),导致生成的随机数的顺序是固定的,继而导致余数是固定的。
public void hook_libc() {IHookZz hookZz = HookZz.getInstance(emulator);hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {@Overridepublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {}@Overridepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {int old = ctx.getIntArg(0);System.out.println("Origin rand:" + old);ctx.setR0(1);}});hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback<HookZzArm32RegisterContext>() {@Overridepublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {Pointer pointer = ctx.getR0Pointer();ctx.push(pointer);}@Overridepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {Pointer pointer = ctx.pop();pointer.setLong(0, 1639388888);pointer.setLong(4, 0);}});
}
public static void main(String[] args) {JingDong test = new JingDong();test.hook_libc();test.callSign();
}
可以看到,时间戳已经被固定下来了,而打印出来的两个随机数,他们的除以3的余数都为1,这也说明了为什么固定参数之前sv的值一直是111。
unidbg完整代码
package com.jingdong;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.hook.hookzz.*;
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.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.ParsingException;import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.security.cert.X509Certificate;public class JingDong extends AbstractJni {private final AndroidEmulator emulator;private final VM vm;private final Module module;public static String pkgName = "com.jingdong.app.mall";public static String apkPath = "unidbg-android/src/test/java/com/jingdong/jingdong9.2.2.apk";public static String soPath = "unidbg-android/src/test/java/com/jingdong/libjdbitmapkit.so";private static final String APK_PATH = "/data/app/com.jingdong.app.mall.apk";JingDong() {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();final Memory memory = emulator.getMemory();memory.setLibraryResolver(new AndroidResolver(23));vm = emulator.createDalvikVM(new File(apkPath));DalvikModule dm = vm.loadLibrary(new File(soPath), false);vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);module = dm.getModule();}@Overridepublic DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {switch (signature) {case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);}}return super.getStaticObjectField(vm, dvmClass, signature);}@Overridepublic DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {switch (signature) {case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": {return new StringObject(vm, APK_PATH);}}return super.getObjectField(vm, dvmObject, signature);}@Overridepublic DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {switch (signature) {case "com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B": {StringObject apkPath = varArg.getObjectArg(0);StringObject directory = varArg.getObjectArg(1);StringObject filename = varArg.getObjectArg(2);if (APK_PATH.equals(apkPath.getValue()) &&"META-INF/".equals(directory.getValue()) &&".RSA".equals(filename.getValue())) {byte[] data = vm.unzip("META-INF/JINGDONG.RSA");return new ByteArray(vm, data);}}case "com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B": {DvmObject<?> obj = varArg.getObjectArg(0);byte[] bytes = objectToBytes(obj.getValue());return new ByteArray(vm, bytes);}}return super.callStaticObjectMethod(vm ,dvmClass, signature, varArg);}@Overridepublic DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {switch (signature) {case "sun/security/pkcs/PKCS7-><init>([B)V": {ByteArray array = varArg.getObjectArg(0);try {return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(new PKCS7(array.getValue()));} catch (ParsingException e) {throw new IllegalStateException(e);}}}return super.newObject(vm, dvmClass, signature, varArg);}@Overridepublic DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {switch (signature) {case "sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;": {PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();X509Certificate[] certificates = pkcs7.getCertificates();return ProxyDvmObject.createObject(vm, certificates);}}return super.callObjectMethod(vm, dvmObject, signature, varArg);}@Overridepublic DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "java/lang/StringBuffer-><init>()V": {return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());}case "java/lang/Integer-><init>(I)V": {return DvmInteger.valueOf(vm, vaList.getIntArg(0));}}return super.newObjectV(vm, dvmClass, signature, vaList);}@Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;": {StringBuffer buffer = (StringBuffer) dvmObject.getValue();StringObject str = vaList.getObjectArg(0);buffer.append(str.getValue());return dvmObject;}case "java/lang/Integer->toString()Ljava/lang/String;": {return new StringObject(vm, ((Integer)dvmObject.getValue()).toString());}case "java/lang/StringBuffer->toString()Ljava/lang/String;": {return new StringObject(vm, ((StringBuffer)dvmObject.getValue()).toString());}}return super.callObjectMethodV(vm, dvmObject, signature, vaList);}private static byte[] objectToBytes(Object obj) {try {ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);oos.writeObject(obj);oos.flush();byte[] array = baos.toByteArray();oos.close();baos.close();return array;} catch (IOException e) {throw new IllegalStateException(e);}}public void hook_libc() {IHookZz hookZz = HookZz.getInstance(emulator);hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {@Overridepublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {}@Overridepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {int old = ctx.getIntArg(0);System.out.println("Origin rand:" + old);ctx.setR0(1);}});hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback<HookZzArm32RegisterContext>() {@Overridepublic void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {Pointer pointer = ctx.getR0Pointer();ctx.push(pointer);}@Overridepublic void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {Pointer pointer = ctx.pop();pointer.setLong(0, 1639388888);pointer.setLong(4, 0);}});}public void callSign() {DvmClass cBitmapkitUtils = vm.resolveClass("com/jingdong/common/utils/BitmapkitUtils");StringObject ret = cBitmapkitUtils.callStaticJniMethodObject(emulator, "getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",vm.resolveClass("android/content/Context").newObject(null),"clientImage","{\"moduleParams\":{\"18\":\"1565611060638\",\"19\":\"1565229712150\",\"25\":\"1567478504636\",\"27\":\"1602488415048\",\"28\":\"1631069159956\",\"30\":\"1567404005627\",\"32\":\"1567997588476\",\"34\":\"1593508185597\",\"35\":\"1568708316462\",\"37\":\"1630293538664\",\"42\":\"1623741761542\",\"44\":\"1569247647090\",\"46\":\"1588839806224\",\"47\":\"1571295610042\",\"61\":\"1582091758495\",\"70\":\"1585279774645\",\"74\":\"1586781606615\"}}","d5a585639f505b18","android","10.2.0");System.out.println(ret.getValue());}public static void main(String[] args) {JingDong test = new JingDong();test.hook_libc();test.callSign();}
}
关于京东加密
其实京东app有3套加密方案,会根据随机数不同来选择不同的方案,而unidbg生成的随机数和京东cv的生成方案导致sv一直是固定的,从而一直调用其中一套方案。我们在逆向的时候,其实只需要逆向出其中一套方案即可,那个简单选哪个,如果刚好是调用的那套方案的话那还好。如果不是,一时之间又不知道怎么处理的话就有点倒霉了。
我将这个解密方法封装成api了,直接调用api可以解密,很方便了
技术交流QQ 408737515
相关文章:
固定参数-以京东sign逆向为例
前言 在逆向过程中,需要结合frida或unidbg,对整个算法进行一步步的分析,有时候我们分析完某一部分,想要继续往下分析时,需要重新发起请求,这时候的参数往往都已经改变了,这样会打断我们的节奏&a…...
在macOS 上执行sed命令报错问题
错误描述 在macOS 上执行sed命令,报错 sed -i s/book/books/g demo.txt sed: 1: extra characters at the end of d command解决方法 原因是mac的和linux写法不一样 linux sed -i s/book/books/g demo.txtmac sed -i s/book/books/g demo.txt...
ARP欺骗
ARP协议: 地址解析协议,将IP地址转换为对应的mac地址,属链路层协议 ip地址: ip地址本义是为互联网上的每一个网络和每一台主机配置一个唯一的逻辑地址,它的格式表示为:(A.B.C.D)。其…...
pip切换下载源(多种国内源)
pip切换下载源 一、pip二、使用步骤1.查看源2.切换源 一、pip pip 是一个现代的,通用的 Python 包管理工具 二、使用步骤 1.查看源 使用以下命令查看当前pip的下载源 pip config list2.切换源 在国内使用官方下载依赖往往速度慢,易出错,…...
ARM Cortex-M 的 SP
文章目录 1、栈2、栈操作3、Cortex-M中的栈4、MDK中的SP操作流程5、Micro-Lib的SP差别1. 使用 Micro-Lib2. 未使用 Micro-Lib 在嵌入式开发中,堆栈是一个很基础,同时也是非常重要的名词,堆栈可分为堆 (Heap) 和栈 (Stack) 。 栈(Stack): 一种…...
【原创】鲲鹏ARM构架openEuler操作系统安装Oracle 19c
作者:einyboy 【原创】鲲鹏ARM构架openEuler操作系统安装Oracle 19c | 云非云计算机科学、自然科学技术科谱http://www.nclound.com/index.php/2023/09/03/%E3%80%90%E5%8E%9F%E5%88%9B%E3%80%91%E9%B2%B2%E9%B9%8Farm%E6%9E%84%E6%9E%B6openeuler%E6%93%8D%E4%BD%9C%E7%B3%BB%…...
k8s之存储篇---数据卷-挂载
挂载是指将定义在 Pod 中的数据卷关联到容器,同一个 Pod 中的同一个数据卷可以被挂载到该 Pod 中的多个容器上。 数据卷内子路径 有时候我们需要在同一个 Pod 的不同容器间共享数据卷。使用 volumeMounts.subPath 属性,可以使容器在挂载数据卷时指向数…...
无涯教程-JavaScript - TDIST函数
The TDIST function replaces the T.DIST.2T & T.DIST.RT functions in Excel 2010. 描述 该函数返回学生t分布的百分点(概率),其中数值(x)是t的计算值,将为其计算百分点。 t分布用于小样本数据集的假设检验。使用此函数代替t分布的临界值表。 语法 TDIST(x,deg_fr…...
IP子网的划分
文章目录 一、子网掩码1. 产生背景2. 定义3. 分类 二、VLSM算法1. 得出下列参数2. 计算划分结果3. 举例子计算 三、常见子网划分对应关系四、练习IP编址题目需求解题1. 192.168.1.100/282. 172.16.0.58/263. 25.83.149.222/254. 100.100.243.18/205. 10.100.100.100/10 首先可以…...
弹性盒子的使用
一、定义 弹性盒子是一种用于按照布局元素的一维布局方法,它可以简便、完整、响应式地实现各种页面布局。 容器中存在两条轴,主轴和交叉轴(相当于我们坐标轴的x轴和y轴)。我们可以通过flex-direction来决定主轴的方向。 主轴(main axis&am…...
软件测试/测试开发丨Selenium 网页frame与多窗口处理
点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接:https://ceshiren.com/t/topic/27048 一、多窗口处理. 1.1、多窗口简介 点击某些链接,会重新打开⼀个窗⼜,对于这种情况,想在新页⾯上操作࿰…...
MySQL高阶语句(三)
一、NULL值 在 SQL 语句使用过程中,经常会碰到 NULL 这几个字符。通常使用 NULL 来表示缺失 的值,也就是在表中该字段是没有值的。如果在创建表时,限制某些字段不为空,则可以使用 NOT NULL 关键字,不使用则默认可以为空…...
链表OJ练习(2)
一、分割链表 题目介绍: 思路:创建两个链表,ghead尾插大于x的节点,lhead尾插小于x的节点。先遍历链表。最后将ghead尾插到lhead后面,将大小链表链接。 我们需要在创建两个链表指针,指向两个链表的头节点&…...
ssh常用操作
ssh常用操作 SSH是一种安全协议,ssh是该协议的客户端程序,openssh-server则是该协议的服务端程序 常用系统都自带了ssh客户端程序,服务端程序则可能要安装 密码远程登陆 前提:服务器安装了openssh-server,未安装时…...
从AD迁移至AAD,看体外诊断领军企业如何用网络准入方案提升内网安全基线
摘要: 某医用电子跨国集团中国分支机构在由AD向AzureAD Global迁移时,创新使用宁盾网络准入,串联起上海、北京、无锡等国内多个职场与海外总部,实现平滑、稳定、全程无感知的无密码认证入网体验,并通过合规基线检查,确…...
Flutter系列文章-Flutter在实际业务中的应用
不同场景下的解决方案 1. 跨平台开发: 在移动应用开发中,面对不同的平台(iOS和Android),我们通常需要编写两套不同的代码。而Flutter通过一套代码可以构建适用于多个平台的应用,大大提高了开发效率&#x…...
FPGA | Verilog仿真VHDL文件
当VHDL模块中有Generic块时,应该怎么例化? VHDL模块代码 entity GenericExample isgeneric (DATA_WIDTH : positive : 8; -- 泛型参数:数据宽度ENABLE_FEATURE : boolean : true -- 泛型参数:是否启用特定功能);Port ( clk : …...
微服务--Gatway:网关
routes: - id:order_route(路由唯一 标识,路由到order) uri:http://localhost:8020 #需要转发的地址 #断言规则(用于路由规则的匹配) predicates: -path/order-serv/** -pathlb://order-service # lb: 使用nacos中的本地…...
Django传递dataframe对象到前端网页
在django前端页面上展示的数据,还是使用django模板自带的语法 方式1 不推荐使用 直接使用 【df.to_html(indexFalse)】 使用to_html他会生成一个最基本的表格没有任何的样式,一点都不好看,如果有需要的话可以自行修改表格的样式,…...
iOS swift5 弹出提示文字(停留1~2s)XHToastSwift
CoderZhuXH/XHToastSwift - github // // XHToast.swift // XHToastSwiftExample // // Created by xiaohui on 16/8/12. // Copyright © 2016年 CoderZhuXH. All rights reserved. // 代码地址:https://github.com/CoderZhuXH/XHToastSwiftimport UIKit/*** Toast…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
