• 反调试与破反调试笔记


     静态反调试

    反调试技术知识点

    TEB 线程环境块

    TEB 是个结构体

    TEB结构中的两个重要成员

    +0x000             NtTib :_NT_TIB . . .

    +0x30                ProcessEnvironmentBlock :Ptr32_PEB

    TEB 的Offet30移位处就是PEB的结构体指针

    PEB 进程环境块,每个进程都对应一个PEB结构体

    NtTib   线程信息块

    结构体信息如下:

    typedef struct _NT_TIB{

              struct     _EXCEPTION_REGISTRATION_RECORD *ExceptionList;

              PVOID StackBase;

              PVOID StackLimit;

              PVOID SubSystemTib;

              union {

                      PVOID FiberData;

                      DWORD Version;

              };

              PVOID ArbitraryUserPointer;

             struct     _NT_TIB *Self;

    } NT_TIB;

    typedef NT_TIB *PNT_TIB;

    ExceptionList成员指向_EXCEPTION_REGISTRATION_RECORD结构体组成的链表,它用于WindowsOS的SEH, SEH是Wiondows操作系统中的结构化异常处理机制,常用于反调试技术。

    Self成员是_NT_TIB结构体的自引用指针,它指向_NT_TIB结构体,又因为_NT_TIB是TEB结构体的第一个成员,所以它也是指向TEB结构体的指针(它里面存着TEB结构体的地址)。

    使用Ntdll.NtCurrentTeb()API访问TEB结构体,它返回当前线程的的TEB结构体的地址,通过OD看它的实现方法就是返回FS:[0x18]处的地址值(FS段寄存器用来指示当前线程的TEB结构体),而FS:0x18处正是Self指针,其含有的也正是TEB结构体的地址。

    用一个公式总结:

    FS:[0x18] = TEB.NtTib.Self = address of TIB = address of TEB = FS:0

    所以:

    FS:[0x18] = TEB起始地址

    FS:[0x30] = PEB的起始地址

    FS:[0] = TEB.NtTib.ExceptionList = address of SEH

    就算不理解反正记住就行了。

    PEB 进程环境块

    PEB中重要的几个成员

    +002         BeingDebugged               ;Uchar(可用于反调试技术)

    ...

    +008         ImageBaseAddress           ;Ptr32 Void

    ...

    +00c          Ldr                    ;Ptr32 _PEB_LDR_DATA(可用于反调试技术)

    ...

    +018         ProcessHeap  ;Ptr32 Void(可用于反调试技术)

    ...

    +068         NtGlobalFlag   ;uint4B(可用于反调试技术)

    PEB.BeingDebugged

    BeingDebugged的作用就是标识当前进程是否处于调试状态,Kernel32.dll中的IsDebuggerPresent() API就是用来获取该处的值的(是,则返回1;否,则返回0)

    IsDebuggerPresent() API的汇编代码形式

    MOV EAX,DWORD PTR FS:[18]

    MOV EAX,DWORD PTR DS:[EAX+30]

    MOVZX EAX,BYTE PTR DS:[EAX+2]

    RETN

    该值在代码逆向分析领域主要用于反调试技术。检测该值,若进程处于调试中,则终止进程。

    破解之法
    只要借助OllyDbg调试器的编辑功能,将PEB.BeingDebugged的值修改为0(FALSE)即可。

    PEB.ImageBaseAddress成员用来表示进程的ImageBase
    GetModuleHandle()API用来获取ImageBase。

    将lpModuleName参数赋值为NULL,调用GetModuleHandle()函数将返回进程被加载的ImageBase

    GetModuleHandle() API的部分代码

    MOV         EAX,DWORD PTR FS:[18]         ;FS:[18] = TEB

    MOV         EAX,DWORD PTR DS:[EAX+30]     ;DS:[EAX+30] = PEB

    MOV         EAX,DWORD PTR DS:[EAX+8]      ;DS:[EAX+8] = PEB.ImageBaseAddress

    PEB.Ldr

    PEB.Ldr成员是指向_PEB_LDR_DATA结构体的指针,_PEB_LDR_DATA结构体如下。

    +000 Length                         :Uint4B

    +004 Initialized                             :UChar

    +008 SsHandle                       :Ptr32 Void

    +00c InLoadOrderModulelist          :_LIST_ENTRY

    +014 InMemoryOrderModulelist        :_LIST_ENTRY

    +01c InInitializationOrderModulelist:               _LIST_ENTRY

    +024 EntryInProgress                :Ptr32 Void

    +028 ShutdownInProgress             :UChar

    +02c ShutdownThreadId               :Ptr32 Void

    当模块(DLL)加载到进程后,通过PEB.Ldr成员可以直接获得该模块的加载基址,_PEB_LDR_DATA结构体中含有3个_LIST_ENTRY类型的成员,_LIST_ENTRY结构体如下。

    typedef struct LIST_ENTRY{

            struct _LIST_ENTRY *Flink;

            struct _LIST_ENTRY *Bink;

    }LIST_ENTRY,*PLIST_ENTRY;

    从上述结构体可以看出,_LIST_ENTRY结构体提供双向链表机制。链表中保存的是_LDR_DATA_TABLE_ENTRY结构体的信息,给结构体如下。

    typedef struct _LDR_DATA_TABLE_ENTRY {

             PVOID Reserved1[2];

             LIST_ENTRY InMemoryOrderLinks;

             PVOID Reserved2[2];

             PVOID D1lBase;

             PVOID EntryPoint;

             PVOID Reserved3;

             Unicode_STRING Ful1D1lName;

             BYTE Reserved4[8];

             PVOID Reserved5[3];

             union{

                      ULONG CheckSum;

                      PVOID Reserved6;

             };

             ULONG TimeDateStamp;

    }LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;

    每个加载到进程中的DLL模块都对应一个_LDR_DATA_TABLE_ENTRY结构体,这些结构体相互链接,最终形成了_LIST_ENTRY双向链表。_PEB_LDR_DATA结构体中存在3种_LIST_ENTRY双向链表,也就是说,存在多个_LDR_DATA_TABLE_ENTRY结构体,并且有三种链接方法可以将它们链接起来。

    PEB.ProcessHeap & PEB.NtGolbalFlag

    PEB.ProcessHeap与PEB.NtGlobalFlag成员(像PEB.BeingDebugged成员一样)应用于反调试技术。若进程处于调试状态,则ProcessHeap与NtGlobalFlag成员就持有特定值。由于它们具有这一个特征,所以常常应用于反调试技术。

    PEB.ProcessHeap成员是指向HEAP结构体的指针,HEAP结构体如下。

    +0x000 Entry   :_HEAP_ENTRY

    +0x008 Signature     :Uint4B

    +0x00c Flags    :Uint4B

    +0x010 ForceFlags   :Uint4B

    +0x014 VirtualMemoryThreshold :Uint4B

    +0x018 SegmentReserve :Uint4B

    +0x01c SegmentCommit :Uint4B

    +0x020 DeCommitFreeBlockThreshold   :Uint4B

    进程处于被调试状态时,Flags(+0xC)与Force Flags(+ox10)成员被设置成特定的值。

    PEB.ProcessHeap(PEB结构体中偏移0x18的位置)成员既可以从PEB结构体中直接获得,也可以通过GetProcessHeap() API获得。

    GetProcessHeap() API代码的汇编形式基本类似于IsDebuggerPresent(),按照TEB→PEB→PEB.ProcessHeap顺序访问的ProcessHeap

    破解之法

    只要将HEAP.Flags与HEAP.ForceFlags的值重新设置为2与0即可(HEAP.Flags=2,HEAP.ForceFlags=0)。

    注意:该方法仅在WindowsXP系统中有效,Windows7系统不存在以上特征。此外,将运行中的进程附加到调试器时,也不会出现上述特征。

    静态反调试技术总结:

    1.通过PEB.BeingDebugged 标识当前进程是否处于调试状态

    破解之法
    只要借助OllyDbg调试器的编辑功能,将PEB.BeingDebugged的值修改为0(FALSE)即可。

    2. Ldr(+0xC)

    当调试时,在堆内存区会出现一些特殊的标识,标识他正在被调试,比如未使用 的内存全部填充为0XEEFEEEFE

    PEB.Ldr 是指向的_PEB_LDR_DATA结构的指针,而_PEB_LDR_DATA 正好是在堆内存区域创建的,所以扫描该内存区可找到是否存在0XEEFEEEFE

    破解之法

    把填充的0XEEFEEEFE改成NULL即可

    3. PEB.ProcessHeap

    +0x000 Entry   :_HEAP_ENTRY

    +0x008 Signature     :Uint4B

    +0x00c Flags    :Uint4B

    +0x010 ForceFlags   :Uint4B

    +0x014 VirtualMemoryThreshold :Uint4B

    +0x018 SegmentReserve :Uint4B

    +0x01c SegmentCommit :Uint4B

    +0x020 DeCommitFreeBlockThreshold   :Uint4B

    调试时,Flags(+0xC) 与+0x010 ForceFlags 被设定成特定值

    可以通过GetProcessHeap() API获得ProcessHeap

    进程正常运行,Flags=0x2,ForceFlags =0x0,被调试的时候就会改变

    破解之法

    把通过OD把他们的值改回正常的值即可

    4.PEB.NtGolbalFlag

    被调试时,NtGolbalFlag的成员会被设置成0x70,检测该值就可以

    破解之法

    把他设置成0即可

    5.NtQueryInformationProcess 

    // NtQueryInformationProcess 函数原型

    __kernel_entry NTSTATUS NtQueryInformationProcess(

      IN HANDLE           ProcessHandle,                               // 进程句柄

      IN PROCESSINFOCLASS ProcessInformationClass,                 // 检索的进程信息类型

      OUT PVOID           ProcessInformation,                      // 接收进程信息的缓冲区指针

      IN ULONG            ProcessInformationLength,                   // 缓冲区指针大小

      OUT PULONG          ReturnLength                                         // 实际接收的进程信息大小

    );

    用法:为NtQueryInformationProcess  函数的第二个参数ProcessInformationClass 指定特定值并调用,相关信息就会设置到第三个参数ProcessInformation内

    // PROCESSINFOCLASS 结构体原型

    typedef enum _PROCESSINFOCLASS

    {

        ProcessBasicInformation,                                           // 0x0

        ProcessQuotaLimits,

        ProcessIoCounters,

        ProcessVmCounters,

        ProcessTimes,

        ProcessBasePriority,

        ProcessRaisePriority,

        ProcessDebugPort,                                                        // 0x7

        ProcessExceptionPort,

        ProcessAccessToken,

        ProcessLdtInformation,

        ProcessLdtSize,

        ProcessDefaultHardErrorMode,

        ProcessIoPortHandlers,

        ProcessPooledUsageAndLimits,

        ProcessWorkingSetWatch,

        ProcessUserModeIOPL,

        ProcessEnableAlignmentFaultFixup,

        ProcessPriorityClass,

        ProcessWx86Information,

        ProcessHandleCount,

        ProcessAffinityMask,

        ProcessPriorityBoost,

        ProcessDeviceMap,

        ProcessSessionInformation,

        ProcessForegroundInformation,

        ProcessWow64Information,                                     // 0x1A

        ProcessImageFileName,                                                       // 0x1B

        ProcessLUIDDeviceMapsEnabled,

        ProcessBreakOnTermination,

        ProcessDebugObjectHandle,                                       // 0x1E

        ProcessDebugFlags,                                                              // 0x1F

        ProcessHandleTracing,

        ProcessIoPriority,

        ProcessExecuteFlags,

        ProcessResourceManagement,

        ProcessCookie,

        ProcessImageInformation,

        ProcessCycleTime,

        ProcessPagePriority,

        ProcessInstrumentationCallback,

        ProcessThreadStackAllocation,

        ProcessWorkingSetWatchEx,

        ProcessImageFileNameWin32,

        ProcessImageFileMapping,

        ProcessAffinityUpdateMode,

        ProcessMemoryAllocationMode,

        ProcessGroupInformation,

        ProcessTokenVirtualizationEnabled,

        ProcessConsoleHostProcess,

        ProcessWindowInformation,

        ProcessHandleInformation,

        ProcessMitigationPolicy,

        ProcessDynamicFunctionTableInformation,

        ProcessHandleCheckingMode,

        ProcessKeepAliveCount,

        ProcessRevokeFileHandles,

        ProcessWorkingSetControl,

        ProcessHandleTable,

        ProcessCheckStackExtentsMode,

        ProcessCommandLineInformation,

        ProcessProtectionInformation,

        ProcessMemoryExhaustion,

        ProcessFaultInformation,

        ProcessTelemetryIdInformation,

        ProcessCommitReleaseInformation,

        ProcessDefaultCpuSetsInformation,

        ProcessAllowedCpuSetsInformation,

        ProcessSubsystemProcess,

        ProcessJobMemoryInformation,

        ProcessInPrivate,

        ProcessRaiseUMExceptionOnInvalidHandleClose,

        ProcessIumChallengeResponse,

        ProcessChildProcessInformation,

        ProcessHighGraphicsPriorityInformation,

        ProcessSubsystemInformation,

        ProcessEnergyValues,

        ProcessActivityThrottleState,

        ProcessActivityThrottlePolicy,

        ProcessWin32kSyscallFilterInformation,

        ProcessDisableSystemAllowedCpuSets,

        ProcessWakeInformation,

        ProcessEnergyTrackingState,

        ProcessManageWritesToExecutableMemory,REDSTONE3

        ProcessCaptureTrustletLiveDump,

        ProcessTelemetryCoverage,

        ProcessEnclaveInformation,

        ProcessEnableReadWriteVmLogging,

        ProcessUptimeInformation,

        ProcessImageSection,

        ProcessDebugAuthInformation,

        ProcessSystemResourceManagement,

        ProcessSequenceNumber,

        ProcessLoaderDetour,

        ProcessSecurityDomainInformation,

        ProcessCombineSecurityDomainsInformation,

        ProcessEnableLogging,

        ProcessLeapSecondInformation,

        ProcessFiberShadowStackAllocation,

        ProcessFreeFiberShadowStackAllocation,

        MaxProcessInfoClass

    } PROCESSINFOCLASS;

    跟调试探测有关的的为:

    ProcessDebugPort,                                                        // 0x7

    ProcessDebugObjectHandle,                                       // 0x1E

    ProcessDebugFlags,                                                              // 0x1F

    ProcessDebugPort

    进程在调试的时候的系统会为他分配调试端口,当PROCESSINFOCLASS的 ProcessDebugPort 的值为7,调用NtQueryInformationProcess 就可以获取调试端口,如果没有调试则返回为0

    ProcessDebugObjectHandle

    调试的时候会生成调试对象,当PROCESSINFOCLASS的ProcessDebugObjectHandle

     为0x1E时,第三个参数就可以获取到调试对象句柄,否则该句柄为NULL

    ProcessDebugFlags

    当 ProcessDebugFlags为0x1F 时,第三个参数返回为0为被调试状态,正常运行为1

    破解之法

    Hook NtQueryInformationProcess 函数修改返回值

    基于环境检测反调试
    NtQuerySystemInformation

    基于调试环境的反调试技术. NTQuerySystemInformation()API是一个系统函数, 可以获取OS信息.

    第一个参数SystemInformationClass传入SystemKernelDebuggerInformation, 参数中指定需要系统信息类型,返回的时候会把结构体地址放在第二个参数里

    当第一个参数传入 0x23 (SystemInterruptInformation) 时,会返回一个 SYSTEM_KERNEL_DEBUGGER_INFORMATION 结构,里面的成员KdKdDebuggerEnable=1 和 KdDebuggerNotPresent 标志系统是否启用内核调试。

    破解之法

    命令行执行 “bcdedit/debug off” 若重启系统要用正常模式启动

    Xp下  系统中编辑boot.ini 文件。删除 /debugport=com1 /baudrate=115200 /Debug  的值

    NtQueryObject

    除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。逆向论坛中讨论的一个有趣的方法就是检查DebugObject类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一

    个DebugObject类型的对象。

    DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3):

    NTSTATUS NTAPI NtQueryObject(                                                                

    IN    HANDLE                                         ObjectHandle,                              

    IN    OBJECT_INFORMATION_CLASS   ObjectInformationClass,           

    OUT   PVOID                                           ObjectInformation,                     

    IN    ULONG                                           Length                                            ,

    OUT   PULONG                                        ResultLength                             

    )                                                                                                                            

    这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:

    typedef struct _OBJECT_ALL_INFORMATION{

    ULONG                                          NumberOfObjectsTypes;

    OBJECT_TYPE_INFORMATION         ObjectTypeInformation[1];

    }

    检测例程将遍历拥有如下结构的ObjectTypeInformation数组:

    typedef struct _OBJECT_TYPE_INFORMATION{

    [00] UNICODE_STRING        TypeName;

    [08] ULONG                          TotalNumberofHandles;

    [0C] ULONG                  TotalNumberofObjects;

    ...more fields...

    }

    TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects 或 TotalNumberofHandles 是否为非0值。

    破解之法

    Hook NtQueryObject

    ZwSetInformationThread

    可以用来将线程隐藏,从而使调试器接收不到信息

    其原型如下:

    NTSTATUS

    NTAPI

    ZwSetInformationThread (

        __in HANDLE ThreadHandle,

        __in THREADINFOCLASS ThreadInformationClass,

        __in_bcount(ThreadInformationLength) PVOID ThreadInformation,

        __in ULONG ThreadInformationLength

        );

    //

    // Thread Information Classes定义如下

    //ntpsapi.h

    typedef enum _THREADINFOCLASS {

        ThreadBasicInformation,

        ThreadTimes,

        ThreadPriority,

        ThreadBasePriority,

        ThreadAffinityMask,

        ThreadImpersonationToken,

        ThreadDescriptorTableEntry,

        ThreadEnableAlignmentFaultFixup,

        ThreadEventPair_Reusable,

        ThreadQuerySetWin32StartAddress,

        ThreadZeroTlsCell,

        ThreadPerformanceCount,

        ThreadAmILastThread,

        ThreadIdealProcessor,

        ThreadPriorityBoost,

        ThreadSetTlsArrayAddress,

        ThreadIsIoPending,

       ThreadHideFromDebugger,//这个就是用来将线程对调试器隐藏

        ThreadBreakOnTermination,

        ThreadSwitchLegacyState,

        ThreadIsTerminated,

        MaxThreadInfoClass

        } THREADINFOCLASS;

    // end_ntddk end_ntifs

    如下:ZwSetInformationThread(GetCurrentThread( ), ThreadHideFromDebugger, NULL, 0); 这样就可以将当前线程对调试器隐藏了!

    破解之法

    Hook  ZwSetInformationThread

    利用TLS技术,因为TLS回调函数会先于EP代码执行,所以可以在该函数内部使用IsDebuggerPresent 等函数判断是否在调试
    还可以通过检测OD的窗口、进程是否存在来判断是否调试,判断虚拟机进程等

    动态反调试

    SEH

    在触发异常时,若程序正常运行,会触发异常处理程序,但若在调试过程中,调试器会接管异常处理,所以程序往往会故意触发异常,让调试器接管后执行无法正确运行的程序,若未调试,则会在异常程序那继续执行程序

     正常运行:

    转到安装的SEH处理函数中,该处理函数通过设置CONTEXT.Eip进行跳转到正常运行的代码段。

    调试运行:

                  调试器因为什么也不做走入程序终止/垃圾代码段(极大消耗逆向人员精力)。

    破解之法

    通过OD等调试器的异常处理选项,对异常进行不处理,让异常还给程序本身处理即可

    SetUnhandledExceptionFilter

    如果程序发生异常,SEH未处理或者不存在,那么系统就会运行最后一个异常处理器Top Level Exception Filter,弹出错误消息框,程序停止运行。

    反调试技术的程序会:先特意触发异常;进入UnhandledExceptionFilter()内部;再进入使用SetUnhandledException()注册的顶级异常函数,即TopLevelExceptionFilter()API;判断程序正常还是调试运行,以设置eip的值。

    注意:UnhandledExceptionFilter()函数内部有ZwQueryInformationProcess()函数,见静态反调试B)

    破解之法

    先解除UnhandledExceptionEilter()中的静态反调试,然后看看程序打算注册的新TopLevelExceptionFilter()函数打算返回到哪个地址。

    通过时间判断

    在一段重要代码执行前后进行执行时间判断,若超过一定时间则认为被下了断点或者单步执行,

    破解之法

    测试时间一般分两类,1.Cpu计数 2采用系统实际时间

    X86CPU中有一个TSC (time stamp counter 时间戳计数器)的64位寄存器,cpu对每个clock cycle 计数,然后保存到TSC。 RDTSC是一条汇编指令,用来把TSC值读入EDX:EAX寄存器,TSC为64位,其高32位被保存在EDX,低32位保存在EAX

    在OD里,遇到RDTSC指令,将其关键判断条件或者值改写即可

    陷阱标志

    陷进标志是EFLAGS 寄存器的第九个比特位 TF

    TF=1  进入单步执行,cpu执行一条指令,会触发EXCEPTION_SINGLE_STEP异常,然后陷进标志会清零,

    破解之法:

    OD调试选项设置忽略这个异常

    INT2D

    原为内核模式中用来触发断点的异常命令,也可以在用户模式下触发异常。

    但是调试状态下不会触发异常,只会忽略。

    他有个特征:在调试下他执行完INT2D指令后,会忽略下一个指令的第一个字节后继续执行,在非调试模式下,会进入SE Handler

    破解之法:

    很多利用这个特征的方法通常会带有cmp等判断,通过修改判断条件来达到目的

    API断点

    一般断API断点都在API头部入口,有些程序会检测API头部是否有0XCC等断点标志来判断是否被调试

    破解之法

    把断点下在API中间位置或者其他位置

    比较校验和

    比较特定代码区域的校验和值也用于反调试,比如在程序401000-401070 区域的校验和是0x12346578,如果在这个区域内下断,新的校验和跟原来的就不一样了,这就触发了反调试

    破解之法

    最好的办法还是修改CRC比较的逻辑

    高级反调试技术:

    1.     垃圾代码

    垃圾代码中引入很多可以相互抵消的指令,减缓逆向人员的分析速度。

    2.     扰乱代码对齐

    加入额外代码,令分析程序错误解析。

    比如415115-415119被调试器解析为一条指令,而前面程序有jmp到415116的,其实软件本身是415116-415119为一条指令的。

    3.     加密/解密

    程序一部分代码用于将其余部分代码解密,以隐藏程序与数据。

    注:反转储技术中,加密代码被解码为正常代码后,有时会被再次加密,所以转储的代码可能还是处于加密状态。

    特殊情况:代码重组——指令修改程序的代码。

    4.     Stolen Bytes(Remove OEP)

    顾名思义:

    将源代码OEP(一部分代码)转移到压缩器/保护器创建的内存区域运行。

    注:有些保护器先保存Stolen Bytes再运行,还有些保护器将它们直接从内存中删除。

    5.     API重定向

    防止在API处被设下断点

    类似于API钩取,将本来call API的地址CALL到另一个地址,另一个地址预先创建好和原先API同一个功能的代码。

    6.     Debug Blocker(Self Debugging)

    调试模式下运行自身进程。

    这种技术是自我创建技术(以子进程形式运行自身进程)的演进形式。

    有如下优点:
            1.防止代码调试,因为子进程运行实际的源代码处于调试之中,原则上无法再使用其他调试器附加。(57章会讲到一种方法破解)

            2.能够控制子进程,因为对于调试进程发生的异常,调试器拥有优先处理权,所以可以把异常处理器的代码放到调试进程中。——因此,想要调试子进程,要先断开已有调试器的连接,而这样子进程又无法执行。

  • 相关阅读:
    leetcode--Interleaving String
    SR4000自带软件修改(二)
    修改SR4000自带软件,支持opencv
    获取当前进程的寄存器内容
    sr4000自带API和opencv结合获取图像
    远程进程注入
    【转+心得】WinDbg+VM9双机调试无法连接解决方案
    boost库的使用(一)
    SR4K的API使用(libMesaSR.dll)
    java含多个包的命令行下执行
  • 原文地址:https://www.cnblogs.com/youyaoqi/p/15518314.html
Copyright © 2020-2023  润新知