滴水三期:day36.1-导出表

一、导出表概述

1.引入导出表

  • 前面学过一个Win32下的.exe文件,是由多个PE文件组成。比如通过OD打开一个Ipmsg.exe,查看模块M,会发现一个ipmsg.exe和多个.dll(即模块)构成。

    image-20230404185140430
  • 这种以动态链接库.dll的方式导出函数时,如果此.exe需要用到某个.dll中的函数,那么.exe怎么知道:

    1. 引入的.dll中有哪些函数可以供.exe使用
    2. .dll中的各种函数都存在哪里
  • 所以.dll中应当有一个目录或者结构提供给.exe文件,来记录.dll中的函数有哪些,函数的起始地址等信息。所以如果.dll或者.exe文件想要给别的程序提供函数,就必须同时给别的程序一个“清单”,这个“清单”就是导出表

  • 故当我们分析上面例子中的ole32.dll,或者LPK.dll等.dll文件结构时,就可以看到有导出表

2.常见误区

  • 不是只有.dll才提供函数给别的程序使用,有些.exe也可以提供函数
  • 有些.exe的导出表数据目录不是全0,表示有此.exe程序有导出表,即可对外提供函数;有些没有导出表,就不对外提供函数
  • 所以一个PE文件(如.dll/.exe/.sys)如果对外提供函数,那么就需要有导出表,且需要遵守一定的格式
  • 一个安全的PE文件,是不会显示导出表的,但可以对外提供函数:正常情况下如果一个PE文件不提供导出表,别人是无法使用其函数的;但是逆向一个游戏时,如果知道了别人的导出表,就不用找call了(不现实)。所以游戏会做安全加固,我们不知道哪些函数是导出的,就算给我函数地址,我也没办法拿这个地址去调用,只能分析反汇编代码,分析程序入口是什么,分析函数的参数是什么

3.导出表在哪里

PE文件结构图

  • 先定位到可选PE头 --> 再找到可选PE头的最后一个成员:即一个结构体数组,第一个结构体就是导出表数据目录,第一个成员是导出表的内存偏移地址RVA,第二个成员是导出表的大小

    1 image-20230404191027376

  • 通过导出表数据目录的RVA转FOA就可以找到导出表

  • 导出表其实就在这个PE文件的某个节中(后面要学的各种表也是在某个节中)

  • 注意事项:学到现在解析PE结构时就不用拉伸了,用FileBuffer分析即可。因为拉伸只是有助于理解,如果直接在FileBuffer中做,只是需要多使用一个==RVA–>FOA==的地址转换函数,每次遇到RVA地址时转换一下即可,就省去了将文件整个拉伸的过程,会减少很多代码量和多余操作,而且市面上关于PE的书籍几乎都是直接用FileBuffer讲解操作的,所以现在开始,我们就要学会不拉伸的情况下去分析

4.导出表的结构

struct _IMAGE_EXPORT_DIRECTORY{   //40字节		
    DWORD Characteristics;  //未使用		
    DWORD TimeDateStamp;  //时间戳		
    WORD MajorVersion;  //未使用		
    WORD MinorVersion;	//未使用		
    DWORD Name;  //指向该导出表文件名字符串  *
    DWORD Base;  //导出函数起始序号  *
    DWORD NumberOfFunctions;  //所有导出函数的个数  *		
    DWORD NumberOfNames;  //以函数名字导出的函数个数  *
    DWORD AddressOfFunctions;  //导出函数地址表RVA  *					
    DWORD AddressOfNames;  //导出函数名称表RVA  *						
    DWORD AddressOfNameOrdinals;  //导出函数序号表RVA  *						
};
1.Name
  • 指向该导出表文件名字符串的RVA,比如一个DBGHELP.dll的PE文件提供函数,那么这个PE文件的Name指向的字符串为为dbghelp.dll

    image-20230407011302040

    可以用LordPE软件,选择PE Editor,选择你要打开的PE文件,查看其导出表

2.Base
  • 导出函数起始序号(最小的序号)

    比如有序号为14、6、10、8的导出函数,那么Base的值为6

3.NumberOfFunctions
  • 所有导出函数的个数

  • 注意:day35中讲过,这个值是通过导出函数的最大序号 - 最小序号 + 1算出来的;正常来说这个值是多少,那么此PE文件中导出函数的个数就是多少。但是如果使用自定义序号,序号定义时不是连续的,而是中间有空缺的序号,那么此时NumberOfFunctions的值会比实际的定义的导出函数个数多

    • 比如在.def中定义为Plus @12Sub @15 NONAMEMul @13Div @16
    • 那么NumberOfFunctions值 = 16 - 12 + 1 = 5;而不是4!
4.NumberOfNames
  • 函数名字导出的函数个数:比如以动态链接库的方式导出(注意和只以序号导出函数区分)
  • 如果导出时加了NONAME关键字,那么就不计数
5.AddressOfFunctions
  • 导出函数地址表RVA,即这个地址指向一个表!这个表中记录了此PE文件的所有导出函数的地址

    image-20230404193714462
  • 该表中元素宽度:4个字节

  • 元素个数:由NumberOfFunctions决定

  • 注意:day35中讲过导出函数是有序号的,上图中的下标等于导出函数的相对序号

    (参考上图)比如使用自定义序号导出函数,即.def的方式导出,定义了序号13、14、16、17、19的导出函数,那么Base的值应为13,那么序号为13的函数相对相对下标就是0,序号14的导出函数相对下标就是1,序号为15的导出函数虽然没有,但是会把位置空出来,只是地址值为NULL,即0x00000000,序号16的导出函数相对下标就是3…以此类推

    image-20230404222130061
