本文整理了内核pwn中提权返回到用户态时的相关知识点,求大佬轻喷
本章不关注系统调用函数的参数,以及返回值,只关注系统调用指令本身。
这里就拿经典的 int 0x80 与 syscall 来说
int 0x80是传统的系统调用,它过中断/异常实现,在执行 int 指令时,发生 trap。硬件根据向量号0x80找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将用户态ss / sp / eflags / cs / ip / error code 依次压到内核栈。然后会执行eip的entry函数,通常在保存一系列寄存器后会SET_KERNEL_GS设置内核GS。
返回时,最后会执行SWAPGS交换内核和用户GS寄存器,然后执行iret指令将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。
总结一下:在提权时,如要使用64 位的iretq指令 从内核态返回到用户态,我们首先要执行SWAPGS切换GS,然后执行iretq指令时的栈布局应该如下:
根据 Intel SDM,syscall 指令执行时会将当前 rip(syscall的下一条指令地址) 存到 rcx ,将 rflags 保存到 r11 中。 然后使用 MSR寄存器中的 IA32_FMASK屏蔽 rflags,将 IA32_LSTAR 加载到 rip (entry_SYSCALL_64),同时将 IA32_STAR[47:32] 加载到 cs,IA32_STAR[47:32] + 8 加载到 ss (在 GDT 中,ss 就跟在 cs 后面)。
其中的 MSR IA32_LSTAR (MSR_LSTAR) 和 IA32_STAR (MSR_STAR) 在 arch/x86/kernel/cpu/common.c 的 syscall_init 中初始化:
可以看到 MSR_STAR 的第 32-47 位设置为 kernel mode 的 cs,48-63位设置为 user mode 的 cs。而 IA32_LSTAR 被设置为函数 entry_SYSCALL_64 的起始地址。
于是 syscall 时,跳转到 entry_SYSCALL_64 开始执行。
首先通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
然后将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
最后通过 push 保存各寄存器值......
syscall里面的细节我们不探究,直接看从syscall返回那部分
关注一下sysret指令,它是syscall从内核态返回用户态的伴随指令。执行sysret时,它从rcx加载rip,并从r11加载rflags,从 MSR的 IA32_STAR[63:48] 加载CS ,从 IA32_STAR[63:48] + 8 加载SS。SYSRET指令不会修改堆栈指针(ESP或RSP),因此在执行SYSRET之前rsp必须切换到用户堆栈,当然还要切换GS寄存器。
总结一下: 在提权时,当我们使用sysret指令从内核态中返回前,我们需要先设置rcx为用户态rip,设置r11为用户态rflags,设置rsp为一个用户态堆栈,并执行swapgs交换GS寄存器。
在这之前你需要了解内存分页机制。
每个进程都有一套指向进程自身的页表,由CR3寄存器指向。
早期的Linux内核,每当执行用户空间代码(应用程序)时,Linux会在其分页表中保留整个内核内存的映射(内核地址空间和用户地址空间共用一个页全局目录表PGD),并保护其访问。这样做的优点是当应用程序向内核发送系统调用或收到中断时,内核页表始终存在,可以避免绝大多数上下文交换相关的开销(TLB刷新、页表交换等)。
尽管阻止了对这些内核映射的访问,但在之后的一段时间,英特尔x86处理器还是被爆出了可用于页表泄露的旁路攻击,可能绕过KASLR.
KPTI(Kernel PageTable Isolation)全称内核页表隔离,它通过完全分离用户空间与内核空间页表来解决页表泄露。
KPTI中每个进程有两套页表——内核态页表与用户态页表(两个地址空间)。内核态页表只能在内核态下访问,可以创建到内核和用户的映射(不过用户空间受SMAP和SMEP保护)。用户态页表只包含用户空间。不过由于涉及到上下文切换,所以在用户态页表中必须包含部分内核地址,用来建立到中断入口和出口的映射。
当中断在用户态发生时,就涉及到切换CR3寄存器,从用户态地址空间切换到内核态的地址空间。中断上半部的要求是尽可能的快,从而切换CR3这个操作也要求尽可能的快。为了达到这个目的,KPTI中将内核空间的PGD和用户空间的PGD连续的放置在一个8KB的内存空间中(内核态在低位,用户态在高位)。这段空间必须是8K对齐的,这样将CR3的切换操作转换为将CR3值的第13位(由低到高)的置位或清零操作,提高了CR3切换的速度。
开启KPTI后,再想提权就比较有局限性,比如我们常用的ret2usr方式在KPTI下将成为过去时。
下面我们来看一个开启KPTI内核的entry_SYSCALL_64函数
可以看出,在入口和结束的地方都加了SWITCH_CR3相关的宏定义,尝试着分析SWITCH_KERNEL_CR3_NO_STACK,里面汇编实现如下:
拆分FFFFFFFFFFFFE7FF,它的第12和13位是零,这段代码目的就是将CR3的第12位与第13位置零(页表的第12位在CR4寄存器的PCIDE位未开启的情况下,都是保留给OS留做他用),我们只关心13位置零,就相当于CR3-0x1000,从用户态PGD转换成内核态PGD。
再看SWITCH_USER_CR3宏定义的汇编:
同理,将CR3第13位置1,相当于CR3+0x1000,从内核态PGD切换成用户态PGD。
在开启KPTI内核,提权返回到用户态(iretq/sysret)之前如果不设置CR3寄存器的值,就会导致进程找不到当前程序的正确页表,引发段错误,程序退出。
知道KPTI原理,在kernel提权返回用户态的时候绕过kpti的话就很简单了,利用内核映像中现有的gadget
来设置CR3寄存器,并按照iretq/sysret 的需求构造内容,再返回就OK了。
有一种比较懒惰的方法就是利用swapgs_restore_regs_and_return_to_usermode这个函数返回:
cat /proc/kallsyms| grep swapgs_restore_regs_and_return_to_usermode
纯汇编代码如下:
在ROP时,将程序流程控制到 mov rdi, rsp 指令,栈布局如下就行:
当然改modprobe_path也是一个不错的方法,返回后当前进程Segmentation fault也不影响提权。
还可以使用signal(SIGSEGV,shell)捕获常规方式返回导致的SIGSEGV异常信号来get root shell。
如何检查我的Ubuntu上是否启用了KPTI?
【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch
Linux系统调用过程分析 - 知乎
KPTI补丁分析_运维_Linux阅码场-CSDN博客
内核页表隔离_百度百科
https://www.felixcloutier.com/x86
rsp
-
-
-
> rip
cs
rflags
rsp
ss
rsp
-
-
-
> rip
cs
rflags
rsp
ss
void syscall_init(void)
{
wrmsr(MSR_STAR,
0
, (__USER32_CS <<
16
) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned
long
)entry_SYSCALL_64);
wrmsrl(MSR_CSTAR, (unsigned
long
)entry_SYSCALL_compat);
/
*
*
This only works on Intel CPUs.
*
On AMD CPUs these MSRs are
32
-
bit, CPU truncates MSR_IA32_SYSENTER_EIP.
*
This does
not
cause SYSENTER to jump to the wrong location, because
*
AMD doesn't allow SYSENTER
in
long
mode (either
32
-
or
64
-
bit).
*
/
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
0ULL
);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
wrmsrl(MSR_CSTAR, (unsigned
long
)ignore_sysret);
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
0ULL
);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP,
0ULL
);
/
*
Flags to clear on syscall
*
/
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}
void syscall_init(void)
{
wrmsr(MSR_STAR,
0
, (__USER32_CS <<
16
) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned
long
)entry_SYSCALL_64);
wrmsrl(MSR_CSTAR, (unsigned
long
)entry_SYSCALL_compat);
/
*
*
This only works on Intel CPUs.
*
On AMD CPUs these MSRs are
32
-
bit, CPU truncates MSR_IA32_SYSENTER_EIP.
*
This does
not
cause SYSENTER to jump to the wrong location, because
*
AMD doesn't allow SYSENTER
in
long
mode (either
32
-
or
64
-
bit).
*
/
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
0ULL
);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
wrmsrl(MSR_CSTAR, (unsigned
long
)ignore_sysret);
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
0ULL
);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP,
0ULL
);
/
*
Flags to clear on syscall
*
/
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}
ENTRY(entry_SYSCALL_64)
/
*
SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令
*
/
SWAPGS_UNSAFE_STACK
/
*
保存栈值,并设置内核栈
*
/
movq
%
rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack),
%
rsp
/
*
通过push保存寄存器值,形成一个pt_regs结构
*
/
/
*
Construct struct pt_regs on stack
*
/
pushq $__USER_DS
/
*
pt_regs
-
>ss
*
/
pushq PER_CPU_VAR(rsp_scratch)
/
*
pt_regs
-
>sp
*
/
pushq
%
r11
/
*
pt_regs
-
>flags
*
/
pushq $__USER_CS
/
*
pt_regs
-
>cs
*
/
pushq
%
rcx
/
*
pt_regs
-
>ip
*
/
pushq
%
rax
/
*
pt_regs
-
>orig_ax
*
/
pushq
%
rdi
/
*
pt_regs
-
>di
*
/
pushq
%
rsi
/
*
pt_regs
-
>si
*
/
pushq
%
rdx
/
*
pt_regs
-
>dx
*
/
pushq
%
rcx tuichu
/
*
pt_regs
-
>cx
*
/
pushq $
-
ENOSYS
/
*
pt_regs
-
>ax
*
/
pushq
%
r8
/
*
pt_regs
-
>r8
*
/
pushq
%
r9
/
*
pt_regs
-
>r9
*
/
pushq
%
r10
/
*
pt_regs
-
>r10
*
/
pushq
%
r11
/
*
pt_regs
-
>r11
*
/
sub $(
6
*
8
),
%
rsp
/
*
pt_regs
-
>bp, bx, r12
-
15
not
saved
*
/
.......
.......
ENTRY(entry_SYSCALL_64)
/
*
SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令
*
/
SWAPGS_UNSAFE_STACK
/
*
保存栈值,并设置内核栈
*
/
movq
%
rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack),
%
rsp
/
*
通过push保存寄存器值,形成一个pt_regs结构
*
/
/
*
Construct struct pt_regs on stack
*
/
pushq $__USER_DS
/
*
pt_regs
-
>ss
*
/
pushq PER_CPU_VAR(rsp_scratch)
/
*
pt_regs
-
>sp
*
/
pushq
%
r11
/
*
pt_regs
-
>flags
*
/
pushq $__USER_CS
/
*
pt_regs
-
>cs
*
/
pushq
%
rcx
/
*
pt_regs
-
>ip
*
/
pushq
%
rax
/
*
pt_regs
-
>orig_ax
*
/
pushq
%
rdi
/
*
pt_regs
-
>di
*
/
pushq
%
rsi
/
*
pt_regs
-
>si
*
/
pushq
%
rdx
/
*
pt_regs
-
>dx
*
/
pushq
%
rcx tuichu
/
*
pt_regs
-
>cx
*
/
pushq $
-
ENOSYS
/
*
pt_regs
-
>ax
*
/
pushq
%
r8
/
*
pt_regs
-
>r8
*
/
pushq
%
r9
/
*
pt_regs
-
>r9
*
/
pushq
%
r10
/
*
pt_regs
-
>r10
*
/
pushq
%
r11
/
*
pt_regs
-
>r11
*
/
sub $(
6
*
8
),
%
rsp
/
*
pt_regs
-
>bp, bx, r12
-
15
not
saved
*
/
.......
.......
LOCKDEP_SYS_EXIT
/
/
宏的实现与 CONFIG_DEBUG_LOCK_ALLOC 内核配置选项相关,该配置允许在退出系统调用时调试锁。
TRACE_IRQS_ON
/
*
user mode
is
traced as IRQs on
*
/
movq RIP(
%
rsp),
%
rcx
movq EFLAGS(
%
rsp),
%
r11
RESTORE_C_REGS_EXCEPT_RCX_R11
/
/
恢复除 rxc 和 r11 外所有通用寄存器, 因为 rcx 寄存器为调用系统调用的应用程序的返回地址, r11 寄存器为老的 flags register
/
*
根据压栈的内容,恢复 rsp 为用户态的栈顶
*
/
movq RSP(
%
rsp),
%
rsp
USERGS_SYSRET64
/
*
调用宏 USERGS_SYSRET64 ,其扩展调用 swapgs 指令交换用户 GS 和内核GS, sysret 指令执行从系统调用处理退出
*
/
LOCKDEP_SYS_EXIT
/
/
宏的实现与 CONFIG_DEBUG_LOCK_ALLOC 内核配置选项相关,该配置允许在退出系统调用时调试锁。
TRACE_IRQS_ON
/
*
user mode
is
traced as IRQs on
*
/
movq RIP(
%
rsp),
%
rcx
movq EFLAGS(
%
rsp),
%
r11
RESTORE_C_REGS_EXCEPT_RCX_R11
/
/
恢复除 rxc 和 r11 外所有通用寄存器, 因为 rcx 寄存器为调用系统调用的应用程序的返回地址, r11 寄存器为老的 flags register
/
*
根据压栈的内容,恢复 rsp 为用户态的栈顶
*
/
movq RSP(
%
rsp),
%
rsp
USERGS_SYSRET64
/
*
调用宏 USERGS_SYSRET64 ,其扩展调用 swapgs 指令交换用户 GS 和内核GS, sysret 指令执行从系统调用处理退出
*
/
ENTRY(entry_SYSCALL_64)
/
*
*
Interrupts are off on entry.
*
We do
not
frame this tiny irq
-
off block with TRACE_IRQS_OFF
/
ON,
*
it
is
too small to ever cause noticeable irq latency.
*
/
SWAPGS_UNSAFE_STACK
/
/
KPTI 进内核态需要切到内核页表
SWITCH_KERNEL_CR3_NO_STACK
/
*
*
A hypervisor implementation might want to use a label
*
after the swapgs, so that it can do the swapgs
*
for
the guest
and
jump here on syscall.
*
/
GLOBAL(entry_SYSCALL_64_after_swapgs)
/
/
将用户栈偏移保存到 per
-
cpu 变量 rsp_scratch 中
movq
%
rsp, PER_CPU_VAR(rsp_scratch)
/
/
加载内核栈偏移
movq PER_CPU_VAR(cpu_current_top_of_stack),
%
rsp
TRACE_IRQS_OFF
/
*
Construct struct pt_regs on stack
*
/
pushq $__USER_DS
/
*
pt_regs
-
>ss
*
/
pushq PER_CPU_VAR(rsp_scratch)
/
*
pt_regs
-
>sp
*
/
pushq
%
r11
/
*
pt_regs
-
>flags
*
/
pushq $__USER_CS
/
*
pt_regs
-
>cs
*
/
pushq
%
rcx
/
*
pt_regs
-
>ip
*
/
pushq
%
rax
/
*
pt_regs
-
>orig_ax
*
/
pushq
%
rdi
/
*
pt_regs
-
>di
*
/
pushq
%
rsi
/
*
pt_regs
-
>si
*
/
pushq
%
rdx
/
*
pt_regs
-
>dx
*
/
pushq
%
rcx
/
*
pt_regs
-
>cx
*
/
pushq $
-
ENOSYS
/
*
pt_regs
-
>ax
*
/
pushq
%
r8
/
*
pt_regs
-
>r8
*
/
pushq
%
r9
/
*
pt_regs
-
>r9
*
/
pushq
%
r10
/
*
pt_regs
-
>r10
*
/
pushq
%
r11
/
*
pt_regs
-
>r11
*
/
/
/
为r12
-
r15, rbp, rbx保留位置
sub $(
6
*
8
),
%
rsp
/
*
pt_regs
-
>bp, bx, r12
-
15
not
saved
*
/
/
*
*
If we need to do entry work
or
if
we guess we'll need to do
*
exit work, go straight to the slow path.
*
/
movq PER_CPU_VAR(current_task),
%
r11
testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(
%
r11)
jnz entry_SYSCALL64_slow_path
entry_SYSCALL_64_fastpath:
/
*
*
Easy case: enable interrupts
and
issue the syscall. If the syscall
*
needs pt_regs, we'll call a stub that disables interrupts again
*
and
jumps to the slow path.
*
/
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
/
/
确保系统调用号没超过最大值,超过了则跳转到后面的符号
1
处进行返回
cmpq $__NR_syscall_max,
%
rax
andl $__SYSCALL_MASK,
%
eax
cmpl $__NR_syscall_max,
%
eax
ja
1f
/
*
return
-
ENOSYS (already
in
pt_regs
-
>ax)
*
/
/
/
除系统调用外的其他调用都通过 rcx 来传第四个参数,因此将 r10 的内容设置到 rcx
movq
%
r10,
%
rcx
/
*
*
This call instruction
is
handled specially
in
stub_ptregs_64.
*
It might end up jumping to the slow path. If it jumps, RAX
*
and
all
argument registers are clobbered.
*
/
/
/
调用系统调用表中对应的函数
call
*
sys_call_table(,
%
rax,
8
)
.Lentry_SYSCALL_64_after_fastpath_call:
/
/
将函数返回值压到栈中,返回时弹出
movq
%
rax, RAX(
%
rsp)
1
:
/
*
*
If we get here, then we know that pt_regs
is
clean
for
SYSRET64.
*
If we see that no exit work
is
required (which we are required
*
to check with IRQs off), then we can go straight to SYSRET64.
*
/
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
movq PER_CPU_VAR(current_task),
%
r11
testl $_TIF_ALLWORK_MASK, TASK_TI_flags(
%
r11)
jnz
1f
LOCKDEP_SYS_EXIT
/
/
宏的实现与 CONFIG_DEBUG_LOCK_ALLOC 内核配置选项相关,该配置允许在退出系统调用时调试锁。
TRACE_IRQS_ON
/
*
user mode
is
traced as IRQs on
*
/
movq RIP(
%
rsp),
%
rcx
movq EFLAGS(
%
rsp),
%
r11
RESTORE_C_REGS_EXCEPT_RCX_R11
/
/
恢复除 rxc 和 r11 外所有通用寄存器, 因为 rcx 寄存器为调用系统调用的应用程序的返回地址, r11 寄存器为老的 flags register
/
*
*
This opens a window where we have a user CR3, but are
*
running
in
the kernel. This makes using the CS
*
register useless
for
telling whether
or
not
we need to
*
switch CR3
in
NMIs. Normal interrupts are OK because
*
they are off here.
*
/
SWITCH_USER_CR3
/
/
KPTI 返回用户态需要切回用户页表
/
*
根据压栈的内容,恢复 rsp 为用户态的栈顶
*
/
movq RSP(
%
rsp),
%
rsp
USERGS_SYSRET64
/
*
调用宏 USERGS_SYSRET64 ,其扩展调用 swapgs 指令交换用户 GS 和内核GS, sysret 指令执行从系统调用处理退出
*
/
........
........
ENTRY(entry_SYSCALL_64)
/
*
*
Interrupts are off on entry.
*
We do
not
frame this tiny irq
-
off block with TRACE_IRQS_OFF
/
ON,
*
it
is
too small to ever cause noticeable irq latency.
*
/
SWAPGS_UNSAFE_STACK
/
/
KPTI 进内核态需要切到内核页表
SWITCH_KERNEL_CR3_NO_STACK
/
*
*
A hypervisor implementation might want to use a label
*
after the swapgs, so that it can do the swapgs
*
for
the guest
and
jump here on syscall.
*
/
GLOBAL(entry_SYSCALL_64_after_swapgs)
/
/
将用户栈偏移保存到 per
-
cpu 变量 rsp_scratch 中
movq
%
rsp, PER_CPU_VAR(rsp_scratch)
/
/
加载内核栈偏移
movq PER_CPU_VAR(cpu_current_top_of_stack),
%
rsp
TRACE_IRQS_OFF
/
*
Construct struct pt_regs on stack
*
/
pushq $__USER_DS
/
*
pt_regs
-
>ss
*
/
pushq PER_CPU_VAR(rsp_scratch)
/
*
pt_regs
-
>sp
*
/
pushq
%
r11
/
*
pt_regs
-
>flags
*
/
pushq $__USER_CS
/
*
pt_regs
-
>cs
*
/
pushq
%
rcx
/
*
pt_regs
-
>ip
*
/
pushq
%
rax
/
*
pt_regs
-
>orig_ax
*
/
pushq
%
rdi
/
*
pt_regs
-
>di
*
/
pushq
%
rsi
/
*
pt_regs
-
>si
*
/
pushq
%
rdx
/
*
pt_regs
-
>dx
*
/
pushq
%
rcx
/
*
pt_regs
-
>cx
*
/
pushq $
-
ENOSYS
/
*
pt_regs
-
>ax
*
/
pushq
%
r8
/
*
pt_regs
-
>r8
*
/
pushq
%
r9
/
*
pt_regs
-
>r9
*
/
pushq
%
r10
/
*
pt_regs
-
>r10
*
/
pushq
%
r11
/
*
pt_regs
-
>r11
*
/
/
/
为r12
-
r15, rbp, rbx保留位置
sub $(
6
*
8
),
%
rsp
/
*
pt_regs
-
>bp, bx, r12
-
15
not
saved
*
/
/
*
*
If we need to do entry work
or
if
we guess we'll need to do
*
exit work, go straight to the slow path.
*
/
movq PER_CPU_VAR(current_task),
%
r11
testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(
%
r11)
jnz entry_SYSCALL64_slow_path
entry_SYSCALL_64_fastpath:
/
*
*
Easy case: enable interrupts
and
issue the syscall. If the syscall
*
needs pt_regs, we'll call a stub that disables interrupts again
*
and
jumps to the slow path.
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-8-5 14:42
被b0ldfrev编辑
,原因: 新加入些其它技巧