专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
51好读  ›  专栏  ›  看雪学苑

Windows 平台下的最优化 Shellcode 代码编写指引

看雪学苑  · 公众号  · 互联网安全  · 2017-05-10 18:28

正文

前言

在构造一个 Shellcode 载荷时总是存在多种方法,特别是对于 Windows 平台来说。需要手工编写所有的汇编代码,或者说编译器能够有所帮助吗?需要直接使用syscall,还是需要在内存中搜索函数?因为构造载荷一般来说不会很简单,所以我决定写一篇文章来专门论述相关问题。我习惯于用 C 语言来完成所有的工作,并使用 Visual Studio 来对其进行编译:因为 C 语言的源代码更加优美,编译器能够更好地对其进行优化,并且如果需要的话可以使用 LLVM 框架实现自己的混淆器。

在本例中,我将针对于 x86 架构下的 Shellcode 代码;当然,相关的分析完全可以应用于 x86(64位)架构下的 Shellcode 代码或者别的处理器。


寻找基本DLL


简介

当一个 Shellcode 载荷加载到 Windows 系统中的时候,第一步就是要定位需要使用的函数,即搜索存储函数的动态链接库(DLL)。为此,我们需要用到以下各小节所描述的不同的结构。

线程环境块

Windows 系统使用TEB结构来描述一个线程,每个线程通过使用 FS(x86平台)或GS(x86,64 位平台)寄存器来访问其自身的 TEB 结构。TEB结构具体如下:

因此,若想要访问 PEB 结构,只需要进行如下操作:


PEB* getPeb() {

    __asm {

        mov eax, fs:[0x30];

    }

}

进程环境块

如果说 TEB 结构给出了一个线程的相关信息,那么 PEB 结构将告诉我们关于进程自身的信息,其中我们所需要的信息是基本 DLL 的位置。实际上,在 Windows 系统加载一个进程到内存中的时候,至少要映射两个 DLL:

  • ntdll.dll,其中包含执行 syscall 的函数,它们都以前缀 Nt 开头(形如Nt*),并调用内核中以Zw开头(形如Zw*)名称相同的函数;

  • kernel32.dll,在更高层次上使用 NTDLL 中的函数。比如,kernel32!CreateFileA 函数将调用 ntdll!NtCreateFileW 函数,而后者又将调用 ntoskrnl!ZwCreateFileW 函数。

在不同版本的 Windows 系统下,其他的 DLL 可能已经存在于内存中,但是是完全可移植的;因此,我们假设以上两个 DLL 是唯一加载的 DLL 模块。

让我们看一下 PEB 结构,如下所示:


可以看到,其中一个成员名为 PEB.BeingDebugged,可被 IsDebuggerPresent() 函数使用;而我们感兴趣的部分是成员 PEB.Ldr,其对应于如下结构:


0:000> dt nt!_PEB_LDR_DATA

    +0x000 Length           : Uint4B

    +0x004 Initialized      : UChar

    +0x008 SsHandle         : Ptr32 Void

    +0x00c InLoadOrderModuleList : _LIST_ENTRY

    +0x014 InMemoryOrderModuleList : _LIST_ENTRY

    +0x01c InInitializationOrderModuleList : _LIST_ENTRY

    +0x024 EntryInProgress  : Ptr32 Void

    +0x028 ShutdownInProgress : UChar

    +0x02c ShutdownThreadId : Ptr32 Void

顾名思义,成员 PEB.Ldr->In*OrderModuleList 是包含所有已加载到内存中的 DLL 模块的链表(LIST_ENTRY);三个表以不同的顺序指向相同的对象。我更倾向于使用InLoadOrderModuleList,因为可以像使用一个指向 _LDR_DATA_TABLE_ENTRY 的指针一样直接使用 InLoadOrderModuleList.Flink。例如,如果使用InMemoryOrderModuleList,由于 InMemoryOrderModuleList.Flink 指针指向下一个InMemoryOrderModuleList,所以LDR_DATA_TABLE_ENTRY 将位于(_InMemoryOrderModuleList.Flink–0x10)的位置。每个链表成员具有如下结构:

