驱动开发:摘除InlineHook内核钩子

news2024/10/7 16:17:05

在笔者上一篇文章《驱动开发:内核层InlineHook挂钩函数》中介绍了通过替换函数头部代码的方式实现Hook挂钩,对于ARK工具来说实现扫描与摘除InlineHook钩子也是最基本的功能,此类功能的实现一般可在应用层进行,而驱动层只需要保留一个读写字节的函数即可,将复杂的流程放在应用层实现是一个非常明智的选择,与《驱动开发:内核实现进程反汇编》中所使用的读写驱动基本一致,本篇文章中的驱动只保留两个功能,控制信号IOCTL_GET_CUR_CODE用于读取函数的前16个字节的内存,信号IOCTL_SET_ORI_CODE则用于设置前16个字节的内存。

之所以是前16个字节是因为一般的内联Hook只需要使用两条指令就可实现劫持,如下是通用ARK工具扫描到的被挂钩函数的样子。

首先将内核驱动程序代码放到如下,内核驱动程序没有任何特别的,仅仅只是一个通用驱动模板,在其基础上使用CR3读写,如果不理解CR3读写的原理您可以去看《驱动开发:内核CR3切换读写内存》这一篇中的详细介绍。

// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com

#include <ntifs.h>
#include <intrin.h>
#include <windef.h>

#define	DEVICE_NAME			L"\\Device\\WinDDK"
#define LINK_NAME			L"\\DosDevices\\WinDDK"
#define LINK_GLOBAL_NAME	L"\\DosDevices\\Global\\WinDDK"

// 控制信号 IOCTL_GET_CUR_CODE 用于读 | IOCTL_SET_ORI_CODE 用于写
#define IOCTL_GET_CUR_CODE	CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_SET_ORI_CODE	CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

// 引用__readcr0等函数必须增加
#pragma intrinsic(_disable)
#pragma intrinsic(_enable)

// 定义读写结构体
typedef struct
{
	PVOID Address;
	ULONG64 Length;
	UCHAR data[256];
} KF_DATA, *PKF_DATA;

KIRQL g_irql;

// 关闭写保护
void WPOFFx64()
{
	ULONG64 cr0;
	g_irql = KeRaiseIrqlToDpcLevel();
	cr0 = __readcr0();
	cr0 &= 0xfffffffffffeffff;
	__writecr0(cr0);
	_disable();
}

// 开启写保护
void WPONx64()
{
	ULONG64 cr0;
	cr0 = __readcr0();
	cr0 |= 0x10000;
	_enable();
	__writecr0(cr0);
	KeLowerIrql(g_irql);
}

// 设备创建时触发
NTSTATUS DispatchCreate(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;

	DbgPrint("[LyShark] 设备已创建 \n");
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

// 设备关闭时触发
NTSTATUS DispatchClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;

	DbgPrint("[LyShark] 设备已关闭 \n");
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

// 主派遣函数
NTSTATUS DispatchIoctl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
	NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
	PIO_STACK_LOCATION pIrpStack;
	ULONG uIoControlCode;
	PVOID pIoBuffer;
	ULONG uInSize;
	ULONG uOutSize;

	// 获取当前设备栈
	pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
	uIoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;

	// 获取缓冲区
	pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;

	// 获取缓冲区长度
	uInSize = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;

	// 输出缓冲区长度
	uOutSize = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;

	switch (uIoControlCode)
	{
		// 读内存
	case IOCTL_GET_CUR_CODE:
	{
		KF_DATA dat = { 0 };

		// 将缓冲区格式化为KF_DATA结构体
		RtlCopyMemory(&dat, pIoBuffer, 16);
		WPOFFx64();

		// 将数据写回到缓冲区
		RtlCopyMemory(pIoBuffer, dat.Address, dat.Length);
		WPONx64();
		status = STATUS_SUCCESS;
		break;
	}
	// 写内存
	case IOCTL_SET_ORI_CODE:
	{
		KF_DATA dat = { 0 };

		// 将缓冲区格式化为KF_DATA结构体
		RtlCopyMemory(&dat, pIoBuffer, sizeof(KF_DATA));
		WPOFFx64();

		// 将数据写回到缓冲区
		RtlCopyMemory(dat.Address, dat.data, dat.Length);
		WPONx64();
		status = STATUS_SUCCESS;
		break;
	}
	}

	if (status == STATUS_SUCCESS)
		pIrp->IoStatus.Information = uOutSize;
	else
		pIrp->IoStatus.Information = 0;

	pIrp->IoStatus.Status = status;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

// 驱动卸载
VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
	UNICODE_STRING strLink;

	// 删除符号链接卸载设备
	RtlInitUnicodeString(&strLink, LINK_NAME);
	IoDeleteSymbolicLink(&strLink);
	IoDeleteDevice(pDriverObj->DeviceObject);
}

