精华内容
下载资源
问答
  • 利用AppScan生成安全报告,可以提前对要生成的安全报告的内容进行选择,如下图,最全的安全报告内容,包括摘要,安全问题,咨询和修订建议,应用程序数据等。 生成的安全报告,基本上包含了以上的内容,具体的...

    目录

    1 AppScan生成的安全报告分析

    2 漏洞的处理

    3 漏洞修复案例

    4 结语


    1 AppScan生成的安全报告分析

    利用AppScan生成安全报告,可以提前对要生成的安全报告的内容进行选择,如下图,最全的安全报告内容,包括摘要,安全性问题,咨询和修订建议,应用程序数据等。

    生成的安全报告,基本上包含了以上的内容,具体的样例如下图。

    其中的介绍部分,主要介绍了WEB安全不同严重级别的漏洞数量,扫描的时间,测试策略,主机IP,端口,登录方法,登录相关设置等信息。

    摘要部分,主要描述了问题的类型说明,有漏洞的URL列表,修复任务和问题数量,安全风险和问题数量,漏洞原因和问题数量,WASC威胁分类和问题数量等。

    按问题类型分类的问题部分,按照严重性从高到低列出了不同类型的漏洞问题,以及问题的分析,风险的描述,风险原因,测试的请求和响应信息等。

    修订建议部分,针对前面发现的不同漏洞问题,提出了对应的修复建议。

    咨询部分,也是针对不同的漏洞问题,从多个角度进行分析说明。

    应用程序数据部分,列出了扫描过程中,应用程序的一些信息,访问的URL,访问的参数,失败的请求,JavaScript等等。

    2 漏洞的处理

    安全报告中的漏洞从严重性上可分为高、中、低三级,对于不同严重级别漏洞的处理,可以采用不同的处理方式,一般情况下,需要将高、中级别的漏洞解决掉,低级别的漏洞不做处理。

    具体的处理方法,基本上按照漏洞处理建议,进行对应的处理。可以采用的处理措施包括安装第三方产品的最新补丁或修订程序,完善Web应用程序编程或配置,对用户输入字符进行必要的处理等,针对具体的漏洞问题,根据修订建议进行处理。

    3 漏洞修复案例

    在实际的安全漏洞扫描中,我们收到的安全报告,碰到了一些高危漏洞。这里通过一个高危漏洞的示例,说明漏洞的解决方法。

    漏洞名称:通过 Bash 绑定远程命令执行(也称为 Shellshock,也称为 Bashdoor)Bashdoor)。

    解决该漏洞的修订建议是应用适合的Bash版本补丁,而且给出了补丁地址。

    在http://ftp.gnu.org/pub/gnu/bash/中,查找bash版本补丁,可以查找bash较新的版本,然后下载这个bash-5.0.tar.gz版本。

    将文件上传到有漏洞的linux服务器上,利用tar命令解压该文件,然后cd到解压目录,执行

    ./configure && make && make install

    安装编译bash-5.0,完成后,查看版本信息。

    [root@localhost ~]# bash --version

    GNU bash,版本 5.0.0(1)-release (x86_64-pc-linux-gnu)

    Copyright (C) 2019 Free Software Foundation, Inc.

    许可证 GPLv3+: GNU GPL 许可证第三版或者更新版本 http://gnu.org/licenses/gpl.html

    通过版本说明,可以看到bash5.0已经安装成功。

    然后再通过AppScan工具进行安全漏洞扫描,结果发现,Bash漏洞已经消失,说明通过安装新版本补丁,已经解决了Bash漏洞。

    4 结语

    分析了AppScan生成的安全报告,并且通过安全报告提供的漏洞解决方法,利用一个示例,解决了高危漏洞,通过文章介绍,可以利用AppScan进行系统的安全漏洞检测,并且解决相应的安全漏洞,提升Web应用的安全防护能力。

    展开全文
  • C++异常处理源码与安全性分析

    千次阅读 2021-12-10 21:21:42
    C++异常处理需要DWARF的支持,其业界实际标准是 IA-64 C++ABI[1],本文主要描述整个异常处理的流程以及在libgcc中的实现.关于基于DWARF的栈回溯可参考 [2], 关于C++异常处理其他分析可参考[3-8]。 一、异常处理代码...

    版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/lidan113lidan/article/details/121865210

    更多内容可关注微信公众号  

      C++异常处理需要DWARF的支持, 其业界实际标准是 IA-64 C++ABI[1],本文主要描述整个异常处理的流程以及在libgcc中的实现. 关于基于DWARF的栈回溯可参考 [2], 关于C++异常处理其他分析可参考[3-8]。


    一、异常处理代码举例

      先以一个简单的C++程序为例:

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3
    4 class x {
    5         public:
    6         x(void) {
    7             printf("x:x() called\n");
    8         }
    9         ~x(void) {
    10            printf("x:~x() called\n");
    11        }
    12 };
    13
    14 void test() {
    15         x a;
    16         throw "test";
    17 }
    18
    19 int main()
    20 {
    21         try {
    22                 test();
    23                 throw 1;
    24         }
    25         catch(int x) {                        //loc1
    26                 printf("Int: %d\n", x);
    27                 return 0;
    28         }
    29         catch(const char * s) {               //loc2
    30                 printf("String: %s\n", s);
    31                 return 0;
    32         }
    33         return 0;
    34 }

      编译与输出:

    tangyuan@ubuntu:~/compiler_test/gcc_test/aarch64/test_exception$ aarch64-linux-gnu-g++ -static main.cc -O0 -o main
    tangyuan@ubuntu:~/compiler_test/gcc_test/aarch64/test_exception$ ./main
    x:x() called
    x:~x() called
    String: test1

       在此函数中 main函数调用了test, test函数在创建了类实例a后主动抛出了异常, 最终结果是test的父函数main捕获到了此异常, 在异常处理之前实例a的析构函数先被执行.


    二、术语定义

      按照IA-64 C++ ABI的描述, 在抛出异常后的处理可以分为两个阶段: 1) 异常处理handler的搜索 2)逐级cleanup直到执行到handler, 这里先给出一些术语的定义:

       1. 异常处理中的handler代码片段:

          是最终捕获并处理异常的这段代码片段,如对于上面的代码来说:

          * throw 1;  的handler就是loc1的这一段代码片段

          * throw "test"; 的handler就是loc2的这一段代码片段

           一个包含多个catch的try/catch语句, 其多个catch case会在同一个handler代码片段中,运行时异常处理需为其传入参数来区分具体case。

       2. 异常处理中的cleanup代码片段:

          一个函数或block中可能有收尾工作要做,如test函数在返回前需要执行类实例a的析构, 这些为block做收尾工作的代码片段称为cleanup, test函数的cleanup代码片段中需要析构类实例a; (一个函数中如果存在类实例定义又存在函数调用的话,此函数中通常都会存在一段cleanup代码以确保子函数中抛出异常时此cleanup代码可被用来执行类实例的析构函数)。

       3. landing_pad: 

           landing_pad指的是一个cleanup或handler,或二者结合的一段代码片段:

           * 函数中的任何一地址最多只能对应此函数内的一个landing_pad:

              - 如果函数某地址处抛出了异常(可能是直接throw或子函数throw导致的)且此地址(在当前函数内)不需要执行cleanup/handler,则其landing_pad为空

              - 如果函数某地址处抛出了异常且此地址同时需要cleanup和handler, 则其landing_pad中会先执行cleanup再执行handler.

           * 函数中不同地址可能对应不同的landing_pad:

             - 如函数中若存在多个try { ... } [catch {..}], 那么不同try中的地址都有自己的landing_pad

       4. DWARF/.eh_frame:

           DWARF是一种调试文件格式, .eh_frame是加载到内存的节区,其中保存了代码中所有函数的CFI(Call Frame Infomation)信息,栈回溯中需要依赖此节区的内容(细节见[2]).

           注: 异常处理虽然是C++的标准, 但C编译输出的二进制中也可以存在.eh_frame段. C中虽然无法使用异常处理,但异常处理的栈回溯可以正常经过C代码(此时C代码编译时需开启-fexceptions支持)

       5. 异常处理过程中的两个阶段:

           在C++中可以通过throw抛出异常,异常的处理要经过两个阶段:

           1) phase1(search phase):  phase1要从throw语句所在函数开始逐级的unwind, 直到在某一级栈帧中找到了可以处理此throw抛出的异常的handler为止.

           2) phase2(cleanup phase): phase2 要再次从throw语句所在的函数开始逐级的unwind, 并依次执行每一级栈帧中的cleanup函数(若有),直到执行到handler为止(这里注意区分cleanup phase和cleanup函数)

            实际上两段式异常处理不是必须的,但这样会带来一定的好处[1]:

        A two-phase exception-handling model is not strictly necessary to implement C++ language semantics, but it does provide some benefits. For example, the first phase allows an exception-handling mechanism to dismiss an exception before stack unwinding begins, which allows resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised). While C++ does not support resumptive exception handling, other languages do, and the two-phase model allows C++ to coexist with those languages on the stack.

      6. LSDA(language-specifi data area): 

          异常相关一段数据(格式见四), 一个try/throw/catch 中是否有cleanup需要执行,是否有某类异常的handler等信息都记录在LSDA中.

      7. personality routine: 

          在异常处理phase1/phase2均会调用的栈回溯回调函数,phase1/2中每unwind到一个栈帧时都会调用此函数,随着传入参数的不同此函数的作用也不同: 

          * 在phase1: 此函数负责回溯每一级栈帧,直到在某个栈帧对应函数中找到当前抛出的异常类型的handler为止

          * 在phase2: 此函数同样回溯每一级栈帧,并逐级调用每一级中的cleanup函数,直到执行到handler函数为止

          personality routine的指针记录在CIE中(见[2]), 最常用的personality routine是 __gxx_personality_v0,本文的后续分析也基于此函数.

       需要注意的是: C++异常与windows的SEH异常不同,C++中只能支持主动触发的异常(也就是通过throw抛出的异常), 而windows SEH可以捕获如除零异常等。


    三、异常处理的流程和关键函数

      以上面的测试代码为例,其异常处理的流程如图:

         test函数中 throw "test"; 的整个异常处理流程为:

      test: throw "test"; => _Unwind_RaiseException => test:cleanup => test:_Unwind_Resume => main:handler => main:函数返回

        其中涉及到的关键函数/代码片段描述如下:

        1. _Unwind_RaiseException:

            test中的throw函数最终调用调用到libgcc的_Unwind_RaiseException, 此函数的作用:

            * 首先执行phase1, 逐级栈回溯并通过personality routine搜索到此异常类型最终的handler(即main:handler)

            * 之后调用_Unwind_RaiseException_Phase2开始执行phase2, phase2回溯到一个landing_pad即返回,这里第一个找到的是test:cleanup(此信息记录在LSDA中), 说明test函数有收尾工作需要做, 此函数返回.

            * _Unwind_RaiseException跳转到test:cleanup执行test函数的收尾工作

        2. test:cleanup:

            test:cleanup的代码是编译器生成的,其作用是执行test中的收尾工作,在这里则是调用x:~x()这个析构函数; 收尾工作完成后则需要再次调用_Unwind_Resume继续phase2.

        3) _Unwind_Resume:

             _Unwind_Resume中重新调用_Unwind_RaiseException_Phase2继续phase2 (1=>2=>3 的过程中通过指针传递了异常相关的全局数据)

            * phase2还是从test函数开始栈回溯,但由于调用_Unwind_Resume的代码并不属于try块之内,test在此PC位置没有cleanup代码片段, phase2继续栈回溯到其父函数main;

            * main函数调用test时是在一个try块中的, 此时phase2找到了此try块的handler, 并跳转到main:handler

        4. main:handler:

            main:handler执行此异常的处理(这里是一个printf), 执行完毕后main函数正常退出其try/catch块继续执行,直到main函数返回(_Unwind_xxx在跳转前会修复栈帧,确保跳转到main:handler时的上下文(callee-saved reg)和main函数中导致抛出异常位置的上下文相同(即main函数调用test函数的位置)。


    三、异常处理源码分析

    1. __cxa_allocate_exception

        抛出异常前需要先调用__cxa_allocate_exception函数分配一个全局结构体用于在栈回溯中传递信息,其定义如下:

    //./libstdc++-v3/libsupc++/eh_alloc.cc
    /*
      分配大小为thrown_size + __cxa_refcounted_exception的空间
      ---------------------------   <== 分配 thrown_size + exception 
      __cxa_refcounted_exception
      ---------------------------   <== 函数返回的是指向这里的指针
      thrown_size
      ---------------------------   
    */
    extern "C" void *
    __cxxabiv1::__cxa_allocate_exception(std::size_t thrown_size) _GLIBCXX_NOTHROW
    {
      void *ret;
    
      thrown_size += sizeof (__cxa_refcounted_exception);
      ret = malloc (thrown_size);
    
      if (!ret)
        ret = emergency_pool.allocate (thrown_size);
    
      if (!ret)
        std::terminate ();
    
      memset (ret, 0, sizeof (__cxa_refcounted_exception));
    
      return (void *)((char *)ret + sizeof (__cxa_refcounted_exception));
    }

    2. _Unwind_RaiseException

        throw语句最终调用_Unwind_RaiseException抛出异常,源码如下:

    //./gcc/config/aarch64/aach64.h, 指定此属性的函数在pro/epilogue中会push/pop栈帧寄存器x29
    #define LIBGCC2_UNWIND_ATTRIBUTE __attribute__((optimize ("no-omit-frame-pointer")))
    
    //./libgcc/unwind.inc
    /*  
        此函数最终实现异常抛出(throw), 其:
       * 首选执行phase1 search, 通过栈回溯确定当前异常类型最终的handler
       * 开始phase2 cleanup: 
          - 栈回溯找到一个landing_pad后则停止了, 设置上下文为栈回溯时landing_pad所在函数的上下文,并跳转到landing_pad
        landing_pad中如果需要继续栈回溯,则需在最后一条语句中调用 _Unwind_Resume.
      参数exc会在整个异常期间作为全局变量传递
     */
    _Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE _Unwind_RaiseException(struct _Unwind_Exception *exc)
    {
      struct _Unwind_Context this_context, cur_context;
      _Unwind_Reason_Code code;
      unsigned long frames;
    
      /* 
         此函数基于_Unwind_RaiseException的DWARF信息, 将_Unwind_RaiseException入口时各个已保存到栈中的寄存器的值初始化到this_context中(细节可参考[2]),
         需要注意的是由于当前函数中调用了_builtin_eh_return(见后), 故和_Unwind_Backtrace不同的是uw_init_context除了初始化callee-saved寄存器外,还同时
         初始化了x0-x3, x0-x3的内存位置后续可用来为_builtin_eh_return传递参数
      */
      uw_init_context (&this_context);
    
      /* 此上下文初始化一次即可,这里先复制一份用于phase1 search 的栈回溯 */
      cur_context = this_context;
    
      /* phase 1: search, 其作用是逐级栈回溯直到找到可以处理当前异常的那个handler */
      while (1)
      {
          _Unwind_FrameState fs;
        
          /* 根据cur_context->ra, 计算当前函数caller各个寄存器的回溯规则(细节参考[2]), 结果记录到fs中*/
          code = uw_frame_state_for (&cur_context, &fs);
    
          if (code == _URC_END_OF_STACK) return _URC_END_OF_STACK;    /* 如果已经到栈底了则直接返回 */
          if (code != _URC_NO_REASON) return _URC_FATAL_PHASE1_ERROR; /* 出错则返回error */
    
          /* 为context->ra所在函数(caller)执行personality routine, 查看其中是否有异常的handler(第一次遍历是从_Unwind_RaiseException的父函数开始的) */
          if (fs.personality)
          {
            /* personality routine的地址记录在CIE中,这里以__gxx_personality_v0函数为例, 此时传入参数_UA_SEARCH_PHASE代表这是phase1, search; */
            code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class, exc, &cur_context);
    
            if (code == _URC_HANDLER_FOUND) break;    /* 找到handler则直接break; 发生错误则返回error; 否则继续循环遍历下一个栈帧 */
            else if (code != _URC_CONTINUE_UNWIND)
                return _URC_FATAL_PHASE1_ERROR;
          }
          /* context->ra所在函数中未找到handler则将fs信息更新到cur_context中,此后cur_context记录当前栈帧函数的caller函数的信息, 再次循环则从caller的caller中查找handler */
          uw_update_context (&cur_context, &fs);     
      }
    
      /* 执行到这里说明异常handler已经找到, handler和context->ra在同一函数, context中记录其caller指向到context->ra时的各个寄存器信息 */
      exc->private_1 = 0;
      /* 将找到handler时的context->CFA记录到exc->praivate_2中, phase2栈回溯时再次遍历到此栈帧时则可以直接执行的handler并结束处理 
         context记录的是handler的calee函数入口的寄存器信息,这也是context->ra所在函数执行到context->ra时的寄存器信息
      */
      exc->private_2 = uw_identify_context (&cur_context);    
    
      /* 这里开始执行phase2 cleanup, 此过程也需要从最后一级栈帧开始一边unwind一边执行cleanup函数, 此时可以复用前面已经初始化的上下文 */
      cur_context = this_context;
    
      /* 此函数执行phase2, 其通过栈回溯找到某级函数的一个landing_pad(cleanup/handler),并将landding_pad和参数保存到cur_context->ra/reg[0/1]中 */
      code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);
    
      /* 返回_URC_INSTALL_CONTEXT代表上下文已经设置好, 后续真正执行landing_pad, 未返回此值代表出错 */
      if (code != _URC_INSTALL_CONTEXT)  return code;
    
    /* 这里调用的是 uw_install_context,为了便于分析直接将此宏展开
       uw_install_context的作用是恢复到this_context时的上下文(this_context记录的是callee入口时寄存器信息,同时也是caller调用callee前的寄存器信息), 并跳转到caller中的landing_pad代码执行,因此landing_pad执行时和之前执行到this_context->ra时拥有相同的寄存器上下文. 
       caller中landing_pad执行完毕后可以直接通过ret正常返回父函数, 也可以调用_Unwind_Resume继续处理异常,这取决于当前的landing_pad是cleanup函数还是handler.
       这里要恢复的寄存器上下文包括:
       * 当前的sp指向caller函数的栈顶
       * 所有callee-saved寄存器必须和caller调用callee时一致(callee可能是throw函数,也可能是任意一个内部调用了throw抛出异常的函数)
         注意:
         1) 这里不是和caller入口时一致,而是和caller调用callee时一致,因为callee保存的是此时的状态
         2) 跳转到landing_pad时必须保证所有callee-saved硬件寄存器全部恢复, 因为如A=>B=>C, 函数B中使用了x19,C中使用了x20, 那么函数B返回时会修复x19但不会修复x20,函数B自身的epilogue无法保证所有callee-saved寄存器全被修复。
       * 所有callee-used寄存器不必恢复,因为函数调用中这些寄存器被破坏是正常的,任何调用子函数的代码在子函数返回后都不应该期待这些寄存器还拥有原始值(但恢复应该也是没有错的).
       * caller执行landing_pad时的状态和其调用callee时的状态一致
    
       libgcc在_Unwind_RaiseException返回时是利用编译器为其生成的一系列pop指令恢复硬件寄存器的,这与libunwind实现不同, 后者是通过汇编代码主动将所有context中的寄存器pop到硬件寄存器中的.在libgcc中:
       * 由于 _Unwind_RaiseException => uw_init_context => __builtin_unwind_init 会导致所有callee-saved寄存器入栈, 故修改callee-saved寄存器实际上只需要修改当前函数栈上这些内存中的值即可, 当前函数_Unwind_RaiseException返回时会自动将这些内存值同步到硬件寄存器中.
       * 同时由于_Unwind_RaiseException => uw_install_context =>  __builtin_eh_return 会导致x0-x3寄存器入栈, 故修改当前函数栈上的x0-x3内存值同样可以在函数返回时将其同步到硬件寄存器中.
    */
    //  uw_install_context (&this_context, &cur_context, frames);
    
      /*
         将this_context中的寄存器值全部写入到current中的寄存器指针指向的内存单元中(current指向的内存即为epilouge中pop寄存器的内存单元),
         landing_pad属于this_context的caller函数,但caller函数的运行时环境是记录在this_context中的.
      */
      long offset = uw_install_context_1 ((this_context), (cur_context));   
      
      void *handler = uw_frob_return_addr ((this_context), (cur_context));        /* 获取返回地址,实际上就是landing_pad的地址 */
    
      _Unwind_DebugHook ((cur_context)->cfa, handler);    
    
      _Unwind_Frames_Extra (frames);                                              /* 为Intel CET修复SCS栈帧 */
      
      /* 异常返回, 此函数和正常函数返回一样在返回前都需要执行epilogue(也就是pop各个寄存器的代码), 区别在于:
         * 正常函数返回是 ret;
         * 此函数返回代码类似 sp = sp + offset; goto handler;
         故此函数最终恢复到landing_pad时的函数栈, 并跳转到landing_pad的代码执行
      */
      __builtin_eh_return (offset, handler);           
    }

    3. _Unwind_RaiseException_Phase2

    //./libgcc/unwind.inc
    /*
        此函数负责phase2的cleanup, 此函数并非完成整个phase2,而是在发现一个landing_pad(cleanup/handler)时就返回,返回前需要为此landing_pad修改context上下文. 
        整个phase2 是通过: _Unwind_RaiseException => [func:cleanup;/function:handler => _Unwind_Resume]* 循环完成的
    */
    static _Unwind_Reason_Code _Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc,
                      struct _Unwind_Context *context, unsigned long *frames_p)
    {
      _Unwind_Reason_Code code;
      unsigned long frames = 1;
    
      while (1)
      {
          _Unwind_FrameState fs;
          int match_handler;
    
          /* 通过栈回溯获取执行到context->ra时的上下文, 结果暂时保存在fs中*/
          code = uw_frame_state_for (context, &fs);
    
          /* 若当前栈帧就是phase1中找到handler的那个栈帧(context->cfa == exc->private_2), 
             则后面调用personality routine时添加flag _UA_HANDLER_FRAME,代表本次直接执行handler即可 */
          match_handler = (uw_identify_context (context) == exc->private_2 ? _UA_HANDLER_FRAME : 0);
    
          if (code != _URC_NO_REASON) return _URC_FATAL_PHASE2_ERROR;
    
          if (fs.personality)        
          {
             /* phase2 同样调用personality routine, 此时personality routine需要找到一个cleanup函数(或最后的handler),
               并将需要传递给cleanup/handler的参数写入 context->reg[0/1], context->ra中 */
             code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler, exc->exception_class, exc, context);
    
             if (code == _URC_INSTALL_CONTEXT) break;     /* 代表已经为一个landing_pad设置好了context,此函数直接返回 */
    
             if (code != _URC_CONTINUE_UNWIND) return _URC_FATAL_PHASE2_ERROR;    /* 出错则返回错误 */
          }
    
          gcc_assert (!match_handler);         /* 执行到handler所在栈帧后则栈回溯就结束了*/
    
          uw_update_context (context, &fs);    /* 当前栈帧中没有cleanup函数/handler需要执行,继续栈回溯上一帧 */
         
          _Unwind_Frames_Increment (context, frames);     /* 记录一共回溯了多少个栈帧,在Intel CET中需要根据其来更新影子栈SP */
      }
    
      *frames_p = frames;
      return code;
    }

    4. __gxx_personality_v0

    //./libstdc++-v3/libsupc++/eh_personallity.cc
    /*
       phase1/phase2均会执行此personality routine, 在两个阶段其作用不同:
       * phase1中负责确定context->ra上下文中是否有当前异常类型的handler, 如果有将其记录到ue_header全局结构体中(handler与context->ra属于同一函数),没有则返回继续回溯.
       * phase2中负责确定context->ra上下文中是否有需要执行的cleanup代码或是否已经回溯到了handler所在栈帧(的子函数), 如果是则更新当前context中 reg[0/1]/ra寄存器并返回, 没有则返回继续回溯.
    */
    extern "C" _Unwind_Reason_Code __gxx_personality_v0 (int version, _Unwind_Action actions,
                  _Unwind_Exception_Class exception_class,  struct _Unwind_Exception *ue_header, struct _Unwind_Context *context)
    {
      ......
      const unsigned char *language_specific_data;   /* 指向当前函数LSDA的指针 */
      const unsigned char *action_record;            /* action_record = 0, 代表此时找到的landing_pad是一个纯cleanup函数, 非0则可能是handler和cleanup的混合,也可能是单独的handler */
      _Unwind_Ptr landing_pad;                       /* context所在函数的landing_pad地址(若有),这是运行时获取的一个可执行代码地址 */
      int handler_switch_value;                      /* 一个整形值,来自LSDA的解析结果,此值最终会传递给landing_pad,以帮助landing_pad确定当前应该执行哪个catch分支 */
      ......
    
      /* 如果当前context是handler所在栈帧,则直接从phase1 personality routine保存的结果中获取handler相关信息,并写入context 即可 */
      if (actions == (_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME) && !foreign_exception)
        {
          /* 恢复phase1 personality routine存入的handler信息 */
          restore_caught_exception(ue_header, handler_switch_value, language_specific_data, landing_pad);
          found_type = (landing_pad == 0 ? found_terminate : found_handler);
          goto install_context;        /* 将handler信息写入context */
        }
    
      /* 如果当前context不是handler所在栈帧, 则不论phase1还是phase2都需要获取当前函数的LSDA信息:
         * 在phase1中是用来确认当前函数是否有handler
         * 在phase2中是用来确认当前函数是否有cleanup函数
        这里pass了LSDA解析相关代码
      */
      language_specific_data = (const unsigned char *) _Unwind_GetLanguageSpecificData (context);
    
      if (! language_specific_data) CONTINUE_UNWINDING;    /* 当前函数没有LSDA则直接继续上一个栈帧(return _URC_CONTINUE_UNWIND) */
    
      p = parse_lsda_header (context, language_specific_data, &info);    /* 解析LSDA头 => info */
      ......
    
      /* 获取当前栈帧对应的函数的返回地址, 在fs初始化时会初始化context->lsda, context->lsda/ra记录的都是当前context caller的信息  */
      ip = _Unwind_GetIP (context);    
      landing_pad = 0;
      action_record = 0;
      handler_switch_value = 0;
      .......
      /* 查找当前函数返回地址(context->ra)所在的代码位置是否有landing_pad(cleanup/handler) */
      while (p < info.action_table)
      {
          _Unwind_Ptr cs_start, cs_len, cs_lp;
          _uleb128_t cs_action;
          p = read_encoded_value (0, info.call_site_encoding, p, &cs_start);
          .......
    
          if (cs_lp) landing_pad = info.LPStart + cs_lp;            /* 如果有landing_pad则记录其地址 */
          /*
            action = 0,代表landing_pad是context->ra所在位置需要的一个cleanup代码片段
            action !=0,代表landing_pad是context->ra所在位置的一个handler代码片段
          */
          if (cs_action)    action_record = info.action_table + cs_action - 1;
          goto found_something;
        }
      }
      .......
    
      /* 这里判断找到的是cleanup函数还是handler,如果是handler还需确定当前抛出的异常类型对应catch 的handler_switch_value */
    found_something:
      if (landing_pad == 0)  found_type = found_nothing;        /* 若context->ra所在函数未找到landing_pad则返回遍历下一个栈帧 */
      else if (action_record == 0)
          found_type = found_cleanup;                           /* action_record=0代表这是一个纯cleanup函数 */
      else                                   /* 否则是一个[cleanup +] handler(在landing_pad的最开始通常先执行cleanup,然后再执行handler的代码片段),此时需要根据LSDA信息
                                                确定本次throw的异常对应的编码(handler_switch_value),此编码会传递给handler, handler通过其来判断应该执行哪个catch case */
      {
          bool saw_handler = false;
          while (1)
          {
              p = read_sleb128 (p, &ar_filter);
              .......
    
              if (saw_handler)
              {  
                  handler_switch_value = ar_filter;            /* 确定 handler_switch_value */
                  found_type = found_handler;    
              }
              else
                  found_type = (saw_cleanup ? found_cleanup : found_nothing);
          }
      }
    
    do_something:
      if (found_type == found_nothing)  CONTINUE_UNWINDING;    /* 若最终什么也没发现,则继续unwind */
      if (actions & _UA_SEARCH_PHASE)                          /* 在phase1 search阶段, 只关注handler, 发现纯cleanup则继续unwind */
      {
          if (found_type == found_cleanup) CONTINUE_UNWINDING; /* search阶段发现cleanup则继续unwind (return _URC_CONTINUE_UNWIND) */
          ......
         
          save_caught_exception(ue_header, context, thrown_ptr,     /* 在phase1若发现了最终的handler,则保存到exc中以供后续phase2使用 */
                    handler_switch_value, language_specific_data, landing_pad, action_record);
          
          return _URC_HANDLER_FOUND;                           /* 不论如何,phase1 到此即返回了 */
      }
    
    
    install_context:            /* 若当前处于 phase2, 则发现landing_pad(cleanup/handler)时会走到install_context流程 */
    
      .......
      /* install_context主要是在context->reg[]中设置r0/r1参数分别为: 异常头_Unwind_Exception的指针和handler_switch_value
        并设置landing pad为返回地址(context->ra)并返回, 后续uw_install_context函数会将此上下文设置到硬件寄存器中并跳转到context->ra执行.
      */
      _Unwind_SetGR (context, __builtin_eh_return_data_regno (0), __builtin_extend_pointer (ue_header));
      _Unwind_SetGR (context, __builtin_eh_return_data_regno (1), handler_switch_value);
      _Unwind_SetIP (context, landing_pad);
      .......
      return _URC_INSTALL_CONTEXT;
    }

    5. uw_install_context_1

    /*
       此函数负责将landing_pad所在栈帧的上下文(target) install到当前上下文(current)上, 此install操作只是对current->reg[i]或current->reg[i]指向的内存内容的修改,其并没有修改任何硬件寄存器.
       uw_install_context_1的install只是将target中寄存器的值全部写入到_Unwind_RaiseException栈上各个寄存器对应的内存中,最终是_Unwind_RaiseException自身的epilogue将这些内存值恢复到硬件寄存器的.
       
       这里需要注意的是:
       1) target中记录的是landing_pad所在函数子函数的上下文, 因为只有callee上下文中才会记录caller调用到callee时各个寄存器的状态, caller的上下文中记录的是caller的caller执行到caller时的寄存器状态.
       2) 子函数(callee)中记录的caller上下文通常不会因为子函数执行到某条语句而改变(见附录举例),.cfi指令理论上恢复的是某寄存器上一个保存在栈中的值,但通常编译器的用法是用其来恢复当前函数caller在调用callee时的寄存器状态. 也就是通常来说在一个函数不同位置其CFA或寄存器值只是获取方法不同, 而真正获取到的都应该是caller调用callee时的CFA和register.
    */
    static long uw_install_context_1 (struct _Unwind_Context *current, struct _Unwind_Context *target)
    {
      long i;
      _Unwind_SpTmp sp_slot;
    
      /* 
         遍历所有target时的寄存器状态,将其记录到current中, 后续current返回时即可将这些寄存器值写入到硬件寄存器中
         (libgcc中通过uw_install_context_1 caller的epilogue将 current->regs 写入到硬件寄存器,
         这里只负责将target->regs中寄存器的值复制到 current->regs指向的内存中)
      */
      for (i = 0; i < __LIBGCC_DWARF_FRAME_REGISTERS__; ++i)
      {
    
          /* c为最后一级栈帧中某寄存器在context中的指针(如若当前uw_install_context_1的caller是_Unwind_RaiseException,
             那么*c是_Unwind_RaiseException栈帧中的一个地址).
             t为当前要执行landing_pad的函数子函数context中某寄存器的指针.
          */
          void *c = (void *) (_Unwind_Internal_Ptr) current->reg[i];
          void *t = (void *) (_Unwind_Internal_Ptr)target->reg[i];
     
          /* current->reg[x]中必须记录的是一个指针,此地址在epilogue的会用来恢复其对应硬件寄存器的值
             current->reg[x]中存的若直接是寄存器值,那么epilogue默认无法将其写入硬件寄存器.
            而target->reg[x]是指针还是值就无所谓了,最终都是将其值写入到current对应的内存中.
          */
          gcc_assert (current->by_value[i] == 0);
    
          if (target->by_value[i] && c)
          {
             ......
             memcpy (c, &t, sizeof (_Unwind_Word));         /* 若target->reg[x]中记录的是寄存器的值,那么这个值直接复制到 current->reg[x]指向的内存中即可 */
          }
          else if (t && c && t != c)
             memcpy (c, t, dwarf_reg_size_table[i]);        /* 若target->reg[x]也是一个指针,则将其指向内存中的值复制到current->reg[x]指向的内存中即可 */
      }
      ......
      return target->cfa- current->cfa + target->args_size; /* 此函数返回的是target与current两个CFA的差值,后续用来修正sp */
    }

    6. _Unwind_Resume

    //./libgcc/unwind.inc
    /*
       _Unwind_Resume的作用是基于当前栈帧继续做栈回溯直到找到下一个landing_pad后跳转执行.
       需要注意的是, 如前面_Unwind_RaiseException 最终跳转到函数 A的landing_pad, 函数A需要resume则会再次调用_Unwind_Resume,
       此时_Unwind_Resume的caller是函数A, 在此过程中函数A在栈回溯中被遍历了两次:
      * 第一次来自_Unwind_RaiseException,其发现函数A中有cleanup/handler需要执行
      * 第二次来自_Unwind_Resume,其发现函数A中没有需要执行的landing_pad
      两次遍历结果不同是因为二者栈回溯时的context->ra 不同:
      * _Unwind_RaiseException回溯的通常是try {...} 中的地址, 在LSDA中会找到其cleanup/handler函数
      * _Unwind_Resume 回溯的是非try {...}中的地址, 故其不存在cleanup/handler
    */
    void LIBGCC2_UNWIND_ATTRIBUTE _Unwind_Resume (struct _Unwind_Exception *exc)
    {
      struct _Unwind_Context this_context, cur_context;
      _Unwind_Reason_Code code;
      unsigned long frames;
    
      /* 将当前callee-saved/x0-x3硬件寄存器的值初始化到 this_context上下文中 */
      uw_init_context (&this_context);
        
      cur_context = this_context;            /* 若执行uw_install_context,这里需要一个上下文备份 */
    
      if (exc->private_1 == 0)               /* 此值在_Unwind_RaiseException中设置为0 */
        code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);        /* 继续栈回溯查找landing_pad */
      else
        code = _Unwind_ForcedUnwind_Phase2 (exc, &cur_context, &frames);
    
      gcc_assert (code == _URC_INSTALL_CONTEXT);
    
      uw_install_context (&this_context, &cur_context, frames);        /* 恢复一个landing_pad所在函数的上下文, 并跳转到landing_pad执行 */
    }

    四、异常处理的安全性分析

       注: 这里的内容引自[9],笔者未亲自试验,若有错误之处还请指正.

       GCC编译的所有支持异常处理的二进制中都存在.eh_frame段, .eh_frame非调试段,是无法strip掉的. 且包含异常处理就意味着运行时一定存在DWARF的代码解释器(也就是如_Unwind_RaiseException等函数),  DWARF的bytecode实际上是另类的一种指令集,理论上是可以完成图灵完备的计算的, 同样也可以基于对这些bytecode的修改完成如对syscall、库函数的调用或ROP。 DWARF本质上应该是DOA(Date Oriented Attack) 攻击的一种。利用DWARF可以在不修改二进制可执行段和数据段的情况下完成木马程序的植入, 在[9]发表时(2011)尚无已知有效的检测方式(理论上如果只是木马注入应该是可以做检测的,但这会很麻烦).

       DWARF自身的语义可以:

    • 读取任意内存
    • 利用内存和和寄存器的值执行任意计算
    • 控制部分寄存器并影响程序的控制流

       DWARF自身的限制是没法直接写寄存器和内存(注意这里说的是没法直接写,但配合已有代码理论上总是可以完成写操作的),在GCC4.5.2中只支持64byte的栈. 作者[9]同时也提供可一个修改ELF中DWARF的工具katana,配合dwarfscript脚本可以方便的修改ELF中的DWARF内容.

       作者同时举例了DWARF的以下原语:

      1) 修改CFA:

    DW_CFA_offset r16 1                   //CFA = r16 + 1
    改为
    DW_CFA_offset r16 6                  //CFA = r16 + 6 

         通过修改DWARF可以很容易修改CFA的计算方式, 在知道某个栈帧大小的情况下可以通过改变一个偏移来绕过某一级别的异常处理, 即修改CFA可导致异常处理执行不同的landing_pad(但不能做到任意地址跳转)

         * 修改CFA会直接导致context->ra(也就是返回地址)的修改(ra通常是根据CFA计算的,见下)返回地址是用来决定在哪个函数中查找landing_pad用的, 故修改CFA可导致将控制流重定位到其他landing_pad

    //通常ra是通过*(CFA - x) 来获取的
    00000100 0000000000000024 00000064 FDE cie=000000a0 pc=00000000004007c0..00000000004008a4
       LOC           CFA      x19   x20   x29   ra    
    00000000004007c0 sp+0     u     u     u     u          
    00000000004007c4 sp+48    u     u     c-48  c-40  
    00000000004007cc sp+48    c-32  c-24  c-48  c-40  
    00000000004008a0 sp+0     u     u     u     u  

         * 但context->ra的修改并不会影响最终的函数返回, 在异常处理中其作用只是用来确定landing_pad, 而landing_pad返回到父函数的地址来自程序栈,这个值是DWARF修改不了的.

      2) 修改寄存器:

         通过修改DWARF可以轻易修改寄存器的值,如:

    DW_CFA_val_expression r16      //r16 = 0x600DF00D     //r16=0x600DF00D
    begin EXPRESSION
    DW_OP_constu 0x600DF00D
    end EXPRESSION

         此修改会直接被带入到landing_pad, 可惜大多数情况下landing_pad应该不会直接使用寄存器的原始值(除了参数x0-x3), 但如果landing_pad被修改为任意地址时(见后)此原语就很有用了(注: DWARF虽然不能修改任意内存,但应该还是可以做到栈溢出的任意1字节写的, 如作者提到的DW_CFA_offset_extened原语配合DW_CFA_val_expression原语等).

         修改DWARF虽然可以控制流,但通常只能将控制流重定向到某一个catch块中(或cleanup代码), 这个catch块甚至可以任何函数中的一个catch块,但其缺点是控制流无法走出catch块, 因为DWARF指令始终还是走正常的异常处理流程的,正常异常处理总是要跳转到某个landing_pad。

      为了走出catch块,作者还提出了修改LSDA数据, LSDA数据记录在.gcc_except_table中,如图:

        修改LSDA则代表可以修改landing_pad为任意地址,这就解决了DWARF无法走出catch块的问题. 一个backdoor的运行逻辑如下:

        1) 程序正常执行直到有异常抛出(throw)

        2) 修改LSDA和.eh_frame后可以让异常跳转到任意地址执行(同时也可以控制全部callee-saved寄存器,x0-x3,其他寄存器则取决于当前函数保存了什么)

        此过程中不需要修改程序中原有的任何代码段与数据段。

        在此基础上,由于运行时.eh_frame段的指针是可以修改的, 故运行时也也可以实现动态的插入DWARF指令(旧版本gcc中.eh_frame和.gcc_except_table是可写的)来实现任意代码执行


    参考资料:

    [1] C++ ABI for Itanium: Exception Handling

    [2] AARCH64平台的栈回溯_ashimida-CSDN博客

    [3] C++ exception handling ABI | MaskRay

    [4] Unwinding a Bug - How C++ Exceptions Work - shorne in japan

    [5] c++ 异常处理(1) - twoon - 博客园

    [6] c++ 异常处理(2) - twoon - 博客园

    [7] C++异常机制的实现方式和开销分析

    [8] https://monkeywritescode.blogspot.com/p/c-exceptions-under-hood.html

    [9] https://cs.dartmouth.edu/~sergey/battleaxe/hackito_2011_oakley_bratus.pdf

    [10] Katana

    展开全文
  • 本文主要梳理到公司参与团队后端开发2年多来,总结开发过的项目中遇到的各种安全问题及应对方案。 目前我们后端团队使用的技术主要还是SpringBoot + Mysql + Redis这一套 ,暂未涉及到SpringBoot Cloud,后面内容...

    前言

    本文主要梳理到公司参与团队后端开发2年多来,总结开发过的项目中遇到的各种安全问题及应对方案。
    目前我们后端团队使用的技术主要还是Sprin在这里插入图片描述
    gBoot + Mysql + Redis这一套 ,暂未涉及到SpringBoot Cloud,后面内容主要围绕目前团队使用的技术框架来讲解的。
    平时大家可能更多注重业务层面的开发(CRUD),然后实现产品的需求就行了,但是却忽略了产品上线后的各种安全问题带来的影响。
    下面总结了一些过去发生过的(出现频率高,影响严重的)开发上的问题及带来的严重影响:

    安全问题影响
    操作系统、SpringBoot及相关服务jar包等版本漏洞攻击者通过版本漏洞拿到root权限进入服务器为所欲为!中招挖矿病毒,这个大家都有遇到过吧!
    被ddos攻击服务器持续无法访问,影响正常用户的使用
    弱口令密码出现在登录类的接口、以及redis、mysql等服务的连接信息上
    短信接口、下载文件消耗类的接口被盗刷这个出现比较多,短信业务被消耗,下载流量持续被别人消耗,带来的是公司钱的损失!!
    充值业务问题这个就比较严重了,涉及到钱的业务开发都要慎重!慎重!再慎重!
    服务器带宽占满等问题同被攻击的常见
    22端口开放暴力破解,重要的端口应该加上白名单连接!!!

    一款互联网产品开发出来上线后,既然是存在于互联网上的东西,如果设计之初没有考虑开发上的安全设计规范服务器安全防御方案,那必然是不安全的,记住这点!
    不管自研还是模仿同行竞品,在上线后,使用你开发产品的用户多了,说明就越来越有价值了,关注度也越来越高后,就极有可能就会被别人攻击、破解等行为,特别是当老板说这个产品你也给我做出来一个的时候…同行出现竞争者,大家都想吃这块肉,说不定别人就会来搞你,某天周末你正在和朋友在外面嗨皮,突然接到老板电话说服务器宕机赶紧看下,一看有人在ddos攻击…扯远了,扯远了!
    总之,你不知道你写的程序哪里突然蹦出个安全漏洞,如果被攻击者发现,就会有安全问题发生的风险!
    比如前不久出现一个shiro的安全问题,我们有一个测试项目用的一个基于SpringBoot+Shiro的旧版本框架,该框架没有升级shiro最新版,安全部门的同事测试就发现通过此漏洞拿到了服务器root权限,其漏洞请点击查看
    转发链接:Apache Shiro 默认密钥致命令执行漏洞(CVE-2016-4437)

    开发者也不必担心在安全设计方面花太多时间,过去2年我和团队在公司开发的后端项目经历了很多安全问题(吃了不少亏换来的经验),所以有了今天的总结,按照所讲的内容一一对应到平日的开发工作里,就能一定程度上减少生产事故的发生!
    所以本文主要目的是避免大家开发新的项目重犯以前的错误,把过去常规的一些处理方案整理出来,作为一个平日的开发规范,养成大家在开发方面安全的意识,希望大家在平时开发都应该对每一个接口都有考虑一定的安全处理,对自己每一行代码负责!

    啰嗦了这么多,还是有必要给大家说清楚情况,接下来就是正文,下面的内容主要是分享经验及解决问题方案,涉及到技术的不多,部分方案以链接方式转载,具体实施根据各自公司的情况来。
    对与能看到这里的小伙伴来说,表示感谢,如果后面有能帮助到你的地方,是我莫大的荣幸,有错误或者建议也请联系本人,及时沟通一起进步!
    接下来主要从2个方面来规范后端开发安全:

    1 开发上的安全设计规范

    1.1 接口安全设计:

    (1)所有的开放接口建议都应该有鉴权处理

    前提:对于XX后台管理系统,后台的相关接口除了登录注册等接口,肯定是需要登陆鉴权后才能访问的,这里主要针对于一些提供给第三方使用的开放接口,直接公开就能访问的接口需要做的鉴权!
    就比如,大家应该对微信登录、短信接口、支付宝支付等其他第三方服务有对接过吧,没有对接过的自行百度!!!看下别人写的对接文档,是不是接口的参数都有什么apiId, apiSecret ,MD5加密校验啥的,对接多了就清楚明白了,使用别人的技术也要去思考为什么要这么设计嘛,一是区分渠道(谁来请求的),二就是对应的安全处理。
    网上有很多优秀的博主发过方案,这里转发推荐:
    转载链接1:开放API接口签名验证,让你的接口从此不再裸奔
    转载链接2:API安全接口安全设计
    最好就是公司有一个统一的鉴权中心服务,所有的接口鉴权都走这个服务,目前我们是单个项目按照上面的方式写的鉴权,我正在做这个事情,等后面我们搭建好了一个这样的统一鉴权中心发出来总结给大家分享一下!

    (2)接口请求都应该有一套限流处理方案

    不知道大家平日开发的接口有没有做并发处理?特别是一些新增,更新业务的接口,说不定你写的接口一个简单的并发测试就能看出一些问题。
    我们设计的接口并不需要像淘宝、京东那样亿级高并发,做好一个符合实际公司业务接口性能需求就行了(根据公司业务来定),要对症下药。
    这里说的限流其实和并发相关了,对于高并发的技术讨论不在这里展开讨论,这个东西太大,我们直说场景和处理方案:
    简单来说就是正常请求量放大10倍甚至更多后,你的系统如何应对?
    换句话说,正常测试的情况是你单台服务器最高承受300的并发量,如果突然某个时间并发量3000,6000,1万 又如何应对?
    这里举个简单例子便于理解:
    假如一个登录接口,测试在指定X核XG X带宽 环境下,300并发量同时登录没问题的,突然3000并发,服务器就宕机了。
    单台物理机的性能指标总会有一个阈值,但为了保证服务的稳定性,我们可以怎么做呢:接口限流是一个方案。
    在应对突然增高的访问,我们可以拒绝掉部分请求,保证服务器的稳定性,这个其实和设计一个抢购秒杀程序差不多的原理。对于恶意的请求量,也是同样的道理,但也是一把双刃剑,缺点是正常用户的请求也会被过滤掉。
    接口限流的对象:

    • IP
    • 接口路径
    • 进一步可以拓展到某个用户,某个业务对象(任务)限制…

    目前我们项目中在用的2个的方案:

    1. 通过SpringBoot的拦截器 +redis,通过注解的方式实现:

    参考转载链接: https://blog.csdn.net/qq_22167989/article/details/107341080

    1. 我们使用了nginx代理,nginx层面也可以去做限流方案:

    参考转载链接: https://www.cnblogs.com/xinzhao/p/11465297.html?utm_source=tuicool&utm_medium=referral

    如果你有更好的方案可以评论推荐一下!

    (3)关于充值模块的接口设计

    一个例子:
    谈到充值业务的开发,我就想起以前遇到一个初级程序员同事(真的不是我)犯过的一个典型错:
    业务功能是:后台有个充值按钮,选择单个用户后点击充值按钮,输入金额就可以给该用户增加金额,同时本人减少相应金额。
    看似一个很简单的功能,其实就是一个银行取钱的例子,假设我的余额是200块,正常流程就是:我取了100块,剩下100块没毛病吧,取钱的时候就有一个接口处理业务逻辑,结果这个接口没做任何处理,没加锁,没事务,造成的后果就是,我在0.11111 毫秒内,狂点了100次取钱,然后我钞票机给我了1000块,我的余额还是100块!要是银行真这么设计早就倒闭了!
    就是这个场景,发生在实际公司业务中,被别人撸了羊毛,好在有日志操作记录把这个用户相关操作记录全部给他退回了,最后修复了这个充值接口的bug,加锁,加事务,限流(不能被狂点)
    那他也受到了相应的惩罚,罚了款!
    这里再举例一个反例,不要这么设计:比如 /api/recharge?userId=100&amount=100, 请求访问后就可以给用户id为100的用户充值100块,相信大家不会这么去设计接口,真这么设计怕是要着锤的,这么看也太不专业了,我觉得以后面试人可以增加这个充值接口方案设计题,真有人这么来搞就可以下一个了。

    总结:
    回归充值业务的设计,作为负责这块写代码的你可得打起十二分精神,特别是设计到钱的问题,头脑应该马上有一个安全意识,结合上面(1)(2)点的接口安全设计方案,我们也不说啥高大上的设计,该加锁的加,加事务,即使你的代码冗余,也要安全 > 性能!!!当然我不希望你做团队写代码最笨的人,要不断去优化自己的代码,去学习别人优秀的支付架构设计方案!(这个话也是对自己说的)
    此外充值一定要有对应的日志记录,出现问题也是辅助分析的地方,写到这里,我觉得自己也应该提升下这块业务的开发方法并总结出来,学习使人进步!

    1.2 使用第三方服务(接口)注意事项:

    (1)短信接口

    大家在公司做项目,如果涉及到短信业务,文件上传,这种场景,你是如何设计的?
    我先说下最原始的版本1:
    XX系统对外有一个手机注册页面,正常用户注册需要请求接口发送手机验证码,假如这个发送验证码接口张三是这么设计的: /api/getVerificationCodeByPhone?phone=1234567890,
    张三也知道,对手机号码做了格式校验,就没其他过多的设计了,看到这里你就可能就笑了,突然有一天,客户反馈充值充不上了,老板赶紧打电话让正在度假的张三看看怎么回事,张三看见服务器的短信接口报错余额不足了, 结果一看日志,一大堆获取验证码的接口日志,一想,肯定是别人盗刷接口了。
    那对于短信接口设计:

    • 参照上述 1.1 接口安全设计
    • 增加发送验证码
    • 不仅验证手机号码格式,而且验证数据库里面有这个手机号才会发送

    短信接口方案只是一个例子,其他类似的需要充值才能使用的接口,比如
    谷歌翻译接口
    百度地图API
    邮箱发送验证接口
    等等业务中会用到的第三方接口也都应该做类似的处理!!!

    (2)对象存储

    这里说的对象存储指的是使用的 阿里云对象存储OSS 或者 华为云对象存储OBS 或者其他平台的对象存储,各平台使用方式都差不多,用过的小伙伴应该清楚,没用过的小伙伴可以理解为一个独立的文件服务器,和你部署的SpringBoot 服务器隔离开,文件的上传下载都走这个独立的文件服务器,不要一台服务器集中了所有服务(nginx,springboot,redis,mysql,文件存储…)
    为什么要这么设计呢?这里我推荐一篇好朋友写的博客: 大型网站架构演化发展历程
    目前我们项目的架构类似这样子(nginx 集群部署):
    在这里插入图片描述

    因为公司项目的业务会有大量的文件上传下载,所有服务器都上了云,对象存储这个使用也很简单,有对应的各种语言的SDK接入,我们可以通过API 上传和下载文件。
    这里直接说下使用中出现的问题:
    问题1:秘钥保存在客户端
    最开始有个项目,最高权限的秘钥保存在客户端上,这无疑是最危险的做法,而且客户端已经对外公开,有一定逆向能力的程序员通过反编译APK即可拿到这个秘钥,也等于拿到了我们文件服务器所有权限,
    可以通过秘钥遍历获取所有文件,或者新增删除之类的操作,非常危险,随后发现这个问题立即禁用这个key,重新讨论设计文件存储方案。
    问题2:桶的文件下载链接公开,被别人盗刷流量
    创建对象存储桶的时候有一个选项,桶策略:公开还是私有,以下图华为云为例
    在这里插入图片描述

    我们创建一个桶后如果是公开的,那么上传的文件下载链接可以直接访问,这里存在一个风险就是华为云的下载流量是按使用计费的,如果有对外公开的文件,恶意攻击者拿到公开文件下载链接就可以写个循环下载脚本,恶意消耗你的流量,账号上面的余额很快被吃完。
    围绕这个风险,我们说下做法:

    1. 秘钥的保存
    • 秘钥是应该保存在服务端的,并且要加密保存!
    • 绝对不能保存到客户端!
    1. 桶应该都私有的,对外上传下载都要鉴权和频率限制

    尽可能的不要有公开的文件下载链接,除非你钱多当我没说。
    桶私有的情况下,官网有相应的API,可以根据文件路径生成一个临时上传、下载url的方案给客户端
    请参考官方文档链接: https://support.huaweicloud.com/sdk-java-devg-obs/obs_21_0901.html
    基于此方案,在服务端设计一个接口返回临时url,也要结合1.1 接口安全设计,加上鉴权和频率,
    鉴权的对象及频率限制有很多方案:

    • 登录用户,限制每个用户一定时间的下载次数
    • IP, 限制某个ip的下载次数,更严格一点限制登录过后的ip才能下载及限制下载次数
    • 业务介质,同上
    • 等等

    总之,前面围绕的设计思路核心都是鉴权 和限制请求频率,要学会灵活运用。

    1.3开发框架安全注意事项:

    (1)版本问题

    后端开发工作中,我们常用技术有: SpringBoot、redis、mysql、shiro、nginx等,这里不一一列举,在文本开头有提到一个 shiro安全问题的例子,就是一个典型,得益与我们有优秀的安全工程师,他们可以分析你所开的应用、使用的框架及版本有什么问题,如果你们没有专门的安全部门,也可以自己网上看下相关框架版本漏洞,这里整理几个会经常公布互联网安全漏洞的网站:

    1. 国家信息安全漏洞共享平台: https://www.cnvd.org.cn/
    2. 阿里云或者华为云官网也会不定时公布一些漏洞:

    华为: https://www.huaweicloud.com/notice.html
    阿里云: https://help.aliyun.com/document_detail/60797.html

    1. 各个服务官网,像什么jdk、gitlab、redis、nginx 官网都会有自己的漏洞修复信息

    如果你现在使用的框架版本出现了对应的问题,就应该及时升级,同时升级注意各个组件的兼容性,避免出现了问题再来处理,造成了严重后果,就后悔莫及了!

    (2)项目配置文件信息保存

    这里主要说明一下比如springboot 的yml配置文件信息,像比如连接数据库,连接redis的账号密码建议应该加密保存。
    其次公司都应该有自己内网的git服务器吧,千万不要把公司项目上传到公网公开访问,否则你面临法律方面的危险!
    相关加密方案请参照: 基于spring boot框架的配置信息加密方法总结 ,建议使用其文档描述的4.2.1 命令行加密(推荐)方案。

    (3)秘钥的保存

    这里的秘钥指的是比如对象存储的秘钥,以及系统接口加解密的公钥之类的重要信息。
    目前我们项目接口有做参数加密,服务端基于内存保存秘钥,具体方案设计到项目细节这里不展开描述,大家如果好的方案保存一些秘钥可以分享一下!

    1.4 弱口令密码

    我相信大家都自己搭建过mysql redis等服务,往往我们自己搭建测试的时候,跟着demo一顿操作,密码123456的这种就用上了,然后你的服务器突然某天就中挖矿病毒了,或者mysql数据被清空,你有遇到吗?
    这种密码程序很容易暴力破解出来,中招服务器就完了,大多数出现问题的服务器都首先检查下你的密码问题,
    不知道该设置啥密码,就使用一些密码生成器之类的:https://1password.com/zh-cn/password-generator/
    自己用Java代码写一个最安全!下面整理一些需要注意的地方,提高你的密码强度:

    1. mysql 、redis 的密码
    2. 你的服务器登录密码
    3. 后台管理系统的密码

    2 服务器使用及安全防御方案

    (1)服务器端口添加白名单

    所有云服务重要端口都应该加上白名单,特别是22端口,只能是你所在办公IP才能访问!

    (2)DDOS攻击等外部流量攻击

    了解DDOS

    什么是ddos攻击,网上有很多资料,这里转载一篇,后端开发都应该了解一下,点击了解: DDOS攻击原理
    简述就是:大规模流量(几百G或者更高)涌入服务器,导致服务器处理不过来,只要攻击不停止,服务器就一直处于"无响应状态",部署的web服务也无法正常访问,直接影响项目!
    公司的项目只要是部署生产环境上线后,基本上都遭遇了ddos攻击(太难了)。

    高防服务器

    最简单、最有效的方法就是购买并部署高防服务器应对,缺点是需要加钱。这个时候不是去想着怎么优化代码了,需要从外部去解决,市面上有很多高防服务器厂家,这里不做广告推荐,有被攻击的小伙伴不知道怎么做可以找我沟通一下。
    高防服务器就等于服务器的一个盾牌,所有的请求先到高防服务器,由高防服务器解析是好是坏,再转发的我们的源站服务器来。
    使用高防后注意的问题:

    • 配置高防后要注意回源IP的问题,如果你的业务有跟IP有关,需要配置相应的参数才能拿到

    当然,配置了高防服务器不代表就100%无敌安全了,一山更比一山高,攻击流量大于你的防御流量你也防不住,需要不断去优化策略应对。

    其他攻击

    转载链接: 常见六大Web安全攻防解析

    • XSS (Cross-Site Scripting),跨站脚本攻击
    • CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击
    • SQL注入

    除了ddos攻击,日常的后台接口服务器也会有其他不安全因素,如果使用公司提供的一套基础框架模版(已经包含处理了上述问题风险)去开发,就不用担心这些问题的发生,毕竟我们最终还是得专注于业务开发呀!

    (3)使用域名,不要暴露你的源服务器IP

    结合第(2)点被ddos攻击的情况,原因是因为你的服务器ip暴露了,攻击者直接攻击源服务器,加上高防后,使用域名的方式,不要暴露你的源服务器IP。

    总结:项目正式上线前,应该提前考虑好被ddos攻击的风险及应对措施,提前准备好高防服务器部署,保证后续服务器的稳定运行。

    3 安全环境下进行开发

    (1)使用内网搭建服务

    这里主要针对于初创的中小公司,开发环境各方面都不太规范,比如使用 外网Gitlab,码云 或者GitHub来管理研发项目,大公司应该都有完善的安全的研发环境,像比如华为,阿里,腾讯的朋友,特别是华为的员工,听说基于内网开发,百度还得用手机这种情况大家有没有听说过呢?
    为什么要用内网搭建服务呢?前面也说了服务会有各种漏洞,会有被入侵风险,比如Gitlab官方就随时在更新,我们团队目前就是内网搭建的Gitlab,代码是整个研发团队、公司最重要的资产;

    • 首先项目代码在公网放着本身就是非常不安全的!
    • 如果因为公网原因或者服务漏洞,发生代码泄露,这个无论是对个人和公司,都是灾难级的损失!

    内网环境下一定程度上减少了被泄露的风险,但是管理者也要规范团队每个开发者的日常行为规范(人为因素)。
    下面列出内网搭建的服务及使用地方:

    内网搭建使用范围
    Gitlab管理项目代码仓库
    禅道项目管理

    (2)使用非windows操作系统进行开发

    由于Windows操作系统其普及性和软件适配性(相比Linux),大多数中小公司研发团队可能都会基于Windows进行开发,但是Windows太"成熟"了,开发成本低,一不小心点错、下错东西,打开就是各种病毒、木马、广告,电脑中毒不说,同时又间接影响内网环境。
    所以目前我们团队使用的操作系统是Ubuntu20.04,一样的有图形化界面,实践1年多了,前后端开发都不会受影响,和Windows一样,强烈建议大家还在用Windows系统进行开发的小伙伴换成 ubuntu或者mac,当然mac需要苹果笔记本,成本比较高。

    PS: 写完后发现可以衍生出的系列文档,后续我将持续更新…

    1. 一个安全的登录接口设计方案(待开始)
    2. 企业内部鉴权中心模块开发实例(待开始)
    3. 对象存储模块开发实例(待开始)
    展开全文
  • log4j安全漏洞fix--快速答疑处理篇。

    千次阅读 2021-12-16 10:41:40
    log4j安全漏洞答疑、快速处理办法。

    高危漏洞 log4j漏洞产生原因这里就不赘述了,这里只聊解决办法:替换jar的方式。

    发现身边的开发同学在替换jar的时候遇到如下问题:
    0、一定要替换吗?
    1、不确定要替换那些包
    2、替换后旧版jar仍存在
    1、2问题答案如下:
    1、简单粗暴替换<groupId>org.apache.logging.log4j</groupId>组下面的所有包
    2、因为maven依赖传的导致新引入的报又依赖了旧版本log4j包。
    

    首先我们聊下问题0,一定要替换吗?

    不一定需要替换,虽然项目中引用了jar,如:

    1、没有开启JNDI(在spring.properties里添加spring.jndi.ignore=true 可以禁用)
    2、或者禁止应用对外访问互联网。
    3、移除了JndiLookup类文件,(操作命令: zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class)
    

    以上都是无害的。但是堵不如疏,没准啥时候就出幺蛾子了,有精力最好还是升级下jar包,花不了多少时间。

    idea下jar升级操作方法:

    1、直接查看当前项目jar包(见下图左侧)版本等比较直观。
    2、进入pom文件,查看jar包依赖视图(比较方便看依赖关系和操作)
    在这里插入图片描述
    jar包依赖视图进入方式:右键选择下图红框处。
    在这里插入图片描述

    查看log4j系列版本后,如发现不符合安全漏洞要求的jar包,
    进入jar包依赖视图选定该包,然后右键exclude(从当前父依赖下屏蔽此jar包,会自动更新pom文件),
    然后重新引入(直接在pom文件下添加对应的最新版本jar包)。
    

    在这里插入图片描述
    注意:
    log4j 下的jar包之间也会存在依赖关系,需要再次屏蔽,解决屏蔽后仍存在的问题,如下:
    在这里插入图片描述
    依次对所有包进行如上操作,直到替换所有旧版本log4j系列jar包。

    展开全文
  • 1、大数据机遇和网络安全挑战 大数据是近年来非常热的一个话题,目前IT科学问题基本三年为一个周期,但是大数据据预计会有6~9年的话题周期,因为云计算话题从成熟到应用,已经走过约八九年的历程。 大数据分析...
  • 该篇是2021年发表在《Future Generation Computer Systems》期刊上的一篇综述论文,主要介绍了联邦学习中可能面对的所有安全与隐私威胁问题,比较详细和具体的进行了综述,是篇还不错的文章,建议读原文。...
  • 安全测试面试常见问题有哪些?

    千次阅读 2021-11-19 14:39:44
    1. 什么是安全测试 在所有类型的软件测试中,安全测试可以被认为是最重要的。其主要目的是在任何软件(Web或基于网络)的应用程序中找到漏洞,并保护其数据免受可能的攻击或入侵者。由于许多应用程序包含机密数据,...
  • Python协程安全问题:Context Variables

    千次阅读 2021-12-17 19:28:20
    Python协程安全问题:Context Variables 一、问题: 最近,同事压测遇到奇诡问题,添加好友功能在少量并发压测时没有问题;但是,增加并发压力后,出现增加好友失败。和开发等位问题时,发现是不同用户重复发送同一...
  • 服务器是IT基础设施的关键,但是网络攻击每天都在发生。IT Governance报告显示,仅在2020年11月就有586,771,602条泄露记录。...而当攻击者绕过安全防线发起攻击时,往往都有行为、进程的足迹可以溯源,有.
  • 大家好,我是前端岚枫,今天主要跟大家分享我整理的笔记2021前端面试题系列:fetch与axios、浏览器内标签页之间的通讯方法、XSS 和CSRF以及如何防范,此方面内容在我们的工作中常用到, 也是面试官经常提问的问题,...
  • 为了保障数据安全,促进数据开发利用,保护公民、组织的合法权益,维护国家主权、安全和发展利益,制定本法。 【解读】 本条规定了《数据安全法(草案)》的立法目的。 第二条 在中华人民共和国境内开展数据活动...
  • 信息安全发展的三个阶段:通信保密,信息安全,信息保障 Wind River的安全专家则针对IoT设备安全提出了如下建议: 安全启动 设备首次开机时,理应采用数字证书对运行的系统和软件作认证; 访问控制 采用不同...
  • 服务正常,阿里安全组的端口号也正常开放,那么就是可能存在1的情况了,我怕写错,然后将所有的IP和端口都进行了复制处理,再在本地重新启动。发现错误依旧存在,还是一样的连接不上。 不应该啊?! 我再用beyond...
  • 达梦数据库常见问题处理

    千次阅读 2021-08-30 15:34:06
    用户登录disql报错[-70070]初始化SSL环境失败 先查询数据库版本,如图,可见其为安全版 这是因为安全版数据库中默认开启了SSL加密。 可将ENABLE_ENCRYPT参数的值设置为0,数据库执行以下语句 sp_set_para_value(2,...
  • 提高微服务安全性的11个方法

    千次阅读 多人点赞 2020-12-21 08:41:47
    1.通过设计确保安全 OWASP 2.扫描依赖 3.随处使用HTTPS 安全的GraphQL API 安全的RSocket端点 4.使用身份令牌 授权服务器:多对一还是一对一? 在JWT上使用PASETO令牌 5.加密和保护密钥 6.通过交付流水线...
  • 非常感谢举办方让我们学到了新知识,DataCon也是我比较喜欢和推荐的大数据安全比赛,这篇文章2020年10月就进了我的草稿箱,但由于小珞珞刚出生,所以今天才发表,希望对您有所帮助!感恩同行,不负青春。
  • 网络安全漏洞深度剖析

    千次阅读 2021-10-16 16:23:05
    一、漏洞背景 2021年7月13日,美国微软威胁情报中心发布安全公告[1],文中指出黑客利用Serv-U 0day对极少数美国军工部门成功进行了攻击,同日Serv-U母公司solarwinds也发布安全公告[2],...截至9月10日,互联网上未发现
  • flutter空安全适配

    千次阅读 2021-06-24 14:06:33
    一、空安全介绍 1.什么是空安全 从Flutter 2开始,Flutter便在配置中默认启用了空安全,通过将空检查合并到类型系统中,可以在开发过程中捕获这些错误,从而防止再生产环境导致的崩溃。 时至今日,空安全已经是一...
  • SimpleDateFormat类不是线程安全的根本原因和解决方案,冰河吐血整理,建议收藏!!
  • 网络安全--安全攻防概述

    千次阅读 2021-08-18 18:27:21
    课程目标:这节课我们来介绍安全攻防以及信息安全的职业发展历程,去了解什么是安全攻防以及安全攻防的基本概念,攻击和防御的由来。 任务目标:通过对这节课的学习,能够对安全攻防有进一步的了解和认识,掌握安全...
  • 安全测试 web常规安全漏洞问题介绍和防范说明 SQL: SQL注入攻击 XSS: 跨站点脚本攻击 CSC: 注释与异常信息泄露 CSRF: 跨站点请求伪造 FB: 路径遍历与强制浏览 BAC: 越权访问 web应用防火墙(Web Application ...
  • 数据安全分类分级剖析

    千次阅读 2021-09-15 00:04:46
    数据分类分级对于数据的安全管理至关重要,安全分类分级是一个“硬核课题”,从数据治理开始,除了标准化和价值应用,重要的课题就是质量+安全安全是底线,是价值应用的前提和基础。数据分类可以为数据资产结构化...
  • 关于BGP安全那些事儿

    千次阅读 2021-10-20 01:20:57
    图5(截图来自外部) 这里也总结罗列了下BGP安全问题可能造成的影响: 1) BGP路由劫持 a. 流量被“黑洞”,正常访问中断 b. 流量被中间人监听或攻击,或重定向到虚假网站以窃取数据 c. IP被用于进行垃圾邮件活动 d...
  • 安全体系建设-基础安全

    千次阅读 2021-11-30 21:55:08
    安全体系建设安全层级建设步骤组建安全团队工作内容**生产:** 安全层级 安全体系的建设需要进行分级,粗略的分为以下三个层级。 基础安全 第一阶段大部分公司关注的层级,充当应急的角色,在不断的应急中了解公司...
  • 这里写自定义目录标题写在前面判断题正确单选题错误单选题...F 项目TD是项目网络安全管理的第一责任人,项目组在任命时要明确网络安全管理职责,负责网络安全管理措施在本项目组的执行,组织网络安全现场培训。 F 接
  • 算法概念教案设计本案例对应的课标中选修部分的算法与程序设计模块中的计算机解决问题的基本过程中(1)条:结合实例,经历分析问题、确立算法、编程求解等用计算机解决问题的基本过程,认识算法和程序设计在其中的...
  • 安全帽佩戴检测数据集训练YOLOv5--数据集处理参考链接数据集处理数据集 参考链接 SafetyHelmetWearing-Dataset(安全帽佩戴检测数据集) Train Custom Data(YOLOv5 训练自定义数据集) yolov5汉化版 数据集 ...
  • 手头的项目的登录模块,基本都是集成了部门内封装出的基于CAS的中心鉴权组件,在安全扫描中暴露了一些问题,有些是因为没有合理的使用这一开源框架导致的,有的是通用的问题,在此记录问题和解决方案。 1、密码明文...
  • • 物联网感知层信息安全问题是物联网安全的核心内容 • 物联网感知层面临的安全威胁主要表现为 感知层中节点自身故障(如节点被捕获、被控制、功能失效或服务中断、 身份伪造等) 节点间的链接关系不正常(如...
  • Spring Cloud中如何保证各个微服务之间调用的安全性?

    千次阅读 多人点赞 2021-08-19 15:39:58
    导读:在微服务的架构下...阅读下文之前思考几个问题: 如何在restTemplate远程调用请求增加添加统一认证? 服务认证如何规范加密和解密? 远程调用统一什么协议比较合适? 如下图,三个服务注册到同一个注册中心.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 601,422
精华内容 240,568
关键字:

发现安全问题如何处理