0:000> dt nt!_LDR_DATA_TABLE_ENTRY

    +0x000 InLoadOrderLinks : _LIST_ENTRY

    +0x008 InMemoryOrderLinks : _LIST_ENTRY

    +0x010 InInitializationOrderLinks : _LIST_ENTRY

    +0x018 DllBase          : Ptr32 Void

    +0x01c EntryPoint       : Ptr32 Void

    +0x020 SizeOfImage      : Uint4B

    +0x024 FullDllName      : _UNICODE_STRING

    +0x02c BaseDllName      : _UNICODE_STRING

    ...

    +0x0a0 DependentLoadFlags : Uint4B

BaseDllName 包含 DLL 模块的名称(比如,ntdll.dll),而 DllBase 包含 DLL 模块所加载到内存中的地址。通常,InLoadOrderModuleList 中的第一个成员就是可执行程序自身,在之后我们能够找到 NTDLL 和 KERNEL32;然而,我们并不确定在所有版本的 Windows 系统中都是这个次序,所以最好根据 DLL 名称(大写/小写)进行搜索。

DJB散列

如前所述,我们并不信任 DLL 顺序而选择根据 DLL 名称进行搜索。然而在一段 Shellcode 代码中,使用 ASCII 字符串(或更糟的,UNICODE 字符串)并不是一个好主意:这将使得我们的 Shellcode 代码过于臃肿!因此我建议,使用散列机制来比较 DLL 名称。由于其简洁有效性我选择使用DJB散列算法,具体代码如下所示:

