【Windows内核】Ntdll解除挂钩学习

news/2025/1/9 19:15:26/文章来源:https://www.cnblogs.com/o-O-oO/p/18662773

简介

我们之前都是直接通过使用直接系统调用的方式来绕过用户态钩子,通过在项目文件中创建并调用系统调用来实现此目标。还有另外一种方法也可以绕过用户态的钩子,那么这种方法是将已经加载到进程中的钩子DLL替换为一个未经修改且未被钩主的版本来达到相同的目标。

将勾住的DLL替换为一个未被勾住的版本需要手动设置导入地址表,修复重定位表以及其他繁琐的过程。为了避免这一复杂的过程。我们可以直接替换DLL文件的一部分,特别是包含钩子的.text区域。.text区域中包含了DLL导出函数的代码。一般钩子都会安装在这个区域。

那么要替换.text区域是非常简单的,只需要获取到其基址和大小,这些信息都是位于IMAGE_OPTIONAL_HEADER头部中的BaseOfCode字段和SizeOfCode字段。

另外一种方法是获取到.text区域基址和大小的方法是通过IMAGE_SECTION_HEADER头部,搜索.text字符串在IMAGE_SECTION_HEADER.Name数组中的位置。

那么为了替换.text区域的内容,就需要去更改该区段的内存权限。通常情况下,.text段被标记为可读可执行的权限。为了能够替换为新的.text区域,那么就必须修改内存权限以允许写入数据,可以通过VirtualProtect Win API来修改内存权限。我们必须将.text区域的权限设置为PAGE_EXECUTE_READWRITE权限。

对于大多数的DLL文件来说,.text区域在磁盘上的偏移量为0x400,也就是1024。我们可以使用Pe-Bear来查看。

那么我们在想为什么偏移是400?

在Windows PE文件格式中,.text区域存放的是程序的代码,例如DLL中的导出函数,PE文件的结构通常要求 .text 区段从特定的内存位置开始,以确保内存的对齐和访问效率。

那么当DLL被加载到内存中时,文件中的偏移量会发生变化,对于大多数的DLL文件,.text区域的偏移通常会被设置为0x1000。这是因为在内存中,Windows通常采用4KB,作为默认的内存页大小。其实也是为了对其。

磁盘上的偏移与内存上的偏移

DLL的.text段在磁盘上的偏移和加载到内存中的偏移是存在差异的。在磁盘上的偏移DLL的.text段通常会以1kb(1024字节)为对其单位。而在内存中,当DLL被加载到进程的内存空间中时,操作系统会将它映射到虚拟内存,并且会使用4KB的页面对其。这意味着DLL的.text段在内存中的偏移会被对齐到4KB的边界。

接下来我们将从磁盘上来获取ntdll.dll。从磁盘上获取到的ntdll.dll文件是从未被篡改的版本。在Windows操作系统中,Ntdll.dll通常位于C:\Windows\System32\目录中,通过这种方式,可以从原始的磁盘文件中获取一个干净,未被修改的ntdll.dll。并将其加载到内存中,替换到目标进程中已经被篡改的版本。

Ntdll解除挂钩-磁盘

首先我们肯定是需要从磁盘上读取Ntdll.dll文件的。那么我们可以通过GetWindowsDirectoryA函数来获取当前操作系统的Windows安装目录的路径。

函数原型如下:

UINT GetWindowsDirectoryA(LPSTR lpBuffer,UINT  nSize
);

通过CreateFileA函数来读取ntdll.dll文件返回文件句柄。

获取该ntdll.dll文件的大小,再去申请一块内存用于将Ntdll.dll读取到内存中。

如下代码:

#include <Windows.h>
#define NTDLL "NTDLL.DLL"  // 定义 ntdll.dll 的文件名// 从磁盘读取 ntdll.dll 文件到缓冲区
BOOL ReadNtdllFromDisk(OUT PVOID* ppNtdllBuf) {CHAR cWinPath[MAX_PATH / 2] = { 0 };  // 存储 Windows 目录的路径CHAR cNtdllPath[MAX_PATH] = { 0 };    // 存储 ntdll.dll 的完整路径HANDLE hFile = NULL;                   // 用于存储文件句柄DWORD dwNumberOfBytesRead = NULL,      // 读取的字节数dwFileLen = NULL;                // 文件的总字节长度PVOID pNtdllBuffer = NULL;             // 用于存储 ntdll.dll 内容的缓冲区// 获取 Windows 目录路径(例如 C:\Windows)if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {printf("[!] GetWindowsDirectoryA 失败,错误代码:%d \n", GetLastError());goto _EndOfFunc;  // 如果失败,跳转到结束部分}// 使用 Windows 目录路径构建 ntdll.dll 的完整路径// 示例路径:C:\Windows\System32\ntdll.dllsprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);// 打开 ntdll.dll 文件,获取文件句柄hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hFile == INVALID_HANDLE_VALUE) {printf("[!] CreateFileA 失败,错误代码:%d \n", GetLastError());goto _EndOfFunc;  // 如果打开文件失败,跳转到结束部分}// 获取文件大小dwFileLen = GetFileSize(hFile, NULL);// 为文件内容分配足够的内存pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);// 读取 ntdll.dll 文件内容if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead) {printf("[!] ReadFile 失败,错误代码:%d \n", GetLastError());printf("[i] 读取了 %d 字节,预期读取 %d 字节 \n", dwNumberOfBytesRead, dwFileLen);goto _EndOfFunc;  // 如果读取文件失败,跳转到结束部分}// 将读取到的文件内容传递给调用者*ppNtdllBuf = pNtdllBuffer;_EndOfFunc:// 清理资源if (hFile)CloseHandle(hFile);  // 关闭文件句柄if (*ppNtdllBuf == NULL)return FALSE;  // 如果没有成功读取文件内容,返回 FALSEelsereturn TRUE;   // 成功读取文件,返回 TRUE
}int main() {PVOID ntdllbuffer = NULL;ReadNtdllFromDisk(&ntdllbuffer);
}

接下来需要使用CreateFileMappingA和MapViewOfFile函数来映射Ntdll了。

如果我们要使用CreateFileMappingA和MapViewOfFile函数来从C:\Windows\System32读取并映射ntdll.dll。你可以利用Windows加载DLL并处理内存对齐的方式。使用 CreateFileMappingA 和 MapViewOfFile 时,内存中的.text段偏移将为4096 字节。

这是Windows默认的页面大小。如果你希望将文件映射到内存,但是避免触发安全回调(PsSetLoadImageNotifyRoutine),可以使用SEC_IMAGE_NO_EXECUTE标记。该标记确保文件映射时不会赋予执行权限。从而避免EDR等工具检测到。

这里的安全回调PsSetLoadImageNotifyRoutine例程会注册一个驱动程序提供的回调。虽然每当无论是EXE还是DLL被加载的时候,都会接收到通知。

如下代码:

#define NTDLL "NTDLL.DLL"BOOL MapNtdllFromDisk(OUT PVOID* ppNtdllBuf) {HANDLE  hFile                           = NULL,    // 文件句柄,用于打开 ntdll.dll 文件hSection                        = NULL;    // 映射文件的句柄CHAR    cWinPath    [MAX_PATH / 2]      = { 0 };    // 存储 Windows 系统目录的路径CHAR    cNtdllPath  [MAX_PATH]          = { 0 };    // 存储 ntdll.dll 的完整路径PBYTE   pNtdllBuffer                    = NULL;     // 存储映射到内存中的 ntdll.dll 文件数据// 获取 Windows 系统目录路径(如 C:\Windows)if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {printf("[!] GetWindowsDirectoryA 获取系统目录失败. 错误: %d \n", GetLastError());goto _EndOfFunc;}// 使用更安全的 sprintf_s 函数,拼接出 ntdll.dll 的完整路径sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);// 打开 ntdll.dll 文件,获取文件句柄hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hFile == INVALID_HANDLE_VALUE) {printf("[!] CreateFileA 打开文件失败. 错误: %d \n", GetLastError());goto _EndOfFunc;}// 创建文件映射对象,使用 'SEC_IMAGE_NO_EXECUTE' 标志,禁止执行映射区域hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);if (hSection == NULL) {printf("[!] CreateFileMappingA 创建文件映射失败. 错误: %d \n", GetLastError());goto _EndOfFunc;}// 将文件映射到内存中,创建视图(只读)pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);if (pNtdllBuffer == NULL) {printf("[!] MapViewOfFile 映射文件失败. 错误: %d \n", GetLastError());goto _EndOfFunc;}// 返回映射后的 ntdll.dll 内存基址*ppNtdllBuf = pNtdllBuffer;_EndOfFunc:// 关闭文件句柄和文件映射句柄if (hFile)CloseHandle(hFile);if (hSection)CloseHandle(hSection);// 如果映射失败,返回 FALSE;否则返回 TRUEif (*ppNtdllBuf == NULL)return FALSE;elsereturn TRUE;
}

