64位下使用回调函数实现监控

news2025/2/24 15:25:28

前言

在32位的系统下,我们想要实现某些监控十分简单,只需要找到对应的API实现挂钩操作即可检测进程。但在64位系统下随着Patch Guard的引入,导致我们如果继续使用挂钩API的方式进行监控会出现不可控的情况发生。微软也考虑到了用户程序的开发,所以开放了方便用户调用的系统回调API函数,在64位系统下的监控,使用系统回调相对于直接hook的方式往往是更值得青睐的一方。

进程监控&保护

PsSetCreateProcessNotifyRoutineEx

这个函数主要是设置进程回调监控进程创建与退出

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  [in] BOOLEAN                           Remove
);

PsSetCreateProcessNotifyRoutineEx这个函数并不是随便就能够使用的,微软为了确保安全性要求拥有数字签名的驱动才能够使用此函数。这里微软如何检测是否有数字签名呢?这里就使用到了强制完整性检查

强制完整性检查是一种确保正在加载的二进制文件在加载前需要使用签名的策略,IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY标志在链接时通过使用/integritycheck链接器标志在PE头中进行设置,让正在加载的二进制文件必须签名,这个标志使windows内存管理器在加载时对二进制文件进行签名检查

那么微软就是通过加载二进制文件时是否存在标志来确认驱动的发布者身份是否为已知状态,这就是强制完整性检查

这里在内核里面,windows使用到MmVerifyCallbackFunction 这个内核函数来判断

到IDA里面继续跟MmVerifyCallbackFunction这个函数,发现其逻辑就是通过比较[rax+68h]是否包含了0x20来判断是否拥有正确的数字签名

这里的rax表示DriverSection,而DriverSection指向的是_LDR_DATA_TABLE_ENTRY结构,那么[rax + 0x68]指向的就是ProcessStaticImport

那么如果我们要使用PsSetCreateProcessNotifyRoutineEx这个函数就需要拥有数字签名,这里我们就可以将DriverObject->DriverSection->Flags的值与0x20按位或即可

这里我们就可以编写一个绕过强制完整性检查的函数,注意一下在32位和64位结构体的定义不同,需要分开定义

BOOLEAN bypass_signcheck(PDRIVER_OBJECT pDriverObject)
{
#ifdef _WIN64
 typedef struct _KLDR_DATA_TABLE_ENTRY
 {
  LIST_ENTRY listEntry;
  ULONG64 __Undefined1;
  ULONG64 __Undefined2;
  ULONG64 __Undefined3;
  ULONG64 NonPagedDebugInfo;
  ULONG64 DllBase;
  ULONG64 EntryPoint;
  ULONG SizeOfImage;
  UNICODE_STRING path;
  UNICODE_STRING name;
  ULONG   Flags;
  USHORT  LoadCount;
  USHORT  __Undefined5;
  ULONG64 __Undefined6;
  ULONG   CheckSum;
  ULONG   __padding1;
  ULONG   TimeDateStamp;
  ULONG   __padding2;
 } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
#else
 typedef struct _KLDR_DATA_TABLE_ENTRY
 {
  LIST_ENTRY listEntry;
  ULONG unknown1;
  ULONG unknown2;
  ULONG unknown3;
  ULONG unknown4;
  ULONG unknown5;
  ULONG unknown6;
  ULONG unknown7;
  UNICODE_STRING path;
  UNICODE_STRING name;
  ULONG   Flags;
 } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
#endif
 PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection;
 pLdrData->Flags = pLdrData->Flags | 0x20;

 return TRUE;

}

到这里我们就已经绕过了微软的强制完整性检查,能够调用PsSetCreateProcessNotifyRoutineEx函数,可以看到PsSetCreateProcessNotifyRoutineEx的第一个参数指向CREATE_PROCESS_NOTIFY_ROUTINE_EX,来执行我们需要执行的回调函数,这里我们继续看PCREATE_PROCESS_NOTIFY_ROUTINE_EX这个结构

PCREATE_PROCESS_NOTIFY_ROUTINE_EX

第一个参数是Process,指向EPROCESS结构,第二个参数ProcessId就是PID,第三个参数CreateInfo是一个指向PS_CREATE_NOTIFY_INFO的指针,当它为NULL时表明进程退出,不为NULL时表明进程创建,里面存储着要创建的进程信息

PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx;

