Donut:从内存中加载.NET程序集

栏目: ASP.NET · 发布时间: 5年前

内容简介:我们可以在运行微软Windows系统的大多数设备上看到.NET Framework的身影,.NET在针对Windows设备的攻击(红队)以及防御(蓝队)场景中也深受大家欢迎。2015年,微软将

Donut:从内存中加载.NET程序集

0x00 前言

我们可以在运行微软Windows系统的大多数设备上看到.NET Framework的身影,.NET在针对Windows设备的攻击(红队)以及防御(蓝队)场景中也深受大家欢迎。2015年,微软将 AMSI(Antimalware Scan Interface) 与执行脚本(VBScript、JScript、PowerShell)的各种Windows组件集成在一起。大约在同一时间,PowerShell中也添加了增强型日志记录或者 Script Block Logging 功能,用来捕捉执行脚本的的所有内容,从而解决攻击者使用的任何混淆技术。为了能在红蓝对抗中占据上风,红队必须直接使用程序集(assembly),进一步深入.Net Framework。程序集通常采用C#语言开发,可以为蓝队提供PowerShell支持的所有功能,并且还具备内存加载和执行的独特优势。在本文中,我将向大家简单介绍Donut这款工具,当我们提供一个.NET程序集、类名、方法以及其他可选参数时, Donut 将生成一段位置无关代码(PIC)或者shellcode,可以从内存中加载.NET程序集。我和 TheWover 共同合作开发了这款工具,此外TheWover也写了介绍donut的一篇 文章 ,欢迎大家参考。

Donut:从内存中加载.NET程序集

0x01 CLR托管接口

CLR(Common Language Runtime)是一个虚拟机组件,微软从v1.0版Framework(2002年发布)就开始提供 ICorRuntimeHost 接口,用来托管.NET程序集。该接口在2006年发布的v2.0版Framework中被 ICLRRuntimeHost 所替代,而后者又在2009年发布的v4.0版Framew中被 ICLRMetaHost 替代。虽然已被弃用,但 ICorRuntimeHost 目前仍是从内存中加载程序集的最简单方法。我们可以使用多种方法来实例化该接口,最常用的有如下几种方法:

CorBindToRuntime 以及 CorBindToRuntimeEx 执行的是同样的操作,但 CorBindToRuntimeEx 函数可以让我们指定CLR的具体行为。使用 CLRCreateInstance 时我们不必初始化COM(Component Object Model),但v4.0版之前的Framework并没有实现该函数。如下C++代码可以从内存中加载.NET程序集:

#include <windows.h>
#include <oleauto.h>
#include <mscoree.h>
#include <comdef.h>

#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <sys/stat.h>

#import "mscorlib.tlb" raw_interfaces_only

void rundotnet(void *code, size_t len) {
    HRESULT                  hr;
    ICorRuntimeHost          *icrh;
    IUnknownPtr              iu;
    mscorlib::_AppDomainPtr  ad;
    mscorlib::_AssemblyPtr   as;
    mscorlib::_MethodInfoPtr mi;
    VARIANT                  v1, v2;
    SAFEARRAY                *sa;
    SAFEARRAYBOUND           sab;

    printf("CoCreateInstance(ICorRuntimeHost).n");
    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

    hr = CoCreateInstance(
      CLSID_CorRuntimeHost, 
      NULL, 
      CLSCTX_ALL,
      IID_ICorRuntimeHost, 
      (LPVOID*)&icrh);

    if(FAILED(hr)) return;

    printf("ICorRuntimeHost::Start()n");
    hr = icrh->Start();
    if(SUCCEEDED(hr)) {
      printf("ICorRuntimeHost::GetDefaultDomain()n");
      hr = icrh->GetDefaultDomain(&iu);
      if(SUCCEEDED(hr)) {
        printf("IUnknown::QueryInterface()n");
        hr = iu->QueryInterface(IID_PPV_ARGS(&ad));
        if(SUCCEEDED(hr)) {
          sab.lLbound   = 0;
          sab.cElements = len;
          printf("SafeArrayCreate()n");
          sa = SafeArrayCreate(VT_UI1, 1, &sab);
          if(sa != NULL) {
            CopyMemory(sa->pvData, code, len);
            printf("AppDomain::Load_3()n");
            hr = ad->Load_3(sa, &as);
            if(SUCCEEDED(hr)) {
              printf("Assembly::get_EntryPoint()n");
              hr = as->get_EntryPoint(&mi);
              if(SUCCEEDED(hr)) {
                v1.vt    = VT_NULL;
                v1.plVal = NULL;
                printf("MethodInfo::Invoke_3()n");
                hr = mi->Invoke_3(v1, NULL, &v2);
                mi->Release();
              }
              as->Release();
            }
            SafeArrayDestroy(sa);
          }
          ad->Release();
        }
        iu->Release();
      }
      icrh->Stop();
    }
    icrh->Release();
}

