1.15 自实现GetProcAddress

news2024/11/25 0:33:40

在正常情况下,要想使用GetProcAddress函数,需要首先调用LoadLibraryA函数获取到kernel32.dll动态链接库的内存地址,接着在调用GetProcAddress函数时传入模块基址以及模块中函数名即可动态获取到特定函数的内存地址,但在有时这个函数会被保护起来,导致我们无法直接调用该函数获取到特定函数的内存地址,此时就需要自己编写实现LoadLibrary以及GetProcAddress函数,该功能的实现需要依赖于PEB线程环境块,通过线程环境块可遍历出kernel32.dll模块的入口地址,接着就可以在该模块中寻找GetProcAddress函数入口地址,当找到该入口地址后即可直接调用实现动态定位功能。

首先通过PEB/TEB找到自身进程的所有载入模块数据,获取TEB也就是线程环境块。在编程的时候TEB始终保存在寄存器 FS 中。

0:000> !teb
TEB at 00680000
    ExceptionList:        008ff904
    StackBase:            00900000
    StackLimit:           008fc000
    RpcHandle:            00000000
    Tls Storage:          0068002c
    PEB Address:          0067d000

0:000> dt _teb 00680000
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : (null) 
   +0x02c ThreadLocalStoragePointer : 0x0068002c Void
   +0x030 ProcessEnvironmentBlock : 0x0067d000 _PEB      // 偏移为30,PEB

从该命令的输出可以看出,PEB 结构体的地址位于 TEB 结构体偏移0x30 的位置,该位置保存的地址是 0x0067d000。也就是说,PEB 的地址是 0x0067d000,通过该地址来解析 PEB并获得 LDR结构。

0:000> dt nt!_peb 0x0067d000
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00f30000 Void
   +0x00c Ldr              : 0x774c0c40 _PEB_LDR_DATA    // LDR

从如上输出结果可以看出,LDRPEB 结构体偏移的 0x0C 处,该地址保存的地址是 0x774c0c40 通过该地址来解析 LDR 结构体。WinDBG 输出如下内容:

0:000> dt _peb_ldr_data 0x774c0c40
ntdll!_PEB_LDR_DATA
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x9e3208 - 0x9e5678 ]
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x9e3210 - 0x9e5680 ]
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x9e3110 - 0x9e35f8 ]
   +0x024 EntryInProgress  : (null) 
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null) 

0:000> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr32 _LIST_ENTRY
   +0x004 Blink            : Ptr32 _LIST_ENTRY

现在来手动遍历第一条链表,输入命令0x9e3208:在链表偏移 0x18 的位置是模块的映射地址,即 ImageBase;在链表
偏移 0x28 的位置是模块的路径及名称的地址;在链表偏移 0x30 的位置是模块名称的地址。

0:000> dd 0x9e3208
009e3208  009e3100 774c0c4c 009e3108 774c0c54
009e3218  00000000 00000000 00f30000 00f315bb
009e3228  00007000 00180016 009e1fd4 00120010
009e3238  009e1fda 000022cc 0000ffff 774c0b08

0:000> du 009e1fd4
009e1fd4  "C:\main.exe"
0:000> du 009e1fda
009e1fda  "main.exe"

读者可自行验证,如下所示的确是模块的名称。既然是链表,就来下一条链表的信息,009e3100保存着下一个链表结构。依次遍历就是了。

0:000> dd 009e3100
009e3100  009e35e8 009e3208 009e35f0 009e3210
009e3110  009e39b8 774c0c5c 773a0000 00000000
009e3120  0019c000 003c003a 009e2fe0 00140012

0:000> du 009e2fe0 
009e2fe0  "C:\Windows\SYSTEM32\ntdll.dll"

上述地址009e3100介绍的结构,是微软保留结构,只能从网上找到一个结构定义,然后自行看着解析就好了。