void PcreateProcessNotifyRoutineEx(
  [_Inout_]           PEPROCESS Process,
  [in]                HANDLE ProcessId,
  [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo
)
{...}

msdn的定义如下

然后我们再去看一下PS_CREATE_NOTIFY_INFO

PS_CREATE_NOTIFY_INFO

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;
      ULONG IsSubsystemProcess : 1;
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

msdn定义如下

这里的话我们要注意两个值,一个是ImageFileName即要创建的进程名,一个是CreationStatus,我们可以看到msdn里面说驱动程序可以将此值修改为错误代码以防止创建进程,这里我们如果想阻止进程创建就可以把这个值设置为STATUS_UNSUCCESSFUL

我们去WRK里面看一下实现,这个API是64位才有的,所以在WRK里面是没有PsSetCreateProcessNotifyRoutineEx这个函数的,但是在32位下有一个PsSetCreateProcessNotifyRoutine,我们看一下

通过源码可以发现是操作数组,这个数组里面存放的是我们填写的回调,而操作系统会依次调用回调,那我们跟随数组查看发现是个定长数组,里面只有8项,在64位系统下,这个数组的长度变为了64项

根据PCREATE_PROCESS_NOTIFY_ROUTINE_EX的结构定义回调函数

VOID CreateProcessNotifyEx(
    __inout PEPROCESS  Process,
    __in HANDLE  ProcessId,
    __in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo
    );

那么我们这里通过PsSetCreateProcessNotifyRoutineEx设置回调函数,通过判断status的返回值判断回调函数是否设置成功

NTSTATUS SetReFunction()
{
 NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)CreateProcessNotifyEx, FALSE);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("回调函数设置失败, status=%X", status);
    }
    else
    {
        DbgPrint("进程监控已开启\r\n");
    }
}

然后进行回调函数的实现

VOID CreateProcessNotifyEx(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo)

首先判断CreateInfo的值,如果为NULL则表示进程退出,如果不为NULL才为进程的创建

if (CreateInfo == NULL)
{
    DbgPrint("进程退出\n");
    return;
}

那么这里通过PsGetProcessImageFileName获取进程名之后进行判断,如果是我们想要拦截的进程就通过设置CreationStatus的值为STATUS_UNSUCCESSFUL来阻止进程的创建

else
{
    pszImageFileName = PsGetProcessImageFileName(Process);

    if (pszImageFileName) 
        DbgPrint("新创建的进程是:%s\r\n", pszImageFileName);
    if (strcmp(pszImageFileName, "test.exe") == 0)
    {
        CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;
        DbgPrint("拦截进程:%s成功\r\n", pszImageFileName);
    }
} 

这里我们的回调函数就已经完成,这里需要注意,在卸载驱动的时候就需要将回调函数摘除,否则新创建或者退出的进程会因为找不到回调函数而导致蓝屏

VOID DriverUnload(IN PDEVICE_OBJECT driverObject)
{
    NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)CreateProcessNotifyEx, TRUE);
 
    if (!NT_SUCCESS(status))
    {
        DbgPrint("回调函数删除失败\r\n status=%X", status);
    }
    else
    {
        DbgPrint("回调函数成功删除\r\n");
    }
    DbgPrint("驱动卸载完成\r\n");
}

实现效果

首先注册一下驱动

然后这里首先执行一下我们的exe

然后加载我们的驱动可以看到这里test.exe已经不能够运行

那么这里我们再卸载一下驱动可以发现又可以运行成功

这里可能有点不太明显,我们将拦截的exe改成notepad.exe看下效果

启动驱动可以看到这里启动失败

卸载驱动即可启动成功

线程监控&保护

PsSetCreateThreadNotifyRoutine

线程监控使用到的API相对于进程监控简单,使用到PsSetCreateThreadNotifyRoutine,而这个值并不能像进程操作的API一样进行操作,这里我们首先先使用这个API来进行线程的监控