6.AddressOfNames
  • 导出函数名称表RVA(拉伸后的内存地址偏移,所以要先转成FOA),即这个地址指向一个表,这个表中记录的是导出函数的名称字符串地址!!不是直接存储名称(且此字符串地址也是RVA)

    就像C语言中的字符串char* name = "abc";,name变量值为abc这个字符串的首地址。所以导出函数名称表中存储的是name值(即名称的地址),而不是直接存储的abc这个名称

    image-20230404202707507
  • 该表中元素宽度:4个字节

  • 表中元素的数量:由NumberOfNames决定

  • 注意:如果函数导出时添加了NONAME,即函数没有名称,那么这个表中就不会出现这个函数名地址

    • 所以AddressOfNames表中元素个数可能比AddressOfFunctions表中元素个数少!(AddressOfFunctions表中不管导出函数有没有名字,都会有地址)
    • 也有可能AddressOfNames表中元素个数比AddressOfFunctions表中元素个数多!因为导出函数时可以让多个不同名字的函数指向同一个函数地址
  • 函数名称表中是按名字排序的(根据ASCII码排序)

7.AddressOfNameOrdinals
  • 导出函数序号表RVA,即这个地址指向一个表,表中存的是导出函数的相对序号

    image-20230404211521937
  • 即==该表中存储的内容 + Base = 函数的导出序号==

  • 表中元素个数:由NumberOfNames决定,且和AddressOfName表一一对应的

    image-20230404212302495

    比如:现在要找名字叫Sub的导出函数,经查找在AddressOfNames表的下标为1的位置,那么这个函数所对应的相对序号则在AddressOfNameOrdinals表中下标为1的位置,即由上图可知Sub函数对应的序号为0x0001 + Base

  • 该表中元素宽度:2个字节

5.NONAME导出函数注意事项

  • 如果导出时,定义的无名字函数,即Div @13 NONAME,那么函数名称表中就不会有指向Div函数名的元素,同样函数序号表中也不会有Div的相对序号,但是在函数地址表中会留出来一个元素位置存储Div函数地址

二、由导出表获取函数地址

1.按名字找函数地址

  • 找到导出表后,先根据AddressOfNames,将RVA转成FOA,即可定位到函数名称表,遍历函数名称表,用名字依次与函数名称表每个元素指向的字符串做比较,直到名字匹配,记录下此时元素在函数名称表的下标i
  • 再根据AddressOfNameOrdinals,将RVA转成FOA,即可定位到函数序号表,得到此表下标为i的元素值n
  • 最后根据AddressOfFunctions,将RVA转成FOA,即可定位到函数地址表,则此表下标为n的元素值就是该名字对应函数的RVA地址,将RVA转成FOA就得到了该导出函数在FileBuffer中的地址

image-20230404232004619

比如要根据名字查找导出函数mul(),假设在函数名称表中下标为2指向的位置找了"mul"字符串与函数名称匹配,所以再去查找函数序号表下标为2的元素为0x0004,最后再函数地址表中找下标为4的元素值,即为mul函数的RVA,转成FOA即可

2.按序号找函数地址

  • 找到导出表后,根据AddressOfFunctions,将RVA转成FOA,定位到函数地址表

  • 再用给定的序号 - Base = 相对序号i,得到相对序号i

  • 最后找函数地址表中下标为i的元素值即为该序号对应函数RVA地址,将RVA转成FOA就得到了该函数在FileBuffer中的地址

    image-20230404232004619
  • 比如要根据序号找导出函数mul(),因为mul的序号为17,Base值为13,那么17 - 13 = 4,所以直接再函数地址表中找到下标为4的元素值,即为mul函数的RVA,最后转成FOA即可

  • 所以根据导出序号找,跟函数序号表没有关系!函数序号表的存在就是为了用于按名字查找

  • 上图可以发现,t_method函数用NONAME导出,所以函数名称表和函数序号表都没有t_method的信息,但是由于此函数序号为19,相对序号为19 - 13 = 6,所以会在函数地址表中下标为6的位置存储t_method函数的RVA

三、作业

1.编写程序打印导出表的信息

#include "stdafx.h"
#include <stdlib.h>

typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef unsigned char BYTE;

#define MZ 0x5A4D
#define PE 0x4550
#define IMAGE_SIZEOF_SHORT_NAME 8

//DOS头
struct _IMAGE_DOS_HEADER {
	WORD e_magic;  //MZ标记
	WORD e_cblp;
	WORD e_cp;
	WORD e_crlc;
	WORD e_cparhdr;
	WORD e_minalloc;
	WORD e_maxalloc;
	WORD e_ss;
	WORD e_sp;
	WORD e_csum;
	WORD e_ip;
	WORD e_cs;
	WORD e_lfarlc;
	WORD e_ovno;
	WORD e_res[4];
	WORD e_oemid;
	WORD e_oeminfo;
	WORD e_res2[10];
	DWORD e_lfanew;  //PE文件真正开始的偏移地址
};

//标准PE头
struct _IMAGE_FILE_HEADER {
	WORD Machine;  //文件运行平台
	WORD NumberOfSections;  //节数量
	DWORD TimeDateStamp;  //时间戳
	DWORD PointerToSymbolTable;
	DWORD NumberOfSymbols;
	WORD SizeOfOptionalHeader;  //可选PE头大小
	WORD Characteristics;  //特征值
};

//数据目录
struct _IMAGE_DATA_DIRECTORY{
    DWORD VirtualAddress;
    DWORD Size;
};