如上无论是从磁盘中读取Ntdll还是以文件映射的方式,其实都是将Ntdll到内存中。需要注意的是Ntdll文件如果是从磁盘上读取而不是映射到内存时,其.text段的偏移量可能是4096,而不是1024,所以将文件映射到内存时比较可靠的。
因为.text偏移量始终等于IMAGE_SECTION_HEADER.VirtualAddressDLL 文件的偏移量。

为了解除Ntdll.dll的挂钩Hook,需要执行一系列的操作。为了替换本地被Hook的ntdll.dll的.text段,必须首先获取基地址和大小。这可以通过多种方式完成。但是首先需要获取到本地ntdll.dll模块的句柄。

我们可以通过GetModuleHandle来获取到ntdll.dll模块的句柄。但是这种方式依赖于Windows API的实现。

我们都知道在x64系统上,PEB的地址存储在GS寄存器的偏移0x60处。在x86系统上,PEB的地址存储在FS寄存器的偏移0x30处。

我们可以通过内联汇编指令来获取PEB.

#ifdef _WIN64PPEB pPeb = (PPEB)__readgsqword(0x60); // 读取 GS 寄存器 0x60 偏移
#elif _WIN32PPEB pPeb = (PPEB)__readfsdword(0x30); // 读取 FS 寄存器 0x30 偏移
#endif

那么获取到PEB的地址之后就可以通过遍历LDR链表。首先通过pPeb⏩Ldr⏩InMemoryOrderModuleList获取到双向链表。该双向链表中包含了已加载模块的信息。

Flink指向链表中的下一个节点,第一次Flink指向当前模块,这通常是EXE文件,第二次Flink指向Ntdll模块,减去0x10的偏移及为模块的PLDR_DATA_TABLE_ENTRY结构。

PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);

最后返回该模块的基地址。

return pLdr->DllBase;

如下测试代码:

#include <Windows.h>
#define NTDLL "NTDLL.DLL"  // 定义 ntdll.dll 的文件名
// 定义泛型的 PEB 和 TEB 类型
typedef struct _PEB_LDR_DATA {ULONG Length;UCHAR Initialized;PVOID SsHandle;LIST_ENTRY InLoadOrderModuleList;LIST_ENTRY InMemoryOrderModuleList;LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;typedef struct _UNICODE_STRING {USHORT Length;USHORT MaximumLength;PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;typedef struct _LDR_DATA_TABLE_ENTRY {LIST_ENTRY InLoadOrderLinks;LIST_ENTRY InMemoryOrderLinks;LIST_ENTRY InInitializationOrderLinks;PVOID DllBase;PVOID EntryPoint;ULONG SizeOfImage;UNICODE_STRING FullDllName;UNICODE_STRING BaseDllName;ULONG Flags;SHORT LoadCount;SHORT TlsIndex;LIST_ENTRY HashLinks;PVOID TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;typedef struct _PEB {BOOLEAN InheritedAddressSpace;BOOLEAN ReadImageFileExecOptions;BOOLEAN BeingDebugged;BOOLEAN BitField;PVOID Mutant;PVOID ImageBaseAddress;PPEB_LDR_DATA Ldr;PVOID ProcessParameters;PVOID SubSystemData;PVOID ProcessHeap;PVOID FastPebLock;PVOID AtlThunkSListPtr;PVOID IFEOKey;ULONG CrossProcessFlags;PVOID KernelCallbackTable;ULONG SystemReserved[1];ULONG AtlThunkSListPtr32;PVOID ApiSetMap;
} PEB, * PPEB;
PVOID FetchLocalNtdllBaseAddress() {#ifdef _WIN64PPEB pPeb = (PPEB)__readgsqword(0x60); // 获取 PEB 地址
#elif _WIN32PPEB pPeb = (PPEB)__readfsdword(0x30); // 获取 PEB 地址
#endif// 获取 ntdll.dll 模块(LDR 链表中的第二个条目)PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);// 返回 ntdll.dll 的基址return pLdr->DllBase;
}
int main() {PVOID ntdllbuffer = NULL;ReadNtdllFromDisk(&ntdllbuffer);MapNtdllFromDisk(&ntdllbuffer);PVOID ntdllbase = NULL;ntdllbase = FetchLocalNtdllBaseAddress();
}

那么现在就可以通过可选PE头来获取.text段信息了。在可选PE头中提供了.text段的基地址。

那么首先的话肯定是需要解析DOS头。

PIMAGE_DOS_HEADER	pLocalDosHdr = (PIMAGE_DOS_HEADER)ntdllbase;

那么下来就是获取Nt头。

PIMAGE_NT_HEADERS 	pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)ntdllbase + pLocalDosHdr->e_lfanew);

获取Nt头之后,通过Nt头定位到可选PE头的BaseCode字段。通过BaseCode字段的值加上ntdll的基地址就可以获取到.text段的地址了。

PVOID	pLocalNtdllTxt	= (PVOID)(pLocalNtHdrs->OptionalHeader.BaseOfCode + (ULONG_PTR)ntdllbase);

也可以通过可选PE头中的SizeOfCode字段来获取到.text段的大小。

获取到.text段的基地址以及大小之后。

接下来,我们需要获取到未挂钩的ntdll.dll文件的.text段的基地址。为此我们可以使用ReadNtdllFromDisk函数或MapNtdllFromDisk函数。需要注意的是如果使用ReadNtdllFromDisk函数,则.text段的偏移量为1024个字节。这是因为我们从磁盘读取文件时。那么如果使用MapNtdllFromDisk,.text段的偏移量等于ntdll.dll在映射后的IMAGE_SECTION_HEADER.VirtualAddress。

那么其实说白了如果你通过映射文件的方式,.text 段的基地址通常会通过 IMAGE_SECTION_HEADER.VirtualAddress 来确定,所以我们需要通过Ntdll模块的基地址加上IMAGE_SECTION_HEADER.VirtualAddress。那么如果你是通过文件读取的方式,.text段的偏移固定为1024,所以通过Ntdll模块的基地址加上1024即可。

下一步我们将替换本地已经挂钩的ntdll.dll模块的.text段,并使用未挂钩的.text段来替换。所以我们首先肯定是需要更改目标.text段的内存权限,因为我们需要写入,所以需要通过VirtualProtect函数将其.text段设置为PAGE_EXECUTE_READWRITE。然后使用memcpy函数来进行替换,最后再将权限更改回去。

这里定义了一个ReplaceNtdllTxtSection函数,该函数的目标是将本地Hook的Ntdll.dll的.text部分替换为未Hook的版本。该函数使用预处理指令根据ntdll.dll的方式来调整.text部分的偏移量。

如下代码:

该函数需要接受一个参数,它需要接受未Hook的Ntdll.dll的基地址。

BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();  // 获取本地已钩的 Ntdll.dll 基地址// 打印本地和未钩住的 Ntdll 基地址printf("\t[i] 'Hooked' Ntdll Base Address : 0x%p \n\t[i] 'Unhooked' Ntdll Base Address : 0x%p \n", pLocalNtdll, pUnhookedNtdll);printf("[#] Press <Enter> To Continue ... ");getchar();// 获取 DOS 头PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)  // 检查 DOS 头签名是否正确return FALSE;// 获取 NT 头PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)  // 检查 NT 头签名是否正确return FALSE;PVOID pLocalNtdllTxt = NULL, pRemoteNtdllTxt = NULL;  // 本地已钩住的文本段基地址,未钩住的文本段基地址SIZE_T sNtdllTxtSize = NULL;  // 文本段的大小// 获取文本段PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {// 判断该节是否为文本段if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {// 计算本地文本段基地址pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);#ifdef MAP_NTDLL// 如果定义了 MAP_NTDLL,使用映射方法获取未钩住的文本段基地址pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
#endif // MAP_NTDLL#ifdef READ_NTDLL// 如果定义了 READ_NTDLL,使用读取方法获取未钩住的文本段基地址pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + 1024);
#endif // READ_NTDLL// 获取文本段的大小sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;break;}}// 打印本地和未钩住的文本段地址及其大小printf("\t[i] 'Hooked' Ntdll Text Section Address : 0x%p \n\t[i] 'Unhooked' Ntdll Text Section Address : 0x%p \n\t[i] Text Section Size : %d \n", pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);printf("[#] Press <Enter> To Continue ... ");getchar();//---------------------------------------------------------------------------------------------------------------------------// 检查是否获取到了所有必需的信息if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)return FALSE;#ifdef READ_NTDLL// 检查 'pRemoteNtdllTxt' 是否为文本段的基地址if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {printf("\t[i] Text section is of offset 4096, updating base address ... \n");// 如果不是,说明读取的文本段的偏移量为 4096,所以我们需要加上 3072(因为已经加过 1024)(ULONG_PTR)pRemoteNtdllTxt += 3072;// 再次检查if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)return FALSE;printf("\t[+] New Address : 0x%p \n", pRemoteNtdllTxt);printf("[#] Press <Enter> To Continue ... ");getchar();}
#endif // READ_NTDLL//---------------------------------------------------------------------------------------------------------------------------// 打印替换文本段的提示printf("[i] Replacing The Text Section ... ");DWORD dwOldProtection = NULL;// 修改文本段的内存权限,使其可写且可执行if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());return FALSE;}// 复制新的文本段内容memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);// 恢复原先的内存保护权限if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());return FALSE;}// 打印完成提示printf("[+] DONE !\n");return TRUE;
}

