定位API的原理

要开发通用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了。