//可选PE头
struct _IMAGE_OPTIONAL_HEADER {
	WORD Magic;  //文件类型
	BYTE MajorLinkerVersion;
	BYTE MinorLinkerVersion;
	DWORD SizeOfCode;   //代码节文件对齐后的大小
	DWORD SizeOfInitializedData;  //初始化数据文件对齐后的大小
	DWORD SizeOfUninitializedData;  //未初始化数据文件对齐后大小
	DWORD AddressOfEntryPoint;  //程序入口点(偏移量)
	DWORD BaseOfCode;  //代码基址
	DWORD BaseOfData;  //数据基址
	DWORD ImageBase;   //内存镜像基址
	DWORD SectionAlignment;  //内存对齐粒度
	DWORD FileAlignment;  //文件对齐粒度
	WORD MajorOperatingSystemVersion;
	WORD MinorOperatingSystemVersion;
	WORD MajorImageVersion;
	WORD MinorImageVersion;
	WORD MajorSubsystemVersion;
	WORD MinorSubsystemVersion;
	DWORD Win32VersionValue;
	DWORD SizeOfImage;  //文件装入虚拟内存后大小
	DWORD SizeOfHeaders;  //DOS、NT头和节表大小
	DWORD CheckSum;  //校验和
	WORD Subsystem;
	WORD DllCharacteristics;
	DWORD SizeOfStackReserve;  //预留堆栈大小
	DWORD SizeOfStackCommit;  //实际分配堆栈大小
	DWORD SizeOfHeapReserve;  //预留堆大小
	DWORD SizeOfHeapCommit;  //实际分配堆大小
	DWORD LoaderFlags;
	DWORD NumberOfRvaAndSizes;  //目录项数目
	_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
};

//NT头
struct _IMAGE_NT_HEADERS {
	DWORD Signature;  //PE签名
	_IMAGE_FILE_HEADER FileHeader;
	_IMAGE_OPTIONAL_HEADER OptionalHeader;
};

//节表
struct _IMAGE_SECTION_HEADER{
	BYTE Name[IMAGE_SIZEOF_SHORT_NAME];  //节表名
	union{
		DWORD PhysicalAddress;
		DWORD VirtualSize;  //内存中未对齐大小
	}Misc;
	DWORD VirtualAddress;  //该节在内存中偏移地址
	DWORD SizeOfRawData;  //该节在硬盘上文件对齐后大小
	DWORD PointerToRawData;  //该节在硬盘上文件对齐后偏移地址
	DWORD PointerToRelocations;
	DWORD PointerToLinenumbers;
	WORD NumberOfRelocations;
	WORD NumberOfLinenumbers;
	DWORD Characteristics;  //该节特征属性
};

//导出表
struct _IMAGE_EXPORT_DIRECTORY{						
    DWORD Characteristics;  //未使用		
    DWORD TimeDateStamp;  //时间戳		
    WORD MajorVersion;  //未使用		
    WORD MinorVersion;	//未使用		
    DWORD Name;  //指向该导出表文件名字符串  *
    DWORD Base;  //导出函数起始序号  *
    DWORD NumberOfFunctions;  //所有导出函数的个数  *		
    DWORD NumberOfNames;  //以函数名字导出的函数个数  *
    DWORD AddressOfFunctions;  //导出函数地址表RVA  *					
    DWORD AddressOfNames;  //导出函数名称表RVA  *						
    DWORD AddressOfNameOrdinals;  //导出函数序号表RVA  *						
};

/*计算文件大小函数
参数:文件绝对路径
返回值:返回文件大小(单位字节)
*/
int compute_file_size(char* filePath){
	FILE* fp = fopen(filePath,"rb");
    if(!fp){
		printf("打开文件失败");
		exit(0);
	}
    fseek(fp,0,2);
	int size = ftell(fp);
	//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
	fclose(fp);
	return size;
}

/*将文件读入FileBuffer函数
参数:文件绝对路径
返回值:FileBuffer起始地址
*/
char* to_FileBuffer(char* filePath){
    FILE* fp = fopen(filePath,"rb");
    if(!fp){
		printf("打开文件失败");
		exit(0);
	}
    int size = compute_file_size(filePath);
    
	char* mp = (char*)malloc(sizeof(char) * size);  //分配内存空间
    if(!mp){
        printf("分配空间失败");
        fclose(fp);
        exit(0);
    }
    
    int isSucceed = fread(mp,size,1,fp);
    if(!isSucceed){
        printf("读取数据失败");
        free(mp);
        fclose(fp);
        exit(0);
    }
    
    fclose(fp);
    return mp;
}

/*虚拟内存偏移地址->文件偏移地址函数
参数:FileBuffer起始地址,RVA地址值
返回值:RVA对应的FOA,返回0则表示非法RVA
*/
DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
	_IMAGE_FILE_HEADER* _image_file_header = NULL;
	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
	_IMAGE_SECTION_HEADER* _image_section_header = NULL;

	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
	_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + 
		_image_file_header->SizeOfOptionalHeader);

	bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
	if(RVA < _image_section_header->VirtualAddress)
		return RVA;
	for(int i = 0;i < _image_file_header->NumberOfSections;i++){
		if(RVA >= _image_section_header->VirtualAddress 
			&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
			flag = 1;
			break;
		}else{
			_image_section_header++;
		}
	}
	if(!flag)
		return 0;
	DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
	return _image_section_header->PointerToRawData + mem_offset_from_section;	
}