DWORD djbHashW(wchar_t* str) {

    unsigned int hash = 5381;

    unsigned int i = 0;

  

    for (i = 0; str[i] != 0; i++) {

        hash = ((hash <

    }

  

    return hash;

}

由于 DLL 名称可能大写也可能小写,因此在散列算法中最好满足如下等式关系:

djbHashW(L"ntdll.dll") == djbHashW(L"NTDLL.DLL")


代码

现在我们已经讨论了如何完成以上工作,是时候来编码实现我们的想法了。具体代码如下所示:

如果想要使用其他的 DLL 模块,只需要使用 LoadLibrary() 函数来将其加载。不要着急,我们将在 user32.dll 相关的 Shellcode 代码中进行该项工作。


函数地址

简介

现在我们已经找到 DLL,下一步需要在 DLL 内存空间中搜寻所需的函数在哪儿。幸运的是,透彻理解 PE 文件头部结构的前提下这并不复杂。要牢记的是,当我们讨论 PE 文件头部时,提到的大部分地址都与可执行程序地址相关。

可移植的执行体头部

执行体的开始位置是一个 DOS 头部,但它只是为了兼容(DOS 系统)而存在。具体结构如下所示:

0:000> dt nt!_IMAGE_DOS_HEADER

    +0x000 e_magic          : Uint2B

    +0x002 e_cblp           : Uint2B

    +0x004 e_cp             : Uint2B

    +0x006 e_crlc           : Uint2B

    +0x008 e_cparhdr        : Uint2B

    +0x00a e_minalloc       : Uint2B

    +0x00c e_maxalloc       : Uint2B

    +0x00e e_ss             : Uint2B

    +0x010 e_sp             : Uint2B

    +0x012 e_csum           : Uint2B

    +0x014 e_ip             : Uint2B

    +0x016 e_cs             : Uint2B

    +0x018 e_lfarlc         : Uint2B

    +0x01a e_ovno           : Uint2B

    +0x01c e_res            : [4] Uint2B

    +0x024 e_oemid          : Uint2B

    +0x026 e_oeminfo        : Uint2B

    +0x028 e_res2           : [10] Uint2B

    +0x03c e_lfanew         : Int4B

成员 e_lfanew 将指示 NT 头部的位置。由于这是一个相对地址,所以需要进行 pFile+e_lfanew 操作。NT 头部具体结构如下所示:

0:000> dt -r1 nt!_IMAGE_NT_HEADERS

    +0x000 Signature        : Uint4B

    +0x004 FileHeader       : _IMAGE_FILE_HEADER

        +0x000 Machine          : Uint2B

        +0x002 NumberOfSections : Uint2B

        +0x004 TimeDateStamp    : Uint4B

        +0x008 PointerToSymbolTable : Uint4B

        +0x00c NumberOfSymbols  : Uint4B

        +0x010 SizeOfOptionalHeader : Uint2B

        +0x012 Characteristics  : Uint2B

    +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER

        +0x000 Magic            : Uint2B

        +0x002 MajorLinkerVersion : UChar

        +0x003 MinorLinkerVersion : UChar

        +0x004 SizeOfCode       : Uint4B

        +0x008 SizeOfInitializedData : Uint4B

        +0x00c SizeOfUninitializedData : Uint4B

        +0x010 AddressOfEntryPoint : Uint4B

        +0x014 BaseOfCode       : Uint4B

        +0x018 BaseOfData       : Uint4B

        +0x01c ImageBase        : Uint4B

        +0x020 SectionAlignment : Uint4B

        +0x024 FileAlignment    : Uint4B

        +0x028 MajorOperatingSystemVersion : Uint2B

        +0x02a MinorOperatingSystemVersion : Uint2B

        +0x02c MajorImageVersion : Uint2B

        +0x02e MinorImageVersion : Uint2B

        +0x030 MajorSubsystemVersion : Uint2B

        +0x032 MinorSubsystemVersion : Uint2B

        +0x034 Win32VersionValue : Uint4B

        +0x038 SizeOfImage      : Uint4B

        +0x03c SizeOfHeaders    : Uint4B

        +0x040 CheckSum         : Uint4B

        +0x044 Subsystem        : Uint2B

        +0x046 DllCharacteristics : Uint2B

        +0x048 SizeOfStackReserve : Uint4B

        +0x04c SizeOfStackCommit : Uint4B

        +0x050 SizeOfHeapReserve : Uint4B

        +0x054 SizeOfHeapCommit : Uint4B

        +0x058 LoaderFlags      : Uint4B

        +0x05c NumberOfRvaAndSizes : Uint4B

        +0x060 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY

数据目录中包含了一些有趣成员的地址;而对我们来说,它包含了所有导出函数的地址。

0:000> dt nt!_IMAGE_DATA_DIRECTORY

    +0x000 VirtualAddress   : Uint4B

    +0x004 Size             : Uint4B

因此,我们可以使用DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress 来直接获取导出目录的地址:

typedef struct _IMAGE_EXPORT_DIRECTORY {

    DWORD   Characteristics;

    DWORD   TimeDateStamp;

    WORD    MajorVersion;

    WORD    MinorVersion;

    DWORD   Name;

    DWORD   Base;

    DWORD   NumberOfFunctions;

    DWORD   NumberOfNames;

    DWORD   AddressOfFunctions;     

    DWORD   AddressOfNames;         

    DWORD   AddressOfNameOrdinals;  

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

可以通过名字或者通过序号来导出一个函数;因此,以下三个数组保持更新:

  • AddressOfFunctions,按照序号顺序依次保存函数的地址;

  • AddressOfNames,保存函数名称;

  • AddressOfNameOrdinals,保存序号,该数组与AddressOfNames数组次序相同。

因此,如果想要根据名称来查找函数地址,我们需要浏览 AddressOfNames 数组中的所有名称,并将数组索引用于 AddressOfNameOrdinals[index];而这将返回一个序号用于 AddressOfFunctions[ordinal]。整个过程的伪代码如下所示:

int i = 0;

while (AddressOfNames[i] != searchedName) {

    i++;

}

  

return AddressOfFunctions[ AddressOfNamesOrdinals[i] ];

代码

正如搜索 DLL 时所做的那样,我们将使用 DJB 散列算法(不过这次用 ASCII 字符串):

PVOID getFunctionAddr(DWORD dwModule, DWORD functionHash) {

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)dwModule;

    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);

    PIMAGE_DATA_DIRECTORY dataDirectory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    if (dataDirectory->VirtualAddress == 0) {

        return NULL;

    }

  

    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwModule + dataDirectory->VirtualAddress);

    PDWORD ardwNames = (PDWORD)(dwModule + exportDirectory->AddressOfNames);

    PWORD arwNameOrdinals = (PWORD)(dwModule + exportDirectory->AddressOfNameOrdinals);

    PDWORD ardwAddressFunctions = (PDWORD)(dwModule + exportDirectory->AddressOfFunctions);

    char* szName = 0;

    WORD wOrdinal = 0;

  

    for (unsigned int i = 0; i NumberOfNames; i++) {

        szName = (char*)(dwModule + ardwNames[i]);

  

        if (djbHash(szName) == functionHash) {

            wOrdinal = arwNameOrdinals[i];

            return (PVOID)(dwModule + ardwAddressFunctions[wOrdinal]);

        }

    }

  

    return NULL;

}