typedef struct _LDR_DATA_TABLE_ENTRY
{
  PVOID Reserved1[2];
  LIST_ENTRY InMemoryOrderLinks;
  PVOID Reserved2[2];
  PVOID DllBase;
  PVOID EntryPoint;
  PVOID Reserved3;
  UNICODE_STRING FullDllName;
  BYTE Reserved4[8];
  PVOID Reserved5[3];
  union {
  ULONG CheckSum;
  PVOID Reserved6;
  };
  ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; 

根据如上流程,想要得到kernel32.dll模块的入口地址,我们可以进行这几步,首先得到TEB地址,并在该地址中寻找PEB线程环境块,并在该环境块内得到LDR结构,在该结构中获取第二条链表地址,输出该链表中的0x10以及0x20即可得到当前模块的基地址,以及完整的模块路径信息,该功能的实现分为32位于64位,如下代码则是实现代码。

#include <iostream>
#include <Windows.h>

// 将不同的节压缩为单一的节
#pragma comment(linker, "/merge:.data=.text") 
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")

// 得到32位模式下kernel32.dll地址
DWORD GetModuleKernel32()
{
  DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL;
  DWORD *BaseAddress = NULL, *FullDllName = NULL;

  __asm
  {
    mov eax, fs:[0x30]      // FS保存着TEB
    mov PEB, eax            // +30定位到PEB
  }

  // 得到LDR
  Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));

  // 在LDR基础上找到第二条链表
  Flink = *((DWORD **)((unsigned char *)Ldr + 0x14));
  p = Flink;

  p = *((DWORD **)p);

  // 计数器
  int count = 0;

  while (Flink != p)
  {
    BaseAddress = *((DWORD **)((unsigned char *)p + 0x10));
    FullDllName = *((DWORD **)((unsigned char *)p + 0x20));

    if (BaseAddress == 0)
      break;

    // printf("镜像基址 = %08x \r\n 模块路径 = %S \r\n", BaseAddress, (unsigned char *)FullDllName);
    // 第二个模块是kernel32.dll
    if (count == 1)
    {
      // printf("address =%x \n", BaseAddress);
      return reinterpret_cast<DWORD>(BaseAddress);
    }

    p = *((DWORD **)p);
    count = count + 1;
  }

  // 未找到Kernel32模块
  return 0;
}

// 获取64位模式下的kernel32.dll基址
ULONGLONG GetModuleKernel64()
{
  ULONGLONG dwKernel32Addr = 0;

  // 获取TEB的地址
  _TEB* pTeb = NtCurrentTeb();
  // 获取PEB的地址
  PULONGLONG pPeb = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pTeb + 0x60);
  // 获取PEB_LDR_DATA结构的地址
  PULONGLONG pLdr = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pPeb + 0x18);
  // 模块初始化链表的头指针InInitializationOrderModuleList
  PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);

  // 获取链表中第一个模块信息,exe模块
  PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
  //printf("EXE Base = > %X \n", pModuleExe[6]);

  // 获取链表中第二个模块信息,ntdll模块
  PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
  //printf("Ntdll Base = > %X \n", pModuleNtdll[6]);

  // 获取链表中第三个模块信息,Kernel32模块
  PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
  //printf("Kernel32 Base = > %X \n", pModuleKernel32[6]);

  // 获取kernel32基址
  dwKernel32Addr = pModuleKernel32[6];
  return dwKernel32Addr;
}

int main(int argc, char *argv[])
{
  // 输出32位kernel32
  DWORD kernel32BaseAddress = GetModuleKernel32();
  std::cout << "kernel32 = " << std::hex << kernel32BaseAddress << std::endl;

  // 输出64位kernel32
  ULONGLONG kernel64BaseAddress = GetModuleKernel64();
  std::cout << "kernel64 = " << std::hex << kernel32BaseAddress << std::endl;

  system("pause");
  return 0;
}

如上代码中分别实现了32位于64位两种获取内存模块基址GetModuleKernel32用于获取32位模式,GetModuleKernel64则用于获取64位内存基址,读者可自行调用两种模式,输出如下图所示;

我们通过调用GetModuleKernel32()函数读入kernel32.dll模块入口地址后,则下一步就可以通过循环,遍历该模块的导出表并寻找到GetProcAddress导出函数地址,找到该导出函数内存地址后,则可以通过kernel32模块基址加上dwFunAddrOffset相对偏移,获取到该函数的内存地址,此时通过函数指针就可以将该函数地址读入到内存指针内。