/*打印导出表函数
参数:指定可执行文件绝对路径
返回值:无
*/
void exportTable_print(char* filePath){
	char* fileBufferp = to_FileBuffer(filePath);

	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
	_IMAGE_FILE_HEADER* _image_file_header = NULL;
	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
	_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
	
	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
	_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
	if(_image_data_directory->VirtualAddress == 0){
		printf("此PE文件没有导出表");
		getchar();
		exit(0);
	}
    
    //打印导出表
	_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)(RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress) + (DWORD)fileBufferp);
	char* name = (char*)(RVA_to_FOA(fileBufferp,_image_export_directory->Name) + (DWORD)fileBufferp);
	printf("***************ExportTable****************\n");
	printf("Name:%08X --> %s\n",_image_export_directory->Name,name);
	printf("Base:%08X\n",_image_export_directory->Base);
	printf("NumberOfFunctions:%08X\n",_image_export_directory->NumberOfFunctions);
	printf("NumberOfNames:%08X\n",_image_export_directory->NumberOfNames);
	printf("AddressOfFunctions:%08X\n",_image_export_directory->AddressOfFunctions);
	printf("AddressOfNames:%08X\n",_image_export_directory->AddressOfNames);
	printf("AddressOfNameOrdinals:%08X\n\n",_image_export_directory->AddressOfNameOrdinals);

    //打印函数地址表
	DWORD* functionAddressTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfFunctions) + (DWORD)fileBufferp);
	printf("***************FunctionAddressTable****************\n");
	for(DWORD i = 0;i < _image_export_directory->NumberOfFunctions;i++){
		printf("%08X\n",*(functionAddressTable + i));
	}
	
    //打印函数序号表(别忘了加Base)
	WORD* functionOrdinalsTable = (WORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNameOrdinals) + (DWORD)fileBufferp);
	printf("\n***************FunctionOrdinalsTable****************\n");
	for(i = 0;i < _image_export_directory->NumberOfNames;i++){
		printf("%04X\n",*(functionOrdinalsTable + i) + _image_export_directory->Base);
	}

    //打印函数名称表(别忘了转FOA)
	DWORD* functionNameTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNames) + (DWORD)fileBufferp);
	printf("\n***************FunctionNameTable****************\n");
	for(i = 0;i < _image_export_directory->NumberOfNames;i++){
		printf("%08X --> %s\n",*(functionNameTable + i),(char*)(RVA_to_FOA(fileBufferp,*(functionNameTable + i)) + (DWORD)fileBufferp));
	}
}

int main(int argc, char* argv[])
{
	char* filePath = "D:/LordPE/16Edit.DLL";  //选一个有导出表的PE文件
	exportTable_print(filePath);
	return 0;
}

验证一下:

  • 使用LordPE打开16Edit.DLL(因为这个PE文件有导出表)和VC6打印数据对比

    image-20230407230723457

    230352

