名词:
作为一个PWN手,平常总是跟GDB打交道,这篇文章简单来介绍一下GDB相关的基本原理~(在会用工具的同时也要了解工具的基本原理)
GDB整体简要的结构体如下:
当我们用gdb调试一个可执行文件时都发生了什么?
以这种方式直接运行时,首先,gdb解析a.out文件的符号。接下来我们输入 run
命令,gdb通过 fork()
一个新进程,然后通过 ptrace(PTRACE_TRACEME, 0, NULL, NULL);
设置traceme模式。最后执行 exec
启动加载要调试的文件。
在调试PWN题时,通过attach pid来追踪要调试的进程。gdb通过执行 ptrace(PTRACE_ATTACH,pid, 0, 0)
来对目标进程进行追踪。
在gdb+qemu调试内核时,经常用到target remote来attach到qemu上对vmlinux进行调试。二者之间有特殊的定义好的数据信息通信的格式,进行通信。
ptrace可以说是gdb的灵魂了。
https://man7.org/linux/man-pages/man2/ptrace.2.html
ptrace原型如下:
官方对其进行了DESCRIPTION如下:
翻译一下:ptrace()系统调用提供了一种方法可以使得追踪者(tracer)来对被追踪者(tracee)进行观察与控制。具体表现为可以检查tracee中内存以及寄存器的值。ptrace首要地被用于实现断点debug与系统调用追踪。
首先,tracee process必须要被tracer attach上(也就是我们启动gdb后的 attach pid),需要注意的是,attach和后续的命令是针对每个线程来说的。如果是一个多线程的程序,每个线程都要被单独的attach才行。这里主要强调了,tracee(被追踪者)是一个单独的thread,而非一个整个的多线程程序。
当追踪时,tracee每次发送一个信号就会停一次,即使这个signal被会忽略掉。而tracer将会捕捉到tracee的下一个调用(通过waitpid或wait类似系统调用)。而这个调用将会告诉tracer,tracee停止的原因以及相关信息。所以当tracee停下来,tracer可以通过ptrace的多种模式来进行监控甚至修改tracee,然后tracer会告诉tracee继续运行。
ptrace四个参数的含义解释如下:
ptrace的第一个参数可以是如下的值:
接下来我们来看request中富有代表性的几个模式。
这个模式 只被tracee使用,使用它的进程将会被其父进程追踪。父进程通过wait()获知子进程的信号。
当我们只用traceme模式时,内核首先会让写者拿到读写锁,并禁止本地中断。
接下来判断是否我们当前进程已经被追踪,接着将子进程链接到父进程的ptrace链表中。
最后放掉锁。
在attach模式下,通过指定一个tracee的pid,tracee向tracer发送SIGSTOP信号。而tracer使用 waitpid()等待tracee停止。
tracer通过这个模式,向tracee发信号,让停止的tracee继续运行。
在tracee的USER区域的addr处读取一个word。读取的这个字为返回值。
而具体到ptrace中的 PTRACE_SINGLESTEP 来说,这是基于eflags寄存器的TF位(陷阱标志)实现的。他强迫子进程,执行下一条汇编指令,然后又停止他,此时子进程产生一个 debug exception 而对应的异常处理程序负责清掉这个标志位,并强迫当前进程停止。然后发送SIGCHLD信号给父进程。
有了以上基础后我们来看如下demo
这段程序输出如下:
我们来梳理一下他的过程。
需要注意的是,让子进程停住的是子进程中的 exec族 函数。
当tracee触发一个exec的时候,会通过 send_sig(SIGTRAP, current, 0)
产生一个 SIGTRAP ,导致停止。
接下来总结一下ptrace是如何起作用的:
在 enable_single_step
中设置了 X86_EFLAGS_TF
以及 TIF_SINGLESTEP
标志位。
在 test_tsk_thread_flag
中检查了对应进程 thread_info中的 TIF_BLOCKSTEP
标志位。
接下来在 set_task_blockstep 中设置或清除了 DEBUGCTLMSR_BTF
以及 对应thread info的 TIF_BLOCKSTEP
。
首先明确一点,breakpoints并不是ptrace的实现的一部分。并且,当处于attach和traceme状态下,交付给tracee的任何信号首先都会被GDB截获。
breakpoints大体的实现如下:
假设我们想在addr处停下来。
那么GDB会做如下事情。
1.读取addr处的指令的位置,存入GDB维护的断点链表中。
2.将中断指令 INT 3 (0xCC)打入原本的addr处。也就是将addr处的指令掉换成INT 3
3.当执行到addr处(INT 3)时,CPU执行这条指令的过程也就是发生断点异常(breakpoint exception),tracee产生一个SIGTRAP,此时我们处于attach模式下,tracee的SIGTRAP会被tracer(GDB)捕捉。
然后GDB去他维护的断点链表中查找对应的位置,如果找到了,说明hit到了breakpoint。
4.接下来,如果我们想要tracee继续正常运行,GDB将INT 3指令换回原来正常的指令,回退重新运行正常指令,然后接着运行。
我们可以看如下demo:
当我们独立运行时:
当放到GDB中时:
可以看到已经由于INT 3的存在停下来了。如果我们c下去,就可以正常运行结束。
在GDB中另一个非常有用的是watch命令。用于监控某一内存位置或者寄存器的变化。
watch的实现与CPU的相关寄存器有关。我们以80386为例。
存在DR0到DR7这八个特殊的寄存器来实现硬件断点。
(1)DR0-DR3:每个寄存器都保存着对应条件断点的线性地址。而每个断点更进一步的信息储存在DR7中。需要注意的是,由于储存的是线性地址,所以是否开启分页是不影响的。如果开启paging,那么线性地址由mmu转换到物理地址;否则线性地址与物理地址等效。
(2)DR7 调试控制寄存器debug control:DR7的低八位(0、2、4、6和1、3、5、7)有选择地启用四个条件断点。启用级别有两个:本地(0,2,4,6)和全局(1,3,5,7)。处理器会在每个任务切换时自动重置本地启用位,以避免在新任务中出现不必要的断点情况。全局启用位不会由任务开关重置;因此,它们可以用于所有任务的全局条件。
16-17位(对应于DR0),20-21(DR1),24-25(DR2),28-29(DR3)定义了断点触发的时间。每个断点都有一个两位对应,用于指定它们是在执行(00b),数据写入(01b),数据读取还是写入(11b)时中断。10b被定义为表示IO读取或写入中断,但没有硬件支持它。位18-19(DR0),22-23(DR1),26-27(DR2),30-31(DR3)定义了断点监视多大的内存区域。同样,每个断点都有一个两位对应,指定他们watch一个(00b),两个(01b),八(10b)还是四(11b)个字节。
(3)DR6 调试状态寄存器:告诉调试器哪些断点已经发生了。
这部分主要是参考:
On the subject of debuggers and tracers
完整代码加完整注释已放到Github:
OrangeGzY/tiny_debugger
我们希望能写一个迷你的 tracer 来实现打断点和追踪。
最简单的,我们需要两个进程,子进程负责启动 tracee 程序,主进程负责追踪tracee。
tracee 程序如下:
tracee程序由fork出的子进程execl启动。
接下来我们来看 tracer 的实现。
首先,我们需要解析tracee对应的一些ELF信息。以获取对应的 函数符号、函数名、函数地址 的信息。
我们首先读取ELF header,然后将文件内部指针 移动到段表的位置。
接下来我们扫描每一个 section header ,直到找到我们 符号表 的信息。
找到符号表后,我们定位到 字符串表header(strtab header) 在段表中的位置,并读取相关信息。
现在符号表的字符串表的基本信息我们都有了。可以接着进行下一步。
我们扫描符号表中的每一项 Elf64_Sym
如果这个符号是一个函数并且地址存在且合法,那么这就是我们要追踪的函数之一。
我们通过在每个函数开始的位置打断点实现追踪。
扫描到了需要追踪的函数,我们将其对应的信息(函数地址、函数名、地址处对应的指令)放入定义好的 Breakpoint 结构体中。
当扫描结束时,所有的需要追踪(打断点)的函数都被加入我们的 bp_list 中。
我们通过遍历 Breakpoint bp_list[N]
来对每一个需要追踪的函数实现断点注入。
步骤如下:
(1)读出要打断点处的原本的指令并保存在 bp_list[i].orig_code
中。
(2)通过ptrace向addr处注入 0xCC (INT 3)。相当于修改了此处的指令,动态的进行的patch。
(3)检查是否注入成功。
至此,我们的断点就打完了,INT 3已经注入到每一个函数起始位置。
在tracer中我们做完了断点注入之后。
首先等待子进程execl执行。
接下来判断wait等到的信号的类型。如果是一个SIGSTOP的话进一步判断是否是SIGTRAP。
如果是 SIGTRAP 再去判断是否踩到了断点。此时我们做如下步骤。
(1)保存用户态寄存器,为回退做准备。
(2)扫描断点列表,看当前的rip寄存器中的值减1是否与断点列表中某一项的地址相同,如果是,说明断点命中(hit),若命中,输出函数名称。
(3)接下来,为了程序的正常执行,我们用ptrace将之前注入INT 3的地址处的指令恢复成 orgi_code
。
(4)之后,通过ptrace设置寄存器,让执行流回退一步,执行应该执行的正常指令(int 3此时被改回来了)。
(5)单步步进一下,越过这条正常指令,再次重新对这个地址注入断点。
至此,实现了断点的追踪与维持,并维护了程序的正常执行流程和断点信息。
最终输出:
如果我们开启debug模式,输出如下:
可以看到,整个流程非常清晰。
https://man7.org/linux/man-pages/man2/ptrace.2.html
https://sourceware.org/gdb/wiki/Internals
https://www.jianshu.com/p/b1f9d6911c90
https://blog.csdn.net/u012417380/article/details/60468697
https://blog.csdn.net/reliveIT/article/details/108269437
Ptrace--Linux中一种代码注入技术的应用
long
ptrace(enum __ptrace_request request, pid_t pid,void
*
addr, void
*
data);
long
ptrace(enum __ptrace_request request, pid_t pid,void
*
addr, void
*
data);
The ptrace() system call provides a means by which one process
(the
"tracer"
) may observe
and
control the execution of another
process (the
"tracee"
),
and
examine
and
change the tracee's
memory
and
registers. It
is
primarily used to implement
breakpoint debugging
and
system call tracing.
The ptrace() system call provides a means by which one process
(the
"tracer"
) may observe
and
control the execution of another
process (the
"tracee"
),
and
examine
and
change the tracee's
memory
and
registers. It
is
primarily used to implement
breakpoint debugging
and
system call tracing.
A tracee first needs to be attached to the tracer. Attachment
and
subsequent commands are per thread:
in
a multithreaded
process, every thread can be individually attached to a
(potentially different) tracer,
or
left
not
attached
and
thus
not
debugged. Therefore,
"tracee"
always means
"(one) thread"
, never
"a (possibly multithreaded) process"
. Ptrace commands are always
sent to a specific tracee using a call of the form
A tracee first needs to be attached to the tracer. Attachment
and
subsequent commands are per thread:
in
a multithreaded
process, every thread can be individually attached to a
(potentially different) tracer,
or
left
not
attached
and
thus
not
debugged. Therefore,
"tracee"
always means
"(one) thread"
, never
"a (possibly multithreaded) process"
. Ptrace commands are always
sent to a specific tracee using a call of the form
While being traced, the tracee will stop each time a signal
is
delivered, even
if
the signal
is
being ignored. (An exception
is
SIGKILL, which has its usual effect.) The tracer will be
notified at its
next
call to waitpid(
2
) (
or
one of the related
"wait"
system calls); that call will
return
a status value
containing information that indicates the cause of the stop
in
the tracee. While the tracee
is
stopped, the tracer can use
various ptrace requests to inspect
and
modify the tracee. The
tracer then causes the tracee to
continue
, optionally ignoring
the delivered signal (
or
even delivering a different signal
instead).
While being traced, the tracee will stop each time a signal
is
delivered, even
if
the signal
is
being ignored. (An exception
is
SIGKILL, which has its usual effect.) The tracer will be
notified at its
next
call to waitpid(
2
) (
or
one of the related
"wait"
system calls); that call will
return
a status value
containing information that indicates the cause of the stop
in
the tracee. While the tracee
is
stopped, the tracer can use
various ptrace requests to inspect
and
modify the tracee. The
tracer then causes the tracee to
continue
, optionally ignoring
the delivered signal (
or
even delivering a different signal
instead).
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个字节,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个字节,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH,追踪指定pid的进程
PTRACE_DETACH, 结束追踪
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个字节,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个字节,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH,追踪指定pid的进程
PTRACE_DETACH, 结束追踪
/
*
*
*
ptrace_traceme
-
-
helper
for
PTRACE_TRACEME
*
*
Performs checks
and
sets PT_PTRACED.
*
Should be used by
all
ptrace implementations
for
PTRACE_TRACEME.
*
/
static
int
ptrace_traceme(void)
{
int
ret
=
-
EPERM;
write_lock_irq(&tasklist_lock);
/
*
Are we already being traced?
*
/
if
(!current
-
>ptrace) {
ret
=
security_ptrace_traceme(current
-
>parent);
/
*
*
Check PF_EXITING to ensure
-
>real_parent has
not
passed
*
exit_ptrace(). Otherwise we don't report the error but
*
pretend
-
>real_parent untraces us right after
return
.
*
/
if
(!ret && !(current
-
>real_parent
-
>flags & PF_EXITING)) {
current
-
>ptrace
=
PT_PTRACED;
__ptrace_link(current, current
-
>real_parent);
}
}
write_unlock_irq(&tasklist_lock);
return
ret;
}
/
*
*
*
ptrace_traceme
-
-
helper
for
PTRACE_TRACEME
*
*
Performs checks
and
sets PT_PTRACED.
*
Should be used by
all
ptrace implementations
for
PTRACE_TRACEME.
*
/
static
int
ptrace_traceme(void)
{
int
ret
=
-
EPERM;
write_lock_irq(&tasklist_lock);
/
*
Are we already being traced?
*
/
if
(!current
-
>ptrace) {
ret
=
security_ptrace_traceme(current
-
>parent);
/
*
*
Check PF_EXITING to ensure
-
>real_parent has
not
passed
*
exit_ptrace(). Otherwise we don't report the error but
*
pretend
-
>real_parent untraces us right after
return
.
*
/
if
(!ret && !(current
-
>real_parent
-
>flags & PF_EXITING)) {
current
-
>ptrace
=
PT_PTRACED;
__ptrace_link(current, current
-
>real_parent);
}
}
write_unlock_irq(&tasklist_lock);
return
ret;
}
if
(request
=
=
PTRACE_ATTACH) {
if
(child
=
=
current)
goto out;
if
((!child
-
>dumpable ||
/
/
这里检查了进程权限
(current
-
>uid !
=
child
-
>euid) ||
(current
-
>uid !
=
child
-
>suid) ||
(current
-
>uid !
=
child
-
>uid) ||
(current
-
>gid !
=
child
-
>egid) ||
(current
-
>gid !
=
child
-
>sgid) ||
(!cap_issubset(child
-
>cap_permitted, current
-
>cap_permitted)) ||
(current
-
>gid !
=
child
-
>gid)) && !capable(CAP_SYS_PTRACE))
goto out;
if
(child
-
>flags & PF_PTRACED)
goto out;
child
-
>flags |
=
PF_PTRACED;
/
/
设置进程标志位PF_PTRACED
write_lock_irqsave(&tasklist_lock, flags);
if
(child
-
>p_pptr !
=
current) {
/
/
设置进程为当前进程的子进程。
REMOVE_LINKS(child);
child
-
>p_pptr
=
current;
SET_LINKS(child);
}
write_unlock_irqrestore(&tasklist_lock, flags);
send_sig(SIGSTOP, child,
1
);
/
/
向子进程发送一个SIGSTOP,使其停止
ret
=
0
;
goto out;
}
if
(request
=
=
PTRACE_ATTACH) {
if
(child
=
=
current)
goto out;
if
((!child
-
>dumpable ||
/
/
这里检查了进程权限
(current
-
>uid !
=
child
-
>euid) ||
(current
-
>uid !
=
child
-
>suid) ||
(current
-
>uid !
=
child
-
>uid) ||
(current
-
>gid !
=
child
-
>egid) ||
(current
-
>gid !
=
child
-
>sgid) ||
(!cap_issubset(child
-
>cap_permitted, current
-
>cap_permitted)) ||
(current
-
>gid !
=
child
-
>gid)) && !capable(CAP_SYS_PTRACE))
goto out;
if
(child
-
>flags & PF_PTRACED)
goto out;
child
-
>flags |
=
PF_PTRACED;
/
/
设置进程标志位PF_PTRACED
write_lock_irqsave(&tasklist_lock, flags);
if
(child
-
>p_pptr !
=
current) {
/
/
设置进程为当前进程的子进程。
REMOVE_LINKS(child);
child
-
>p_pptr
=
current;
SET_LINKS(child);
}
write_unlock_irqrestore(&tasklist_lock, flags);
send_sig(SIGSTOP, child,
1
);
/
/
向子进程发送一个SIGSTOP,使其停止
ret
=
0
;
goto out;
}
case PTRACE_CONT:
long
tmp;
ret
=
-
EIO;
if
((unsigned
long
) data > _NSIG)
/
/
信号是否超过范围?
goto out;
if
(request
=
=
PTRACE_SYSCALL)
child
-
>flags |
=
PF_TRACESYS;
/
/
如果是PTRACE_SYSCALL就设置PF_TRACESYS标志
else
child
-
>flags &
=
~PF_TRACESYS;
/
/
如果是PF_CONT,去除PF_TRACESYS标志
child
-
>exit_code
=
data;
/
/
设置继续处理的信号
tmp
=
get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG;
/
/
清除TRAP_FLAG
put_stack_long(child, EFL_OFFSET,tmp);
wake_up_process(child);
/
/
唤醒停止的子进程
ret
=
0
;
goto out;
case PTRACE_CONT:
long
tmp;
ret
=
-
EIO;
if
((unsigned
long
) data > _NSIG)
/
/
信号是否超过范围?
goto out;
if
(request
=
=
PTRACE_SYSCALL)
child
-
>flags |
=
PF_TRACESYS;
/
/
如果是PTRACE_SYSCALL就设置PF_TRACESYS标志
else
child
-
>flags &
=
~PF_TRACESYS;
/
/
如果是PF_CONT,去除PF_TRACESYS标志
child
-
>exit_code
=
data;
/
/
设置继续处理的信号
tmp
=
get_stack_long(child, EFL_OFFSET) & ~TRAP_FLAG;
/
/
清除TRAP_FLAG
put_stack_long(child, EFL_OFFSET,tmp);
wake_up_process(child);
/
/
唤醒停止的子进程
ret
=
0
;
goto out;
int
main()
{
char
*
argv[ ]
=
{
"ls"
,
"-al"
,
"/etc/passwd"
,(char
*
)
0
};
char
*
envp[ ]
=
{
"PATH=/bin"
,
0
};
pid_t child;
long
orig_rax;
child
=
fork();
if
(child
=
=
0
)
{
ptrace(PTRACE_TRACEME,
0
, NULL, NULL);
printf(
"Try to call: execl\n"
);
execve(
"/bin/ls"
,argv,envp);
printf(
"child exit\n"
);
}
else
{
wait(NULL);
/
/
等待子进程
orig_rax
=
ptrace(PTRACE_PEEKUSER,
child,
8
*
ORIG_RAX,
NULL);
printf(
"The child made a "
"system call %ld\n"
, orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
printf(
"Try to call:ptrace\n"
);
}
return
0
;
}
int
main()
{
char
*
argv[ ]
=
{
"ls"
,
"-al"
,
"/etc/passwd"
,(char
*
)
0
};
char
*
envp[ ]
=
{
"PATH=/bin"
,
0
};
pid_t child;
long
orig_rax;
child
=
fork();
if
(child
=
=
0
)
{
ptrace(PTRACE_TRACEME,
0
, NULL, NULL);
printf(
"Try to call: execl\n"
);
execve(
"/bin/ls"
,argv,envp);
printf(
"child exit\n"
);
}
else
{
wait(NULL);
/
/
等待子进程
orig_rax
=
ptrace(PTRACE_PEEKUSER,
child,
8
*
ORIG_RAX,
NULL);
printf(
"The child made a "
"system call %ld\n"
, orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
printf(
"Try to call:ptrace\n"
);
}
return
0
;
}
root@ubuntu:~
/
tiny_debugger
Try to call: execl
The child made a system call
59
Try to call:ptrace
root@ubuntu:~
/
tiny_debugger
root@ubuntu:~
/
tiny_debugger
Try to call: execl
The child made a system call
59
Try to call:ptrace
root@ubuntu:~
/
tiny_debugger
/
*
*
*
ptrace_event
-
possibly stop
for
a ptrace event notification
*
@event:
%
PTRACE_EVENT_
*
value to report
*
@message: value
for
%
PTRACE_GETEVENTMSG to
return
*
*
Check whether @event
is
enabled
and
,
if
so, report @event
and
@message
*
to the ptrace parent.
*
*
Called without locks.
*
/
static inline void ptrace_event(
int
event, unsigned
long
message)
{
if
(unlikely(ptrace_event_enabled(current, event))) {
current
-
>ptrace_message
=
message;
ptrace_notify((event <<
8
) | SIGTRAP);
}
else
if
(event
=
=
PTRACE_EVENT_EXEC) {
/
*
legacy EXEC report via SIGTRAP
*
/
if
((current
-
>ptrace & (PT_PTRACED|PT_SEIZED))
=
=
PT_PTRACED)
send_sig(SIGTRAP, current,
0
);
}
}
/
*
*
*
ptrace_event
-
possibly stop
for
a ptrace event notification
*
@event:
%
PTRACE_EVENT_
*
value to report
*
@message: value
for
%
PTRACE_GETEVENTMSG to
return
*
*
Check whether @event
is
enabled
and
,
if
so, report @event
and
@message
*
to the ptrace parent.
*
*
Called without locks.
*
/
static inline void ptrace_event(
int
event, unsigned
long
message)
{
if
(unlikely(ptrace_event_enabled(current, event))) {
current
-
>ptrace_message
=
message;
ptrace_notify((event <<
8
) | SIGTRAP);
}
else
if
(event
=
=
PTRACE_EVENT_EXEC) {
/
*
legacy EXEC report via SIGTRAP
*
/
if
((current
-
>ptrace & (PT_PTRACED|PT_SEIZED))
=
=
PT_PTRACED)
send_sig(SIGTRAP, current,
0
);
}
}
void user_enable_single_step(struct task_struct
*
child)
{
enable_step(child,
0
);
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-1-24 21:27
被Roland_编辑
,原因: