由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习)
该方法最早由 Stephen Fewer 在2009年提出,顾名思义,其是通过Hash来条用函数,替代传统的直接使用函数的名称。
我们先来简单的了解一下PE结构
PE文件
PE文件是由许许多多的结构体组成,其从上到下依次是Dos头、Nt头、节表、节区和调试信息(可选)。
IMAGE_DOS_HEADER
IMAGE_DOS_HEADER的结构如下
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中最最要的就是e_magic、e_lfanew
e_magic:用十六进制表示就是4D 5A,其是Mark Zbikowski(MZ)的姓名缩写,他是最初的MS-DOS设计者之一
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DPJU2N69-1693021214748)(https://image.3001.net/images/20230331/1680245160_642681a83538c752273e1.png!small)]e_lfanew:它保存着IMAGE_NT_HEADERS32这个结构体在PE文件中的偏移地址,PE文件运行时只有通过该文件才能定位到PE签名。
IMAGE_DOS_STUB
在文件的第一个字节之后,将启动一个dos存根。内存中的这个区域大部分都是零
IMAGE_NT_HEADERS
可以看见其分为32位、64位,结构都差不多一样的
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:PE的签名,相对该结构的偏移0x00
FileHeader:结构体,相对该结构的偏移0x04
OptionalHeader:结构体,相对该结构的偏移0x18
Signature
其DOS头的MZ一样,都是PE文件的标准特征
IMAGE_FILE_HEADER
我们看看IMAGE_FILE_HEADER这个结构体,32位、64位中该结构体都是一样的
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine
指定该PE文件能够在32位上运行,还是在64位上执行
NumberOfSections
表示当前PE文件节区的数量
TimeDateStamp
该文件创建的时间,时间从1970年1月1日00:00开始计算
PointerToSymbolTable
指向COFF符号表偏移的指针,由于其已经被Debug格式所替代,因此COFF符号表在现今的PE文件中已较少应用
NumberOfSymbols
符号表中符号的数量,由于COFF符号表是一个大小固定的结构,因此只有通过这个字段才能计算出COFF符号表结构的结尾。
SizeOfOptionalHeader
存储该PE文件的可选PE头的大小,在32位的系统中是0x00E0,而在64位系统下则为0x00F0
Characteristics
该值描述PE文件的一些属性信息,比如是否可执行、是否是一个动态连接库等。该值可以是一个也可以是多个值的和
常用的文件属性值如下
比如我们这的和是22,分别是2、20分别对应上面的IMAGE_FILE_EXECUTABLE_IMAGE、IMAGE_FILE_LARGE_ADDRESS_AWARE
IMAGE_OPTIONAL_HEADER
接下来我们看看IMAGE_OPTIONAL_HEADER,其同样也分32位、64位,由于其成员较多因此这里只列出32位且只介绍重要的成员
IMAGE_OPTIONAL_HEADER32
typedef 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;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic
文件类型标识
-
普通可执行映像0x010B
-
ROM镜像为0x0170
-
PE32+为0x020B
AddressOfEntryPoint
程序执行入口的RAV,在大多数可执行文件中,入口点(AddressOfEntryPoint)并不指向main()、winmaim()或dllmain()等函数的入口,而是指向运行时库代码,再由其调用这些函数。
ImageBase
文件在内存中的首选装入地址(对于DLL文件来说,即使其未能在此地址装入,也可以将其实际装入地址称为ImageBase)。如果该地址被占用,则会选用其它地址,但是如果文件被载入其它地址,那么就必须要通过重定位表对其进行资源的重定位,这就会导致文件的载入速度变慢
SectionAlignment
映像文件在被装入内存时的区段对齐大小(该成员的默认大小为系统的页面大小)
FileAlignment
映像文件在磁盘上的区段对齐大小
SizeOfImage
映像文件装入内存后的总大小(从ImageBase到最后一个区段的总大小)
SizeOfHeaders
MS-DOS头、PE头、区块表的尺寸之和
NumberOfRvaAndSizes
数据目录成员的数量,一般为0x00000010
IMAGE_DATA_DIRECTORY
其结构如下
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress:指向某个数据的相对虚拟地址RAV
Size:某个数据块的大小
这两个成员就是定位各种表的关键信息,以下表格就是它的对应关系,其不同成员的信息如下
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
由于很多,这里只介绍后面要用到的IMAGE_EXPORT_DIRECTORY(导出表),其结构体的结构如下,同样只介绍 重要的部分
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
NumberOfFunctions
实际导出的函数个数,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions
= 4 - 0 + 1 = 5个
NumberOfNames
导出的函数中具名的函数个数
AddressOfFunctions
导出函数地址数组(数组元素个数=NumberOfFuntions),其可以用来定位导出表中所有函数的地址表,其长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。
AddressOfNames
函数名称地址数组(数组元素个数=NumberOfNames),其可以用来定位导出表中所有函数的名称表,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。
AddressOfNameOrdinals
Ordinal
地址数组(数组元素个数=NumberOfNames),可以用来定位导出表中所有函数的序号表,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
IMAGE_SECTION_HEADER这个本篇文章用不到,读者感兴趣的话可以去看看参考文章
GetProcAddress() 的原理
-
利用AddressOfName成员转到"函数名称地址数组"(IMAGE_EXPORT_DIRECTORY.AddressOfNames)
-
该地址处存储着此模块的所有的导出名称字符串,通过比较字符串(strcmp),找到指定的函数名称。此时数组的索引记为i
-
利用AddressOfNameOrdinals成员,转到ordinal数组(IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals)
-
在ordinal数组中通过i查找相应的值(AddressOfNameOrdinals[i],即ordinal[i])
-
查找并跳转到导出函数地址数组所在的地址(IMAGE_EXPORT_DIRECTORY.AddressOfFunctions)
-
将刚刚得到的ordinal的值作为数组索引,得到最终的函数起始地址,类似(AddressOfFunctions[ordinal[i]])
代码实现
我们用c++实现,去查找MessageBoxW,同时打印出利用GetProcAddress函数获取的地址
#include <windows.h>
#include <stdio.h>
#include <iostream>
using namespace std;
int main(){
HMODULE hand = LoadLibraryW(L"user32.dll");
PIMAGE_DOS_HEADER Dos_Header = (PIMAGE_DOS_HEADER)hand;
PIMAGE_NT_HEADERS Nt_Headers = (PIMAGE_NT_HEADERS)((LPBYTE)hand + Dos_Header->e_lfanew);
PIMAGE_EXPORT_DIRECTORY Export_Directory = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)hand + Nt_Headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD fAddr = (PDWORD)((LPBYTE)hand + Export_Directory->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)hand + Export_Directory->AddressOfNames);
PWORD ordinal = (PWORD)((LPBYTE)hand + Export_Directory->AddressOfNameOrdinals);
for (DWORD i = 0; i < Export_Directory->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)hand + fNames[i]);
if (strcmp(pFuncName,"MessageBoxW") == 0) {
printf("%s\n",pFuncName);
cout << "Export_Directory value is: " << (LPVOID)((LPBYTE)hand + fAddr[ordinal[i]]) << endl;
cout << "GetProcAddress value is: " << GetProcAddress(hand, "MessageBoxW") << endl;
}
}
}
我们使用python实现简单的Hash加密
import sys
hash = 0x35
if(len(sys.argv) != 2):
print("usage:\npython3 Hash.py MessageBoxW")
else:
data = sys.argv[1]
for i in range(0, len(data)):
hash += ord(data[i]) + (hash << 1)
print (hash)
c++中逻辑也相同的
DWORD HashEncode(char* data) {
DWORD hash = 0x35;
for (int i = 0; i < strlen(data); i++) {
hash += data[i] + (hash << 1);
}
return hash;
}
实现如下
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (WINAPI* _MessageBoxW)(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType
);
DWORD HashEncode(char* data) {
DWORD hash = 0x35;
for (int i = 0; i < strlen(data); i++) {
hash += data[i] + (hash << 1);
}
return hash;
}
LPVOID FindHash(HANDLE hand) {
PIMAGE_DOS_HEADER Dos_Header = (PIMAGE_DOS_HEADER)hand;
PIMAGE_NT_HEADERS Nt_Headers = (PIMAGE_NT_HEADERS)((LPBYTE)hand + Dos_Header->e_lfanew);
PIMAGE_EXPORT_DIRECTORY Export_Directory = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)hand + Nt_Headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD fAddr = (PDWORD)((LPBYTE)hand + Export_Directory->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)hand + Export_Directory->AddressOfNames);
PWORD ordinal = (PWORD)((LPBYTE)hand + Export_Directory->AddressOfNameOrdinals);
for (DWORD i = 0; i < Export_Directory->AddressOfFunctions; i++) {
LPSTR pFuncName = (LPSTR)((LPBYTE)hand + fNames[i]);
if (HashEncode(pFuncName) == 17036718) {
cout << "Success Found: " << pFuncName << endl;
return (LPVOID)((LPBYTE)hand + fAddr[ordinal[i]]);
}
}
}
int main() {
HMODULE hand = LoadLibraryW(L"user32.dll");
_MessageBoxW lMessageBoxW = (_MessageBoxW)FindHash(hand);
lMessageBoxW(NULL, L"Hello", L"Hello", MB_OK);
}
参考:
https://bbs.kanxue.com/thread-252795.htm#msg_header_h2
r[ordinal[i]]);
}
}
}
int main() {
HMODULE hand = LoadLibraryW(L"user32.dll");
_MessageBoxW lMessageBoxW = (_MessageBoxW)FindHash(hand);
lMessageBoxW(NULL, L"Hello", L"Hello", MB_OK);
}
[外链图片转存中…(img-OuzFABRI-1693021214759)]
参考:
https://bbs.kanxue.com/thread-252795.htm#msg_header_h2
接下来我将给各位同学划分一张学习计划表!
学习计划
那么问题又来了,作为萌新小白,我应该先学什么,再学什么?
既然你都问的这么直白了,我就告诉你,零基础应该从什么开始学起:
阶段一:初级网络安全工程师
接下来我将给大家安排一个为期1个月的网络安全初级计划,当你学完后,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web渗透、安全服务、安全分析等岗位;其中,如果你等保模块学的好,还可以从事等保工程师。
综合薪资区间6k~15k
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)
2、渗透测试基础(1周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等
3、操作系统基础(1周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)
4、计算机网络基础(1周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现
5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固
6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
那么,到此为止,已经耗时1个月左右。你已经成功成为了一名“脚本小子”。那么你还想接着往下探索吗?
阶段二:中级or高级网络安全工程师(看自己能力)
综合薪资区间15k~30k
7、脚本编程学习(4周)
在网络安全领域。是否具备编程能力是“脚本小子”和真正网络安全工程师的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。
零基础入门的同学,我建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习
搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP,IDE强烈推荐Sublime;
Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,没必要看完
用Python编写漏洞的exp,然后写一个简单的网络爬虫
PHP基本语法学习并书写一个简单的博客系统
熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选)
了解Bootstrap的布局或者CSS。
阶段三:顶级网络安全工程师
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
学习资料分享
当然,只给予计划不给予学习资料的行为无异于耍流氓,这里给大家整理了一份【282G】的网络安全工程师从入门到精通的学习资料包,可点击下方二维码链接领取哦。