2.编写按名字、序号找导出函数地址信息

  • 说明:昨天用到的GetProcAddress()系统函数,也可以按照这两种方式获取DLL中导出函数的地址,今天要编写的两个函数和GetProcAddress()有相同的功能,只是没有系统函数写的那么好那么复杂。区别是GetProcAddress()第一个参数HMODULE是DLL的ImageBase,即PE文件拉伸后的起始地址;自定义函数的第一个参数FileBuffer指针是PE文件未拉伸时的起始地址,即FileBuffer首地址

  • 按名字查找函数:GetFunctionAddrByName(FileBuffer指针,函数名指针)

    #include "stdafx.h"
    #include <stdlib.h>
    #include <string.h>
    
    typedef unsigned short WORD;
    typedef unsigned int DWORD;
    typedef unsigned char BYTE;
    
    #define MZ 0x5A4D
    #define PE 0x4550
    #define IMAGE_SIZEOF_SHORT_NAME 8
    
    //DOS头
    struct _IMAGE_DOS_HEADER {
    	WORD e_magic;  //MZ标记
    	WORD e_cblp;
    	WORD e_cp;
    	WORD e_crlc;
    	WORD e_cparhdr;
    	WORD e_minalloc;
    	WORD e_maxalloc;
    	WORD e_ss;
    	WORD e_sp;
    	WORD e_csum;
    	WORD e_ip;
    	WORD e_cs;
    	WORD e_lfarlc;
    	WORD e_ovno;
    	WORD e_res[4];
    	WORD e_oemid;
    	WORD e_oeminfo;
    	WORD e_res2[10];
    	DWORD e_lfanew;  //PE文件真正开始的偏移地址
    };
    
    //标准PE头
    struct _IMAGE_FILE_HEADER {
    	WORD Machine;  //文件运行平台
    	WORD NumberOfSections;  //节数量
    	DWORD TimeDateStamp;  //时间戳
    	DWORD PointerToSymbolTable;
    	DWORD NumberOfSymbols;
    	WORD SizeOfOptionalHeader;  //可选PE头大小
    	WORD Characteristics;  //特征值
    };
    
    //数据目录
    struct _IMAGE_DATA_DIRECTORY{
        DWORD VirtualAddress;
        DWORD Size;
    };
    
    //可选PE头
    struct _IMAGE_OPTIONAL_HEADER {
    	WORD Magic;  //文件类型
    	BYTE MajorLinkerVersion;
    	BYTE MinorLinkerVersion;
    	DWORD SizeOfCode;   //代码节文件对齐后的大小
    	DWORD SizeOfInitializedData;  //初始化数据文件对齐后的大小
    	DWORD SizeOfUninitializedData;  //未初始化数据文件对齐后大小
    	DWORD AddressOfEntryPoint;  //程序入口点(偏移量)
    	DWORD BaseOfCode;  //代码基址
    	DWORD BaseOfData;  //数据基址
    	DWORD ImageBase;   //内存镜像基址
    	DWORD SectionAlignment;  //内存对齐粒度
    	DWORD FileAlignment;  //文件对齐粒度
    	WORD MajorOperatingSystemVersion;
    	WORD MinorOperatingSystemVersion;
    	WORD MajorImageVersion;
    	WORD MinorImageVersion;
    	WORD MajorSubsystemVersion;
    	WORD MinorSubsystemVersion;
    	DWORD Win32VersionValue;
    	DWORD SizeOfImage;  //文件装入虚拟内存后大小
    	DWORD SizeOfHeaders;  //DOS、NT头和节表大小
    	DWORD CheckSum;  //校验和
    	WORD Subsystem;
    	WORD DllCharacteristics;
    	DWORD SizeOfStackReserve;  //预留堆栈大小
    	DWORD SizeOfStackCommit;  //实际分配堆栈大小
    	DWORD SizeOfHeapReserve;  //预留堆大小
    	DWORD SizeOfHeapCommit;  //实际分配堆大小
    	DWORD LoaderFlags;
    	DWORD NumberOfRvaAndSizes;  //目录项数目
    	_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
    };
    
    //NT头
    struct _IMAGE_NT_HEADERS {
    	DWORD Signature;  //PE签名
    	_IMAGE_FILE_HEADER FileHeader;
    	_IMAGE_OPTIONAL_HEADER OptionalHeader;
    };
    
    //节表
    struct _IMAGE_SECTION_HEADER{
    	BYTE Name[IMAGE_SIZEOF_SHORT_NAME];  //节表名
    	union{
    		DWORD PhysicalAddress;
    		DWORD VirtualSize;  //内存中未对齐大小
    	}Misc;
    	DWORD VirtualAddress;  //该节在内存中偏移地址
    	DWORD SizeOfRawData;  //该节在硬盘上文件对齐后大小
    	DWORD PointerToRawData;  //该节在硬盘上文件对齐后偏移地址
    	DWORD PointerToRelocations;
    	DWORD PointerToLinenumbers;
    	WORD NumberOfRelocations;
    	WORD NumberOfLinenumbers;
    	DWORD Characteristics;  //该节特征属性
    };
    
    //导出表
    struct _IMAGE_EXPORT_DIRECTORY{						
        DWORD Characteristics;  //未使用		
        DWORD TimeDateStamp;  //时间戳		
        WORD MajorVersion;  //未使用		
        WORD MinorVersion;	//未使用		
        DWORD Name;  //指向该导出表文件名字符串  *
        DWORD Base;  //导出函数起始序号  *
        DWORD NumberOfFunctions;  //所有导出函数的个数  *		
        DWORD NumberOfNames;  //以函数名字导出的函数个数  *
        DWORD AddressOfFunctions;  //导出函数地址表RVA  *					
        DWORD AddressOfNames;  //导出函数名称表RVA  *						
        DWORD AddressOfNameOrdinals;  //导出函数序号表RVA  *						
    };
    
    /*计算文件大小函数
    参数:文件绝对路径
    返回值:返回文件大小(单位字节)
    */
    int compute_file_size(char* filePath){
    	FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        fseek(fp,0,2);
    	int size = ftell(fp);
    	//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
    	fclose(fp);
    	return size;
    }
    
    /*将文件读入FileBuffer函数
    参数:文件绝对路径
    返回值:FileBuffer起始地址
    */
    char* to_FileBuffer(char* filePath){
        FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        int size = compute_file_size(filePath);
        
    	char* mp = (char*)malloc(sizeof(char) * size);  //分配内存空间
        if(!mp){
            printf("分配空间失败");
            fclose(fp);
            exit(0);
        }
        
        int isSucceed = fread(mp,size,1,fp);
        if(!isSucceed){
            printf("读取数据失败");
            free(mp);
            fclose(fp);
            exit(0);
        }
        
        fclose(fp);
        return mp;
    }
    
    /*虚拟内存偏移地址->文件偏移地址函数
    参数:FileBuffer起始地址,RVA地址值
    返回值:RVA对应的FOA,返回0则表示非法RVA
    */
    DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_SECTION_HEADER* _image_section_header = NULL;
    
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + 
    		_image_file_header->SizeOfOptionalHeader);
    
    	bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
    	if(RVA < _image_section_header->VirtualAddress)
    		return RVA;
    	for(int i = 0;i < _image_file_header->NumberOfSections;i++){
    		if(RVA >= _image_section_header->VirtualAddress 
    			&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
    			flag = 1;
    			break;
    		}else{
    			_image_section_header++;
    		}
    	}
    	if(!flag)
    		return 0;
    	DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
    	return _image_section_header->PointerToRawData + mem_offset_from_section;	
    }
    
    /*按名字查找导出函数地址
    参数:PE文件的FileBuffer起始地址,函数名字符串指针
    返回值:该导出函数的RVA(如果想返回FOA也可以,代码都在下面)
    */
    DWORD GetFunctionAddrByName(char* fileBufferp,char* funcName){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
    	_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
    	
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
    	if(_image_data_directory->VirtualAddress == 0){
    		printf("此PE文件没有导出表,没有函数导出");
    		getchar();
    		exit(0);
    	}
    	_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)(RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress) + (DWORD)fileBufferp);
    	
        //先遍历函数名称表
    	DWORD* functionNameTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNames) + (DWORD)fileBufferp);
    	bool flag = 0;
    	for(DWORD index = 0;index < _image_export_directory->NumberOfNames;index++){
    		char* nameTemp = (char*)(RVA_to_FOA(fileBufferp,*functionNameTable) + (DWORD)fileBufferp);
    		if(strcmp(nameTemp,funcName) == 0){  //小贴士:strcmp执行完,参数值并不改变
    			flag = 1;
    			break;
    		}
    		functionNameTable++;
    	}
    	if(!flag){
    		printf("没有此导出函数");
    		getchar();
    		exit(0); 
    	}
    	
        //再找函数序号表对应下标index中的值funcIndex
    	WORD* functionOrdinalsTable = (WORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNameOrdinals) + (DWORD)fileBufferp);
    	WORD funcIndex = *(functionOrdinalsTable + index);
    	
        //最后找函数地址表下标为funcIndex中的值
    	DWORD* functionAddressTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfFunctions) + (DWORD)fileBufferp);
        
        //返回该导出函数RVA
    	return *(functionAddressTable + funcIndex);
    	//return RVA_to_FOA(fileBufferp,*(functionAddressTable + funcIndex));
    }
    
    int main(int argc, char* argv[])
    {
    	char* filePath = "D:/LordPE/16Edit.DLL";  //选一个有导出表的PE文件
    	char* funcName = "HEEnterWindowLoopInNewThread"; //使用LordPE看有哪些导出函数
    	char* fileBufferp = to_FileBuffer(filePath);
    	printf("%08X",GetFunctionAddrByName(fileBufferp,funcName));
    	return 0;
    }
    

    验证一下:

    • 先用LordPE打开LordPE文件夹下的一个dll:16Edit.DLL(因为这个文件有导出表),接着查看此dll文件中的导出函数有哪些,我们选择其中之一HEEnterWindowLoopInNewThread进行验证

      image-20230407200020370 200231

    • 运行C程序,发现得到的RVA = 0x000019A0,与答案一致

      image-20230407200515190
  • 按序号查找函数:GetFunctionAddrByOrdinals(FileBuffer指针,函数名导出序号)

    #include "stdafx.h"
    #include <stdlib.h>
    #include <string.h>
    
    typedef unsigned short WORD;
    typedef unsigned int DWORD;
    typedef unsigned char BYTE;
    
    #define MZ 0x5A4D
    #define PE 0x4550
    #define IMAGE_SIZEOF_SHORT_NAME 8
    
    //DOS头
    struct _IMAGE_DOS_HEADER {
    	WORD e_magic;  //MZ标记
    	WORD e_cblp;
    	WORD e_cp;
    	WORD e_crlc;
    	WORD e_cparhdr;
    	WORD e_minalloc;
    	WORD e_maxalloc;
    	WORD e_ss;
    	WORD e_sp;
    	WORD e_csum;
    	WORD e_ip;
    	WORD e_cs;
    	WORD e_lfarlc;
    	WORD e_ovno;
    	WORD e_res[4];
    	WORD e_oemid;
    	WORD e_oeminfo;
    	WORD e_res2[10];
    	DWORD e_lfanew;  //PE文件真正开始的偏移地址
    };
    
    //标准PE头
    struct _IMAGE_FILE_HEADER {
    	WORD Machine;  //文件运行平台
    	WORD NumberOfSections;  //节数量
    	DWORD TimeDateStamp;  //时间戳
    	DWORD PointerToSymbolTable;
    	DWORD NumberOfSymbols;
    	WORD SizeOfOptionalHeader;  //可选PE头大小
    	WORD Characteristics;  //特征值
    };
    
    //数据目录
    struct _IMAGE_DATA_DIRECTORY{
        DWORD VirtualAddress;
        DWORD Size;
    };
    
    //可选PE头
    struct _IMAGE_OPTIONAL_HEADER {
    	WORD Magic;  //文件类型
    	BYTE MajorLinkerVersion;
    	BYTE MinorLinkerVersion;
    	DWORD SizeOfCode;   //代码节文件对齐后的大小
    	DWORD SizeOfInitializedData;  //初始化数据文件对齐后的大小
    	DWORD SizeOfUninitializedData;  //未初始化数据文件对齐后大小
    	DWORD AddressOfEntryPoint;  //程序入口点(偏移量)
    	DWORD BaseOfCode;  //代码基址
    	DWORD BaseOfData;  //数据基址
    	DWORD ImageBase;   //内存镜像基址
    	DWORD SectionAlignment;  //内存对齐粒度
    	DWORD FileAlignment;  //文件对齐粒度
    	WORD MajorOperatingSystemVersion;
    	WORD MinorOperatingSystemVersion;
    	WORD MajorImageVersion;
    	WORD MinorImageVersion;
    	WORD MajorSubsystemVersion;
    	WORD MinorSubsystemVersion;
    	DWORD Win32VersionValue;
    	DWORD SizeOfImage;  //文件装入虚拟内存后大小
    	DWORD SizeOfHeaders;  //DOS、NT头和节表大小
    	DWORD CheckSum;  //校验和
    	WORD Subsystem;
    	WORD DllCharacteristics;
    	DWORD SizeOfStackReserve;  //预留堆栈大小
    	DWORD SizeOfStackCommit;  //实际分配堆栈大小
    	DWORD SizeOfHeapReserve;  //预留堆大小
    	DWORD SizeOfHeapCommit;  //实际分配堆大小
    	DWORD LoaderFlags;
    	DWORD NumberOfRvaAndSizes;  //目录项数目
    	_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
    };
    
    //NT头
    struct _IMAGE_NT_HEADERS {
    	DWORD Signature;  //PE签名
    	_IMAGE_FILE_HEADER FileHeader;
    	_IMAGE_OPTIONAL_HEADER OptionalHeader;
    };
    
    //节表
    struct _IMAGE_SECTION_HEADER{
    	BYTE Name[IMAGE_SIZEOF_SHORT_NAME];  //节表名
    	union{
    		DWORD PhysicalAddress;
    		DWORD VirtualSize;  //内存中未对齐大小
    	}Misc;
    	DWORD VirtualAddress;  //该节在内存中偏移地址
    	DWORD SizeOfRawData;  //该节在硬盘上文件对齐后大小
    	DWORD PointerToRawData;  //该节在硬盘上文件对齐后偏移地址
    	DWORD PointerToRelocations;
    	DWORD PointerToLinenumbers;
    	WORD NumberOfRelocations;
    	WORD NumberOfLinenumbers;
    	DWORD Characteristics;  //该节特征属性
    };
    
    //导出表
    struct _IMAGE_EXPORT_DIRECTORY{						
        DWORD Characteristics;  //未使用		
        DWORD TimeDateStamp;  //时间戳		
        WORD MajorVersion;  //未使用		
        WORD MinorVersion;	//未使用		
        DWORD Name;  //指向该导出表文件名字符串  *
        DWORD Base;  //导出函数起始序号  *
        DWORD NumberOfFunctions;  //所有导出函数的个数  *		
        DWORD NumberOfNames;  //以函数名字导出的函数个数  *
        DWORD AddressOfFunctions;  //导出函数地址表RVA  *					
        DWORD AddressOfNames;  //导出函数名称表RVA  *						
        DWORD AddressOfNameOrdinals;  //导出函数序号表RVA  *						
    };
    
    /*计算文件大小函数
    参数:文件绝对路径
    返回值:返回文件大小(单位字节)
    */
    int compute_file_size(char* filePath){
    	FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        fseek(fp,0,2);
    	int size = ftell(fp);
    	//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
    	fclose(fp);
    	return size;
    }
    
    /*将文件读入FileBuffer函数
    参数:文件绝对路径
    返回值:FileBuffer起始地址
    */
    char* to_FileBuffer(char* filePath){
        FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        int size = compute_file_size(filePath);
        
    	char* mp = (char*)malloc(sizeof(char) * size);  //分配内存空间
        if(!mp){
            printf("分配空间失败");
            fclose(fp);
            exit(0);
        }
        
        int isSucceed = fread(mp,size,1,fp);
        if(!isSucceed){
            printf("读取数据失败");
            free(mp);
            fclose(fp);
            exit(0);
        }
        
        fclose(fp);
        return mp;
    }
    
    /*虚拟内存偏移地址->文件偏移地址函数
    参数:FileBuffer起始地址,RVA地址值
    返回值:RVA对应的FOA,返回0则表示非法RVA
    */
    DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_SECTION_HEADER* _image_section_header = NULL;
    
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + 
    		_image_file_header->SizeOfOptionalHeader);
    
    	bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
    	if(RVA < _image_section_header->VirtualAddress)
    		return 0;
    	for(int i = 0;i < _image_file_header->NumberOfSections;i++){
    		if(RVA >= _image_section_header->VirtualAddress 
    			&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
    			flag = 1;
    			break;
    		}else{
    			_image_section_header++;
    		}
    	}
    	if(!flag)
    		return 0;
    	DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
    	return _image_section_header->PointerToRawData + mem_offset_from_section;	
    }
    
    /*按名字查找导出函数地址信息函数
    参数:PE文件的FileBuffer起始地址,函数名字符串指针
    返回值:该导出函数的RVA(如果想返回FOA也可以,代码都在下面)
    */
    /*DWORD GetFunctionAddrByName(char* fileBufferp,char* funcName){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
    	_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
    	
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
    	if(_image_data_directory->VirtualAddress == 0){
    		printf("此PE文件没有导出表,没有函数导出");
    		getchar();
    		exit(0);
    	}
    	_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)(RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress) + (DWORD)fileBufferp);
    	
        //先遍历函数名称表
    	DWORD* functionNameTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNames) + (DWORD)fileBufferp);
    	bool flag = 0;
    	for(DWORD index = 0;index < _image_export_directory->NumberOfNames;index++){
    		char* nameTemp = (char*)(RVA_to_FOA(fileBufferp,*functionNameTable) + (DWORD)fileBufferp);
    		if(strcmp(nameTemp,funcName) == 0){  //小贴士:strcmp执行完,参数值并不改变
    			flag = 1;
    			break;
    		}
    		functionNameTable++;
    	}
    	if(!flag){
    		printf("没有此导出函数");
    		getchar();
    		exit(0); 
    	}
    	
        //再找函数序号表对应下标index中的值funcIndex
    	WORD* functionOrdinalsTable = (WORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfNameOrdinals) + (DWORD)fileBufferp);
    	WORD funcIndex = *(functionOrdinalsTable + index);
    	
        //最后找函数地址表下标为funcIndex中的值
    	DWORD* functionAddressTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfFunctions) + (DWORD)fileBufferp);
        
        //返回该导出函数RVA
    	return *(functionAddressTable + funcIndex);
    	//return RVA_to_FOA(fileBufferp,*(functionAddressTable + funcIndex));
    }*/
    
    /*按序号查找导出函数地址信息函数
    参数:PE文件的FileBuffer起始地址,函数名字符串指针
    返回值:该导出函数的RVA(如果想返回FOA也可以,代码都在下面)
    */
    DWORD GetFunctionAddrByOrdinals(char* fileBufferp,DWORD ordinal){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
    	_IMAGE_EXPORT_DIRECTORY* _image_export_directory = NULL;
    	
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_data_directory = (_IMAGE_DATA_DIRECTORY*)_image_optional_header->DataDirectory;
    	if(_image_data_directory->VirtualAddress == 0){
    		printf("此PE文件没有导出表,没有函数导出");
    		getchar();
    		exit(0);
    	}
    	_image_export_directory = (_IMAGE_EXPORT_DIRECTORY*)(RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress) + (DWORD)fileBufferp);
    	
        //判断导出序号是否在范围内
        if(ordinal - _image_export_directory->Base > _image_export_directory->NumberOfFunctions - 1){  //因为ordinal用的WORD类型,所以<0的情况不用考虑
    		printf("导出序号不合法");
    		getchar();
    		exit(0);
    	}
    
    	//定位到函数地址表起始地址
    	DWORD* functionAddressTable = (DWORD*)(RVA_to_FOA(fileBufferp,_image_export_directory->AddressOfFunctions) + (DWORD)fileBufferp);
    	
    	//返回该序号导出函数的RVA
    	return *(functionAddressTable + (ordinal - _image_export_directory->Base));
    	//return RVA_to_FOA(fileBufferp,*(functionAddressTable + (ordinal - _image_export_directory->Base)));
    }
    
    int main(int argc, char* argv[])
    {
    	char* filePath = "D:/OllyICE_1.10/OllyDBG.EXE";  //选一个有导出表的PE文件
    	char* fileBufferp = to_FileBuffer(filePath);
        //char* funcName = "_Go";  //使用LordPE看有哪些导出函数
        //printf("%08X",GetFunctionAddrByName(fileBufferp,funcName));
    	
    	printf("%08X",GetFunctionAddrByOrdinals(fileBufferp,0xA5)); //使用LordPE看有哪些导出序号
    	return 0;
    }
    

    验证一下:

    • 使用LordPE打开OllyDBG.EXE(因为这个PE文件有导出表)查看有哪些导出函数以及序号是什么,我选择序号为0xA5的导出函数进行验证

      203017
    • 运行C程序,RVA = 0x0008DD10,与答案一致

      image-20230407203302989
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: mysql-installer-community-5.7.36.1.msi是MySQL数据库的安装程序。MySQL是一个开源的关系型数据库管理系统,广泛用于各种Web应用程序和服务器环境中。 通过运行mysql-installer-community-5.7.36.1.msi程序,用户可以方便地安装和配置MySQL数据库。安装程序提供了一系列选项,用户可以根据自己的需求进行选择。例如,用户可以选择安装MySQL Server,这是MySQL数据库的核心组件;同时,用户还可以选择安装MySQL Workbench,这是一个强大的可视化工具,用于管理和操作MySQL数据库;此外,安装程序还提供了其他一些辅助工具和插件,用户根据需要进行选择安装。 一旦安装完成,用户就可以通过MySQL服务器访问和管理数据库。用户可以创建、修改、删除数据库和,执行SQL查询和操作,设置数据库用户和权限等。MySQL提供了强大的功能和高性能,可以处理大规模的数据集和高负载的访问请求。 mysql-installer-community-5.7.36.1.msi是MySQL社区版的安装程序,适用于个人用户、学生和小型企业。MySQL还提供了其他版本,如企业版和集群版,用于满足不同规模和需求的用户。总的来说,mysql-installer-community-5.7.36.1.msi是一个方便易用的工具,帮助用户快速搭建和管理MySQL数据库环境。 ### 回答2: mysql-installer-community-5.7.36.1.msi是MySQL的一个安装程序文件。它是MySQL官方发布的一款针对Windows操作系统的安装程序,用于安装MySQL数据库服务器和相关工具。 通过运行mysql-installer-community-5.7.36.1.msi文件,用户可以简便地在Windows系统上安装MySQL数据库服务器。安装程序提供了直观的图形界面,帮助用户完成MySQL的安装配置过程。用户可以选择安装MySQL服务器的版本、设置用户名和密码、指定安装路径等。安装程序还支持创建数据库实例、启动和停止数据库服务等操作。 mysql-installer-community-5.7.36.1.msi还包括了其他一些实用工具和组件,例如Workbench、Connector/J等。Workbench是MySQL官方提供的数据库管理工具,可以进行数据库设计、开发和管理。Connector/J是MySQL官方提供的Java驱动程序,用于在Java应用程序中连接和操作MySQL数据库。 通过mysql-installer-community-5.7.36.1.msi的安装,用户可以快速搭建并配置MySQL数据库环境,方便进行开发和测试工作。此外,MySQL数据库具有开源、跨平台、高性能等特点,被广泛应用于各种类型的项目中。 总之,mysql-installer-community-5.7.36.1.msi是一款方便易用的MySQL安装程序,通过它可以快速安装和配置MySQL数据库服务器和相关工具,为用户提供高效可靠的数据库环境。 ### 回答3: mysql-installer-community-5.7.36.1.msi是MySQL的安装文件。MySQL是一个开源的关系型数据库管理系统。它能够提供可靠和高效的数据存储和检索,并且支持多种编程语言。MySQL Installer是MySQL官方提供的一个安装软件,它方便用户在Windows操作系统上安装和配置MySQL数据库。 这个安装文件是MySQL的社区版,因此是免费提供的。它的版本号是5.7.36.1,代了MySQL的特定版本号。版本号的变化通常意味着一些功能的更新、修复或改进。 安装程序提供了一个图形界面来引导用户进行MySQL的安装过程。在安装过程中,用户可以选择安装MySQL数据库服务器以及其他MySQL的组件,如MySQL Workbench和MySQL命令行工具等。用户还可以选择所需的安装路径和服务器的配置参数等。 通过运行这个安装文件,用户可以快速而方便地将MySQL安装在他们的计算机上,并开始使用这个强大的数据库管理系统。用户需要按照安装向导提供的步骤进行操作,直到安装完成。 总之,mysql-installer-community-5.7.36.1.msi提供了一种简单的方式来安装MySQL数据库,并允许用户通过图形界面进行配置和管理。这个安装文件对于想要在Windows操作系统上使用MySQL的用户来说是非常有价值的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值