NTSTATUS PsSetCreateThreadNotifyRoutine(
  [in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

NotifyRoutine指向PCREATE_THREAD_NOTIFY_ROUTINE

PcreateThreadNotifyRoutine的结构如下,第一个参数为PID,第二个参数为TID,第三个参数表示是创建线程还是删除线程,创建线程则为TRUE,删除线程则为FALSE

PCREATE_THREAD_NOTIFY_ROUTINE PcreateThreadNotifyRoutine;

void PcreateThreadNotifyRoutine(
  [in] HANDLE ProcessId,
  [in] HANDLE ThreadId,
  [in] BOOLEAN Create
)
{...}

那么我们这里就可以写出CREATE_THREAD_NOTIFY_ROUTINE函数

VOID CreateThreadNotifyRoutine(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create)
{
    if (Create)
    {
        DbgPrint("新创建的线程ID为:%d,所属进程ID为:%d\r\n", ThreadId, ProcessId);
    }
    else
    {
        DbgPrint("新销毁的线程ID为:%d,所属进程ID为:%d\r\n", ThreadId, ProcessId);
    }
}

这里如果要将函数摘除,就需要用到PsRemoveCreateThreadNotifyRoutine函数,定义如下

NTSTATUS PsRemoveCreateThreadNotifyRoutine(
  [in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
NTSTATUS status = PsRemoveCreateThreadNotifyRoutine(CreateThreadNotifyRoutine);
 
if (!NT_SUCCESS(status))
{
    DbgPrint("回调函数删除失败\r\n status=%X", status);
}
else
{
    DbgPrint("回调函数成功删除\r\n");
}

我们如果想让进行线程的保护该如何操作呢?我们想要阻止线程的创建,就首先要找到对应的进程,再去进行拦截,因为在64位下都是通过回调的方式实现,那么我们就可以通过找到线程回调的地址,然后直接改为ret即可起到拦截线程创建的效果

1.通过PID找到EPROCESS

2.通过TID找到ETHREAD

3.通过EPROCESS得到进程路径

4.通过进程路径对应进程名

5.判断进程名是否相同

<1>若相同则找到线程回调函数的地址修改内容为ret

<2>若不相同则退出

那么我们该如何找到线程回调函数的地址呢?这里查阅资料后发现,3环将回调函数的地址放在了ETHREAD + 0x410偏移的Win32StartAddress里面

那么这里我们就能进行函数的编写,首先我们通过PsSetCreateThreadNotifyRoutine注册一个线程回调函数

NTSTATUS status = PsSetCreateThreadNotifyRoutine(CreateThreadNotify);

    if (!NT_SUCCESS(status))
    {
        DbgPrint("回调函数设置失败, status=%X", status);
    }
    else
    {
        DbgPrint("线程监控已开启\r\n");
    }

然后我们再去写CreateThreadNotify这个回调函数,首先获取EPROCESSETHREAD,然后通过EPROCESS获取进程名

status = PsLookupProcessByProcessId(ProcessId, &Process);  
if (!NT_SUCCESS(status))
  return;

status = PsLookupThreadByThreadId(ThreadId, &Thread);    

pszImageName = PsGetProcessImageFileName(Process);     

然后再判断进程名是否为我们要保护的线程

if (strstr(pszImageName, "notepad") != NULL) 

定位到回调函数的地址判断内存空间是否可用

pWin32Address = *(UCHAR**)((UCHAR*)Thread + 0x410);
if (MmIsAddressValid(pWin32Address))

这里的话定位到了回调函数的地址,如果我们要修改回调函数的值就要修改页保护属性,但是在64位下是不允许使用内联汇编的,这里的话就需要使用到汇编生成.obj文件来使用

      ClosePageProtect();
      if (MmIsAddressValid(pWin32Address))
      {
        *pWin32Address = 0xC3;                 
      }
      OpenPageProtect();

修改完成之后这里我们使用ObDereferenceObject ,减少引用计数

  if( Process )
    ObDereferenceObject(Process);  
  if( Thread )
    ObDereferenceObject(Thread);

这里因为我在win10 x64上做的实验,这里在关闭保护属性的时候一直报错导致0xC3一直修改不成功,这里就不放图了

模块监控&保护

PsSetLoadImageNotifyRoutine

和之前的函数一样都是指向一个结构,这里是LOAD_IMAGE_NOTIFY_ROUTINE

NTSTATUS PsSetLoadImageNotifyRoutine(
  [in] PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

PLOAD_IMAGE_NOTIFY_ROUTINE

这里第一个参数是指向缓冲的Unicode字符串的指针,用于标识可执行映像文件,第二个参数表示PID,第三个参数指向IMAGE_INFO

PLOAD_IMAGE_NOTIFY_ROUTINE SetLoadImageNotifyRoutine;
 
void SetLoadImageNotifyRoutine(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
)
{ ... }

IMAGE_INFO

typedef struct _IMAGE_INFO {
  union {
    ULONG Properties;
    struct {
      ULONG ImageAddressingMode : 8;
      ULONG SystemModeImage : 1;
      ULONG ImageMappedToAllPids : 1;
      ULONG ExtendedInfoPresent : 1;
      ULONG MachineTypeMismatch : 1;
      ULONG ImageSignatureLevel : 4;
      ULONG ImageSignatureType : 3;
      ULONG ImagePartialMap : 1;
      ULONG Reserved : 12;
    };
  };
  PVOID  ImageBase;
  ULONG  ImageSelector;
  SIZE_T ImageSize;
  ULONG  ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;

具体成员的作用如下

  • Properties ImageAddressingMode 始终设置为IMAGE_ADDRESSING_MODE_32BIT。

  • SystemModeImage 设置为一个用于新加载的内核模式组件(如驱动程序),或者对于映射到用户空间的映像设置为 0。

  • ImageMappedToAllPids 始终设置为0。

  • ExtendedInfoPresent 如果设置了ExtendedInfoPresent标志,则IMAGE_INFO结构是图像信息结构的较大扩展版本的一部分(请参阅IMAGE_INFO_EX)。在Windows Vista中添加。有关详细信息,请参阅本备注部分的“扩展版本的图像信息结构”。

  • MachineTypeMismatch 始终设置为 0。在Windows 8 / Windows Server 2012中添加。

  • ImageSignatureLevel 代码完整性标记为映像的签名级别。该值是ntddk.h中的#define SESIGNING_LEVEL *常量之一。在Windows 8.1 / Windows Server 2012 R2中添加。

  • ImageSignatureType 代码完整性标记为映像的签名类型。该值是在ntddk.h中定义的SE_IMAGE_SIGNATURE_TYPE枚举值。在Windows 8.1 / Windows Server 2012 R2中添加。

  • ImagePartialMap 如果调用的映像视图是不映射整个映像的部分视图,则该值不为零; 0如果视图映射整个图像。在Windows 10 / Windows Server 2016中添加。

  • Reserved 始终设置为 0。

  • ImageBase 设置为映像的虚拟基地址。

  • ImageSelector 始终设置为 0。

  • ImageSize 映像的虚拟大小(以字节为单位)。

  • ImageSectionNumber 始终设置为 0。

那么我们首先还是定义一下回调函数

void SetLoadImageNotifyRoutine(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
)

我们的回调函数在接收到消息的时候模块已经加载完成了,那么这里我们就只能够进行模块的卸载操作,在模块的ImageInfo 结构里面提供了加载的ImageBase,那么我们只需要找到OEP,即可计算得到DriverEntry的地址。那么我们找到入口点函数的地址之后,就可以修改错误码为STATUS_ACCESS_DENIED 0xC0000022,就能够达到卸载驱动模块的效果

mov eax, 0xC0000022
ret

对应的硬编码为B8 22 00 00 C0 C3

那么这里我们就可以进行卸载驱动模块函数的编写,首先定义指针指向OEP

PIMAGE_DOS_HEADER pDosHeader = pLoadImageBase;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PCHAR)pDosHeader + pDosHeader->e_lfanew);
PVOID pAddressOfEntryPoint = (PVOID)((PCHAR)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint);

然后写入shellcode,通过MmCreateMdl写入,我们知道在内核里面是不能够随便进行读写操作的,这里就可以通过MDL写入的方式映射到虚拟内存实现shellcode的写入

ULONG CodeSize = 6;
UCHAR pShellCode[6] = { 0xB8, 0x22, 0x00, 0x00, 0xC0, 0xC3 };
PMDL pMdl = MmCreateMdl(NULL, pAddressOfEntryPoint, CodeSize);

使用MmBuildMdlForNonPagedPool更新MDL对物理内存的描述并映射到虚拟内存,然后写入数据,释放MDL

MmBuildMdlForNonPagedPool(pMdl);
PVOID pVoid = MmMapLockedPages(pMdl, KernelMode);
RtlCopyMemory(pVoid, pShellCode, ulShellCodeSize);
MmUnmapLockedPages(pVoid, pMdl);
IoFreeMdl(pMdl);

然后我们再尝试对DLL模块进行卸载,这里的话就不能像卸载驱动模块直接在入口点返回,因为DLL的入口点函数的返回值并不能够确定DLL能否加载成功。这里windows提供了一个未文档化的函数MmUnmapViewOfSection用来卸载进程中已经加载的模块

那么我们要想卸载模块,首先就肯定要获取所有的模块,使用到PsSetLoadImageNotifyRoutine设置回调函数来获取模块的加载信息

windows为了避免死锁,在进行模块加载回调函数的时候不能够进行其他操作,也就是说我们想要卸载DLL模块则需要等所有模块加载完毕之后才能进行卸载操作

这里来进行函数的编写

NTSTATUS NoLoadDll(HANDLE ProcessId, PVOID pImageBase)
{
    NTSTATUS status = STATUS_SUCCESS;
    PROCESS pEProcess = NULL;
    
    status = PsLookupProcessByProcessId(ProcessId, &pEProcess);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("PsLookupProcessByProcessId error : %d\n", status);
        return status;
    }
    
    status = MmUnmapViewOfSection(pEProcess, pImageBase);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("MmUnmapViewOfSection error : %d\n", status);
        return status;
    }
    
    return status;
}

实现效果

这里要实现的效果就是阻止DriverTest.sys的加载

 

首先启动我们的监控驱动,然后加载DriverTest.sys可以看到拒绝访问

然后再卸载我们的监控驱动之后DriverTest.sys加载成功

然后我们再尝试注入Test.dll,可以看到注入失败

我们再去xp上尝试一下,首先加载驱动

当我们打开一个程序的时候都会打印出当前进程加载的dll模块

然后注入DLL,也是被拦截

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux shell脚本之笔记及实用笔记

一、前言 二、shell脚本之数据类型 2.1、数组遍历 1)数组定义 如果说变量是存储单个变量的内存空间,那么数组就是多个变量的集合,它存储多个元素在一片连续的内存空间中。在bash中,只支持一维数组,不支持多维数组。Linux Shell 数组用括号来表示,Bash Shell 只支持一维…

15. “接口隔离模式”之 Proxy模式

1. 动机 在面向对象系统中&#xff0c;有些对象由于某些原因&#xff08;比如对象创建的开销很大&#xff0c;或者某些操作需要安全控制&#xff0c;或者需要进程外的访问等&#xff09;&#xff0c;直接访问会给使用者、或者系统结构带来很多麻烦。如何在不失去透明操作对象的…

Java中值得注意的『运算符、逻辑控制、输入输出』

目录前言一、运算符&#x1f351;1、取模运算符%&#x1f351;2、增量运算符&#x1f351;3、逻辑运算符&#x1f351;4、位运算符二、逻辑控制语句&#x1f351;1、switch语句三、Java输入和输出&#x1f351;1、输出到控制台&#x1f351;2、从键盘输入四、猜数字游戏——Jav…

软件过程与项目管理复习(1)

文章目录Week1Project Introduction定义特点Project management项目管理的价值项目管理的5要素Project manager项目经理的技能要求project manager 的核心任务&#xff08;key activities)规划 planning组织 organizing领导 leading掌控 controllingAgile Scrum master 的核心任…

结构体超详解(小白一看就懂,多维度分析!!!!)

目录 一、前言 二、结构体详解 &#x1f350;什么是结构体 &#x1f34e;结构体的定义与基础结构 &#x1f351;结构体的使用 &#x1f4a6;结构体的初始化 &#x1f4a6;结构体的成员访问 &#x1f4a6;结构体数组 &#x1f4a6;结构体指针--------------指向结构体变…

PNAS:人类头皮记录电位的时间尺度

导读 人类的许多行为都是由在不同时间尺度上发生的共同过程所支配的。标准的事件相关电位分析假设有关实验事件的响应持续时间是固定的。然而&#xff0c;最近对动物的单个单元记录显示&#xff0c;在需要灵活计时的行为中&#xff0c;神经活动尺度跨越了不同的持续时间。本研…

vue3——使用axios

1、Axios 是什么? 浏览器页面在向服务器请求数据时&#xff0c;因为返回的是整个页面的数据&#xff0c;页面都会强制刷新一下&#xff0c;这对于用户来讲并不是很友好。并且我们只是需要修改页面的部分数据&#xff0c;但是从服务器端发送的却是整个页面的数据&#xff0c;十…

Keras深度学习实战(33)——基于LSTM的序列预测模型

Keras深度学习实战&#xff08;33&#xff09;——基于LSTM的序列预测模型0. 前言1. 序列学习任务1.1 命名实体提取1.2 文本摘要1.3 机器翻译2. 从输出网络返回输出序列2.1 传统模型体系结构2.2 返回每个时间戳的网络中间状态序列2.3 使用双向 LSTM 网络小结系列链接0. 前言 在…

Qt易忘样式表总结

目录前言1、Qt设置样式的几种方式2、几种复合控件的样式设置QTableWidgetQCalendarWidgetQTreeWidgetQSpinBoxQComboBox前言 在使用Qt框架开发软件时&#xff0c;为了美观和更好的用户体验&#xff0c;需要为各种控件设置样式。一些通用且简单的样式如背景色、边框、字体字号等…

js实现图片懒加载

js实现图片懒加载 1、介绍getBoundingClientRect()函数 该函数没有参数&#xff0c;用于获取元素位置&#xff0c;返回一个对象&#xff0c;具有六个属性分别是&#xff1a; ele.getBoundingClientRect().top – 返回元素上边到视窗上边的距离 ele.getBoundingClientRect().l…

【经典面试题-LeetCode134:加油站问题(Java实现)】

加油站问题一、题目描述1.题目内容2.样例二、解决方案1.分析2.核心代码一、题目描述 1.题目内容 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i…

自动控制原理 - 2 控制系统的数学模型 节2.4-2.6

------------------------------------------------------------------------------ 2 控制系统的数学模型2.4 控制系统的传递函数2.5 典型环节的传递函数2.6 结构图的绘制 2 控制系统的数学模型 2.4 控制系统的传递函数 为何引入传递函数&#xff1f; 微分方程模型的优缺…

Webpack面试题总结

说说你对webpack的理解&#xff1f;解决了什么问题&#xff1f; webpack最初的目标是实现前端项目模块化&#xff0c;目的在于更高效的管理和维护项目中的每一个资源 模块化&#xff1a; 最早的时候&#xff0c;我们通过文件划分的形式实现模块化&#xff0c;也就是将每个功能…

JVM性能——开启回收日志和实时查看GC信息

JVM性能——开启回收日志和实时查看GC信息 JDK版本&#xff1a;OpenJDK 1.8.0_352-b08 操作系统&#xff1a;CentOS 7 如果文章内出现测试数据测试代码 depth:23 关于JVM的其他文章 JVM性能——垃圾回收器的优化策略 JVM性能——垃圾回收器的介绍 JVM性能——开启回收日…

网络赚钱项目 - 虚拟项目如何选择产品

今日一个老粉找我&#xff0c;他去年3月份就重视我跟我互动了&#xff0c;他上来并不是索取什么&#xff0c;而是给我信息&#xff0c;比如他最近测验了什么产品&#xff0c;什么产品好卖&#xff0c;都会把测验的数据告诉我&#xff0c;当然作为回报&#xff0c;我也会告诉他一…

Centos7宝塔部署python

前言&#xff1a;宝塔本身有python项目管理器&#xff0c;但是有些依赖安装会存在问题&#xff0c;比如paddlehub安装总是失败&#xff0c;本文在宝塔安装了anaconda基础上配合python项目管理器去部署项目&#xff0c;保障依赖隔离不混乱。 centos7宝塔安装conda流程地址&…

Linux使用YUM安装程序

目录 前言 什么是YUM&#xff1f; 1.本地源&#xff1a;系统安装光盘 目的 1&#xff09;观察YUM核心配置目录 2&#xff09;删除官方YUM库 3&#xff09;编写本地YUM库配置文件 4&#xff09;挂载安装光盘 保证把光盘放到服务器上&#xff0c;通电 挂载 5&#xff0…

np中的normalize/histogram/cumsum/interp函数

一、normalize函数 void normalize(InputArray src,OutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray() ) 归一化就是要把需要处理的数据经过处理后 (通过某种算法)限制在你需要的一定范围内。首先归一…

深入注册表监控

前言 注册表是windows的重要数据库&#xff0c;存放了很多重要的信息以及一些应用的设置&#xff0c;对注册表进行监控并防止篡改是十分有必要的。在64位系统下微软提供了CmRegisterCallback这个回调函数来实时监控注册表的操作&#xff0c;那么既然这里微软提供了这么一个方便…

用红黑树封装map和set

文章目录:红黑树模拟实现完整代码红黑树参数适配改造仿函数正向迭代器反向迭代器红黑树封装后的代码map完整代码set完整代码红黑树模拟实现完整代码 如下是红黑树kv模型的模拟实现完整代码&#xff0c;现在我们需要基于此代码封装出map和set基本的接口实现。 #pragma once #i…