// 封装基地址获取功能
ULONGLONG MyGetProcAddress()
{
  // 获取32位基址
  ULONGLONG dwBase = GetModuleKernel32();
  
  // 获取64位基址
  // ULONGLONG dwBase = GetModuleKernel64();
  
  // 获取DOS头
  PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase;
  
  // 获取32位NT头
  PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(dwBase + pDos->e_lfanew);
  
  // 获取64位NT头
  // PIMAGE_NT_HEADERS64  pNt = (PIMAGE_NT_HEADERS64)(dwBase + pDos->e_lfanew);
  
  // 获取数据目录表
  PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory;
  pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]);
  DWORD dwOffset = pExportDir->VirtualAddress;
  
  // 获取导出表信息结构
  PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset);
  DWORD dwFunCount = pExport->NumberOfFunctions;
  DWORD dwFunNameCount = pExport->NumberOfNames;
  DWORD dwModOffset = pExport->Name;

  // 获取导出地址表
  PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
  
  // 获取导出名称表
  PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
  
  // 获取导出序号表
  PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);

  for (DWORD dwOrdinal = 0; dwOrdinal < dwFunCount; dwOrdinal++)
  {
    if (!pEAT[dwOrdinal])
    {
      continue;
    }

    // 获取序号
    DWORD dwID = pExport->Base + dwOrdinal;
    
    // 获取导出函数地址
    ULONGLONG dwFunAddrOffset = pEAT[dwOrdinal];

    for (DWORD dwIndex = 0; dwIndex < dwFunNameCount; dwIndex++)
    {
      // 在序号表中查找函数的序号
      if (pEIT[dwIndex] == dwOrdinal)
      {
        // 根据序号索引到函数名称表中的名字
        ULONGLONG dwNameOffset = pENT[dwIndex];
        char* pFunName = (char*)((ULONGLONG)dwBase + dwNameOffset);
        if (!strcmp(pFunName, "GetProcAddress"))
        {
          // 根据函数名称返回函数地址
          return dwBase + dwFunAddrOffset;
        }
      }
    }
  }
  return 0;
}

// 定义名称指针
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);

int main(int argc, char *argv[])
{

  DWORD kernel32BaseAddress = GetModuleKernel32();
  if (kernel32BaseAddress == 0)
  {
    return 0;
  }

  // 获取kernel32基址/获取GetProcAddress的基址
  fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
  std::cout << pfnGetProcAddress << std::endl;

  // 获取Kernel32核心API地址
  fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)kernel32BaseAddress, "LoadLibraryA");
  printf("自定义读入LoadLibrary = %x \n", pfnLoadLibraryA);

  system("pause");
  return 0;
}

输出效果如下图所示,我们即可读入fnLoadLibraryA函数的内存地址;

上述代码的使用也很简单,当我们能够得到GetProcAddress的内存地址后,就可以使用该内存地址动态定位到任意一个函数地址,我们通过得到LoadLibrary函数地址,与GetModuleHandleA函数地址,通过两个函数就可以定位到Windows系统内任意一个函数,我们以调用MessageBox弹窗为例,动态输出一个弹窗,该调用方式如下所示。

// 定义名称指针
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType);
typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName);
typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);

int main(int argc, char * argv[])
{
  // 获取kernel32基址 / 获取GetProcAddress的基址
  fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
  ULONGLONG dwBase = GetModuleKernel32();
  printf("fnGetProcAddress = %x \n", pfnGetProcAddress);
  printf("GetKernel32Addr = %x \n", dwBase);

  // 获取Kernel32核心API地址
  fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)dwBase, "LoadLibraryA");
  printf("pfnLoadLibraryA = %x \n", pfnLoadLibraryA);

  fnGetModuleHandleA pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress((HMODULE)dwBase, "GetModuleHandleA");
  printf("pfnGetModuleHandleA = %x \n", pfnGetModuleHandleA);

  fnVirtualProtect pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress((HMODULE)dwBase, "VirtualProtect");
  printf("pfnVirtualProtect = %x \n", pfnVirtualProtect);

  // 有了核心API之后,即可获取到User32.dll的基地址
  pfnLoadLibraryA("User32.dll");
  HMODULE hUser32 = (HMODULE)pfnGetModuleHandleA("User32.dll");
  fnMessageBox pfnMessageBoxA = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA");
  printf("User32 = > %x \t MessageBox = > %x \n", hUser32, pfnMessageBoxA);

  HMODULE hKernel32 = (HMODULE)pfnGetModuleHandleA("kernel32.dll");
  fnExitProcess pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
  printf("Kernel32 = > %x \t ExitProcess = > %x \n", hKernel32, pfnExitProcess);

  // 弹出信息框
  int nRet = pfnMessageBoxA(NULL, "hello lyshark", "MsgBox", MB_YESNO);
  if (nRet == IDYES)
  {
    printf("你点击了YES \n");
  }

  system("pause");
  pfnExitProcess(0);
  return 0;
}