// 驱动程序入口
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryString)
{
	NTSTATUS status = STATUS_SUCCESS;
	UNICODE_STRING ustrLinkName;
	UNICODE_STRING ustrDevName;
	PDEVICE_OBJECT pDevObj;

	// 初始化派遣函数
	pDriverObj->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
	pDriverObj->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
	pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;

	DbgPrint("hello lysahrk.com \n");

	// 初始化设备名
	RtlInitUnicodeString(&ustrDevName, DEVICE_NAME);

	// 创建设备
	status = IoCreateDevice(pDriverObj, 0, &ustrDevName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDevObj);
	if (!NT_SUCCESS(status))
	{
		return status;
	}

	// 创建符号链接
	RtlInitUnicodeString(&ustrLinkName, LINK_NAME);
	status = IoCreateSymbolicLink(&ustrLinkName, &ustrDevName);
	if (!NT_SUCCESS(status))
	{
		IoDeleteDevice(pDevObj);
		return status;
	}

	pDriverObj->DriverUnload = DriverUnload;
	return STATUS_SUCCESS;
}

接着来分析下应用层做了什么,首先GetKernelBase64函数的作用,该函数内部通过GetProcAddress()函数动态寻找到ZwQuerySystemInformation()函数的内存地址(此函数未被到处所以只能动态找到),找到后调用ZwQuerySystemInformation()直接拿到系统中的所有模块信息,通过pSystemModuleInformation->Module[0].Base得到系统中第一个模块的基地址,此模块就是ntoskrnl.exe,该模块也是系统运行后的第一个启动的,此时我们即可拿到KernelBase也就是系统内存中的基地址。

此时通过LoadLibraryExA()函数动态加载,此时加载的是磁盘中的被Hook函数的所属模块,获得映射地址后将此地址装入hKernel变量内,此时我们拥有了内存中的KernelBase以及磁盘中加载的hKernel,接着调用RepairRelocationTable()让两者的重定位表保持一致。

此时当用户调用GetSystemRoutineAddress()则执行如下流程,想要获取当前内存地址,则需要使用当前内存中的KernelBase模块基址加上通过GetProcAddress()动态获取到的磁盘基址中的函数地址减去磁盘中的基地址,将内存中的KernelBase加上磁盘中的相对偏移就得到了当前内存中加载函数的实际地址。

  • address1 = KernelBase + (ULONG64)GetProcAddress(hKernel, “NtWriteFile”) - (ULONG64)hKernel
  • address2 = KernelBase - (ULONG64)hKernel + (ULONG64)GetProcAddress(hKernel, “NtWriteFile”)

调用GetOriginalMachineCode()则用于获取相对偏移地址,该地址的获取方式如下,用户传入一个Address当前地址,该地址减去KernelBase内存中的基址,然后再加上hKernel磁盘加载的基址来获取到相对偏移。

  • OffsetAddress = Address - KernelBase + hKernel