int main(int argc, char *argv[])
{
    void *mem;
    struct stat fs;
    FILE *fd;

    if(argc != 2) {
      printf("usage: rundotnet <.NET assembly>n");
      return 0;
    }

    // 1. get the size of file
    stat(argv[1], &fs);

    if(fs.st_size == 0) {
      printf("file is empty.n");
      return 0;
    }

    // 2. try open assembly
    fd = fopen(argv[1], "rb");
    if(fd == NULL) {
      printf("unable to open "%s".n", argv[1]);
      return 0;
    }
    // 3. allocate memory 
    mem = malloc(fs.st_size);
    if(mem != NULL) {
      // 4. read file into memory
      fread(mem, 1, fs.st_size, fd);
      // 5. run the program from memory
      rundotnet(mem, fs.st_size);
      // 6. free memory
      free(mem);
    }
    // 7. close assembly
    fclose(fd);

    return 0;
}

如下是C#版的“Hello, World!”程序,当使用 csc.exe 编译后能生成一个.NET程序集,可以用来测试加载器。

// A Hello World! program in C#.
using System;
namespace HelloWorld
{
    class Hello 
    {
        static void Main() 
        {
            Console.WriteLine("Hello World!");
        }
    }
}

编译并运行这些代码后,我们可以得到如下输出:

Donut:从内存中加载.NET程序集

这是执行.NET程序集的基本方式,其中并没有考虑到Framework的具体版本。shellcode的实现有点不一样,会解析 CorBindToRuntime 以及 CLRCreateInstance 的地址(这与 subTee 开发的 AssemblyLoader 类似)。如果成功解析 CLRCreateInstance ,并且调用后返回 E_NOTIMPL 或者“Not implemented”,我们就会执行 CorBindToRuntime (其中 pwszVersion 参数设置为NULL),请求可用的最新版本。如果我们使用 CorBindToRuntime 请求系统当前不支持的某个版本,那么运行shellcode的托管进程可能会弹出错误消息。比如,当Windows 7系统只支持v3.5.30729.5420版时,如果我们请求v4.0.30319,就会看到如下错误信息:

Donut:从内存中加载.NET程序集

大家可能有疑问,为什么之前使用的OLE函数没有在shellcode中使用。除了OLE32之外,OLE函数有时候会在其他DLL中引用,比如COMBASE。xGetProcAddress可以处理转发引用,但至少目前为止,shellcode使用的是 CorBindToRuntime 以及 CLRCreateInstance 。在新版框架中,我们还可以使用 CoCreateInstance

0x02 定义.NET类型

在非托管(unmanaged)C++程序中,我们可以使用 #import 指令来访问类型(Types)。前文代码使用的是在 mscorlib.tlb 中定义的 _AppDomain_Assembly 以及 _MethodInfo 接口。然而问题在于,在公开版的Windows SDK中并没有定义这些接口。为了在较低级语言(如汇编语言或者C)中使用.NET类型,我们首先得手动定义这些接口。我们可以使用 LoadTypeLib API来枚举类型信息,该函数会返回指向 ITypeLib 接口的一个指针。该接口可以提取相关信息,比如库接口、方法以及变量。我发现 Olewoo 这款 工具 可以用来查看 mscorlib.tlb 信息。如果我们忽略面向对象编程(OOP)方面的相关信息,比如类、对象、继承、封装、抽象、多态……等,我们可以从底层来分析接口,毕竟接口只是指向某种数据结构的一个指针,而该数据结构包含指向函数/方法的指针而已。除了 phplib 中的一个文件之外(该文件定义了 _AppDomain 接口),我无法在网上找到所需接口的定义。根据找到的示例,我构造了加载程序集所需的其他接口。如下即为 _AppDomain 接口中的某个方法:

HRESULT (STDMETHODCALLTYPE *InvokeMember_3)(
          IType        *This,
          BSTR         name,
          BindingFlags invokeAttr,
          IBinder      *Binder,
          VARIANT      Target,
          SAFEARRAY    *args,
          VARIANT      *pRetVal);

虽然shellcode中没有使用 IBinder 接口的任何方法,我们可以将类型安全地改成 void * ,但为了以后使用方便,我还是定义了如下接口。 DUMMY_METHOD 宏简单定义了一个函数指针:

typedef struct _Binder IBinder;

    #undef DUMMY_METHOD
    #define DUMMY_METHOD(x) HRESULT ( STDMETHODCALLTYPE *dummy_##x )(IBinder *This)

    typedef struct _BinderVtbl {
        HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
          IBinder * This,
          /* [in] */ REFIID riid,
          /* [iid_is][out] */ void **ppvObject);

        ULONG ( STDMETHODCALLTYPE *AddRef )(
          IBinder * This);

        ULONG ( STDMETHODCALLTYPE *Release )(
          IBinder * This);

        DUMMY_METHOD(GetTypeInfoCount);
        DUMMY_METHOD(GetTypeInfo);
        DUMMY_METHOD(GetIDsOfNames);
        DUMMY_METHOD(Invoke);
        DUMMY_METHOD(ToString);
        DUMMY_METHOD(Equals);
        DUMMY_METHOD(GetHashCode);
        DUMMY_METHOD(GetType);
        DUMMY_METHOD(BindToMethod);
        DUMMY_METHOD(BindToField);
        DUMMY_METHOD(SelectMethod);
        DUMMY_METHOD(SelectProperty);
        DUMMY_METHOD(ChangeType);
        DUMMY_METHOD(ReorderArgumentArray);
    } BinderVtbl;

    typedef struct _Binder {
      BinderVtbl *lpVtbl;
    } Binder;

我在 payload.h 中定义了内存加载程序集所需的方法。

0x03 Donut实例

我们会将shellcode与某个数据块实例绑定在一起,这个数据块可以看成shellcode的“数据段”(data segment),其中包含解析API之前待加载的DLL名、API字符串对应的64位哈希、内存加载.NET程序集的相关COM GUID,如果实例和模块存储在staging服务器上,那么数据段也可以包含实例对应的解密秘钥。许多使用 C语言 编写的shellcode都倾向于在栈上存储字符串,但像 FireEye Labs Obfuscated String Solver 之类的工具可以轻易恢复这些信息,帮助我们更好分析代码。当涉及代码位置排列时,在独立的数据块中保存字符串就能体现出优势。我们可以在保持功能的同时修改代码,并且永远不需要处理“只读”的立即值,这些值将使整个过程变得复杂,大大增加代码量。在 call 操作码(opcode)之后以及 pop ecx / pop rcx 之前我们使用的结构如下所示。在x86以及x86-64 shellcode中我们使用了 fastcall 约定,使代码便于加载指向保存在 ecxrcx 寄存器中实例的指针。

