异常篇——VEH与SEH

博客 分享
0 155
张三
张三 2022-02-28 16:55:54
悬赏:0 积分 收藏

异常篇—— VEH 与 SEH

异常篇之 VEH 与 SEH ,介绍 VEH 与 SEH 的基础知识和相关细节。

写在前面

??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

??看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。

?


?? 华丽的分割线 ??


?

概述

??当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接进行处理 ,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了。这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行,这个函数就是我们重点关注对象,我们先看一下它的流程:

  1. 调用RtlDispatchException,查找并执行异常处理函数。
  2. 如果RtlDispatchException返回真,调用ZwContinue再次进入0环,但线程再次返回3环时,会从修正后的位置开始执行。
  3. 如果RtlDispatchException返回假,调用ZwRaiseException进行第二轮异常分发。

??看完上面的流程之后,我们看看其反汇编:

; void __stdcall __noreturn KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextFrame)                public _KiUserExceptionDispatcher@8_KiUserExceptionDispatcher@8 proc near  ; DATA XREF: .text:off_7C923428↑ovar_C           = dword ptr -0Chvar_8           = dword ptr -8var_4           = dword ptr -4ExceptionRecord = dword ptr  4ContextFrame    = dword ptr  8                mov     ecx, [esp+ExceptionRecord]                mov     ebx, [esp]                push    ecx             ; ContextRecord                push    ebx             ; ExceptionRecord                call    _RtlDispatchException@8 ; RtlDispatchException(x,x)                or      al, al                jz      short loc_7C92E47A                pop     ebx                pop     ecx                push    0                push    ecx                call    _ZwContinue@8   ; ZwContinue(x,x)                jmp     short loc_7C92E485; ---------------------------------------------------------------------------loc_7C92E47A:                           ; CODE XREF: KiUserExceptionDispatcher(x,x)+10↑j                pop     ebx                pop     ecx                push    0               ; FirstChance                push    ecx             ; ContextRecord                push    ebx             ; ExceptionRecord                call    _ZwRaiseException@12 ; ZwRaiseException(x,x,x)loc_7C92E485:                           ; CODE XREF: KiUserExceptionDispatcher(x,x)+1C↑j                add     esp, -14h                mov     [esp+EXCEPTION_RECORD.ExceptionCode], eax                mov     [esp+EXCEPTION_RECORD.ExceptionFlags], 1                mov     [esp+EXCEPTION_RECORD.ExceptionRecord], ebx                mov     [esp+EXCEPTION_RECORD.NumberParameters], 0                push    esp             ; ExceptionRecord                call    _RtlRaiseException@4 ; RtlRaiseException(x)_KiUserExceptionDispatcher@8 endp ; sp-analysis failed

??可以看出该函数会调用RtlDispatchException,为了节省篇幅用伪代码如下:

BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord){  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]  result = 0;  if ( RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord) )    return 1;  RtlpGetStackLimits(&LowLimit, &HighLimit);  ExceptionRecorda = 0;  exRecord = RtlpGetRegistrationHead();         // ExceptionList  if ( exRecord != -1 )  {    while ( 1 )    {      if ( exRecord < LowLimit        || &exRecord[1] > HighLimit        || (exRecord & 3) != 0        || (handler = exRecord->Handler, handler >= LowLimit) && handler < HighLimit        || !RtlIsValidHandler(exRecord->Handler) )      {        ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID;        return result;      }      if ( byte_7C99B3FA < 0 )        v11 = RtlpLogExceptionHandler(ExceptionRecord, ContextRecord, 0, exRecord, 0x10u);      RtlpExecuteHandlerForException(ExceptionRecord, exRecord, ContextRecord, &a4, exRecord->Handler);      v6 = v5;      if ( byte_7C99B3FA < 0 )        RtlpLogLastExceptionDisposition(v11, v5);      if ( ExceptionRecorda == exRecord )      {        ExceptionRecord->ExceptionFlags &= 0xFFFFFFEF;        ExceptionRecorda = 0;      }      if ( !v6 )        break;      if ( v6 == 1 )      {        if ( (ExceptionRecord->ExceptionFlags & 8) != 0 )          return result;      }      else      {        if ( v6 != 2 )        {          e.ExceptionCode = EXCEPTION_INVALID_DISPOSITION;          e.ExceptionFlags = 1;          e.ExceptionRecord = ExceptionRecord;          e.NumberParameters = 0;          RtlRaiseException(&e);        }        v8 = a4;        ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;        if ( v8 > ExceptionRecorda )          ExceptionRecorda = v8;      }      exRecord = exRecord->Next;      if ( exRecord == -1 )        return result;    }    if ( (ExceptionRecord->ExceptionFlags & 1) != 0 )    {      e.ExceptionCode = EXCEPTION_NONCONTINUABLE_EXCEPTION;      e.ExceptionFlags = EXCEPTION_NONCONTINUABLE;      e.ExceptionRecord = ExceptionRecord;      e.NumberParameters = 0;      RtlRaiseException(&e);    }    result = 1;  }  return result;}

??RtlCallVectoredExceptionHandlers这个函数就是用来执行VEH的。如果返回假,则说明没有,后面的RtlpGetRegistrationHead就会获取SEH,如果有就执行,它是在堆栈中的。
??有了这些铺垫后,我们来介绍VEHSEH

VEH

??对于VEH,这个是XP及其之后才有的,中文为向量化异常结构处理。我们先看看它的处理流程:

  1. CPU捕获异常信息;
  2. 通过KiDispatchException进行分发;
  3. KiUserExceptionDispatcher调用RtlDispatchException
  4. RtlDispatchException查找VEH处理函数链表 并调用相关处理函数;
  5. 代码返回到KiUserExceptionDispatcher
  6. 调用ZwContinue再次进入0环(ZwContinue调用NtContinue,主要作用就是恢复_TRAP_FRAME然后通过KiServiceExit返回到3环);
  7. 线程再次返回3环后,从修正后的位置开始执行;

??如下是执行VEH的伪代码:

BOOLEAN __stdcall RtlCallVectoredExceptionHandlers(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord){  PRTL_VECTORED_HANDLER_ENTRY p; // esi  int (__stdcall *VectoredHandler)(EXCEPTION_POINTERS *); // eax  EXCEPTION_POINTERS ExceptionInfo; // [esp+4h] [ebp-8h] BYREF  BOOLEAN v6; // [esp+17h] [ebp+Bh]  if ( IsListEmpty(&RtlpCalloutEntryList) )    return 0;  ExceptionInfo.ExceptionRecord = ExceptionRecord;  ExceptionInfo.ContextRecord = ContextRecord;  RtlEnterCriticalSection(&RtlpCalloutEntryLock);  for ( p = RtlpCalloutEntryList.Flink; ; p = p->ListEntry.Flink )  {    if ( p == &RtlpCalloutEntryList )    {      v6 = 0;      goto EndProc;    }    VectoredHandler = RtlDecodePointer(p->VectoredHandler);    if ( VectoredHandler(&ExceptionInfo) == -1 )      break;  }  v6 = 1;EndProc:  RtlLeaveCriticalSection(&RtlpCalloutEntryLock);  return v6;}

??剩余的细节将会在总结与提升进行讲解,下面我们来看看如何使用VEH,如下是实验代码:

#include "stdafx.h"#include <windows.h>#include <stdlib.h>typedef PVOID (NTAPI *VectoredExceptionHandler)(ULONG,_EXCEPTION_POINTERS*);LONG NTAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo){    puts("进入异常处理函数……");    if (pExceptionInfo->ExceptionRecord->ExceptionCode==0xC0000094)    {        puts("异常函数处理了……");        pExceptionInfo->ContextRecord->Ecx = 1;        return EXCEPTION_CONTINUE_EXECUTION;    }    return EXCEPTION_CONTINUE_SEARCH;}int main(int argc, char* argv[]){    HMODULE lib = LoadLibrary("kernel32.dll");    VectoredExceptionHandler AddVectoredExceptionHandler = (VectoredExceptionHandler)GetProcAddress(lib,"AddVectoredExceptionHandler");    AddVectoredExceptionHandler(1,(_EXCEPTION_POINTERS*)&MyVectoredExceptionHandler);            _asm    {        xor edx,edx;        xor ecx,ecx;        mov eax,0x10;        idiv ecx;    }    puts("继续执行……");    system("pause");    return 0;}

??执行后会正常执行,并显示异常处理信息。

SEH

??SEH意为结构化异常处理,它的结构如下图所示:

??也就是说包装的异常处理项目是以单向链表的形式管理的。必须具有两个如上图所示的成员,也就是说,这个结构是可以扩展的,有关扩展的将会在后续介绍,下面我们来看实验代码:

#include "stdafx.h"#include <windows.h>#include <stdlib.h>struct MyException {    MyException* prev;    DWORD handle;};EXCEPTION_DISPOSITION MyExceptionHandler(_EXCEPTION_RECORD* ExceptionRecord,void* Establisherframe,CONTEXT* context,void* DispatcherContext){    puts("进入异常处理……");    if (ExceptionRecord->ExceptionCode==0xC0000094)    {        puts("开始处理异常……");        context->Eip+=2;        return ExceptionContinueExecution;    }    return ExceptionContinueSearch;}int main(int argc, char* argv[]){    DWORD tmp;    //初始化异常结构    MyException ex={(MyException*)tmp,(DWORD)MyExceptionHandler};    //加入 SEH    _asm    {        mov eax,fs:[0];        mov tmp,eax;        lea ecx,ex;        mov fs:[0],ecx;    }    //制造异常        _asm    {        xor edx,edx;        xor ecx,ecx;        mov eax,0x10;        idiv ecx;    }            //撤掉 SEH    _asm    {        mov eax,tmp;        mov fs:[0],eax;    }            puts("正常运行……");    system("pause");    return 0;}

??该程序正常执行,并打印异常处理结果。

编译器扩展 SEH

初识

??前面我们用自己的方式实现了SEH的使用。异常处理很重要,但是,这个对于开发者很不友好。每次都要构造SEH,退出函数要撤掉。编译器提供了关键字,并对SEH进行了扩充,使用如下图所示:

_try    // 挂入 SEH 链表{       }_except(/*过滤表达式*/) //异常过滤{  //异常处理程序}  

??对于过滤表达式的结果值,只能是-101,它们表示的含义如下:

  1. EXCEPTION_EXECUTE_HANDLER (1) 执行except里面的代码
  2. EXCEPTION_CONTINUE_SEARCH (0) 寻找下一个异常处理函数
  3. EXCEPTION_CONTINUE_EXECUTION (-1) 返回出错位置重新执行

??我说只能是这三值,并没有说只能写这三个数字,你可以写入表达式或者函数,使其得到的结果或者返回值是这仨值其中之一就可以,如下是我们的实验程序:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){    _try    {        _asm        {            xor edx,edx;            xor ecx,ecx;            mov eax,0x10;            idiv ecx;        }        puts("继续跑……");    }_except(1)    {        puts("异常处理……");    }    system("pause");    return 0;}

??运行该程序,只打印了except里面的,得到正确结果。

初步深入

??我们接下来在汇编层面查看它是如何实现的,首先我们查看一下编译器为我们扩展的结构,否则看代码是看不懂的。

struct _EXCEPTION_REGISTRATION{  struct _EXCEPTION_REGISTRATION *prev;  void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);  struct scopetable_entry *scopetable;  int trylevel;  int _ebp;};       

??然后我们所谓的结构就成立这样子:

??图中的_except_handler3是啥我们看它的反汇编是什么就知道了:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){00401010   push        ebp00401011   mov         ebp,esp00401013   push        0FFh00401015   push        offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed"+0Ch (00424030)0040101A   push        offset __except_handler3 (00401400)0040101F   mov         eax,fs:[00000000]00401025   push        eax00401026   mov         dword ptr fs:[0],esp0040102D   add         esp,0B8h00401030   push        ebx00401031   push        esi00401032   push        edi00401033   mov         dword ptr [ebp-18h],esp00401036   lea         edi,[ebp-58h]00401039   mov         ecx,10h0040103E   mov         eax,0CCCCCCCCh00401043   rep stos    dword ptr [edi]    _try00401045   mov         dword ptr [ebp-4],0    {        _asm        {            xor edx,edx;0040104C   xor         edx,edx            xor ecx,ecx;0040104E   xor         ecx,ecx            mov eax,0x10;00401050   mov         eax,10h            idiv ecx;00401055   idiv        eax,ecx        }        puts("继续跑……");00401057   push        offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed" (00424024)0040105C   call        puts (004011e0)00401061   add         esp,4    }_except(1)00401064   mov         dword ptr [ebp-4],0FFFFFFFFh0040106B   jmp         $L865+17h (0040108a)$L864:0040106D   mov         eax,1$L866:00401072   ret$L865:00401073   mov         esp,dword ptr [ebp-18h]    {        puts("异常处理……");00401076   push        offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00425140)0040107B   call        puts (004011e0)00401080   add         esp,4    }00401083   mov         dword ptr [ebp-4],0FFFFFFFFh    system("pause");0040108A   push        offset string "pause" (0042401c)0040108F   call        system (004010d0)00401094   add         esp,4    return 0;00401097   xor         eax,eax}00401099   mov         ecx,dword ptr [ebp-10h]0040109C   mov         dword ptr fs:[0],ecx004010A3   pop         edi004010A4   pop         esi004010A5   pop         ebx004010A6   add         esp,58h004010A9   cmp         ebp,esp004010AB   call        __chkesp (004012d0)004010B0   mov         esp,ebp004010B2   pop         ebp004010B3   ret

??看不懂吗?我们来画个堆栈图,如下所示:

??标注*的表示原来的值,是不是和结构体的成员对应起来了?注意不要以为只有黄色的区域,由于通常的函数采用ebp寻址,所以我没有把ebp*打上黄色底色。
??下面我们来看看scopetable成员,它的结构如下:

struct scopetable_entry{  DWORD previousTryLevel; //上一个try{}结构编号   PDWRD lpfnFilter; //过滤函数的起始地址  PDWRD lpfnHandler;  //异常处理程序的地址     }

??我们来看看这个结构的内容是啥,最终它的成员如下:

scopetable.previousTryLevel = -1;scopetable.lpfnFilter = 0x40106D;scopetable.lpfnHandler = 0x401073;

??正好把代码指令和地址逐个对应起来了。

继续深入

??如果异常处理有嵌套调用的情况会是怎么样呢?如下是测试代码:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){  _try  {    _try    {      _asm      {        xor edx,edx;        xor ecx,ecx;        mov eax,0x10;        idiv ecx;      }     }_except(1)    {      puts("测试");    }     puts("继续跑……");  }_except(1)  {    puts("异常处理……");  }  system("pause");  return 0;}

??然后查看反汇编结果:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){00401010   push        ebp00401011   mov         ebp,esp00401013   push        0FFh00401015   push        offset string "\xb2\xe2\xca\xd4"+0Ch (00424050)0040101A   push        offset __except_handler3 (00401450)0040101F   mov         eax,fs:[00000000]00401025   push        eax00401026   mov         dword ptr fs:[0],esp0040102D   add         esp,0B8h00401030   push        ebx00401031   push        esi00401032   push        edi00401033   mov         dword ptr [ebp-18h],esp00401036   lea         edi,[ebp-58h]00401039   mov         ecx,10h0040103E   mov         eax,0CCCCCCCCh00401043   rep stos    dword ptr [edi]    _try00401045   mov         dword ptr [ebp-4],0    {        _try0040104C   mov         dword ptr [ebp-4],1        {            _asm            {                xor edx,edx;00401053   xor         edx,edx                xor ecx,ecx;00401055   xor         ecx,ecx                mov eax,0x10;00401057   mov         eax,10h                idiv ecx;0040105C   idiv        eax,ecx            }        }_except(1)0040105E   mov         dword ptr [ebp-4],000401065   jmp         $L872+17h (0040f5d4)$L871:00401067   mov         eax,1$L873:0040106C   ret$L872:0040106D   mov         esp,dword ptr [ebp-18h]        {            puts("测试");00401070   push        offset string "\xb2\xe2\xca\xd4" (00424044)00401075   call        puts (00401230)0040107A   add         esp,4        }0040107D   mov         dword ptr [ebp-4],0    puts("继续跑……");00401084   push        offset string "\xbc\xcc\xd0\xf8\xc5\xdc\xa1\xad\xa1\xad" (00424034)00401089   call        puts (00401230)0040108E   add         esp,4    }_except(1)00401091   mov         dword ptr [ebp-4],0FFFFFFFFh00401098   jmp         $L868+17h (004010b7)$L867:0040109A   mov         eax,1$L869:0040109F   ret$L868:004010A0   mov         esp,dword ptr [ebp-18h]    {        puts("异常处理……");004010A3   push        offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024)004010A8   call        puts (00401230)004010AD   add         esp,4    }004010B0   mov         dword ptr [ebp-4],0FFFFFFFFh    system("pause");004010B7   push        offset string "pause" (0042401c)004010BC   call        system (00401120)004010C1   add         esp,4    return 0;004010C4   xor         eax,eax}004010C6   mov         ecx,dword ptr [ebp-10h]004010C9   mov         dword ptr fs:[0],ecx004010D0   pop         edi004010D1   pop         esi004010D2   pop         ebx004010D3   add         esp,58h004010D6   cmp         ebp,esp004010D8   call        __chkesp (00401320)004010DD   mov         esp,ebp004010DF   pop         ebp004010E0   ret

??看代码发现还是只是挂了一次,我们得看看scopetable的内容是啥了:

00425168  FFFFFFFF  0040109A  004010A0  00425174  00000000  00401067  0040106D  00425180  00000000  00000000  00000000  0042518C  00000000  00000000  00000000

