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

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

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

#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切换将原始机器码覆盖到特定位置替换即可,这段程序的完整代码如下;

#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个函数的挂钩情况。

5.10 Windows驱动开发:摘除InlineHook内核钩子的更多相关文章

  1. Windows驱动开发(中间层)

    Windows驱动开发 一.前言 依据<Windows内核安全与驱动开发>及MSDN等网络质料进行学习开发. 二.初步环境 1.下载安装WDK7.1.0(WinDDK\7600.16385 ...

  2. [Windows驱动开发](一)序言

    笔者学习驱动编程是从两本书入门的.它们分别是<寒江独钓——内核安全编程>和<Windows驱动开发技术详解>.两本书分别从不同的角度介绍了驱动程序的制作方法. 在我理解,驱动程 ...

  3. windows驱动开发推荐书籍

    [作者] 猪头三 个人网站 :http://www.x86asm.com/ [序言] 很多人都对驱动开发有兴趣,但往往找不到正确的学习方式.当然这跟驱动开发的本土化资料少有关系.大多学的驱动开发资料都 ...

  4. windows 驱动开发入门——驱动中的数据结构

    最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书--<独钓寒江 windows安全编程> 和 <windows驱动 ...

  5. Windows驱动——读书笔记《Windows驱动开发技术详解》

    =================================版权声明================================= 版权声明:原创文章 谢绝转载  请通过右侧公告中的“联系邮 ...

  6. Windows驱动开发-IRP的完成例程

    <Windows驱动开发技术详解 >331页, 在将IRP发送给底层驱动或其他驱动之前,可以对IRP设置一个完成例程,一旦底层驱动将IRP完成后,IRP完成例程立刻被处罚,通过设置完成例程 ...

  7. C++第三十八篇 -- 研究一下Windows驱动开发(二)--WDM式驱动的加载

    基于Windows驱动开发技术详解这本书 一.简单的INF文件剖析 INF文件是一个文本文件,由若干个节(Section)组成.每个节的名称用一个方括号指示,紧接着方括号后面的就是节内容.每一行就是一 ...

  8. C++第三十三篇 -- 研究一下Windows驱动开发(一)内部构造介绍

    因为工作原因,需要做一些与网卡有关的测试,其中涉及到了驱动这一块的知识,虽然程序可以运行,但是不搞清楚,心里总是不安,觉得没理解清楚.因此想看一下驱动开发.查了很多资料,看到有人推荐Windows驱动 ...

  9. Windows 驱动开发 - 5

    上篇<Windows 驱动开发 - 4>我们已经完毕了硬件准备. 可是我们还没有详细的数据操作,比如接收读写操作. 在WDF中进行此类操作前须要进行设备的IO控制,已保持数据的完整性. 我 ...

  10. Windows 驱动开发 - 7

    在<Windows 驱动开发 - 5>我们所说的读写操作在本篇实现. 在WDF中实现此功能主要为:EvtIoRead和EvtIoWrite. 首先,在EvtDeviceAdd设置以上两个回 ...

随机推荐

  1. 【Java 进阶】Java8 新特性的理解与应用

    [进阶]Java8新特性的理解与应用 前言 Java 8是Java的一个重大版本,是目前企业中使用最广泛的一个版本. 它支持函数式编程,新的Stream API .新的日期 API等一系列新特性. 掌 ...

  2. 【PS算法理论探讨三】 Photoshop中图层样式之 颜色叠加/渐变叠加/图案叠加 算法原理初探讨。

    这三个表面上看上去很简单,我们就先描述简单的部分. 颜色叠加:这个和编辑菜单下的填充 颜色 基本是一个意思,相当于在原有的图层上部添加了一个纯实色的虚拟图层,选项里的不透明度和混合模式和普通的概念是一 ...

  3. Codeforce:723A. The New Year: Meeting Friends (水题)

    题意:有三个好朋友的家都住在x轴的不同坐标,问新年的时候三个朋友之间问候走的最短距离 max{(a,b,c)} - min{(a,b,c)} 即可 编译器由 VS2017 切换到VScode使用,纪念 ...

  4. 深入理解web协议(二):DNS、WebSocket

    本文首发于 vivo互联网技术 微信公众号链接:https://mp.weixin.qq.com/s/AkbAN4UZLDf841g1ZLFPBQ作者:Wu Yue 本文系统性的讲述了 DNS 协议与 ...

  5. 微信小程序从基础入门到项目实战-黑马程序员-pink老师推荐(持续更新)

    https://www.bilibili.com/video/BV1WJ41197sD/?spm_id_from=333.788.recommend_more_video.0

  6. java基础-构建工具mvn-day20

    目录 1. 初识mvn 2. 用maven创建工程 3. maven工程 之间的关系 4. 父子 mvn工程 5. mvn常见的插件 6. tomcat插件 1. 初识mvn mvn是一个项目构建工具 ...

  7. springboot启动流程 (3) 自动装配

    在SpringBoot中,EnableAutoConfiguration注解用于开启自动装配功能. 本文将详细分析该注解的工作流程. EnableAutoConfiguration注解 启用Sprin ...

  8. LaTeX 公式识别问题

    问题 想要方便的图片公式识别工具来写Latex(论文)/markdown(笔记)文件 工具推荐 1.mathpix 识别成功率最高(无论是多行,表格表现都非常良好),最好用的工具,但是收费高且付费麻烦 ...

  9. js - body的滚动事件的坑

    文章来源 : https://www.cnblogs.com/Zting00/p/7497629.html 踩过些坑,得到的结论,不一定精确 1. body的滚动条,刷新页面的时候不会回到顶部.其他d ...

  10. [转帖]CPU写入512bit要多久:从AVX到NEON

    https://zhuanlan.zhihu.com/p/677124882 写这篇文章的原因是有个项目需要降低延迟,希望能更快地把512bit的数据从内存搬进PCIe设备.原先的做法是软件写寄存器通 ...