运行上述代码,通过动态调用的方式获取到MessageBox函数内存地址,并将该内存放入到pfnMessageBoxA指针内,最后直接调用该指针即可输出如下图所示的效果图;

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

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

相关文章

做答题小程序前期需要准备哪些工作

做一个答题小程序前期需要做哪些准备工作呢&#xff1f; 第一、要有明确的答题活动规则需求&#xff0c;比如是想用个人答题形式、还是pk答题形式&#xff0c;每个模式具体的出题规则和得分规则&#xff0c;这些要计划清楚&#xff0c;让开发答题小程序的公司能够充分理解你的需…

vue3哪个数组方法在vue2上做了升级处理

在 Vue 3 中&#xff0c;v-for 指令的数组更新行为进行了升级处理。在 Vue 2 中&#xff0c;当使用 v-for 渲染数组时&#xff0c;如果对数组进行了以下操作&#xff0c;Vue 无法检测到变化&#xff1a; 直接通过索引修改数组元素&#xff0c;例如 arr[0] newValue修改数组的…

MySQL 存储引擎,你了解几个?

引言 MySQL是一种流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它支持多种不同的数据库引擎。数据库引擎是用于存储、管理和检索数据的核心组件&#xff0c;它们直接影响着数据库的性能、可靠性和功能&#xff0c;接下来本文介绍下一些常见的MySQL数据…

华为云云服务器评测 | 从零开始:云耀云服务器L实例的全面使用解析指南

文章目录 一、前言二、云耀云服务器L实例要点介绍2.1 什么是云耀云服务器L实例2.1.1 浅析云耀云服务器L实例 2.2 云耀云服务器L实例的产品定位2.3 云耀云服务器L实例优势2.4 云耀云服务器L实例支持的镜像与应用场景2.5 云耀云服务器L实例与弹性云服务器&#xff08;ECS&#xf…

MySQL基础篇:掌握数据表操作的基础知识

表(table)是一种结构化的文件&#xff0c;可以用来存储特定类型的数据&#xff0c;如&#xff1a;学生信息&#xff0c;课程信息&#xff0c;都可以放到表中。另外表都有特定的名称&#xff0c;而且不能重复。表中具有几个概念&#xff1a;列、行、主键。 列叫做字段(Column),行…

C#,《小白学程序》第十一课:双向链表(Linked-List)其二,链表的插入与删除的方法(函数)与代码

1 文本格式 /// <summary> /// 改进的车站信息类 class /// 增加了 链表 需要的两个属性 Last Next /// </summary> public class StationAdvanced { /// <summary> /// 编号 /// </summary> public int Id { get; set; } 0; ///…

无涯教程-JavaScript - CUBEVALUE函数

描述 CUBEVALUE函数从多维数据集返回一个聚合值。 语法 CUBEVALUE (connection, [member_expression1], [member_expression2], …)争论 Argument描述Required/OptionalconnectionThe name of the connection to the cube. - A text stringRequiredmember_expression 多维表…

ssm+vue网络教学平台源码和论文

ssmvue网络教学平台源码和论文117 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 社会的进步&#xff0c;教育行业发展迅速&#xff0c;人们对教育越来越重视&#xff0c;在当今网络普及的情况下&#x…

易记笔记-Ubuntu 下【netstat】指令全解

文章目录 - 显示所有网络连接- 只显示监听的网络连接- 只显示TCP连接- 只显示UDP连接- 显示PID&#xff08;进程ID&#xff09;和程序名称- 显示数字格式的IP地址和端口号- 显示路由表- 仅显示IPv4或IPv6连接- 显示多重广播功能群组组员名单- netstat在线帮助- netstat的替代工…

Coles 五个月内推出SAP S/4HANA 财务核心

