• 07内核对象


    windwos的对象管理器

    windwos操作系统虽然是以C来编写的,但也使用了面向对象的思想. windwos的内核主要有: 执行体, 微内核,HAL三层. 对象管理器是执行体重的组件. 主要管理执行体对象. windwos对象管理器的目的是:

    1. 为执行体的数据结构提供一种统一而又可扩展的定义和控制机制.

    2. 提供统一的安全访问机制.

    3. 在无须修改已有系统代码的情况下, 加入新的对象类型.

    4. 提供一组标准的API来对对象执行各种操作.

    5. 提供一种命名机制, 与文件系统的命名机制集成在一起. 每个内核对象由两个部分组成:

    6. 对象头

    7. 对象体.

    对象头使用以下结构体来表示:

    typedef struct _OBJECT_HEADER {
       LONG_PTR PointerCount;         /* 对象的引用计数 */
       union {
           LONG_PTR HandleCount;     /* 指向该对象的句柄数 */
           PVOID NextToFree;         /* 对象被延迟删除时加入到的链表 */
       };
       POBJECT_TYPE Type;           /* 对象的类型的结构体指针 */
       UCHAR NameInfoOffset;       /* 名称信息的内存偏移 */
       UCHAR HandleInfoOffset;       /* 句柄信息的内存偏移 */
       UCHAR QuotaInfoOffset;       /* 配额信息的内存偏移 */
       UCHAR Flags;             

       union {
           POBJECT_CREATE_INFORMATION ObjectCreateInfo;
           PVOID QuotaBlockCharged;
       };

       PSECURITY_DESCRIPTOR SecurityDescriptor;   /* 对象的安全描述符 */
       QUAD Body;                                 /* 对象体的开始 */
    } OBJECT_HEADER, *POBJECT_HEADER;
    typedef struct _OBJECT_TYPE {
       ERESOURCE Mutex;
       LIST_ENTRY TypeList;
       UNICODE_STRING Name;             /* 对象类型名,拷贝自对象头(OBJECT_HEADER) */
       PVOID DefaultObject;
       ULONG Index;                   /* 此类型对象在全局数组中的索引 */
       ULONG TotalNumberOfObjects;
       ULONG TotalNumberOfHandles;
       ULONG HighWaterNumberOfObjects;
       ULONG HighWaterNumberOfHandles;
       OBJECT_TYPE_INITIALIZER TypeInfo;
    #ifdef POOL_TAGGING
       ULONG Key;
    #endif //POOL_TAGGING
       ERESOURCE ObjectLocks[ OBJECT_LOCK_COUNT ];
    } OBJECT_TYPE, *POBJECT_TYPE;

    已创建的内核对象类型有:

    ExMutantObjectType
    ExpKeyedEventObjectType
    ExSemphoreObjectType
    ExTimerObjectType
    ExWindowsStationObjectType
    IoAdapterObjectType
    IoCompletionObjectType
    IoControllerObjectType
    IoDeviceHandlerObjectType
    IoFileObjectType
    LpcPortObjectType
    LpcWaitablePortObjectType
    MmSectionObjectType
    ObpDeviceMapObjectType
    ObpDirectoryObjectType
    ObpSymbolicLinkObjectType
    ObpTypeObjectType
    PsJobType
    PsProcessType
    PsThreadType
    SeTokenObjectType
    WmipGuidObjectType
    ObCreateObjectType(   /*创建类型对象的内核API*
       __in PUNICODE_STRING TypeName,                         /* 类型名 */
       __in POBJECT_TYPE_INITIALIZER ObjectTypeInitializer,   /* 类型初始化信息 */
       __in_opt PSECURITY_DESCRIPTOR SecurityDescriptor,       /* 类型的安全描述符 */
       __out POBJECT_TYPE *ObjectType
       );

    这个API能够创建出一个内核对象的类型.在该函数中,可以指定类型的名称,类型的初始化信息. 类型的初始化信息使用一个结构体来描述.在这个结构体中, 主要有一些此种类型的数据成员,和一些基本的函数成员, 当然,这些函数成员都是以函数指针的形式出现的. 因此, 这样的一个API, 就像在C++中定义了一个类一样, TypeName为类名, ObjectTypeInitializer为类中的成员变量和成员函数. 只不过这个API只能创建具有固定成员变量,固定成员函数的类.以下是OBJECT_TYPE_INITIALIZER结构体的定义:

    typedef struct _OBJECT_TYPE_INITIALIZER {
       USHORT Length;
       BOOLEAN UseDefaultObject;
       BOOLEAN CaseInsensitive;
       ULONG InvalidAttributes;
       GENERIC_MAPPING GenericMapping;
       ULONG ValidAccessMask;
       BOOLEAN SecurityRequired;
       BOOLEAN MaintainHandleCount;
       BOOLEAN MaintainTypeList;
       POOL_TYPE PoolType;
       ULONG DefaultPagedPoolCharge;
       ULONG DefaultNonPagedPoolCharge;
       OB_DUMP_METHOD DumpProcedure;               /* dump的函数 */
       OB_OPEN_METHOD OpenProcedure;               /* 打开该类类型对象的函数 */
       OB_CLOSE_METHOD CloseProcedure;               /* 关闭该类类型对象的函数 */
       OB_DELETE_METHOD DeleteProcedure;           /* 删除该类类型对象的函数 */
       OB_PARSE_METHOD ParseProcedure;               /* 解析该类类型对象全局名称的函数 */
       OB_SECURITY_METHOD SecurityProcedure;         /* 该类类型对象的安全操作函数 */
       OB_QUERYNAME_METHOD QueryNameProcedure;         /* 获取该类类型名称的函数 */
       OB_OKAYTOCLOSE_METHOD OkayToCloseProcedure;     /* 该类类型对象OkayToClose*/
    } OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;

    windwos系统中, 有一个全局数组ObpObjectTypes记录了所有被创建出来的类型, 这是一个固定大小的数组, WRK限定它不能超过48种类型. OBJECT_TYPE中的Index成员记录一个类型对象在这个数组中的索引.

    /* 文件: obp.h */
    #define OBP_MAX_DEFINED_OBJECT_TYPES 48
    POBJECT_TYPE ObpObjectTypes[ OBP_MAX_DEFINED_OBJECT_TYPES ]; // 全局数组

    内核对象的创建

    有了对象的类型, 就可以使用这些类型类创建对象, windwos中,使用ObCreateObject来创建对象.

    NTSTATUS
    _ObCreateObject(
       __in KPROCESSOR_MODE ProbeMode,              
       __in POBJECT_TYPE ObjectType,                 /* 对象类型,一般是全局对象类型数组其中一个 */
       __in_opt POBJECT_ATTRIBUTES ObjectAttributes,
       __in KPROCESSOR_MODE OwnershipMode,
       __inout_opt PVOID ParseContext,
       __in ULONG ObjectBodySize,                   /* 对象体的字节数 */
       __in ULONG PagedPoolCharge,
       __in ULONG NonPagedPoolCharge,
       __out PVOID *pObject                       /* 输出新创建出的对象体的首地址 */
       )

    内核对象创建的例子可参考WRK中的base tospspsinit.c中的323~362行.

    //
    // Initialize the common fields of the Object Type Prototype record
    //
    // 将类型结构体初始化为0
    RtlZeroMemory (&ObjectTypeInitializer, sizeof (ObjectTypeInitializer));
    // 初始化一些字段.
    ObjectTypeInitializer.Length = sizeof (ObjectTypeInitializer); // 类型结构体的字节数
    ObjectTypeInitializer.SecurityRequired = TRUE;                   // 是否需要安全检查
    ObjectTypeInitializer.PoolType = NonPagedPool;                   // 该结构体使用的内存的类型
    ObjectTypeInitializer.InvalidAttributes = OBJ_PERMANENT |
                                             OBJ_EXCLUSIVE |
                                             OBJ_OPENIF;
    //
    // Create Object types for Thread and Process Objects.
    // 分别创建进程和线程对象的类型
    //

    // 对象类型的名称
    RtlInitUnicodeString (&NameString, L"Process");
    ObjectTypeInitializer.DefaultPagedPoolCharge = PSP_PROCESS_PAGED_CHARGE;
    ObjectTypeInitializer.DefaultNonPagedPoolCharge = PSP_PROCESS_NONPAGED_CHARGE;
    ObjectTypeInitializer.DeleteProcedure = PspProcessDelete;
    ObjectTypeInitializer.ValidAccessMask = PROCESS_ALL_ACCESS;
    ObjectTypeInitializer.GenericMapping = PspProcessMapping;

    // 创建一个进程的类型
    if (!NT_SUCCESS (ObCreateObjectType (&NameString,
                                         &ObjectTypeInitializer,
                                         (PSECURITY_DESCRIPTOR) NULL,
                                         &PsProcessType))) {
       return FALSE;
    }

    一个对象的构造, 主要流程如下:

    1. 调用ObCreateObject,根据指定的类型来初始化对象头, 并按照传入的ObjectBodySize来分配对象体的内存.

    2. 完成对象体的初始化.

    第一步可以使用统一的方法来完成. 但第二步需要针对不同的类型进行不同的初始化.比如, 进程对象的构造是使用PspCreateProcess来完成的, 而线程对象需要使用PSPCreateThread.

    Windows的对象层次目录(系统全局命名空间)

    Windows允许以名称方式来管理对象,这对于跨进程,跨模块的操作带来了极大的遍历. Windows使用一套全局名称查询机制来支持这种便利性.

    全局变量 ObpRootDirectoryObject定义了对象层次目录的根目录. 在根目录之下, 系统内置了一些子目录,这些子目录由NtCreateDirectoryObject所创建, 系统内置的子目录如下:

    Callback`,`ArcName`, `Device` , `Driver`, `FileSystem` , `KernelObjects` , `ObjectTypes` , `GLOBAL??` , `Security

    在WRK中的base tosioiomgrioinit.c的2326行IopCreateRootDirectoryObject函数中,就有一个创建Driver子目录的例子.

    // Create the root directory object for the Driver directory.

    RtlInitUnicodeString( &nameString, L"\Driver" );
    InitializeObjectAttributes( &objectAttributes,
                               &nameString,
                               OBJ_PERMANENT|OBJ_KERNEL_HANDLE,
                               (HANDLE) NULL,
                               (PSECURITY_DESCRIPTOR) NULL );

    status = NtCreateDirectoryObject( &handle,
                                     DIRECTORY_ALL_ACCESS,
                                     &objectAttributes );
    if (!NT_SUCCESS( status )) {
       return FALSE;
    } else {
       (VOID) ObCloseHandle( handle , KernelMode);
    }

    对象管理器提供的基本操作

    1. ObpLookupDirectoryEntry : 在一个指定的目录中查找一个名称.

    2. ObpInsertDirectoryEnty : 把一个对象插入到一个目录中.

    3. ObpDeleteDirectoryEntry : 删除刚刚找到的那一项.

    4. ObpLookupObjectName : 从指定的目录或根目录递归地查找到指定名称的对象.

    5. ObOpenObjectByName : 通过名称打开一个对象

    6. ObReferenceObjectByName : 通过名称引用一个对象.

    7. ObInsertObject : 把一个对象插入到一个进程的句柄表中.

    其中, ObpLookupObjectName 在指定目录或根目录中递归查找指定名称的对象这个函数, 对理解对象管理器的名称机制很有帮助:

    ObpLookupObjectName (
       IN HANDLE RootDirectoryHandle OPTIONAL,     // 指定在哪个目录中查找
       IN PUNICODE_STRING ObjectName,               // 要查找的名称
       IN ULONG Attributes,
       IN POBJECT_TYPE ObjectType,
       IN KPROCESSOR_MODE AccessMode,
       IN PVOID ParseContext OPTIONAL,
       IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
       IN PVOID InsertObject OPTIONAL,
       IN OUT PACCESS_STATE AccessState,
       OUT POBP_LOOKUP_CONTEXT LookupContext,
       OUT PVOID *FoundObject
       )

    如果调用该函数时, 传入了RootDirectoryHandle目录句柄, 则函数内部会使用这个目录的Parse函数来机械对象名称.

    如果调用该函数是, RootDirectoryHandle 为空, 则函数内部会从全局的根目录对象ObpRootDirectoryObject开始解析. 在这种情况下, 传递进来的对象名称必须以 开始,如果对象名称仅仅是,则执行特殊处理. 否则, 将按照另一套规则进行解析:

    • 如果名称以??开头, 则需要获取当前进程的DeviceMap(设备表)来进一步查询.

    • 如果名称只有??,则直接返回当前进程的DeviceMap(设备表).

    • 调用ObpLookupDirectoryEntry函数, 层层递进, 如果碰到具有Pase函数的对象, 则调用这个对象的Pase函数来解析余下的名称字符串. 如果碰到子目录对象, 则在子目录对象中进一步查询下一级名称.

    对象的Parse方法就是在创建类型时指定的函数指针,每一种对象都可以有自己的名称解析方法, 这使得windwos的名字系统变得非常强大. 既允许以基本的目录方法来管理名称的层次结构, 也允许特定类型的对象有它自己的命名和解析策略. 例如File对象就有它自己的Parse方法, 从而可以方便地支持我们所熟悉的文件系统中的目录结构(文件路径).

    DeviceMap(设备表)

    设备表定义了一个DOS设备的名字空间, 比如驱动器字母(C: , D:)和一些外设(COM1). 当对象管理器看到一个以??开头的名称或像??这样的名称, 它会利用进程的设备表来获得相应的对象目录, 然后进一步解析剩余的名称字符串.

    对象管理器中的对象是执行体对象, 它们位于系统地址空间中, 因而所有的进程都可以访问这些对象.但是进程地址空间中运行的是用户模式的代码, 并不能使用指针的方式来引用这些对象. 它们在调用系统服务时只能通过句柄来引用执行体对象. 句柄是进程相关的 , 它一定要在特定的进程环境中才有意义.

    在内核中, 将一个句柄转换成对应的对象, 可以通过ObReferenceObjectByHandle函数来完成.该函数负责从当前进程环境或内核环境的句柄表中获得指定的对象的引用.

    对象的内存结构

    对象由对象头和对象体组成, 对象头的数据结构都是OBJECT_HEADER, 而对象体的类型则因对象类型而异.

    通过阅读ObpAllocateObject函数的实现可知, 对象头和对象体存储在同一块内存中,而且, 对象头之前可能还有一些可选的内容,包括QuotaInfo,HandleInfo, NameInfoCreateInfo, 这每一部分是否出现, 取决于对象的类型以及创建对象时所提供的参数.

    想要找到对象头, 可以通过以下宏来实现:

    #define OBJECT_TO_OBJECT_HEADER( o ) 
       CONTAINING_RECORD( (o), OBJECT_HEADER, Body )

    而其它信息,如QuotaInfo,HandleInfo, NameInfoCreateInfo, 则可以通过OBJECT_HANDER_TO_QUOTA_INFO , OBJECT_HANDER_TO_HANDLE_INFO , OBJECT_HANDER_TO_NAME_INFO 宏来得到.

    对象的生命周期

    内核对象是通过引用计数来管理其生命周期的. 一旦引用计数为0, 则对象的生命周期结束, 它所占用的内存就会被回收.

    对象的引用计数来源于两个方面:

    1. 内核中的指针引用. 一旦内核中新增了一个对象的引用, 则对象的引用计数自增一,如果一个对象的引用不再有用,则引用计数自减一. 这两种引用的增减是使用ObReferenceObjectByPointerObDereferenceObject导致的.

    2. 一个进程打开一个对象并成功获得一个句柄, 会使得对象头中的句柄计数自增一. 当一个句柄不再被使用时, 句柄计数自减一. 这两种引用的增减来自ObpIncrementHandleCountObpDecrementHandleCount函数.

    以上内容来自 window内核原理与实现

    Object HOOK

    基本原理: Windows系统中的内核对象由三个部分组成

    • 对象头

    • 对象类型信息

    • 对象体 其中, 每个内核对象的对象头都是一样的. 1567144313095

    当一个对象被创建时(例如一个进程), 相应的对象头就会被复制到新对象的对象头中. Windows系统内核已经提前创建好了一些对象类型(可以称之为原型): 1567144343258 每当创建一个对象时, 对象头的内容就来自这些原型. 这些对象类型被保存在一个数组中, 数组名为ObTypeIndexTable,通过windbg可以观察到:

    1567144365406

    这个数组中保存的都是地址, 每个地址都是_OBJECT_TYPE类型结构体变量的.

    1567144387641

    在上面的结构体中, TypeInfo字段保存着大量函数: 1567144414568 这些函数会在特定的时机被调用. OBJECT HOOK的原理实际上就是将这些函数替换掉. 下面是例子:

    #include <ntddk.h>


    typedef NTSTATUS(*FnIopParseFile)(
       IN PVOID ParseObject,
       IN PVOID ObjectType,
       IN PACCESS_STATE AccessState,
       IN KPROCESSOR_MODE AccessMode,
       IN ULONG Attributes,
       IN OUT PUNICODE_STRING CompleteName,
       IN OUT PUNICODE_STRING RemainingName,
       IN OUT PVOID Context OPTIONAL,
       IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
       OUT PVOID *Object
       );


    // 声明函数
    PULONG _declspec(dllimport) ObGetObjectType(PVOID);

    FnIopParseFile g_oldObjectParse;
    POBJECT_TYPE g_pFileObjType;



    typedef struct _OBJECT_TYPE_INITIALIZER
    {
       USHORT Length;
       USHORT type;
       PVOID ObjectTypeCode;
       PVOID InvalidAttributes;
       GENERIC_MAPPING GenericMapping;
       PVOID ValidAccessMask;
       PVOID RetainAccess;
       POOL_TYPE PoolType;
       PVOID DefaultPagedPoolCharge;
       PVOID DefaultNonPagedPoolCharge;
       PVOID DumpProcedure;
       PVOID OpenProcedure;
       PVOID CloseProcedure;
       PVOID DeleteProcedure;
       PVOID ParseProcedure;
       PVOID SecurityProcedure;
       PVOID QueryNameProcedure;
       USHORT OkayToCloseProcedure;
    } OBJECT_TYPE_INITIALIZER, *POBJECT_TYPE_INITIALIZER;

    typedef struct _OBJECT_TYPE
    {
       LIST_ENTRY TypeList;         //         : _LIST_ENTRY
       UNICODE_STRING Name;         //             : _UNICODE_STRING
       PVOID DefaultObject;         //   : Ptr32 Void
       ULONG Index;         //           : UChar
       ULONG TotalNumberOfObjects;         // : Uint4B
       ULONG TotalNumberOfHandles;         // : Uint4B
       ULONG HighWaterNumberOfObjects;         // : Uint4B
       ULONG HighWaterNumberOfHandles;         // : Uint4B
       OBJECT_TYPE_INITIALIZER TypeInfo;         //         : _OBJECT_TYPE_INITIALIZER
       PVOID TypeLock;         //         : _EX_PUSH_LOCK
       ULONG Key;         //             : Uint4B
       LIST_ENTRY CallbackList;         //     : _LIST_ENTRY
    } OBJECT_TYPE, *POBJECT_TYPE;

    typedef struct _OBJECT_CREATE_INFORMATION
    {
       ULONG Attributes;
       HANDLE RootDirectory;
       KPROCESSOR_MODE ProbeMode;
       ULONG PagedPoolCharge;
       ULONG NonPagedPoolCharge;
       ULONG SecurityDescriptorCharge;
       PVOID SecurityDescriptor;
       PSECURITY_QUALITY_OF_SERVICE SecurityQos;
       SECURITY_QUALITY_OF_SERVICE SecurityQualityOfService;
    } OBJECT_CREATE_INFORMATION, *POBJECT_CREATE_INFORMATION;

    typedef struct _OBJECT_HEADER
    {
       //对象头部的指针计数,对对象头指针引用的计数
       LONG_PTR PointerCount;
       union
       {
           //句柄引用计数
           LONG_PTR HandleCount;
           PVOID NextToFree;
       };
       POBJECT_TYPE Type;
       //OBJECT_HEADER_NAME_INFO相对于此结构的偏移
       UCHAR NameInfoOffset;
       //OBJECT_HEADER_HANDLE_INFO相对于此结构的偏移
       UCHAR HandleInfoOffset;
       //OBJECT_HEADER_QUOTA_INFO相对于此结构的偏移
       UCHAR QuotaInfoOffset;
       UCHAR Flags;

       union
       {
           //创建对象是用于创建对象附加头的结构
           //里面保存了和附加对象头类似的信息
           PVOID ObjectCreateInfo;
           PVOID QuotaBlockCharged;
       };
       PSECURITY_DESCRIPTOR SecurityDescriptor;
       QUAD Body;
    } OBJECT_HEADER, *POBJECT_HEADER;



    NTSTATUS MyIopParseFile(
       IN PVOID ParseObject,
       IN PVOID ObjectType,
       IN PACCESS_STATE AccessState,
       IN KPROCESSOR_MODE AccessMode,
       IN ULONG Attributes,
       IN OUT PUNICODE_STRING CompleteName,
       IN OUT PUNICODE_STRING RemainingName,
       IN OUT PVOID Context OPTIONAL,
       IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
       OUT PVOID *Object)
    {
       NTSTATUS status;
       // 调用原始函数.
       status = g_oldObjectParse(ParseObject,
                                 ObjectType,
                                 AccessState,
                                 AccessMode,
                                 Attributes,
                                 CompleteName,
                                 RemainingName,
                                 Context,
                                 SecurityQos,
                                 Object);

       if (CompleteName)
       {
           KdPrint((" [MyIopParseFile]ComplateName=%wZ ", CompleteName));
       }
       if (RemainingName)
       {
           KdPrint((" [MyIopParseFile]RemainingName=%wZ ", RemainingName));
       }
       return status;
    }


    PVOID GetProcAddr(WCHAR* name)
    {
       UNICODE_STRING n;
       RtlInitUnicodeString(&n, name);
       return MmGetSystemRoutineAddress(&n);
    }
    void installHook()
    {
       DbgBreakPoint();

       // 获取ObTypeIndexTable首地址, 表的地址在ObGetObjectType函数内部中出现.
       // 因此, 取函数首地址, 加上一定偏移就能得到该表的首地址.
       // 在win7 32系统下, 该表在函数中的0xF偏移处.
       /*
       kd> u ObGetObjectType
       nt!ObGetObjectType:
       84291a72 8bff           mov     edi,edi
       84291a74 55             push   ebp
       84291a75 8bec           mov     ebp,esp
       84291a77 8b4508         mov     eax,dword ptr [ebp+8]
       84291a7a 0fb640f4       movzx   eax,byte ptr [eax-0Ch]
       84291a7e 8b0485c0801784 mov     eax,dword ptr nt!ObTypeIndexTable (841780c0)[eax*4]
       84291a85 5d             pop     ebp
       84291a86 c20400         ret     4
       */

       ULONG addr = ObGetObjectType;
       OBJECT_TYPE **typeIndexTable = (OBJECT_TYPE*)*(ULONG*)(addr + 0xF);

       ULONG i = 2;
       UNICODE_STRING fileTypeName;
       RtlInitUnicodeString(&fileTypeName, L"File");
       POBJECT_TYPE fileObjType = NULL;

       // 遍历TypeIndex表,找到File对象的原型
       while (typeIndexTable[i])
       {
           KdPrint(("Index=%d, Name=%wZ ", typeIndexTable[i]->Index, &typeIndexTable[i]->Name));

           // 判断类型名是否一致
           if (0==RtlCompareUnicodeString(&typeIndexTable[i]->Name, &fileTypeName, TRUE))
           {
               g_pFileObjType = typeIndexTable[i];
               break;
           }
           ++i;
       }

       // HOOK
       if (g_pFileObjType)
       {
           g_oldObjectParse = (FnIopParseFile)g_pFileObjType->TypeInfo.ParseProcedure;
           g_pFileObjType->TypeInfo.ParseProcedure = (PVOID)MyIopParseFile;
       }
    }

    void uninstallHook()
    {
       if (g_pFileObjType)
       {
           _asm sti; // 屏蔽中断
           // 将原始函数还原回去
           g_pFileObjType->TypeInfo.ParseProcedure = g_oldObjectParse;
           _asm cli; // 允许中断
       }
    }

    void unload(DRIVER_OBJECT* obj)
    {
       // 卸载HOOK
       uninstallHook();
    }

    NTSTATUS DriverEntry(PDRIVER_OBJECT object,
                         PUNICODE_STRING path)
    {
       NTSTATUS status;

       // 安装HOOK
       installHook();

       object->DriverUnload = unload;
       return STATUS_SUCCESS;
    }

    1567147093370

    把警告设置成否

  • 相关阅读:
    .Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow
    ElasticSearch入门 附.Net Core例子
    Asp.net Core 2.1新功能Generic Host(通用主机),了解一下
    CAP带你轻松玩转Asp.Net Core消息队列
    利用Asp.Net Core的MiddleWare思想处理复杂业务流程
    mysql8.0无法给用户授权或提示You are not allowed to create a user with GRANT的问题
    mysql8.0以后安装忘记密码或出现Access denied for user 'root'@'localhost' (using password: YES)
    Excel导出
    centOS7防火墙端口号
    CentOS7安装mysql服务器
  • 原文地址:https://www.cnblogs.com/ltyandy/p/11435349.html
Copyright © 2020-2023  润新知