typedef struct _DONUT_INSTANCE {
    uint32_t    len;                          // total size of instance
    DONUT_CRYPT key;                          // decrypts instance
    // everything from here is encrypted

    int         dll_cnt;                      // the number of DLL to load before resolving API
    char        dll_name[DONUT_MAX_DLL][32];  // a list of DLL strings to load
    uint64_t    iv;                           // the 64-bit initial value for maru hash
    int         api_cnt;                      // the 64-bit hashes of API required for instance to work

    union {
      uint64_t  hash[48];                     // holds up to 48 api hashes
      void     *addr[48];                     // holds up to 48 api addresses
      // include prototypes only if header included from payload.h
      #ifdef PAYLOAD_H
      struct {
        // imports from kernel32.dll
        LoadLibraryA_t             LoadLibraryA;
        GetProcAddress_t           GetProcAddress;
        VirtualAlloc_t             VirtualAlloc;             
        VirtualFree_t              VirtualFree;  

        // imports from oleaut32.dll
        SafeArrayCreate_t          SafeArrayCreate;          
        SafeArrayCreateVector_t    SafeArrayCreateVector;    
        SafeArrayPutElement_t      SafeArrayPutElement;      
        SafeArrayDestroy_t         SafeArrayDestroy;         
        SysAllocString_t           SysAllocString;           
        SysFreeString_t            SysFreeString;            

        // imports from wininet.dll
        InternetCrackUrl_t         InternetCrackUrl;         
        InternetOpen_t             InternetOpen;             
        InternetConnect_t          InternetConnect;          
        InternetSetOption_t        InternetSetOption;        
        InternetReadFile_t         InternetReadFile;         
        InternetCloseHandle_t      InternetCloseHandle;      
        HttpOpenRequest_t          HttpOpenRequest;          
        HttpSendRequest_t          HttpSendRequest;          
        HttpQueryInfo_t            HttpQueryInfo;

        // imports from mscoree.dll
        CorBindToRuntime_t         CorBindToRuntime;
        CLRCreateInstance_t        CLRCreateInstance;
      };
      #endif
    } api;

    // GUID required to load .NET assembly
    GUID xCLSID_CLRMetaHost;
    GUID xIID_ICLRMetaHost;  
    GUID xIID_ICLRRuntimeInfo;
    GUID xCLSID_CorRuntimeHost;
    GUID xIID_ICorRuntimeHost;
    GUID xIID_AppDomain;

    DONUT_INSTANCE_TYPE type;  // PIC or URL 

    struct {
      char url[DONUT_MAX_URL];
      char req[16];            // just a buffer for "GET"
    } http;

    uint8_t     sig[DONUT_MAX_NAME];          // string to hash
    uint64_t    mac;                          // to verify decryption ok

    DONUT_CRYPT mod_key;       // used to decrypt module
    uint64_t    mod_len;       // total size of module

    union {
      PDONUT_MODULE p;         // for URL
      DONUT_MODULE  x;         // for PIC
    } module;
} DONUT_INSTANCE, *PDONUT_INSTANCE;

0x04 Donut模块

.NET使用模块(Module)这种数据结构来存储程序集。模块可以与实例(Instance)一起存储,或者存放在shellcode能够提取的staging服务器上。模块中包含程序集、类名、方法以及可选参数。 sig 值包含随机8字节字符串,当使用 Maru 哈希函数处理时,会生成64bit值,该值与 mac 值相等。这种方式可以用来验证模块的解密是否成功。模块秘钥存放在内嵌于shellcode的实例中。

// everything required for a module goes into the following structure
typedef struct _DONUT_MODULE {
    DWORD   type;                                   // EXE or DLL
    WCHAR   runtime[DONUT_MAX_NAME];                // runtime version
    WCHAR   domain[DONUT_MAX_NAME];                 // domain name to use
    WCHAR   cls[DONUT_MAX_NAME];                    // name of class and optional namespace
    WCHAR   method[DONUT_MAX_NAME];                 // name of method to invoke
    DWORD   param_cnt;                              // number of parameters to method
    WCHAR   param[DONUT_MAX_PARAM][DONUT_MAX_NAME]; // string parameters passed to method
    CHAR    sig[DONUT_MAX_NAME];                    // random string to verify decryption
    ULONG64 mac;                                    // to verify decryption ok
    DWORD   len;                                    // size of .NET assembly
    BYTE    data[4];                                // .NET assembly file
} DONUT_MODULE, *PDONUT_MODULE;

0x05 随机秘钥

在Windows上, CryptGenRandom 可以生成密码学上安全的随机值,在 Linux 上,我们可以使用 /dev/urandom (不使用 /dev/random ,该设备会阻塞读取请求)。Thomas Huhn在关于 urandom 的一篇 文章 中提到 /dev/urandom 是Linux上随机数据流的首选。我们在Donut中使用 CreateRandom 来生成随机秘钥,建议大家参考使用。

0x05 随机字符串

除非用户手动指定,否则我们会使用随机字符串来生成应用程序域(Application Domain)名。如果donut模块存放在staging服务器上,也会生成随机名。负责该操作的函数为 GenRandomString ,其中用到了 CreateRandom 生成的随机字节,配合“HMN34P67R9TWCXYF”字符串生成了最终字符串(这个魔术字符串来源于stackoverflow上的一篇 帖子 )。

0x06 对称加密