Coles是澳大利亚领先的零售企业&#xff0c;在全国拥有2,500多家零售店。100多年来&#xff0c;这家超市一直致力于为每周在Coles购物的2100万顾客提供优质、有价值的服务。 从Wesfarmers西农集团分拆之前&#xff0c;Coles抓住机会在其正在进行的数字化转型战略中向前迈进了一…

QGIS学习3 - 安装与管理插件

QGIS安装与管理插件主要是使用了菜单栏安装与管理插件这个菜单。 1、通过压缩文件等添加非官方插件 通过压缩文件添加有可能会提示存在安全问题等&#xff0c;直接点是即可。 之后点击install plugins即可完成。安装后导入插件 但是load失败了应该是安装没有成功。只能通过u…

智慧武装三维电子沙盘系统

一、概况 智慧武装三维电子沙盘是一种结合了智能技术和虚拟现实技术的沙盘模拟系统。它通过使用三维投影技术和交互式触控技术&#xff0c;将实际战场的地形、建筑物、人员等元素以虚拟的形式呈现在沙盘上。 智慧武装三维电子沙盘可以实时获取各种战场数据&#xff0c;并通过智…

快速切换本地node版本(超简单)

1.查看本地node版本 使用命令&#xff1a; node -v 2.找到你原来版本node所安装的位置 使用命令&#xff1a; where node 3.找到该路径下的node.exe 文件 可以看到 我对应路径下的 node.exe文件 4.到官网找到需要下载的node.exe版本并下载保存在本地 node.js官网下载exe地址…

STM32存储左右互搏 I2C总线读写FRAM MB85RC16

STM32存储左右互搏 I2C总线读写FRAM MB85RC16 在较低容量存储领域&#xff0c;除了EEPROM的使用&#xff0c;还有铁电存储器FRAM的使用&#xff0c;相对于EEPROM, 同样是非易失性存储单元&#xff0c;FRAM支持更高的访问速度&#xff0c; 其主要优点为没有EEPROM持续写操作跨页…

支付宝商户池

随着社会的发展&#xff0c;第三方支付公司的规章制度越来越完善&#xff0c;支付产品的迭代更新速度也越来越快。华北、白条等贷款工具的出现使信用卡失去了大部分的市场份额。我还记得盛大一卡通、俊旺一卡通等流行的常用卡产品现在逐渐被在线微信支付宝产品所取代。 “支付…

深入浅出AXI协议(4)——猝发传输

一、前言 在之前的文章中&#xff0c;我们着重介绍了关于AXI4的握手协议它可以使得传输的双方都可以自如地控制传输的速率&#xff0c;我们主要介绍了握手协议出现的3种可能情况。然后对于AXI4交易通信的握手信号的关系做出了介绍&#xff1a;&#xff08;1&#xff09;在AXI4互…

软件产品登记测试

1. 服务流程 2. 服务内容 对功能性、易用性、可移植性三个特性、五个子特性进行测试。通过测试检测表明软件功能基本实现&#xff0c;运行基本稳定&#xff0c;操作方便&#xff0c;用户手册描述完整正确&#xff0c;是否达到软件产品登记测试规范的要求。 3. 周期 实施测试…

Docker安装部署ElasticSearch(ES)

文章目录 安装前准备创建挂载目录授权相关权限创建elasticsearch.yml文件 拉取镜像运行容器查看运行情况测试 安装前准备 创建挂载目录 用于在宿主机挂载日志&#xff0c;数据等内容 创建/opt/es/data目录 创建/opt/es/logs目录 创建/opt/es/plugins目录 创建/opt/es/conf目录…

三维模型OBJ格式轻量化压缩处理重难点分析

三维模型OBJ格式轻量化压缩处理重难点分析 三维模型的OBJ格式轻量化压缩处理是一个复杂且具有挑战性的任务&#xff0c;涉及到多个重难点。以下是对三维模型OBJ格式轻量化压缩处理的重难点进行分析&#xff1a; 1、保持视觉质量&#xff1a; 在进行轻量化压缩的过程中&#x…

抖音书单视频怎么制作?这些方法很简单

抖音作为一款热门的短视频应用程序&#xff0c;已经成为了一个广泛传播知识和信息的平台。其中&#xff0c;抖音书单视频是一种很受欢迎的形式&#xff0c;可以通过它分享自己的阅读经验和心得&#xff0c;同时也可以向别人推荐好书。以下是一些关于如何制作抖音书单视频的方法…