有了这两条信息那么功能也就实现了,通过GetOriginalMachineCode()得到指定内存地址处原始机器码,通过GetCurrentMachineCode()得到当前内存机器码,两者通过memcmp()函数比对即可知道是否被挂钩了,如果被挂钩则可以通过CR3切换将原始机器码覆盖到特定位置替换即可,这段程序的完整代码如下;

// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com
#include <stdio.h>
#include <Windows.h>

#pragma comment(lib,"user32.lib")
#pragma comment(lib,"Advapi32.lib")

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
#endif

#define BYTE_ARRAY_LENGTH 16
#define SystemModuleInformation 11
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)

typedef long(__stdcall *ZWQUERYSYSTEMINFORMATION)
(
	IN ULONG SystemInformationClass,
	IN PVOID SystemInformation,
	IN ULONG SystemInformationLength,
	IN PULONG ReturnLength OPTIONAL
);

typedef struct
{
	ULONG Unknow1;
	ULONG Unknow2;
	ULONG Unknow3;
	ULONG Unknow4;
	PVOID Base;
	ULONG Size;
	ULONG Flags;
	USHORT Index;
	USHORT NameLength;
	USHORT LoadCount;
	USHORT ModuleNameOffset;
	char ImageName[256];
} SYSTEM_MODULE_INFORMATION_ENTRY, *PSYSTEM_MODULE_INFORMATION_ENTRY;