对合(involution)函数是指自己是自己逆函数的函数,许多工具会使用对合函数来混淆代码。如果大家之前逆向分析过恶意软件,那么肯定对异或(XOR)函数非常熟悉,这种函数非常简单,使用场景也非常广泛。此外, Noekeon 分组加密是一种非线性加密,也是较为复杂的对合方式。Donut并没有使用对合加密方式,而是使用 Chaskey 分组密码(Counter(CTR)模式)来加密模块,其中解密秘钥内嵌在shellcode中。如果Donut模块来自于staging服务器,那么想知道其中所包含的具体信息的唯一方法就是恢复shellcode,寻找 CreateRandom 函数的脆弱点或者打破Chaskey加密算法。

static void chaskey(void *mk, void *p) {
    uint32_t i,*w=p,*k=mk;

    // add 128-bit master key
    for(i=0;i<4;i++) w[i]^=k[i];

    // apply 16 rounds of permutation
    for(i=0;i<16;i++) {
      w[0] += w[1],
      w[1]  = ROTR32(w[1], 27) ^ w[0],
      w[2] += w[3],
      w[3]  = ROTR32(w[3], 24) ^ w[2],
      w[2] += w[1],
      w[0]  = ROTR32(w[0], 16) + w[3],
      w[3]  = ROTR32(w[3], 19) ^ w[0],
      w[1]  = ROTR32(w[1], 25) ^ w[2],
      w[2]  = ROTR32(w[2], 16);
    }
    // add 128-bit master key
    for(i=0;i<4;i++) w[i]^=k[i];
}

之所以选择使用Chaskey算法,是因为该算法简洁紧凑,易于实现,并且不包含容易被检测的常量特征。Chaskey的主要缺点是使用人数较少,因此并没有像AES那样在密码学上被广泛分析。当2014发布Chaskey算法时,官方推荐的加密轮次为8次。2015年,已经有针对7轮加密的攻击技术出现,这表明官方推荐的加密轮次并不是一个足够安全的边界。针对此攻击,设计人员提高了加密轮次,建议使用12轮加密,这里Donut使用的是16轮加密的长期支持(LTS)版本。

0x07 API哈希

如果在内存扫描之前已经掌握API字符串哈希,那么Donut就非常容易被检测出来。我们 建议 在Windows API哈希中使用分组加密方式,增加哈希过程中的熵(entropy),以便进一步规避针对代码的检测机制。Donut使用的是 Maru 哈希函数,该函数基于 Speck 分组加密算法,使用的是Davies-Meyer构建和填充方式,这种方式与MD4及MD5类似。Speck随机生成了一个64bit初始值(IV),以明文方式使用该值来加密,秘钥为API字符串。

static uint64_t speck(void *mk, uint64_t p) {
    uint32_t k[4], i, t;
    union {
      uint32_t w[2];
      uint64_t q;
    } x;

    // copy 64-bit plaintext to local buffer
    x.q = p;

    // copy 128-bit master key to local buffer
    for(i=0;i<4;i++) k[i]=((uint32_t*)mk)[i];

    for(i=0;i<27;i++) {
      // donut_encrypt 64-bit plaintext
      x.w[0] = (ROTR32(x.w[0], 8) + x.w[1]) ^ k[0];
      x.w[1] =  ROTR32(x.w[1],29) ^ x.w[0];

      // create next 32-bit subkey
      t = k[3];
      k[3] = (ROTR32(k[1], 8) + k[0]) ^ i;
      k[0] =  ROTR32(k[0],29) ^ k[3];
      k[1] = k[2]; k[2] = t;
    }
    // return 64-bit ciphertext
    return x.q;
}

0x08 总结

Donut提供了通过shellcode实现CLR注入的一种方法,红队可以基于此建模,从攻击方和防御方角度构建分析和缓解的整体框架。这个过程中肯定会有恶意软件开发者和攻击人员会滥用这款工具,但我们坚信整体优点依然能弥补带来的不足(但愿如此),大家可以访问 此处 获取源代码。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to the Design and Analysis of Algorithms

Introduction to the Design and Analysis of Algorithms

Anany Levitin / Addison Wesley / 2006-2-24 / USD 122.00

Based on a Based on a new classification of algorithm design techniques and a clear delineation of analysis methods, "Introduction to the Design and Analysis of Algorithms" presents the subject in a c......一起来看看 《Introduction to the Design and Analysis of Algorithms》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具