??可以看到,这里有两个成员了。

finally 关键字

??当然不仅仅有try_except,还可以使用finally,该关键字的作用就是只要退出try就执行里面的函数,无论通过那种方式,如下是我们的实验代码:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){  _try  {    return 0;  }__finally  {    puts("异常处理……");    system("pause");  }  return 0;}

??执行结果如下:

异常处理……请按任意键继续. . .

??然后我们看看它在汇编层面是如何实现的,其反汇编如下:

#include "stdafx.h"#include <stdlib.h>int main(int argc, char* argv[]){00401010   push        ebp00401011   mov         ebp,esp00401013   push        0FFh00401015   push        offset string "stream != NULL"+10h (00425168)0040101A   push        offset __except_handler3 (00401450)0040101F   mov         eax,fs:[00000000]00401025   push        eax00401026   mov         dword ptr fs:[0],esp0040102D   add         esp,0B4h00401030   push        ebx00401031   push        esi00401032   push        edi00401033   lea         edi,[ebp-5Ch]00401036   mov         ecx,11h0040103B   mov         eax,0CCCCCCCCh00401040   rep stos    dword ptr [edi]    _try00401042   mov         dword ptr [ebp-4],000401049   push        0FFh0040104B   mov         dword ptr [ebp-1Ch],0    {00401052   lea         eax,[ebp-10h]00401055   push        eax00401056   call        __local_unwind2 (0040139a)0040105B   add         esp,8        return 0;0040105E   mov         eax,dword ptr [ebp-1Ch]00401061   jmp         $L865+2 (00401080)    }__finally    {        puts("异常处理……");00401063   push        offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024)00401068   call        puts (00401230)0040106D   add         esp,4        system("pause");00401070   push        offset string "pause" (0042401c)00401075   call        system (00401120)0040107A   add         esp,4$L863:0040107D   ret    }17:       return 0;0040107E   xor         eax,eax}00401080   mov         ecx,dword ptr [ebp-10h]00401083   mov         dword ptr fs:[0],ecx0040108A   pop         edi0040108B   pop         esi0040108C   pop         ebx0040108D   add         esp,5Ch00401090   cmp         ebp,esp00401092   call        __chkesp (00401320)00401097   mov         esp,ebp00401099   pop         ebp0040109A   ret

??可以看到在调用return 0;之前,被插入了调用__local_unwind2函数,正是这个函数能够调用finally里面的代码的:

__local_unwind2:0040139A   push        ebx0040139B   push        esi0040139C   push        edi0040139D   mov         eax,dword ptr [esp+10h]004013A1   push        eax004013A2   push        0FEh004013A4   push        offset __global_unwind2+20h (00401378)004013A9   push        dword ptr fs:[0]004013B0   mov         dword ptr fs:[0],esp004013B7   mov         eax,dword ptr [esp+20h]004013BB   mov         ebx,dword ptr [eax+8]004013BE   mov         esi,dword ptr [eax+0Ch]004013C1   cmp         esi,0FFh004013C4   je          __NLG_Return2+2 (004013f4)004013C6   cmp         esi,dword ptr [esp+24h]004013CA   je          __NLG_Return2+2 (004013f4)004013CC   lea         esi,[esi+esi*2]004013CF   mov         ecx,dword ptr [ebx+esi*4]004013D2   mov         dword ptr [esp+8],ecx004013D6   mov         dword ptr [eax+0Ch],ecx004013D9   cmp         dword ptr [ebx+esi*4+4],0004013DE   jne         __NLG_Return2 (004013f2)004013E0   push        101h004013E5   mov         eax,dword ptr [ebx+esi*4+8]004013E9   call        __NLG_Notify (0040142e)004013EE   call        dword ptr [ebx+esi*4+8]__NLG_Return2:004013F2   jmp         __local_unwind2+1Dh (004013b7)004013F4   pop         dword ptr fs:[0]004013FB   add         esp,0Ch004013FE   pop         edi004013FF   pop         esi00401400   pop         ebx00401401   ret

??关键调用在call dword ptr [ebx+esi*4+8],执行这个就会调用finally里的代码。具体详细的其他细节将会在总结与提升进行介绍。

下一篇

??异常篇——总结与提升

知识共享许可协议
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15946358.html
posted @ 2022-02-28 16:50 寂静的羽夏 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    张三

    张三 (王者 段位)

    821 积分 (2)粉丝 (41)源码

     

    温馨提示

    亦奇源码

    最新会员