[参考]反调试技术整理

反调试技术整理

前言

本整理基于 LordNoteworthy/al-khaser: Public malware techniques used in the wild: Virtual Machine, Emulation, Debuggers, Sandbox detection. (github.com)

由于技术复杂,并且Windows存在大量未公开的内核参数、函数等,这些技术可能会过时。

反调试技术整理

  1. PEB表、IsDebuggerPresent
    检查FS:[0x30](32位)GS:[0x60](64位),等同于IsDebuggerPresent()

    BOOL IsDebuggerPresent();
    

    参见IsDebuggerPresent - CTF Wiki (ctf-wiki.org)

  2. CheckRemoteDebuggerPresent()/NtQueryInformationProcess
    CheckRemoteDebuggerPresent()调用NtQueryInformationProcess

    __kernel_entry NTSTATUS NtQueryInformationProcess(
      [in]            HANDLE           ProcessHandle,
      [in]            PROCESSINFOCLASS ProcessInformationClass,
      [out]           PVOID            ProcessInformation,
      [in]            ULONG            ProcessInformationLength,
      [out, optional] PULONG           ReturnLength
    );
    

    调用此API,传入参数ProcessInformationClass = 7将返回一个句柄指向调试器

    参见CheckRemoteDebuggerPresent - CTF Wiki (ctf-wiki.org)NtQueryInformationProcess - CTF Wiki (ctf-wiki.org)

  3. 异常捕获和处理

    1. Windows异常处理流程简述

      1. 硬件异常:

        1. CPU转储当前现场

        2. CPU根据IDT查找异常处理例程(KiTrapXX)

          参见CPU和软件模拟异常的执行流程_鬼手56的博客-CSDN博客
          使用IDA打开ntkrnlpa.idb文件,在其中查找_IDT可以看到对应的处理表

          image

        3. 异常处理例程处理异常,完成异常信息封装

        4. 调用CommonDispatchException,完善EXCEPTION_RECORD结构

          typedef struct _EXCEPTION_RECORD {
            DWORD                    ExceptionCode;
            DWORD                    ExceptionFlags;
            struct _EXCEPTION_RECORD *ExceptionRecord;
            PVOID                    ExceptionAddress;
            DWORD                    NumberParameters;
            ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
          } EXCEPTION_RECORD;
          
        5. 将结构传递给KiDispatchException

          VOID KiDispatchException(
            [in]  PEXCEPTION_RECORD	ExceptionRecord,
            [in]  PKEXCEPTION_FRAME	ExceptionFrame,
            [in]  PKTRAP_FRAME		TrapFrame,
            [in]  KPROCESSOR_MODE		PreviousMode,
            [in]  BOOLEAN				FirstChance
          );
          
      2. 软件异常:

        1. 发生throw关键字
        2. 转入_CxxThrowException
          extern "C" void __stdcall _CxxThrowException(
             void* pExceptionObject
             _ThrowInfo* pThrowInfo
          );
          
        3. 通过KERNEL32.DLL!RaiseException填充EXCEPTION_RECORD结构体
          void RaiseException(
            [in] DWORD           dwExceptionCode,
            [in] DWORD           dwExceptionFlags,
            [in] DWORD           nNumberOfArguments,
            [in] const ULONG_PTR *lpArguments
          );
          
        4. 转入NTDLL.DLL!RtlRaiseException
        5. 转入内核NtRaiseException
        6. 转入内核KiRaiseExceptionException Code最高位置零
        7. 传递到分发函数KiDispatchException
      3. 内核异常

        1. 尝试传递给内核调试器
        2. 失败,利用RtlDispatchException传递至SEH
          BOOLEAN RtlDispatchException(
            [in]  PEXCEPTION_RECORD	ExceptionRecord,
            [in]  PCONTEXT			ContextRecord
          );
          
        3. 传递给内核调试器
        4. 终止Windows运行(BSoD)
      4. 用户异常

        1. 传递给内核调试器

        2. 失败或者不存在内核调试器:填充上下文CONTEXT,从KiExceptionDispatch转入KeUserExceptionDispather

          typedef struct _CONTEXT {
            DWORD64 P1Home;
            DWORD64 P2Home;
            DWORD64 P3Home;
            DWORD64 P4Home;
            DWORD64 P5Home;
            DWORD64 P6Home;
            DWORD   ContextFlags;
            DWORD   MxCsr;
            WORD    SegCs;
            WORD    SegDs;
            WORD    SegEs;
            WORD    SegFs;
            WORD    SegGs;
            WORD    SegSs;
            DWORD   EFlags;
            DWORD64 Dr0;
            DWORD64 Dr1;
            DWORD64 Dr2;
            DWORD64 Dr3;
            DWORD64 Dr6;
            DWORD64 Dr7;
            DWORD64 Rax;
            DWORD64 Rcx;
            DWORD64 Rdx;
            DWORD64 Rbx;
            DWORD64 Rsp;
            DWORD64 Rbp;
            DWORD64 Rsi;
            DWORD64 Rdi;
            DWORD64 R8;
            DWORD64 R9;
            DWORD64 R10;
            DWORD64 R11;
            DWORD64 R12;
            DWORD64 R13;
            DWORD64 R14;
            DWORD64 R15;
            DWORD64 Rip;
            union {
              XMM_SAVE_AREA32 FltSave;
              NEON128         Q[16];
              ULONGLONG       D[32];
              struct {
                M128A Header[2];
                M128A Legacy[8];
                M128A Xmm0;
                M128A Xmm1;
                M128A Xmm2;
                M128A Xmm3;
                M128A Xmm4;
                M128A Xmm5;
                M128A Xmm6;
                M128A Xmm7;
                M128A Xmm8;
                M128A Xmm9;
                M128A Xmm10;
                M128A Xmm11;
                M128A Xmm12;
                M128A Xmm13;
                M128A Xmm14;
                M128A Xmm15;
              } DUMMYSTRUCTNAME;
              DWORD           S[32];
            } DUMMYUNIONNAME;
            M128A   VectorRegister[26];
            DWORD64 VectorControl;
            DWORD64 DebugControl;
            DWORD64 LastBranchToRip;
            DWORD64 LastBranchFromRip;
            DWORD64 LastExceptionToRip;
            DWORD64 LastExceptionFromRip;
          } CONTEXT, *PCONTEXT;
          
        3. 内核从KiUserExceptionDispatcher获得控制,调用RtlDispatchException查找并调用异常处理函数:VEH→SEH,从fs:[0]开始。

          VOID KiUserExceptionDispatcher
          ( 
          	[in] PEXCEPTION_RECORD	ExceptionRecord,
          	[in] PCONTEXT			ContextRecord
          );
          

          参考:VEH和SEH_鬼手56的博客-CSDN博客_veh和seh

        4. 失败或者未处理:进入内核再次处理

        5. 如果传递给内核调试器失败或者未处理,终止进程

      5. 参见异常处理流程 - ciyze0101 - 博客园 (cnblogs.com)

    2. CloseHandle()
      如果一个进程在调试器下运行,并且一个无效的句柄被传递给ntdll!NtClose()kernel32!CloseHandle()函数,那么将引发EXCEPTION_INVALID_HANDLE:0xC0000008异常。这个异常可以被一个异常处理程序缓存起来。如果控制被传递给异常处理程序,表明有一个调试器存在。

    3. 植入中断(包括int 3int 2dh
      int 2dh可以用于检测包括RING0,RING3在内的所有调试器。通过提前植入一个断点,并附带植入一个VEH异常解决例程,可以用于判断调试器的存在与否。
      另外,附加调试器的程序在运行完int 2dh 后,会跳过此指令之后的一个字节.

      参见Interrupt 3 - CTF Wiki (ctf-wiki.org)

    4. 利用STATUS_GUARD_PAGE_VIOLATION异常
      分配被保护的内存区,利用属性PAGE_GUARD标记分配的内存区。向其中填入ret指令。当存在OD调试器解释时将返回到上一个入栈地址,而直接执行将产生STATUS_GUARD_PAGE_VIOLATION错误(数据执行保护)

    5. (Windows XP/2000)使用宏OutputDebugString

      void OutputDebugStringA(
        [in, optional] LPCSTR lpOutputString
      );
      

      此宏在没有调试器附加时,执行将产生一个LastError。检查此错误的值在执行前后是否发生改变,可以知道是否有调试器寄生。

    6. 引发EXCEPTION_EXECUTE_HANDLER错误
      触发方法:关闭一个不存在的句柄

      CloseHandle(0x99999999ULL);
      
    7. 使用EFLAGS写入异常标志,然后监视异常
      直接将EFLAGS0x100或运算,利用VEH检查调试器的存在

    8. 利用UnhandledExceptionFilter()在除去try…catch块的同时接管错误

      /*Global*/ BOOL bIsBeinDbg = TRUE;
      	//...
      	//示例
      	LPTOP_LEVEL_EXCEPTION_FILTER Top = SetUnhandledExceptionFilter(myUnhandledExcepFilter);
      	//在myUnhandledExcepFilter里更改全局变量bIsBeinDbg的值为FALSE
      	RaiseException(EXCEPTION_FLT_DIVIDE_BY_ZERO, 0, 0, NULL);//在此处产生任意错误
      	SetUnhandledExceptionFilter(Top);
      	return bIsBeinDbg;
      
  4. 内存扫描与监视

    1. 硬件断点寄存器检查

      1. 提示:对于APIZeroMemory,部分编译器会将此宏直接优化掉。因此建议使用SecureZeroMemory
      2. 利用GetThreadContext获取程序运行上下文,检查DrN寄存器的值是否为0利用MEM_WRITE_WATCH监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。
        BOOL GetThreadContext(
          [in]      HANDLE    hThread,
          [in, out] LPCONTEXT lpContext
        );
        
    2. 利用MEM_WRITE_WATCH监察申请的内存块的访问、写入情况。尤其适合对反调试部分的字节码进行动态写入之后检查是否被修改。
      在分配内存时指定

      LPVOID VirtualAlloc(
        [in, optional] LPVOID lpAddress,
        [in]           SIZE_T dwSize,
        [in]           DWORD  flAllocationType,
        [in]           DWORD  flProtect
      );
      

      使用VirtualAlloc时指定参数[in] flAllocationTypeMEM_WRITE_WATCH = 0x00200000

      需要检查时,使用GetWriteWatch函数,其中参数[in] lpBaseAddress填入上文申请的内存地址

      参见[GetWriteWatch function (memoryapi.h) - Win32 apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/win32/api/me moryapi/nf-memoryapi-getwritewatch)

      UINT GetWriteWatch(
        [in]      DWORD     dwFlags,
        [in]      PVOID     lpBaseAddress,
        [in]      SIZE_T    dwRegionSize,
        [out]     PVOID     *lpAddresses,
        [in, out] ULONG_PTR *lpdwCount,
        [out]     LPDWORD   lpdwGranularity
      );
      
  5. LFH:低碎片堆

    LFH 不是单独的堆。 而是应用程序可以为其堆启用的策略。 启用 LFH 后,系统会在某些预先确定的大小中分配内存。 当应用程序从启用了 LFH 的堆请求内存分配时,系统会分配足够大以包含所请求大小的最小内存块。 无论是否启用 LFH,系统都不会将 LFH 用于大于 16 KB 的分配。

    低碎片堆 | Microsoft Docs

    1. 当多次申请、释放堆内存之后,可能出现堆碎片。堆碎片的产生可能导致无法一次性分配大量连续的内存,尽管这个大小的内存在数值上是空闲的。

    2. 参见/windows 堆分析 - 合天网安实验室/《软件调试》

      在调试模式下,不存在此堆。因此可以通过探测此堆的地址,间接判断调试器存在与否。

      探测方法很简单,只需要查看nt!_HEAP.FrontEndHeap

      对于从Windows10开始的操作系统,此值的偏移经常变动,因此不便于通过硬编码的方式查找

      BOOL LowFragmentationHeap(VOID)
      {
      	PINT_PTR FrontEndHeap = NULL;
      	HANDLE hHeap = GetProcessHeap();
          
      	if (IsWindowsVista() || IsWindows7()) {
      #if defined (ENV64BIT)
      		FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x178);
      
      #elif defined(ENV32BIT)
      		FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd4);
      #endif
      	}
      
      	if (IsWindows8or8PointOne()) {
      #if defined (ENV64BIT)
      		FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0x170);
      
      #elif defined(ENV32BIT)
      		FrontEndHeap = (PINT_PTR)((CHAR*)hHeap + 0xd0);
      #endif
      	}
      
      	// In Windows 10. the offset changes very often.
      	// Ignoring it from now.
      	if (FrontEndHeap && *FrontEndHeap == NULL) {
      		return TRUE;
      	}
      
      	return FALSE;
      }
      
  6. 反HOOK

    • 检查某DLL内部的函数的地址(利用GetProcAddress),并于DLL的地址比较。如果被HOOK,那么函数的地址就将位于对应DLL的地址空间之外。
      判断地址空间利用lpBaseOfDllPE属性和SizeOfImage属性
    • 要检查函数是否被HOOK,可以传入错误的参数,例如传入错误句柄或者错误大小,观察函数对错误参数的处理
  7. NtGlobalFlag
    检查FLG_HEAP_ENABLE_TAIL_CHECK/FLG_HEAP_ENABLE_FREE_CHECK/FLG_HEAP_VALIDATE_PARAMETERS的值。
    在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用.
    简单示例:

    __declspec(naked) BOOL DetectDebuggerUsingNtGlobalFlag32()
    {
        __asm
        {
            push ebp;
            mov ebp, esp;
            pushad;
    
            mov eax, fs:[30h];		//从此处获取PEB表
            mov al, [eax + 68h];
            and al, 70h;
            cmp al, 70h;
            je being_debugged;
            popad;
            mov eax, 0;
            jmp being_debugged + 6
    
            being_debugged:
            popad;
            mov eax, 1;
            leave;
            retn;
    
        }
    }
    

    参见NtGlobalFlag - CTF Wiki (ctf-wiki.org)

  8. NtQueryInformationProcess

    __kernel_entry NTSTATUS NtQueryInformationProcess(
      [in]            HANDLE           ProcessHandle,
      [in]            PROCESSINFOCLASS ProcessInformationClass,
      [out]           PVOID            ProcessInformation,
      [in]            ULONG            ProcessInformationLength,
      [out, optional] PULONG           ReturnLength
    );
    

    NtQueryInformationProcess function (winternl.h) - Win32 apps | Microsoft Docs

    函数ntdll!NtQueryInformationProcess()可以从一个进程中检索不同种类的信息。它接受一个ProcessInformationClass参数,该参数指定了ProcessInformation参数的输出类型。

    typedef enum _PROCESSINFOCLASS {
        ProcessBasicInformation = 0,
        ProcessDebugPort = 7,
        ProcessWow64Information = 26,
        ProcessImageFileName = 27,
        ProcessBreakOnTermination = 29,
        ProcessDebugObjectHandle = 30, 	//undocumented, 0x1e
        ProcessDebugFlags = 31			//undocumented, 0x1f
    } PROCESSINFOCLASS;
    
    • 传递ProcessDebugPort时,如果进程正在被调试,API会检索到一个等于0xFFFFFFFF(十进制-1)的DWORD值。

    • 传递ProcessDebugFlags,将返回一个EPROCESS内核结构

      Windows 内核不透明结构 - Windows drivers | Microsoft Docs

      参见EPROCESS 结构体属性介绍_hambaga的博客-CSDN博客

      //这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构
      //undocumented
      typedef struct _EPROCESS {
          //...
          // 调试端口
          PVOID DebugPort;
          
          // 异常端口
          PVOID ExceptionPort;
          
          //...
          // 指向3环PEB(进程环境块),包含了进程地址空间中的堆和系统模块等信息
          PPEB Peb;
      
          //...
      
          union {
      
              // 包含了进程的标志位,反映了进程当前状态和配置,上面那一大堆宏就是了
              ULONG Flags;
              // 字段只能由 PS_SET_BITS 和其他互锁宏设置。 最好通过位定义来读取字段,因此很容易找到引用
      
              struct {
                  ULONG CreateReported            : 1;
                  ULONG NoDebugInherit            : 1;	// 调试器
                  //...
              }
          }
      } EPROCESS, *PEPROCESS; 
      
    • 传递ProcessDebugObjectHandle,获取调试对象句柄

  9. 通过NtQuerySystemInformation尝试获取调试器句柄

    __kernel_entry NTSTATUS NtQuerySystemInformation(
      [in]            SYSTEM_INFORMATION_CLASS SystemInformationClass,
      [in, out]       PVOID                    SystemInformation,
      [in]            ULONG                    SystemInformationLength,
      [out, optional] PULONG                   ReturnLength
    );
    

    参数SYSTEM_INFORMATION_CLASS结构如下

    typedef enum _SYSTEM_INFORMATION_CLASS {
        SystemBasicInformation = 0,
        //...
        SystemKernelDebuggerInformation = 35, //undocumented, 0x23
        //...
        SystemPolicyInformation = 134,
    } SYSTEM_INFORMATION_CLASS;
    

    ntdll!NtQuerySystemInformation()函数接受要查询的信息类别作为参数,然而该参数大多数类都没有被记录下来,包括SystemKernelDebuggerInformation(0x23)类,它从Windows NT开始就存在了。

    SystemKernelDebuggerInformation返回两个标志寄存器的值:al中的KdDebuggerEnabled,和ah中的KdDebuggerNotPresent。因此,如果内核调试器存在,ah中的返回值为零。

    参见反调试:调试标志寄存器-|bbs.pediy.com

  10. 查询调试对象
    参见调试篇——调试对象与调试事件
    DbgUiConnectToDbg函数会创建调试对象,并把它放到TEBDbgSsReserved[1]成员,也就是该偏移0xF24位置。利用NtQueryObject检查所有调试对象,可以广泛的禁止系统调试,但是容易误伤。

    __kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
      [in, optional]  HANDLE                   Handle,
      [in]            OBJECT_INFORMATION_CLASS ObjectInformationClass,
      [out, optional] PVOID                    ObjectInformation,
      [in]            ULONG                    ObjectInformationLength,
      [out, optional] PULONG                   ReturnLength
    );
    
  11. 利用NtSetInformationThread将线程从调试器中隐藏。

    __kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationThread(
      [in] HANDLE          ThreadHandle,
      [in] THREADINFOCLASS ThreadInformationClass,
      [in] PVOID           ThreadInformation,
      [in] ULONG           ThreadInformationLength
    );
    

    关于ThreadInformationClass的枚举量,请查阅THREADINFOCLASS (geoffchappell.com)

    typedef enum _THREADINFOCLASS {
        ThreadBasicInformation          = 0,
        //...
        ThreadHideFromDebugger          = 17,
        //...
        MaxThreadInfoClass              = 51,
    } THREADINFOCLASS;
    
    //示例
    NtSetInformationThread(handle.get(), ThreadHideFromDebugger, nullptr, 0);
    

    参见ZwSetInformationThread - CTF Wiki (ctf-wiki.org)

    反制[原创]调试陷阱ThreadHideFromDebugger的另一种对抗方法-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com

  12. NtYieldExecution
    这个函数可以让任何就绪的线程暂停执行,等待下一个线程调度。线程放弃剩余时间,让给其他线程执行。如果没有其他准备好的线程,该函数返回false,否则返回true。当前线程如果被调试,那么调试器线程若处于单步状态,随时等待继续运行,则被调试线程执行NtYieldExecution时,调试器线程会恢复执行。此时NtYieldExecution返回true,该线程则认为自身被调试了。
    此方法并不准确,因为检测到的行为可能是由上层应用程序触发。因此会设置一个计数器。

  13. 父进程检查
    如果启动进程不是cmd.exeexplorer.exe,则返回警告

  14. HeapFlagsHeapForceFlags
    检查几个位,注意这些位的值的大小。如果HeapFlags的值大于 2 说明程序处于调试状态;如果 HeapForceFlags的值大于 0 则说明处于调试状态。

    • Flags 字段:
      • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
      • 在 64 位 Windows XP 中, Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.
    • ForceFlags 字段:
      • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
      • 在 64 位 Windows XP 中, ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.

    参见Heap Flags - CTF Wiki (ctf-wiki.org)

  15. 检查作业容器Job Object
    Windows 提供一个作业对象,它允许我们将进程组合在一起并创建一个"沙箱"来限制进程能做什么.可以将作业想象成一个进程容器.但是,只包含一个进程的作业同样有用,因为这样可以对进程施加平时不能施加的限制.
    检查同在一个作业对象中的其他进程。

  16. SeDebugPrivileges
    默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,所以我们可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。

  17. KUSER_SHARED_DATA

    参见KUSER_SHARED_DATA (ntddk.h) - Windows drivers | Microsoft Docs

    //这是一个简略的`EPROCESS`结构,请访问参见来查看完整的结构
    typedef struct _KUSER_SHARED_DATA {
      ULONG                         TickCountLowDeprecated;
      //...
      BOOLEAN                       KdDebuggerEnabled;		//位置: 0x2D4
      //...
      ULONG64                       UserPointerAuthMask;
    } KUSER_SHARED_DATA, *PKUSER_SHARED_DATA;
    

    用户空间和内核空间其实有一块共享区域KUSER_SHARED_DATA,大小为 4 KB。它们的内存地址虽然不一样,但是它们都是有同一块物理内存映射出来的,其中存在内核调试检查位KdDebuggerEnabled可以获取内核调试状态。
    此内存块的地址为0xFFDF0000(x86)、0xFFFFF78000000000(x64),对应的要检查的位在0x2d4

    BOOL SharedUserData_KernelDebugger()
    {
    	const ULONG_PTR UserSharedData = 0x7FFE0000;
    	const UCHAR KdDebuggerEnabledByte = *(UCHAR*)(UserSharedData + 0x2D4); // 0x2D4 = the offset of the field
    
    	// Extract the flags.
    	// The meaning of these is the same as in NtQuerySystemInformation(SystemKernelDebuggerInformation).
    	// Normally if a debugger is attached, KdDebuggerEnabled is true, KdDebuggerNotPresent is false and the byte is 0x3.
    	const BOOLEAN KdDebuggerEnabled = (KdDebuggerEnabledByte & 0x1) == 0x1;
    	const BOOLEAN KdDebuggerNotPresent = (KdDebuggerEnabledByte & 0x2) == 0;
    
    	if (KdDebuggerEnabled || !KdDebuggerNotPresent)
    		return TRUE;
    
    	return FALSE;
    }
    
  18. 扫描关键位置字节码,排除0xCC

  19. 利用TLS进行反调试
    TLS在执行主函数前执行

  20. WUDF驱动框架动态链接库
    "C:\Windows\System32\WUDFPlatform.dll"中导出几个函数,其中存在一个与IsDebuggerPresent相类似的函数WudfIsAnyDebuggerPresent

参考资料

al-khaser学习笔记(一)Anti Debug - CrisCzy - 博客园 (cnblogs.com)

posted @ 2022-08-09 15:53  二氢茉莉酮酸甲酯  阅读(847)  评论(0编辑  收藏  举报