编译


最终代码

我们已经讨论了所用的重要结构和算法,下面看看如何生成 Shellcode 代码。

#pragma comment(linker, "/ENTRY:main")

  

#include "makestr.h"

#include "peb.h"

  

typedef HMODULE (WINAPI* _LoadLibraryA)(LPCSTR lpFileName);

typedef int (WINAPI* _MessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

  

int main();

DWORD getDllByName(DWORD dllHash);

PVOID getFunctionAddr(DWORD dwModule, DWORD functionHash);

DWORD djbHash(char* str);

DWORD djbHashW(wchar_t* str);

  

int main() {

    DWORD hashKernel32 = 0x6DDB9555; // djbHashW(L"KERNEL32.DLL");

    DWORD hKernel32 = getDllByName(hashKernel32);

    if (hKernel32 == 0) {

        return 1;

    }

  

    DWORD hashLoadLibraryA = 0x5FBFF0FB; // djbHash("LoadLibraryA");

    _LoadLibraryA xLoadLibraryA = getFunctionAddr(hKernel32, hashLoadLibraryA);

    if (xLoadLibraryA == NULL) {

        return 1;

    }

  

    char szUser32[] = MAKESTR("user32.dll", 10);

    DWORD hUser32 = xLoadLibraryA(szUser32);

    if (hUser32 == 0) {

        return 1;

    }

  

    DWORD hashMessageBoxA = 0x384F14B4; // djbHash("MessageBoxA");

    _MessageBoxA xMessageBoxA = getFunctionAddr(hUser32, hashMessageBoxA);

    if (xMessageBoxA == NULL) {

        return 1;

    }

  

    char szMessage[] = MAKESTR("Hello World", 11);

    char szTitle[] = MAKESTR(":)", 2);

    xMessageBoxA(0, szMessage, szTitle, MB_OK|MB_ICONINFORMATION);

  

    return 0;

}

  

inline PEB* getPeb() {

    __asm {

        mov eax, fs:[0x30];

    }

}

  

DWORD djbHash(char* str) {

    unsigned int hash = 5381;

    unsigned int i = 0;

  

    for (i = 0; str[i] != 0; i++) {

        hash = ((hash <

    }

  

    return hash;

}

DWORD djbHashW(wchar_t* str) {

    unsigned int hash = 5381;

    unsigned int i = 0;

  

    for (i = 0; str[i] != 0; i++) {

        hash = ((hash <

    }

  

    return hash;

}

  

DWORD getDllByName(DWORD dllHash) {

    PEB* peb = getPeb();

    PPEB_LDR_DATA Ldr = peb->Ldr;

    PLDR_DATA_TABLE_ENTRY moduleList = (PLDR_DATA_TABLE_ENTRY)Ldr->InLoadOrderModuleList.Flink;

  

    wchar_t* pBaseDllName = moduleList->BaseDllName.Buffer;

    wchar_t* pFirstDllName = moduleList->BaseDllName.Buffer;

  

    do {

        if (pBaseDllName != NULL) {

            if (djbHashW(pBaseDllName) == dllHash) {

                return (DWORD)moduleList->BaseAddress;

            }

        }

  

        moduleList = (PLDR_DATA_TABLE_ENTRY)moduleList->InLoadOrderModuleList.Flink;

        pBaseDllName = moduleList->BaseDllName.Buffer;

    } while (pBaseDllName != pFirstDllName);

  

    return 0;

}

  

PVOID getFunctionAddr(DWORD dwModule, DWORD functionHash) {

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)dwModule;

    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);

    PIMAGE_DATA_DIRECTORY dataDirectory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    if (dataDirectory->VirtualAddress == 0) {

        return NULL;

    }

  

  

    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwModule + dataDirectory->VirtualAddress);

    PDWORD ardwNames = (PDWORD)(dwModule + exportDirectory->AddressOfNames);

    PWORD arwNameOrdinals = (PWORD)(dwModule + exportDirectory->AddressOfNameOrdinals);

    PDWORD ardwAddressFunctions = (PDWORD)(dwModule + exportDirectory->AddressOfFunctions);

    char* szName = 0;

    WORD wOrdinal = 0;

  

    for (unsigned int i = 0; i NumberOfNames; i++) {

        szName = (char*)(dwModule + ardwNames[i]);

  

        if (djbHash(szName) == functionHash) {

            wOrdinal = arwNameOrdinals[i];

            return (PVOID)(dwModule + ardwAddressFunctions[wOrdinal]);

        }

    }

  

    return NULL;

}

函数声明的次序非常重要:它将定义编译次序。因此,如果我们想要直接调用Shellcode 代码,最好首先声明我们的main函数,因为它将是入口点。我不知道这是Visual Studio 的特性,还是所有的编译器都这样处理。

ASCII 字符串使用宏定义 MAKESTR 像数组一样来声明字符串,该宏将字符串强制按照如下格式进行分配:

mov  dword ptr [ebp+szUser32],   72657375h   ; user

mov  dword ptr [ebp+szUser32+4], 642E3233h   ; 32.d

mov  word ptr [ebp+szUser32+8],  6C6Ch       ; ll

mov  [ebp+szUser32+0Ah], 0                   ; '\x00'

以上代码由 Python 脚本生成,因为它是冗余的:

#pragma once

  

#define MAKESTR(s, length) MAKESTR_##length(s)

  

/*

for i in range(1,51):

    s = "#define MAKESTR_%d(s) {" % i

    for j in range(i):

        s += "s[%d]," % j

    s += "0}"

  

    print(s)

*/

  

#define MAKESTR_1(s) {s[0],0}

#define MAKESTR_2(s) {s[0],s[1],0}

#define MAKESTR_3(s) {s[0],s[1],s[2],0}

#define MAKESTR_4(s) {s[0],s[1],s[2],s[3],0}

#define MAKESTR_5(s) {s[0],s[1],s[2],s[3],s[4],0}

#define MAKESTR_6(s) {s[0],s[1],s[2],s[3],s[4],s[5],0}

#define MAKESTR_7(s) {s[0],s[1],s[2],s[3],s[4],s[5],s[6],0}

#define MAKESTR_8(s) {s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],0}

#define MAKESTR_9(s) {s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],0}

#define MAKESTR_10(s) {s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9],0}

#define MAKESTR_11(s) {s[0],s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9],s[10],0}


编译配置

我所用的是 Visual Studio 2017,但我认为在其他版本中选项也是相同的:

C / C++

    Optimisation

        Reduce the size /O1

        Smaller code /Os

    Code generation

        Disable security verifications /GS-

Linker

    Entries

        Ignore all the defaults libraries /NODEFAULTLIB

最终得到一个 3KB 大小的文件;一个简单的反汇编器即可从文件中提取 Shellcode 代码。


Shellcode 代码

Shellcode 代码有 339 字节大小,但其中最大的部分是函数加载和 DLL 模块搜索。因此,即使对于一个更大的程序,Shellcode 代码也不会增大很多。我们的 Shellcode 代码是非常简单的,因为它仅仅弹出一个消息框,但你可以很容易地将其改造成比如一个下载器之类的程序。



本文由 看雪翻译小组 木无聊偶 编译,来源Dimitri Fourny 

戳👇 图片加入看雪翻译小组哦!


往期热门内容推荐



更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!

看雪论坛:http://bbs.pediy.com/

微信公众号 ID:ikanxue

微博:看雪安全

投稿、合作:www.kanxue.com

推荐文章
小学生作文  ·  教育培训,一定要加盟思维导图作文
7 年前