如上代码的本质其实就是获取到本地已经Hook的Ntdll的.text段和未Hook的Ntdll的.text段。通过VirtualProtect函数将其.text段的内存保护权限更改为PAGE_EXECUTE_WRITECOPY。然后通过mempcy函数复制新的未Hook得.text段到已经Hook的.text段。最后将权限修改回来。

这里唯一需要解释的是为何要 检查pRemoteNtdllTxt是否为文本段的基地址。

这里需要注意的是在某些情况下,Ntdll.dll的前四个字节可能是会被修改的,如果这些字节不是0xCC 0xCC 0xCC 0xCC,那么说明ntdll.dll文件可能被修改过。

假设我们是通过磁盘读取的方式来读取Ntdll.dll的,判断如果前四个字节是 0xCC 0xCC 0xCC 0xCC,我们认为文件没有被修改过,此时可以直接使用 1024 字节作为文本段的偏移量。如果前面四个字节不是 0xCC 0xCC 0xCC 0xCC,则表示文件已经被篡改或勾住。此时我们需要使用真实的偏移量4096。
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)这里的判断很简单。

pLocalNtdllTxt和pRemoteNtdllTxt分别指向勾住和未勾住的Ntdll.dll的指针。

*(ULONG*)pLocalNtdllTxt是强制转换为 ULONG*(无符号长整型指针)然后解引用它们。

最后取出它们所指向的内存地址中的4字节数据。其实就是0xCC 0xCC 0xCC 0xCC

如上就是Ntdll通过磁盘解除挂钩的学习思路。

原创 relaysec Relay学安全

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

21. 主窗口控件

一、主窗口控件之前,我们使用可视控件创建具体的实例时都选择了一个父窗体,将控件放到这个窗体上,即使在创建实例时没有选择父窗体,也可以用控件的 setParent() 方法将控件放到父窗体上。如果一个控件没有放到任何窗体上,则这个控件可以单独成为窗口,并且可以作为父窗口来…

