要开发通用shellcode,定位API是必不可少的。Windows的API是通过动态链接库中的导出函数来实现的,例如,内存操作等函数在kernel32.dll中实现;大量的图形界面相关的APl则在user32.dll中实现。Win_32平台下的shellcode使用最广泛的方法,就是通过从进程环境块中找到动态链接库的导出表,并搜索出所需的APl地址,然后逐一调用。
所有win32程序都会加载ntdll.dll和kernel32.dll 这两个最基础的动态链接库。如果想要在win_32平台下定位kernel32.dll中的API地址,可以采用如下方法。
(1)首先通过段选择字FS在内存中找到当前的线程环境块TEB。
(2)线程环境块偏移位置为0x30的地方存放着指向进程环境块PEB的指针。
(3)进程环境块中偏移位置为0x0C的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。
(4)PEB_LDR_DATA结构体偏移位置为0x1C的地方存放着指向模块初始化链表的头指针InlnitializationOrderModuleList。
(5)模块初始化链表InlnitializationOrderModuleList中按顺序存放着PE装入运行时初始化模块的信息,第一个链表结点是ntdll.dll,第二个链表结点就是kernel32.dll。
(6)找到属于kernel32.dll的结点后,在其基础上再偏移0×08就是kenel32.dl在内存中的加载基地址。
链表图解

#include<stdio.h>
int main()
{
__asm
{
MOV EAX, DWORD PTR FS:[0x30] ;获取PEB基址
MOV EAX, DWORD PTR DS:[EAX+0xC] ;获取PEB_LDR_DATA结构指针
MOV ESI, DWORD PTR DS:[EAX+0x1C] ;获取InInitializationOrderModuleList成员指针
MOV EAX, DWORD PTR DS:[ESI+0x8] ;取其基地址,该结构当前包含的是ntdll.dll
MOV ESI, DWORD PTR DS:[ESI] ;获取双向链表当前节点的后继指针
MOV EBX, DWORD PTR DS:[ESI+0x8] ;取其基地址,该结构当前包含的是kernel32.dll
}
}
(7)从kernel32.dll的加载基址算起,偏移0x3C的地方就是其PE头。这里就需要一些PE文件的知识了,PE文件的开头是DOS头和PE头。DOS头的结构如下。
//DOS头的结构体
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number 这个必须为MZ,即0x5A4D
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; // 这个成员的值为PE头的偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
实例截图

(8)PE头偏移0×78的地方存放着指向函数导出表的指针。PE头结构如下。
//PE头结构
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //4 bytes PE文件头标志:(e_lfanew)->‘PE’
IMAGE_FILE_HEADER FileHeader; //20 bytes PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//224bytes PE文件逻辑分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
BYTE MajorLinkerVersion; // 链接程序的主版本号
BYTE MinorLinkerVersion; // 链接程序的次版本号
DWORD SizeOfCode; // 所有含代码的节的总大小
DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码的区块的起始RVA
DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
DWORD ImageBase; // 程序的首选装载地址
DWORD SectionAlignment; // 内存中的区块的对齐大小
DWORD FileAlignment; // 文件中的区块的对齐大小
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸
DWORD SizeOfHeaders; // 所有头+ 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和
WORD Subsystem; // 可执行文件期望的子系统
WORD DllCharacteristics; // DllMain()函数何时被调用,默认为0
DWORD SizeOfStackReserve; // 初始化时的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 与调试有关,默认为0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表 ,即为我们的导出表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
(9)至此,我们可以按如下方式在函数导出表中算出所需函数的入口地址,如下图所示。

导出表偏移0xlC处的指针指向存储导出函数偏移地址(RVA)的列表。
导出表偏移0x20处的指针指向存储导出函数函数名的列表。这里我用代码演示一下过程。
#include<stdio.h>
int main()
{
char *str_p;
__asm
{
MOV EAX, DWORD PTR FS:[0X30] ;获取PEB基址
MOV EAX, DWORD PTR DS:[EAX+0xC] ;获取PEB_LDR_DATA结构指针
MOV ESI, DWORD PTR DS:[EAX+0x1C] ;获取InInitializationOrderModuleList成员指针
MOV EAX, DWORD PTR DS:[ESI+0x08] ;取其基地址,该结构当前包含的是ntdll.dll
MOV ESI, DWORD PTR DS:[ESI] ;获取双向链表当前节点的后继指针
MOV EBX, DWORD PTR DS:[ESI+0x08] ;取其基地址,该结构当前包含的是kernel32.dll
;这后面的都是相对于kernel.dll的基地址偏移
MOV ECX, DWORD PTR DS:[EBX+0x3C] ;获得PE头偏移
ADD ECX, EBX ;PE头结构体
MOV ECX, DWORD PTR DS:[ECX+0x78] ;获得导出表指针偏移
ADD ECX, EBX ;导出表指针
MOV ECX, DWORD PTR DS:[ECX+0x20] ;获得函数列表名指针偏移
ADD ECX, EBX ;函数列表名指针
MOV ECX, DWORD PTR DS:[ECX + 0*4] ;获得第一个函数名字符串指针的偏移
ADD ECX, EBX ;第一个函数名字符串指针
MOV str_p,ECX ;
}
printf("%s\n",str_p);
}
//环境:Windows XP + vs2010 express
//参数:无
//输出:
//ActivateActCtx
上面的代码在VS2017中编译会出现问题,源码编译出来的汇编代码和源码不同,导致程序崩溃(少了三个ADD ECX,EBX,可能是VS2017的bug),所以对下面的代码对上面的代码进行了改进,原理还是一样的。并且在Win10上能编译和运行。
#include<stdio.h>
int main()
{
char *str_p;
__asm
{
MOV EAX, DWORD PTR FS:[0X30] ;获取PEB基址
MOV EAX, DWORD PTR DS:[EAX+0xC] ;获取PEB_LDR_DATA结构指针
MOV ESI, DWORD PTR DS:[EAX+0x1C] ;获取InInitializationOrderModuleList成员指针
MOV EAX, DWORD PTR DS:[ESI+0x08] ;取其基地址,该结构当前包含的是ntdll.dll
MOV ESI, DWORD PTR DS:[ESI] ;获取双向链表当前节点的后继指针
MOV EBX, DWORD PTR DS:[ESI+0x08] ;取其基地址,该结构当前包含的是kernel32.dll
;这后面的都是相对于kernel.dll的基地址偏移
MOV ECX, DWORD PTR DS:[EBX+0x3C] ;获得PE头偏移
MOV ECX, DWORD PTR DS:[ECX+EBX+0x78] ;获得导出表指针偏移
MOV ECX, DWORD PTR DS:[ECX+EBX+0x20] ;获得函数列表名指针偏移
MOV ECX, DWORD PTR DS:[ECX+EBX+0*4] ;获得第一个函数名字符串指针的偏移
ADD ECX, EBX ;第一个函数名字符串指针
MOV str_p,ECX ;
}
printf("%s\n",str_p);
}
//环境:Win10 + vs2017
//参数:无
//输出:
//AccessCheck
函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA。
获得RVA后,再加上前边已经得到的动态链接库的加载基址,就获得了所需API此刻在内存中的虚拟地址,这个地址就是我们最终在shellcode中调用时需要的地址。
按照上面的方法,我们已经可以获得kenel32.dl中的任意函数。类似地,我们已经具备了定位ws2_32.dll中的winsock函数来编写一个能够获得远程shell的真正的shellcode了。