typedef struct
{
	ULONG Count;
	SYSTEM_MODULE_INFORMATION_ENTRY Module[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

typedef struct
{
	PVOID Address;
	ULONG64 Length;
	UCHAR data[256];
} KF_DATA, *PKF_DATA;

HANDLE hDriver = 0;
HMODULE	hKernel = 0;
ULONG64	KernelBase = 0;
CHAR NtosFullName[260] = { 0 };

// 生成控制信号
DWORD CTL_CODE_GEN(DWORD lngFunction)
{
	return (FILE_DEVICE_UNKNOWN * 65536) | (FILE_ANY_ACCESS * 16384) | (lngFunction * 4) | METHOD_BUFFERED;
}

// 发送控制信号的函数
BOOL IoControl(HANDLE hDrvHandle, DWORD dwIoControlCode, PVOID lpInBuffer, DWORD nInBufferSize, PVOID lpOutBuffer, DWORD nOutBufferSize)
{
	DWORD lDrvRetSize;
	return DeviceIoControl(hDrvHandle, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, &lDrvRetSize, 0);
}

// 动态获取ntdll.dll模块的基地址
ULONG64 GetKernelBase64(PCHAR NtosName)
{
	ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation;
	PSYSTEM_MODULE_INFORMATION pSystemModuleInformation;
	ULONG NeedSize, BufferSize = 0x5000;
	PVOID pBuffer = NULL;
	NTSTATUS Result;

	// 该函数只能通过动态方式得到地址
	ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwQuerySystemInformation");
	do
	{
		pBuffer = malloc(BufferSize);
		if (pBuffer == NULL) return 0;

		// 查询系统中的所有模块信息
		Result = ZwQuerySystemInformation(SystemModuleInformation, pBuffer, BufferSize, &NeedSize);
		if (Result == STATUS_INFO_LENGTH_MISMATCH)
		{
			free(pBuffer);
			BufferSize *= 2;
		}
		else if (!NT_SUCCESS(Result))
		{
			free(pBuffer);
			return 0;
		}
	} while (Result == STATUS_INFO_LENGTH_MISMATCH);

	// 取模块信息结构
	pSystemModuleInformation = (PSYSTEM_MODULE_INFORMATION)pBuffer;

	// 得到模块基地址
	ULONG64 ret = (ULONG64)(pSystemModuleInformation->Module[0].Base);

	// 拷贝模块名
	if (NtosName != NULL)
	{
		strcpy(NtosName, pSystemModuleInformation->Module[0].ImageName + pSystemModuleInformation->Module[0].ModuleNameOffset);
	}

	free(pBuffer);
	return ret;
}

// 判断并修复重定位表
BOOL RepairRelocationTable(ULONG64 HandleInFile, ULONG64 BaseInKernel)
{
	PIMAGE_DOS_HEADER		pDosHeader;
	PIMAGE_NT_HEADERS64		pNtHeader;
	PIMAGE_BASE_RELOCATION	pRelocTable;
	ULONG i, dwOldProtect;

	// 得到DOS头并判断是否符合DOS规范
	pDosHeader = (PIMAGE_DOS_HEADER)HandleInFile;
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		return FALSE;
	}

	// 得到Nt头
	pNtHeader = (PIMAGE_NT_HEADERS64)((ULONG64)HandleInFile + pDosHeader->e_lfanew);

	// 是否存在重定位表
	if (pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size)
	{
		// 获取到重定位表基地址
		pRelocTable = (PIMAGE_BASE_RELOCATION)((ULONG64)HandleInFile + pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

		do
		{
			// 得到重定位号
			ULONG	numofReloc = (pRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;
			SHORT	minioffset = 0;
			
			// 得到重定位数据
			PUSHORT pRelocData = (PUSHORT)((ULONG64)pRelocTable + sizeof(IMAGE_BASE_RELOCATION));

			// 循环或直接判断*pRelocData是否为0也可以作为结束标记
			for (i = 0; i<numofReloc; i++)
			{
				// 需要重定位的地址
				PULONG64 RelocAddress;

				// 重定位的高4位是重定位类型,判断重定位类型
				if (((*pRelocData) >> 12) == IMAGE_REL_BASED_DIR64)
				{
					// 计算需要进行重定位的地址
					// 重定位数据的低12位再加上本重定位块头的RVA即真正需要重定位的数据的RVA
					minioffset = (*pRelocData) & 0xFFF; // 小偏移

					// 模块基址+重定位基址+每个数据表示的小偏移量
					RelocAddress = (PULONG64)(HandleInFile + pRelocTable->VirtualAddress + minioffset);

					// 直接在RING3修改: 原始数据+基址-IMAGE_OPTINAL_HEADER中的基址
					VirtualProtect((PVOID)RelocAddress, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);

					// 因为是R3直接LOAD的所以要修改一下内存权限
					*RelocAddress = *RelocAddress + BaseInKernel - pNtHeader->OptionalHeader.ImageBase;
					VirtualProtect((PVOID)RelocAddress, 4, dwOldProtect, NULL);
				}
				// 下一个重定位数据
				pRelocData++;
			}
			// 下一个重定位块
			pRelocTable = (PIMAGE_BASE_RELOCATION)((ULONG64)pRelocTable + pRelocTable->SizeOfBlock);
		} while (pRelocTable->VirtualAddress);

		return TRUE;
	}
	return FALSE;
}

// 初始化
BOOL InitEngine(BOOL IsClear)
{
	if (IsClear == TRUE)
	{
		// 动态获取ntdll.dll模块的基地址
		KernelBase = GetKernelBase64(NtosFullName);
		printf("模块基址: %llx | 模块名: %s \n", KernelBase, NtosFullName);
		if (!KernelBase)
		{
			return FALSE;
		}
			
		// 动态加载模块到内存,并获取到模块句柄
		hKernel = LoadLibraryExA(NtosFullName, 0, DONT_RESOLVE_DLL_REFERENCES);

		if (!hKernel)
		{
			return FALSE;
		}

		// 判断并修复重定位表
		if (!RepairRelocationTable((ULONG64)hKernel, KernelBase))
		{
			return FALSE;
		}
		return TRUE;
	}
	else
	{
		FreeLibrary(hKernel);
		return TRUE;
	}
}

// 获取原始函数机器码
VOID GetOriginalMachineCode(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
	ULONG64 OffsetAddress = Address - KernelBase + (ULONG64)hKernel;
	RtlCopyMemory(ba, (PVOID)OffsetAddress, Length);
}

// 获取传入函数的内存地址
ULONG64 GetSystemRoutineAddress(PCHAR FuncName)
{
	return KernelBase + (ULONG64)GetProcAddress(hKernel, FuncName) - (ULONG64)hKernel;
}

// 获取当前函数机器码
VOID GetCurrentMachineCode(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
	ULONG64 dat[2] = { 0 };
	dat[0] = Address;
	dat[1] = Length;
	IoControl(hDriver, CTL_CODE_GEN(0x800), dat, 16, ba, Length);
}

// 清除特定位置的机器码
VOID ClearInlineHook(ULONG64 Address, PUCHAR ba, SIZE_T Length)
{
	KF_DATA dat = { 0 };
	dat.Address = (PVOID)Address;
	dat.Length = Length;

	// 直接调用写出控制码
	RtlCopyMemory(dat.data, ba, Length);
	IoControl(hDriver, CTL_CODE_GEN(0x801), &dat, sizeof(KF_DATA), 0, 0);
}

// 打印数据
VOID PrintBytes(PCHAR DescriptionString, PUCHAR ba, UINT Length)
{
	printf("%s", DescriptionString);
	for (UINT i = 0; i<Length; i++)
	{
		printf("%02x ", ba[i]);
	}
	printf("\n");
}

int main(int argc, char *argv[])
{
	UCHAR OriginalMachineCode[BYTE_ARRAY_LENGTH];
	UCHAR CurrentMachineCode[BYTE_ARRAY_LENGTH];
	ULONG64 Address = 0;

	hDriver = CreateFileA("\\\\.\\WinDDK", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	// 初始化
	if (!InitEngine(TRUE) || hDriver == 0)
	{
		return 0;
	}

	// 需要获取的函数列表
	CHAR *FunctionList[128] = { "PsLookupProcessByProcessId", "NtCommitEnlistment", "NtCommitComplete", "NtCommitTransaction" };

	for (size_t i = 0; i < 4; i++)
	{
		// 清空缓存
		RtlZeroMemory(OriginalMachineCode, 0, BYTE_ARRAY_LENGTH);
		RtlZeroMemory(CurrentMachineCode, 0, BYTE_ARRAY_LENGTH);

		// 获取到当前函数地址
		Address = GetSystemRoutineAddress(FunctionList[i]);

		printf("\n函数地址: %p | 函数名: %s\n", Address, FunctionList[i]);
		if (Address == 0 || Address < KernelBase)
		{
			return 0;
		}

		GetOriginalMachineCode(Address, OriginalMachineCode, BYTE_ARRAY_LENGTH);
		PrintBytes("原始机器码: ", OriginalMachineCode, BYTE_ARRAY_LENGTH);

		GetCurrentMachineCode(Address, CurrentMachineCode, BYTE_ARRAY_LENGTH);
		PrintBytes("当前机器码: ", CurrentMachineCode, BYTE_ARRAY_LENGTH);

		/*
		// 不相同则询问是否恢复
		if (memcmp(OriginalMachineCode, CurrentMachineCode, BYTE_ARRAY_LENGTH))
		{
			printf("按下[ENTER]恢复钩子");
			getchar();
			ClearInlineHook(Address, OriginalMachineCode, BYTE_ARRAY_LENGTH);
		}
		*/
	}

	// 注销
	InitEngine(FALSE);
	system("pause");

	return 0;
}

首先编译驱动程序WinDDK.sys并通过KmdManager将驱动程序拉起来,运行客户端lyshark.exe程序会输出当前FunctionList列表中,指定的4个函数的挂钩情况。

参考文献

WIN64内核编程基础 胡文亮

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

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

相关文章

idea如何使用git指令

&#xff08;1&#xff09;、打开setting,直接搜git (2)、点一下text 如果显示成功就不用管了&#xff0c;但如果失败就要重新设置一下目录&#xff1a; &#xff08;3&#xff09;、找到自己设置gitee ssh目录&#xff1a; 这里我们是不需要用.git对.idea进行管理的&#xff…

vue3原理和源码分析 - VirtualDOM和DOMDIFF

目录 VUE3的h/createVNode函数【vue的概念模型】 Virtual DOM&#xff08;组件化的概念模型&#xff09; VirtualDOM更新 WHY DOM-DIFF&#xff1f; DOM-DIFF原理 DOM-DIFF伪代码 DOM-DIFF分类讨论&#xff1a;属性变更 DOM-DIFF分类讨论&#xff1a;节点类型不同 DOM-…

正负整数小数在内存的存储

目录 补码引入 整数在内存的存储 小数十进制与二进制相互转化 小数的内存存储 本节会主要学习正负的整数小数在内存的存储方式 补码引入 补码在计算机有着重要的地位&#xff0c;计算机本身只能完成的加法&#xff0c;移位运算&#xff0c;减法&#xff0c;乘法&#xff0…

深入解析多人共享云盘:便捷文件协作与分享的全新模式

在当今数字化时代&#xff0c;云盘已成为许多人存储和共享文件的首选方式。但是&#xff0c;传统的个人云盘服务在多人协作方面存在一些限制。为了解决这个问题&#xff0c;多人共享云盘应运而生。什么是多人共享云盘&#xff1f; 多人共享云盘是一种允许多个用户同时访问、编辑…

【数据库六】存储过程

存储过程 1.存储过程概述1.1 存储过程定义1.2 存储过程优点1.3 创建存储过程 2. 存储过程参数2.1 输入参数2.2 输出参数2.3 输入输出参数2.4 存储过程参数总结 3. 删除存储过程4.存储过程的控制语句4.1 条件语句if --else4.2 循环语句4.3 存储过程控制语句总结 1.存储过程概述 …

Qt 实现SQLite全部语法(增删改查、内置函数、高级语法)

Qt 实现SQLite全部语法 【1】SQLite Qt界面设计【2】SQLite Qt数据库创建、打开、关闭、删除【3】SQLite Qt表格的创建【4】SQLite Qt表格的插入【5】SQLite Qt表格的查询【6】SQLite Qt表格的删除【7】SQLite Qt表格的更新【8】SQLite Qt表格的结构【9】SQLite Qt表格的修改【…

Python 教程:从零到大师

首先, 什么是Python? 用python作者Guido van Rossum自己的话来说&#xff0c;Python是这样的一门语言&#xff1a; "它是一门高级编程语言, 它的核心设计理念是让所有代码变得更易阅读&#xff0c;并给开发者们提供一种“仅仅几行代码就能编写编程逻辑”的语法。 那么&am…

SD/StableDiffusion部署图文教程,ai绘画教程,实现谷歌云端零成本部署,中文UI

目录 一、前言 二、准备前提 三、教程说明 四、开始搭建 1、第一步&#xff0c;下载ipynb脚本文件 2、第二步&#xff0c;上传一键脚本文件到谷歌云盘 3、选择该.ipynb文件--右键--打开方式--关联更多应用 4、输入框搜索Colaboratory找到该应用&#xff0c;安装 5、安…

【瑞萨RA6系列】RASC+Keil开发环境搭建和GPIO点灯指南

瑞萨RASCKeil开发环境搭建 一、简单开箱二、资料下载三、芯片简介四、开发环境搭建4.1 安装RASC4.2 安装Keil MDK4.3 安装RA6E1的MDK支持包 五、GPIO点灯指南5.1 创建RASC项目5.2 查阅开发板原理图5.3 设置LED1引脚为输出3.4 编写LED1闪烁的代码5.5 编译Keil项目5.6 修改Keil调…

产品设计.B端设计师不可忽视的产品和用户

B端产品与C端产品不同&#xff0c;前者强调客户价值&#xff0c;企业决策链路长&#xff0c;用户难获得的同时也相对难流失。而作为B端产品设计师&#xff0c;就需要根据B端产品业务特点&#xff0c;从用户、产品等角度进行考量&#xff0c;以求做出符合市场和用户的设计方案。…

【HTTP 协议】

一、HTTP 协议简介 在真实的网络环境中采用 TCP/IP 五层网络传输模型这样的结构传输. 物理层 -> 数据链路层 -> 网络层 -> 传输层 -> 应用层 1. 应用层: 应用层是模型的最顶层&#xff0c;它为用户提供了一种与网络进行通信的方法。应用层包含了各种应用程序&…

【Linux基础及shell脚本】在VMware16中安装CentOS7.6

文章目录 1. Linux和CentOS2. 虚拟机3. 为什么选择在VMware上安装CentOS&#xff1f;4. 准备工作5. 创建新的虚拟机6. 安装CentOS 7.67. 初次启动和设置 在我们了解如何在VMware16中安装CentOS 7.6之前&#xff0c;让我们首先对Linux、CentOS以及虚拟机有一些基本的认识。 1. …

【性能测试一】性能测试概述

目录 &#x1f31f;一、性能测试的基础概念 &#x1f308;1、生活中软件相关的性能问题&#xff1f; &#x1f308;2、性能测试的概念 &#x1f308;3、性能测试与功能测试的区别&#xff1f; &#x1f308;4、什么样的软件属于性能好&#xff1f;什么样的软件属于性能不好…

搭建Vue项目以及项目的常见知识

前言&#xff1a;使用脚手架搭建vue项目&#xff0c;使用脚手架可以开发者能够开箱即用快速地进行应用开发而开发。 搭建 #创建一个基于 webpack 模板的新项目 vue init webpack my-project #选择所需要的选项如图&#xff1a; cd my-project npm run dev访问localhost:808…

分布式数据库架构

分布式数据库架构 1、MySQL常见架构设计 对于mysql架构&#xff0c;一定会使用到读写分离&#xff0c;在此基础上有五种常见架构设计&#xff1a;一主一从或多从、主主复制、级联复制、主主与级联复制结合。 1.1、主从复制 这种架构设计是使用的最多的。在读写分离的基础上…

SpringBoot源码分析(三):SpringBoot的事件分发机制

文章目录 通过源码明晰的几个问题Spring 中的事件Springboot 是怎么做到事件监听的另外两种注册的Listener 源码解析加载listenerSpringApplicationRunListenerEventPublishingRunListenerSimpleApplicationEventMulticaster判断 listener 是否可以接收事件Java 泛型获取 整体流…

【前端|HTML系列第1篇】HTML的基础介绍与初次尝试

大家好&#xff0c;欢迎来到前端入门系列的第一篇博客。在这个系列中&#xff0c;我们将一起学习前端开发的基础知识&#xff0c;从零开始构建网页和Web应用程序。本篇博客将为大家介绍HTML&#xff08;超文本标记语言&#xff09;的基础概念和标签&#xff0c;帮助你快速入门。…

Git进阶系列 | 6. 交互式Rebase

Git是最流行的代码版本控制系统&#xff0c;这一系列文章介绍了一些Git的高阶使用方式&#xff0c;从而帮助我们可以更好的利用Git的能力。本系列一共8篇文章&#xff0c;这是第6篇。原文&#xff1a;Interactive Rebase: Clean up your Commit History[1] 交互式Rebase是Git命…

数据结构:二叉树经典例题(单选题)-->你真的掌握二叉树了吗?(第二弹)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关二叉树的经典例题&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数…

百度开源,一款强大的压测工具,可模拟几十亿并发场景

优点 性能强大统计信息详细使用场景丰富性能 HTTP 每秒新建连接数HTTP 吞吐HTTP 并发连接数UDP TX PPS测试环境配置统计数据开始使用 设置大页编译 DPDK编译 dperf绑定网卡启动 dperf server从客户端发送请求运行测试开源地址 dperf 是一款基于 DPDK 的 100Gbps 网络性能和负载…