智慧帮手:一站式搭建帮助中心,优化客户服务体验

在数字化时代,客户服务已成为企业赢得市场、留住客户的关键。一个高效、便捷的帮助中心,不仅能够快速响应客户需求,还能提升客户满意度和忠诚度。然而,如何搭建一个既满足客户需求又符合企业特色的帮助中心,成为众多企业面临的挑战。本文将探讨如何借助一站式解决方案,如…

解锁餐饮新篇:精准搭建内部知识库,赋能高效运营

在当今竞争激烈的餐饮行业中,提升运营效率和服务质量已成为企业脱颖而出的关键。随着餐饮业务的不断扩展,内部信息管理和知识传承成为了一大挑战。如何构建一个高效、精准的内部知识库,成为解锁餐饮新篇章的重要一环。本文将探讨如何借助智能化工具,如HelpLook,实现这一目…

【YashanDB知识库】进行load data的时候报找不到动态库liblz4.so

本文内容来自YashanDB官网,原文内容请见 https://www.yashandb.com/newsinfo/7863047.html?templateId=1718516 现象 23.2版本的依赖项准备里指明,要依赖动态库:liblz4.so,liblz4.so.1,liblz4.so.1.9.3 在执行load data的时候报找不到动态库liblz4.so 操作系统在/lib64/目…

JAVA-Day 08:For循环语句

For循环 For循环格式 for循环语句格式 for(初始化语句;条件判断语句;条件控制语句){ 循环语句体; } 执行流程: 1、执行初始化语句 2、执行条件判断语句,看其结果是True还是False,如果是True,执行循语句体,如果是False,循环结束 3、执行条件控制语句 4、回到2继续执行条件判…

记录---JS 的蝴蝶效应 —— 事件流

🧑‍💻 写在开头 点赞 + 收藏 === 学会🤣🤣🤣前言 在 JavaScript 的世界里,事件流就像一只永不停歇的蝴蝶,每一个动作、每一个点击、每一个滚动,都会触发一连串的蝴蝶效应。作为一名开发者,掌握事件流的艺术,不仅能让你的网页更加生动、更加交互,也能让用户体验…

HTTPS 加密机制

HTTPS 概述 HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。…

leetcode131 分割回文串

leetcode 131思路:回溯 比如说aab,对于每个元素currentNum,有两种选择: 1.如果currentNum<len-1,可以将当前元素加入到currentStr中,然后dfs(start,currentNum+1)。而currentNum==len-1时不能dfs(start,currentNum+1),这样下一轮循环就执行以下代码了 if (currentNum…

Fake Location强制升级解决办法

Fake Location强制升级解决办法 一、下载链接中的软件并安装其中的APK https://www.123865.com/s/TXeiTd-fla7d 二、进入LSPosed软件,点击应用伪装三、勾选Fake Location四、进入应用伪装软件并点击Fake Location五、往下滑,找到应用版本号,并填写为1599, 然后点击右上角的…

【分布式】优雅实现多系统一致性补偿方案

前言 我们在开发的过程中,如果一个业务操作需要本地写MYSQL数据以及对第三方系统做写操作,那么这种流程就涉及到分布式系统一致性的问题,然而并非所有系统都能使用成熟的分布式事务方案 案例说明 以一个财务报账业务为例,涉及到的系统如下:系统名 作用 实现方案单据系统 申…

【ABAP】S4中的官方demo程序

1. abap demo包含cds、opensql、loop at group 等开发对象及语法的demo程序 包名:SABAPDEMOS 2. salv demo主要是一些salv开发相关的demo,包含树状显示、弹窗显示等包名:SALV_OM_OBJECTS

使用云数据库RDS和低代码开发平台“魔笔”,高效构建门户网站,完成任务领智能台灯!

传统建站涵盖需求分析、设计、开发、运维等环节。需分析软硬件需求并购买配置,设计网页布局,前后端编码调试,涉及业务逻辑、数据库及API。更新时需调整设计与代码。运营期需进行服务器维护、数据监控和安全检查,整个过程耗资费力。 本方案将为您详细介绍如何使用云数据库RD…