本帖最后由 lostidle 于 2022-8-12 13:55 编辑
文章地址:https://sec-in.com/article/1884
翻译:梦幻的彼岸 简介在第一部分中,我们介绍了对C2框架进行威胁猎取的通用方法,然后在第二部分中用实际例子对Cobalt Strike进行了跟进。 在本系列的第三部分,我们将分析Brute Ratel,一个由Dark Vortex. 开发的指挥和控制框架。由于C2的知名度较低,我们可以看到它对自己的描述如下: 在过去的几个月里,该框架受到密切关注,据称最近被APT29 和勒索软件集团BlackCat所滥用。因此,了解我们如何在我们的基础设施中通用地检测这种新兴的C2,对防御者来说是有用的情报。 最初,所有的分析都是在Brute Ratel v1.0.7版本上进行的;在最初审查时是最新的。然而,我们进行了一次粗略的更新(包含在本文末尾),讨论了与v1.1版本有关的发现,该版本在我们最初的x33fcon演讲后不久发布。对于Brute Ratel应该注意的一点是,badger只有有限的可塑性,而且主要是从c2通道的角度来看;但v1.1版除外,它为睡眠混淆技术增加了可塑性。因此,它使得为该工具创建非常具体的检测成为可能。 Brute Ratel的加载器Brute Ratel的坏器有多种形式,包括exe、DLL和shellcode。当badger被注入时,其反射性装载器将立即加载badger所需的所有依赖。由于badger捆绑了大量的post-exploitation功能,这导致了大量的DLLs在初始化时被加: 正如我们所看到的,突出显示的 DLLs 是所有在注入badger时被加载的 DLLs。这个列表包括加载 winhttp.dll 和 wininet.dll。. 然而,还有一些不太常见的DLL被加载,如dbghelp.dll、credui.dll samcli.dll和logoncli.dll等。 这种行为使我们能够为图像负载创建一个签名,并导致一个高信号指标,可以通过图像负载遥测来猎取。 例如,使用Elastic查询语言,我们可以搜索一个进程中在60秒内发生的credui.dll、dbghelp.dll和winhttp.dll加载事件的序列。 sequence by Image with maxspan=1m [any where ImageLoaded == 'C:\\Windows\\System32\\credui.dll'] [any where ImageLoaded == 'C:\\Windows\\System32\\dbghelp.dll'] [any where ImageLoaded == 'C:\\Windows\\System32\\winhttp.dll']使用EQL工具,或者Elastic云,我们可以搜索我们的事件数据,比如下面这个是从sysmon日志中提取的。注意,我们明确排除了badger可执行文件本身,所以我们只能识别注入的badger: eql query -f sysmon-data.json "sequence by Image with maxspan=2m [any where ImageLoaded == 'C:\\Windows\\System32\\credui.dll' and Image != 'C:\\Users\\bob\\Desktop\\badger_x64_aws.exe'] [any where ImageLoaded == 'C:\\Windows\\System32\\dbghelp.dll' and Image != 'C:\\Users\\bob\\Desktop\\badger_x64_aws.exe'] [any where ImageLoaded == 'C:\\Windows\\System32\\winhttp.dll' and Image != 'C:\\Users\\bob\\Desktop\\badger_x64_aws.exe']"这个查询特别强大,因为它允许我们在网络中追溯寻找 Brute Ratel badgers 的指标,而无需直接在终端上运行代码。 内存中Brute Ratel由于大多数信标保持内存驻留,因此了解留下的足迹以猎取它们是很重要的。回顾Brute Ratel 1.0版本的文档,它详细说明了自己对混淆和睡眠的实现: 根据发布的帖子,BRc4使用了 "异步程序调用、Windows事件创建、等待对象和计时器 "的混合物。然而,对badger的分析只能找到基于APC的执行的证据;后面会有更多的内容。 为了分析内存中的badger,我们首先使用pcinject命令将其注入一个进程,然后使用sleep命令让badger进入睡眠状态: 一旦badger处于睡眠状态,我们就可以用Process Hacker恢复进程中的字符串。有趣的是,当badger处于睡眠状态时,我们可以看到诸如以下的字符串: 最初,考虑到前面提到的Brute Ratel博客上描述的所谓睡眠和混淆策略,这是很令人惊讶的。 深入研究,我们可以发现一些有趣的设计决定,其中许多显示在操作员用户界面的字符串是由badger本身填充的。例如,当badger在睡眠时,我们可以在它的内存中看到以下内容: 然后这些字符串被返回到用户界面,我们可以看到下面的内容: 深入挖掘badger,很快就发现只有.text部分在睡眠时被混淆了,使badger容易受到针对字符串和数据的各种签名的影响。 为了说明这一点,逆向badger,我们可以看到加载器的入口点是 "bruteloader"。 当badger在睡眠时,在内存中搜索这个字符串,我们可以在我们的记事本进程中快速找到它。 这些字符串提供了一个很好的点,可以作为内存扫描的Yara规则的基础。例如,下面的规则将在一个进程的内存中搜索bruteloader或bhttp_x64.dll字符串 rulebrc4_badger_strings{meta: author = "@domchell" description = "Identifies strings used in Badger v1.0.x rDLL, even while sleeping"strings: $a = "bruteloader" $b = "bhttp_x64.dll"condition: 1 of them}我们可以在badger睡眠的时候用我们的记事本程序测试这些东西,以证明其有效性: 这些字符串不太可能存在于其他进程中,使用一个简单的单行本,我们可以快速找到我们测试系统中所有被注入的badger: 将这条Yara规则添加到Virus Total中,我们可以快速找到其他样本,例如: 页面权限对Brute Ratel混淆和睡眠策略的分析观察到,badger在睡眠期间对badger的页面权限进行了打乱,试图在badger睡眠时逃避延长可执行的权限。 下面,我们可以看到badger在睡眠0时的操作,badger的页面权限是PAGE_EXECUTE_READ,在一个未映射的页面上;为了执行任务,这是必要的: 将badger置于睡眠状态,我们可以看到混淆和睡眠策略混淆了.text部分,并将badger的页面权限重置为page_READWRITE: 然而,有趣的是,我们注意到,当SMB pivot正在执行时,即当两个badgers链接时,这种行为不会重复。在这里,我们可以看到我们的两个badger联系在一起,并且都处于60秒的睡眠状态: 对两个badger链接时的页面权限的分析显示,无论睡眠时间如何,两者都保持PAGE_EXECUTE_READ 结论是,混淆和睡眠策略只适用于.text部分,而且是在没有对等支点的情况下。 由于好奇混淆和睡眠功能是如何工作的,我们开始对其进行反向工程。通过windbg中的睡眠程序,我们可以初步了解正在发生的事情;badger正在使用WaitForSingleObjectEx来延迟一系列异步过程调用(APC)的执行,并利用一个间接的系统调用来执行NtTestAlert并强迫线程发出警报: IDA深入分析,我们可以更好地感受到正在发生的事情。首先,它创建了一个新的线程,其起始地址被欺骗为TpReleaseCleanupGroupMembers+550的一个固定位置: 然后为一些函数调用创建了一系列上下文结构,包括NtWaitForSingleObject, NtProtectVirtualMemory, , SystemFunction032, NtGetContextThread和SetThreadContext: 接下来,一些APC被排在NtContinue的后面,目的是利用它来代理对上述上下文结构的调用;这种技术是ROP的一种基本形式: 尽管对该badger进行了大量的调试和逆向工程,但我们无法发现v1.0博文中提到的 "Windows事件创建、等待对象和计时器 "技术的任何证据;事实上,这些技术所需的API似乎并没有通过该badger的散列导入导入。 Brute Ratels 线程为了分析Brute Ratel线程在内存中的情况,我们将badger注入到一个新的记事本副本中。随即,我们可以看到睡眠的badger所使用的线程中存在一些可疑的迹象。 首先,我们注意到有一个看起来很可疑的线程,它的起始地址是0x0,并且在调用栈中有一个调用WaitForSingleObjectEx的单帧: 根据对线程调用堆栈的分析,我们可以推测这个线程是用于HTTP通信的,而此时badger正在睡眠: 根据我们从混淆和睡眠策略的逆向工程中获得的信息,我们注意到,新的线程是以硬编码的欺骗性起始地址ntdll!TpReleaseCleanupGroupMembers+0x550创建的: 我们无法找到任何作为起始地址自然发生的实例,因此导致了猎取Brute Ratel线程的一个琐碎的指标。在实践中,这在我们注入的记事本进程中看起来如下: 该线程的调用堆栈也略显不正常,因为它不仅包含延迟执行的调用,而且第一帧指向ntdll.dll!NtTerminateJobObject+0x1f。深入研究一下为什么使用NtNerminateJobObject就会发现,这只是NtTestAlert的一个ROP小工具,用来执行线程上的待定APC。 内存钩子在本系列的第一篇文章中,我们详细介绍了检测基于内存钩子的内存标识的两种潜在方法;通过寻找已知补丁的签名(例如ret to ntdll.dll!EtwEventWrite)和检测写入操作的拷贝。 将这些概念应用于Brute Ratel,我们注意到,在操作者使用其开发后功能之前,badger不会应用任何内存钩。这方面的一个例子是sharpinline命令,它在当前进程中运行一个.NET组件。 一旦汇编完成,信标恢复睡眠,我们可以通过附加调试器和分解ntdll的值来更好地了解发生了什么。dll!ETWAventWrite和amsi.dll!AmsiScanBuffer: 如上所示,这些是禁用.NET ETW数据和禁止AMSI的简单持久补丁。由于补丁是持久性的,我们可以通过上述任何一种技术来检测它们,因为我们不仅会因为ETWAventWrite的第一条指令是ret而接收到高信号检测,而且还会因为清除共享位而指示ETWAvent Write所在的页面已被修改。 使用BeaconHunter,我们可以在解析修改页面上的导出的基础上快速检测这些钩子,从而提供恶意篡改发生的有力指示: Brute Ratel C2 服务器从终端离开,作为猎人,我们也有兴趣检测指挥和控制基础设施,因为这可能有助于为我们提供足够的情报来检测基于网络遥测的信标。 Brute Ratel的C2服务器是用golang开发的,默认情况下只允许操作员修改C2的默认登陆页面。为了对C2服务器进行指纹识别,我们发现在向任何URI发送包含base64的POST请求时,有可能产生一个未处理的异常。例如,考虑以下base64 POST数据与明文的比较: 这很可能发生,因为base64解码的POST数据的预期输入应该符合C2流量格式。一个简单的Nuclei规则可能有助于我们扫描这类基础设施: id: brc4-tsinfo: name: Brute Ratel C2 Server Fingerprint author: Dominic Chell severity: info description: description reference: - https:// tags: tagsrequests: - raw: - |- POST / HTTP/1.1 Host: {{Hostname}} Content-Length: 8 Zm9vYmFy使用一个简单的Shodan查询,我们可以快速找到暴露在互联网上的实时基础设施。 虽然只确定了大约40个团队服务器,但根据地理分布,我们可以更好地了解这些服务器的位置: 这些技术中很可能有一些已经为人所知,因为根据对我们的测试基础设施的报告,防御者正在积极猎取这些C2服务器: Brute Ratel 配置对Badger的分析表明,Brute Ratel在内存中保持着一个加密的配置结构,其中包括C2端点的细节。能够从人工制品或运行中的进程中提取这些信息,可以证明对防御者有帮助。我们的分析显示,这个配置被保存在一个base64和RC4加密的blob中,使用一个固定的密钥 "bYXJm/3#M?:XyMBF",用于badger的构件。而配置则是明文存储在内存中,用于睡眠中的badger。 我们开发了以下的配置提取器,可以用来对付BRC4 v1.0.x的磁盘构件或用Brute Ratel 1.0.x和1.1.x注入的睡眠badger: - #define _CRT_SECURE_NO_WARNINGS
- #include <stdio.h>
- #include <stdlib.h>
- #include <Windows.h>
- #include <string>
- #include <vector>
- #pragma comment(lib, "Crypt32.lib")
- std::string HexDump(void* pBuffer, DWORD cbBuffer)
- {
- PBYTE pbBuffer = (PBYTE)pBuffer;
- std::string strHex;
- #define FORMAT_APPEND_1(a){ char szTmp[256]; sprintf(szTmp, a); strHex += szTmp; }
- #define FORMAT_APPEND_2(a,b){ char szTmp[256]; sprintf(szTmp, a, b); strHex += szTmp; }
- for (DWORD i = 0; i < cbBuffer;)
- {
- FORMAT_APPEND_2("0x8x ", i);
- DWORD n = ((cbBuffer - i) < 16) ? (cbBuffer - i) : 16;
- for (DWORD j = 0; j < n; j++)
- {
- FORMAT_APPEND_2("%02X ", pbBuffer[i + j]);
- }
- for (DWORD j = 0; j < (16 - n); j++)
- {
- FORMAT_APPEND_1(" ");
- }
- FORMAT_APPEND_1(" ");
- for (DWORD j = 0; j < n; j++)
- {
- FORMAT_APPEND_2("%c", (pbBuffer[i + j] < 0x20 || pbBuffer[i + j] > 0x7f) ? '.' : pbBuffer[i + j]);
- }
- FORMAT_APPEND_1("\n");
- i += n;
- }
- return strHex;
- }
- BOOL ReadAllBytes(std::string strFile, PBYTE* ppbBuffer, UINT* puiBufferLength)
- {
- BOOL bSuccess = FALSE;
- PBYTE pbBuffer = NULL;
- *ppbBuffer = NULL;
- *puiBufferLength = 0;
- FILE* fp = fopen(strFile.c_str(), "rb");
- if (fp)
- {
- fseek(fp, 0, SEEK_END);
- long lFile = ftell(fp);
- fseek(fp, 0, SEEK_SET);
- if (!(pbBuffer = (PBYTE)malloc(lFile)))
- goto Cleanup;
- if (fread(pbBuffer, 1, lFile, fp) != lFile)
- goto Cleanup;
- *ppbBuffer = pbBuffer;
- *puiBufferLength = (UINT)lFile;
- pbBuffer = NULL;
- bSuccess = TRUE;
- }
- Cleanup:
- if (fp) fclose(fp);
- if (pbBuffer) free(pbBuffer);
- return bSuccess;
- }
- void Brc4DecodeString(BYTE* pszKey, BYTE* pszInput, BYTE* pszOutput, int cchInput)
- {
- BYTE szCharmap[0x100];
- for (UINT i = 0; i < sizeof(szCharmap); i++)
- {
- szCharmap[i] = (char)i;
- }
- UINT cchKey = strlen((char*)pszKey);
- BYTE l = 0;
- for (UINT i = 0; i < sizeof(szCharmap); i++)
- {
- BYTE x = szCharmap[i];
- BYTE k = pszKey[i % cchKey];
- BYTE y = x + k + l;
- l = y;
- szCharmap[i] = szCharmap[y];
- szCharmap[y] = x;
- }
- l = 0;
- for (UINT i = 0; i < cchInput; i++)
- {
- BYTE x = szCharmap[i + 1];
- BYTE y = x + l;
- l = y;
- BYTE z = szCharmap[y];
- szCharmap[i + 1] = z;
- szCharmap[y] = x;
- x = x + szCharmap[i + 1];
- x = szCharmap[x];
- x = x ^ pszInput[i];
- pszOutput[i] = x;
- }
- }
- BOOL MatchPattern(PBYTE pbInput, PBYTE pbSearch, DWORD cbSearch, BYTE byteMask)
- {
- BOOL bMatch = TRUE;
- for (DWORD j = 0; j < cbSearch; j++)
- {
- if (pbSearch[j] != byteMask && pbInput[j] != pbSearch[j])
- {
- bMatch = FALSE;
- break;
- }
- }
- return bMatch;
- }
- PBYTE FindPattern(PBYTE pbInput, UINT cbInput, PBYTE pbSearch, DWORD cbSearch, BYTE byteMask, UINT* pcSkipMatches)
- {
- if (cbInput > cbSearch)
- {
- for (UINT i = 0; i < cbInput - cbSearch; i++)
- {
- BOOL bMatch = MatchPattern(pbInput + i, pbSearch, cbSearch, byteMask);
- if (bMatch)
- {
- if (!*pcSkipMatches)
- {
- return &pbInput[i];
- }
- (*pcSkipMatches)--;
- }
- }
- }
- return NULL;
- }
- BOOL LocateBrc4Config(PBYTE pbInput, UINT cbInput, PBYTE* ppbConfig)
- {
- #define XOR_RAX_RAX0x48, 0x31, 0xC0,
- #define PUSH_RAX0x50,
- #define MOV_EAX_IMM320xB8, 0xab, 0xab, 0xab, 0xab,
- #define MOV_RAX_IMM640x48, 0xB8, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab,
- #define PUSH_IMM320x68, 0xab, 0xab, 0xab, 0xab,
- #define MOV_EAX_00xB8, 0x00, 0x00, 0x00, 0x00,
- BYTE Pattern1[] =
- {
- XOR_RAX_RAX
- PUSH_RAX
- MOV_EAX_IMM32
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- },
- Pattern2[] =
- {
- XOR_RAX_RAX
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- PUSH_RAX
- MOV_RAX_IMM64
- };
- UINT cSkipMatches = 0;
- if (cbInput < 100)
- {
- return FALSE;
- }
- PBYTE pbConfigStart = FindPattern(pbInput, cbInput, Pattern1, sizeof(Pattern1), 0xab, &cSkipMatches);
- if (!pbConfigStart)
- {
- cSkipMatches = 0;
- pbConfigStart = FindPattern(pbInput, cbInput, Pattern2, sizeof(Pattern2), 0xab, &cSkipMatches);
- if (!pbConfigStart)
- {
- return FALSE;
- }
- }
- BYTE Pattern3[] = {
- PUSH_IMM32
- MOV_EAX_0
- PUSH_RAX
- MOV_EAX_0
- PUSH_RAX
- MOV_EAX_0
- PUSH_RAX
- };
- cSkipMatches = 0;
- PBYTE pbConfigEnd = FindPattern(pbConfigStart, cbInput - (pbConfigStart - pbInput), Pattern3, sizeof(Pattern3), 0xab, &cSkipMatches);
- if (!pbConfigEnd)
- {
- return FALSE;
- }
- *ppbConfig = (PBYTE)malloc(pbConfigEnd - pbConfigStart);
- if (!*ppbConfig)
- {
- return FALSE;
- }
- memset(*ppbConfig, 0, pbConfigEnd - pbConfigStart);
- pbConfigStart += 4; // skip: XOR_RAX_RAX / PUSH_RAX
- BYTE Pattern4[] = {
- MOV_EAX_IMM32
- PUSH_RAX
- },
- Pattern5[] = {
- MOV_RAX_IMM64
- PUSH_RAX
- };
- for (UINT uiIndex = 0, i = 0; i < pbConfigEnd - pbConfigStart;)
- {
- if (MatchPattern(pbConfigStart + i, Pattern4, sizeof(Pattern4), 0xab))
- {
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 4];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 3];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 2];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 1];
- i += sizeof(Pattern4);
- }
- else if (MatchPattern(pbConfigStart + i, Pattern5, sizeof(Pattern5), 0xab))
- {
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 9];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 8];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 7];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 6];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 5];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 4];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 3];
- (*ppbConfig)[uiIndex++] = pbConfigStart[i + 2];
- i += sizeof(Pattern5);
- }
- else if (MatchPattern(pbConfigStart + i, Pattern3, sizeof(Pattern3), 0xab))
- {
- break;
- }
- else
- {
- return FALSE;
- }
- }
- std::string config = (char*)*ppbConfig;
- std::reverse(config.begin(), config.end());
- strcpy((char*)*ppbConfig, config.c_str());
- return TRUE;
- }
- BOOL FromBase64(char* pszString, PBYTE* ppbBinary, UINT* pcbBinary)
- {
- DWORD cbBinary = 0;
- if (FAILED(CryptStringToBinaryA(pszString, 0, CRYPT_STRING_BASE64, NULL, &cbBinary, NULL, NULL)))
- {
- return FALSE;
- }
- *ppbBinary = (PBYTE)malloc(cbBinary + 1);
- if (!*ppbBinary)
- {
- return FALSE;
- }
- if (FAILED(CryptStringToBinaryA(pszString, 0, CRYPT_STRING_BASE64, *ppbBinary, &cbBinary, NULL, NULL)))
- {
- return FALSE;
- }
- *pcbBinary = cbBinary;
- return TRUE;
- }
- BOOL ScanProcessForBadgerConfig(HANDLE hProcess, std::string& badgerId, std::vector<std::wstring>& configStrings)
- {
- SIZE_T nBytesRead;
- PBYTE lpMemoryRegion = NULL, pbBadgerStateStruct = NULL;
- printf("[+] Searching process memory for badger state ...\n");
- while (1)
- {
- MEMORY_BASIC_INFORMATION mbi = { 0 };
- if (!VirtualQueryEx(hProcess, lpMemoryRegion, &mbi, sizeof(mbi)))
- {
- break;
- }
- if ((mbi.State & MEM_COMMIT) && !(mbi.Protect & PAGE_GUARD) &&
- ((mbi.Protect & PAGE_READONLY) || (mbi.Protect & PAGE_READWRITE) || (mbi.Protect & PAGE_EXECUTE_READWRITE)))
- {
- //printf("[+] Searching process memory at 0x%p (size 0x%x)\n", lpMemoryRegion, mbi.RegionSize);
- PBYTE pbLocalMemoryCopy = (PBYTE)malloc(mbi.RegionSize);
- if (!ReadProcessMemory(hProcess, lpMemoryRegion, pbLocalMemoryCopy, mbi.RegionSize, &nBytesRead))
- {
- //printf("[!] Unable to read memory at 0x%p\n", lpMemoryRegion);
- }
- else
- {
- for (UINT i = 0; i < mbi.RegionSize - 128 && !pbBadgerStateStruct; i++)
- {
- if (memcmp(pbLocalMemoryCopy + i, "b-", 2) == 0)
- {
- char* pszEndPtr = NULL;
- int badgerId = strtoul((char*)pbLocalMemoryCopy + i + 2, &pszEndPtr, 10);
- if (pszEndPtr != (char*)pbLocalMemoryCopy + i + 2 && pszEndPtr && *pszEndPtr == '\\' && strnlen(pszEndPtr, 100) > 16)
- {
- pbBadgerStateStruct = lpMemoryRegion + i;
- break;
- }
- }
- }
- }
- free(pbLocalMemoryCopy);
- pbLocalMemoryCopy = NULL;
- }
- lpMemoryRegion += mbi.RegionSize;
- }
- if (!pbBadgerStateStruct)
- {
- printf("[!] Failed to find badger state\n");
- return FALSE;
- }
- printf("[+] Found badger state at 0x%p\n", pbBadgerStateStruct);
- BYTE BadgerState[0x1000];
- memset(BadgerState, 0, sizeof(BadgerState));
- if (!ReadProcessMemory(hProcess, pbBadgerStateStruct, BadgerState, 0x1000, &nBytesRead))
- {
- if (GetLastError() != ERROR_PARTIAL_COPY)
- {
- printf("[!] Unable to read badger state at 0x%p\n", pbBadgerStateStruct);
- return FALSE;
- }
- }
- badgerId = (char*)BadgerState;
- BYTE ConfigString[1024];
- memset(ConfigString, 0, sizeof(ConfigString));
- for (UINT i = 0x100 + (0x10 - ((DWORD64)pbBadgerStateStruct & 0xf)); i < sizeof(BadgerState); i += sizeof(DWORD64))
- {
- DWORD64 pMem = *(DWORD64*)(BadgerState + i);
- if (pMem)
- {
- ConfigString[0] = 0;
- if (!ReadProcessMemory(hProcess, (LPVOID)pMem, ConfigString, 1024, &nBytesRead) || nBytesRead != 1024)
- {
- continue;
- }
- BOOL bIsValid = ConfigString[0] != 0;
- std::wstring badgerString;
- #define MIN_STRING_LENGTH5
- if (bIsValid)
- {
- char* pszConfigString = (char*)ConfigString;
- for (UINT j = 0; j < nBytesRead && pszConfigString[j] != 0; j++)
- {
- if (!isprint(pszConfigString[j]) && !(pszConfigString[j] == '\t' || pszConfigString[j] == '\r' || pszConfigString[j] == '\n'))
- {
- break;
- }
- badgerString.push_back(pszConfigString[j]);
- }
- bIsValid = badgerString.size() >= MIN_STRING_LENGTH;
- }
- if (!bIsValid)
- {
- badgerString.clear();
- bIsValid = TRUE;
- WCHAR* pwszConfigString = (WCHAR*)ConfigString;
- for (UINT j = 0; j < nBytesRead / sizeof(WCHAR) && pwszConfigString[j] != 0; j++)
- {
- if (!iswprint(pwszConfigString[j]) && !(pwszConfigString[j] == '\t' || pwszConfigString[j] == '\r' || pwszConfigString[j] == '\n'))
- {
- break;
- }
- badgerString.push_back(pwszConfigString[j]);
- }
- bIsValid = badgerString.size() >= MIN_STRING_LENGTH;
- }
- if (bIsValid)
- {
- configStrings.push_back(badgerString);
- }
- }
- }
- return TRUE;
- }
- int main(int argc, char *argv[])
- {
- PBYTE key = (PBYTE)"bYXJm/3#M?:XyMBF";
- printf("BruteRatel v1.x Config Extractor\n");
- if (argc < 2)
- {
- printf(
- "Usage: Brc4ConfigExtractor.exe <file> [key]\n"
- " <file|pid> - file to scan for config, or running process ID\n"
- " [key] - key if not default\n"
- );
- return 1;
- }
- if (argc > 2)
- {
- key = (PBYTE)argv[2];
- }
- if (atoi(argv[1]) == 0)
- {
- PBYTE pbBadger = NULL;
- UINT cbBadger = 0;
- if (!ReadAllBytes(argv[1], &pbBadger, &cbBadger))
- {
- printf("[!] Input file '%s' not found\n", argv[1]);
- return 1;
- }
- printf("[+] Analysing file '%s' (%u bytes)\n", argv[1], cbBadger);
- PBYTE pbConfigText = NULL;
- if (!LocateBrc4Config(pbBadger, cbBadger, &pbConfigText))
- {
- printf("[!] Failed to locate BRC4 config\n");
- return 1;
- }
- printf("[+] Located BRC4 config: %s\n", pbConfigText);
- PBYTE pbBinaryConfig = NULL;
- UINT cbBinaryConfig = 0;
- if (!FromBase64((char*)pbConfigText, &pbBinaryConfig, &cbBinaryConfig))
- {
- printf("[!] Failed to decode BRC4 config from base64\n");
- return 1;
- }
- Brc4DecodeString(key, pbBinaryConfig, pbBinaryConfig, cbBinaryConfig);
- printf("[+] Decoded config: %.*s\n", cbBinaryConfig, pbBinaryConfig);
- }
- else
- {
- DWORD dwPid = atoi(argv[1]);
- printf("[+] Analysing process with ID %u\n", dwPid);
- HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
- if (!hProcess)
- {
- printf("[!] Failed to open process\n");
- return 1;
- }
- std::string badgerId;
- std::vector<std::wstring> configStrings;
- if (!ScanProcessForBadgerConfig(hProcess, badgerId, configStrings))
- {
- printf("[!] Failed to locate badger configuration in memory\n");
- return 1;
- }
- printf("[+] Badger '%s' found...\n", badgerId.c_str());
- for (auto configString : configStrings)
- {
- printf(" : %S\n", configString.c_str());
- }
- CloseHandle(hProcess);
- }
- return 0;
- }
复制代码
在构件或运行中的进程上运行提取器工具(甚至在睡眠状态下),将提取进程或构件的Brute Ratel配置状态。 更新后的V1.1版分析在我们在x33fcon会议上关于这个问题的演讲后不久,Brute Ratel宣布了该软件的新版本。因此,鉴于威胁者最近对Brute Ratel的了解,似乎应该对此进行分析,以确保防御者获得准确的建议。 混淆和睡眠技术的分析在v1.1版本中,让我们印象深刻的一件事是,作者宣称发现了新的睡眠和混淆技术。正如这个"Brute Ratel C4 v/s Nighthawk and Open Source Sleep Obfuscation Techniques"中所说,作者说:"在Austin发布这个博文之前,我甚至不知道(SIC)这个技术。然而,Brute Ratel并没有使用我们在这里看到的这两种技术中的任何一种。"在提到Foliage 中使用的APC技术和MDSec的Nighthawk中使用的基于定时器的技术,以及这里的反向工程和这里发布的概念验证。注意到这个视频是在Ekko发布后不久出现的。 在Brute Ratel v1.1中使用的睡眠混淆技术的逆向工程显示,现在有三种睡眠策略可用。第一个,正如我们之前所记录的,是一个与 @ilove2pwn_的Foliage极其相似的实现,甚至是一个完全的拷贝。 将其与Brute Ratel内部应用的技术进行比较: 正如你所看到的,代码几乎是相同的;事实上,为数不多的变化包括用Rtl包装器RtlCreateTimer替换了CreateTimerQueenTimer的WinApi调用,注意到在上述视频演示中避免了Rtl包装器的断点(可能是故意的)。 这给我们带来了Brute Ratel使用的第三种技术,它是定时器的一种变体,没有公开记录。我们在这里可以看到,这个技术使用了定时器的一个微妙变化,而是通过RtlRegisterWait代理了定时器: 虽然这种技术没有公开记录,但它已经在 Nighthawk 中使用了一段时间,巧合的是,许多常量使用了相同的值。 **Brute Ratel v1.1 版本中出现的其他未记录/未发布的功能也出现了更多巧合。**到目前为止,我们只讨论了 Brute Ratel 的 x64 应用中可用的休眠技术。 对 x86 应用的分析表明,混淆和睡眠策略被固定到上述基于 APC Foliage 的应用(注意断点从未命中): 到目前为止,还没有公开或开源的使用定时器的混淆和睡眠策略的x86应用,限制了无需定制开发即可轻松集成此类代码的可用机会。 内存中的检测v1.1版本中的一个更新意味着.rdata部分现在也被混淆了,以隐藏诸如"[+]AMSI Patched "这样的字符串,这些字符串在badger睡眠的内存中是暴露的。然而,即使是粗略的内存分析也表明,在badger睡眠的内存中仍有许多暴露的字符串。因此,这意味着有很多机会可以在终端上拔出Brute Ratel进程,即使是在badger睡眠的时候。例如,考虑到Brute Ratel C2数据是以JSON格式存储的,只要在内存中搜索它的一个独特的参数,如 "chkin",我们就能发现badger: 或者简单地搜索badger标识符(例如b-)会发现它们分散在堆和栈中。另外,这可以作为一种简单的机制来发现Brute Ratel运行的线程,例如: 在这里,我们可以看到线程4344堆栈上存在“b-4\”。我们可以从UI中确认这确实是Brute Ratel的线程: 考虑到这一点,我们能够构建一个简单但有效的Yara规则,从内存中提取休眠的Brute Ratel进程: rulebrc4_badger_strings{meta: author = "@domchell" description = "Identifies strings from Brute Ratel v1.1"strings: $a = "\"chkin\":"condition: $a}执行Yara规则,我们可以发现睡眠状态的badger: 在V1.0版本中记录的对后剥削行为的检测,如写操作中的可疑拷贝,仍然是相关的,并且仍然为BRC4后剥削行为提供有效的检测手段。 线程栈欺骗在Brute Ratel的V1.0版本中,正如我们注意到的,线程的起始地址被硬编码为ntdll!TpReleaseCleanupGroupMembers+0x550。1.1版本宣称提供 "全线程栈伪装"。对Brute Ratel的堆栈欺骗的分析显示了重写线程调用栈的简单应用。这个过程发生在badger进入睡眠之前,使用上述的定时器技术。为了使线程看起来更合法,一个新的线程栈被创建,前两帧的地址被硬编码。硬编码的地址分别在RtlUserThreadStart和BaseThreadInitThunk的偏移量0xa和0x12处: 我们能够使用这些硬编码的起始地址来识别任何其他线程,因此,识别系统上的任何Brute Ratel线程变得非常简单。为了检测这些线程,我们相应地更新了BeaconHunter,以识别RtlUserThreadStart+0xa和BaseThreadInitThunk+0x12处前两帧的线程: 更新rDLL提取功能在我们在x33fcon进行分析后不久,Brute Ratel宣布了一个方法更新,在该方法中,构建隐藏了反射DLL。对这些伪影的分析表明,这是通过使用RC4用随机密钥加密反射DLL应用的;然后踩踏PE头。将8字节的RC4密钥附加到加密的反射DLL,然后是400字节的base64配置文件。 我们开发了以下针对Brute Ratel v1.1的工具,以从DLL和EXE构件中提取反射DLL: - //
- // only works with BRC4 1.1 binaries.
- //
- #include <algorithm
- #include <windows.h>
- #include <cstdio>
- #include <string>
- #include <iostream>
- #include <fstream>
- #include <sstream>
- #include <vector>
- #include <iomanip>
- typedef struct _RC4_CTX {
- BYTE x, y;
- BYTE s[256];
- } RC4_CTX, *PRC4_CTX;
- std::vector<BYTE>
- ReadData(std::string path) {
- std::ifstream instream(path, std::ios::in | std::ios::binary);
- std::vector<BYTE> input((std::istreambuf_iterator<char>(instream)), std::istreambuf_iterator<char>());
- return input;
- }
- bool
- WriteData(std::string path, std::vector<BYTE> data) {
- std::ofstream outstream(path, std::ios::out | std::ios::binary);
- std::copy(data.begin(), data.end(), std::ostreambuf_iterator<char>(outstream));
- return outstream.good();
- }
- BYTE
- start_sig[]={
- #if defined(_WIN64)
- 0x55, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x41,
- 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xE4, 0xF0, 0x48, 0x31,
- 0xC0, 0x50
- #else
- 0x60, 0x89, 0xE5, 0x83, 0xE4, 0xF8, 0x31, 0xC0, 0x50
- #endif
- };
- BYTE
- end_sig[]={
- #if defined(_WIN64)
- 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58,
- 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x5D, 0xC3
- #else
- 0x83, 0xC4, 0x10, 0x61, 0xC3
- #endif
- };
- void
- RC4_set_key(
- PRC4_CTX c,
- PVOID key,
- UINT keylen)
- {
- UINT i;
- UCHAR j;
- PUCHAR k=(PUCHAR)key;
- for (i=0; i<256; i++) {
- c->s[i] = (UCHAR)i;
- }
-
- c->x = 0; c->y = 0;
-
- for (i=0, j=0; i<256; i++) {
- j = (j + (c->s[i] + k[i % keylen]));
- UCHAR t = c->s[i];
- c->s[i] = c->s[j];
- c->s[j] = t;
- }
- }
- void
- RC4_crypt(
- PRC4_CTX c,
- PUCHAR buf,
- UINT len)
- {
- UCHAR x = c->x, y = c->y, j=0, t;
- for (UINT i=0; i<len; i++) {
- x = (x + 1);
- y = (y + c->s[x]);
- t = c->s[x];
- c->s[x] = c->s[y];
- c->s[y] = t;
- j = (c->s[x] + c->s[y]);
- buf[i] ^= c->s[j];
- }
- c->x = x;
- c->y = y;
- }
- std::vector<BYTE>
- extract_encrypted_rdll(PBYTE ptr, DWORD maxlen) {
- std::vector<BYTE> outbuf;
- printf("Searching %ld bytes.\n", maxlen);
-
- for (DWORD i=0; i<maxlen;) {
- if (!memcmp(&ptr[i], end_sig, sizeof(end_sig))) {
- printf("Reached end of signature...\n");
- break;
- }
- #if defined(_WIN64)
- if ((ptr[i] & 0x40) == 0x40 && (ptr[i+1] & 0xB0) == 0xB0)
- {
- BYTE buf[8];
-
- buf[0] = ptr[i + 9];
- buf[1] = ptr[i + 8];
- buf[2] = ptr[i + 7];
- buf[3] = ptr[i + 6];
- buf[4] = ptr[i + 5];
- buf[5] = ptr[i + 4];
- buf[6] = ptr[i + 3];
- buf[7] = ptr[i + 2];
-
- outbuf.insert(outbuf.end(), buf, buf + sizeof(buf));
- i += (ptr[i + 10] == 0x41) ? 12 : 11;
- } else i++;
- #else
- if ((ptr[i] & 0xB0) == 0xB0 && (ptr[i+5] & 0x50) == 0x50) {
- BYTE buf[4];
-
- buf[0] = ptr[i + 4];
- buf[1] = ptr[i + 3];
- buf[2] = ptr[i + 2];
- buf[3] = ptr[i + 1];
-
- outbuf.insert(outbuf.end(), buf, buf + sizeof(buf));
- i += 6;
- } else i++;
- #endif
- }
- std::reverse(outbuf.begin(), outbuf.end());
- return outbuf;
- }
- int
- main(int argc, char *argv[]) {
- if (argc != 2) {
- printf("usage: decrypt_brc4 <DLL|EXE>\n");
- return 0;
- }
-
- std::vector<BYTE> inbuf, infile = ReadData(argv[1]);
- DWORD len=0, ptr=0;
-
- if (infile.empty()) {
- printf("Nothing to read.\n");
- return 0;
- }
-
- do {
- auto dos = (PIMAGE_DOS_HEADER)infile.data();
- auto nt = (PIMAGE_NT_HEADERS)(infile.data() + dos->e_lfanew);
- auto s = IMAGE_FIRST_SECTION(nt);
-
- for (DWORD i=0; i<nt->FileHeader.NumberOfSections; i++) {
- char Name[IMAGE_SIZEOF_SHORT_NAME + 1] = {0};
- memcpy(Name, s[i].Name, IMAGE_SIZEOF_SHORT_NAME);
-
- if (std::string(Name) == ".data") {
- len = s[i].SizeOfRawData;
- ptr = s[i].PointerToRawData;
- break;
- }
- }
-
- if (!len) {
- printf("Unable to locate .data section.\n");
- break;
- }
-
- printf("Searching %ld bytes for loader...\n", len);
-
- for (DWORD idx=0; idx<len - sizeof(start_sig); idx++) {
- if(!memcmp(infile.data() + ptr + idx, start_sig, sizeof(start_sig))) {
- printf("Found signature : %08lX\n", ptr + idx);
- inbuf = extract_encrypted_rdll(infile.data() + ptr + idx, len - idx);
- break;
- }
- }
-
- if (inbuf.size()) {
- printf("size : %zd\n", inbuf.size());
- RC4_CTX c;
- BYTE key[8+1] = {0};
- memcpy((char*)key, inbuf.data() + inbuf.size() - 400 - 8, 8);
-
- //
- // Decrypt RDLL. The additional 400 bytes are base64 configuration.
- //
- RC4_set_key(&c, key, 8);
- RC4_crypt(&c, inbuf.data(), inbuf.size() - 400);
-
- //
- // Fix DOS header.
- //
- inbuf[0] = 'M';
- inbuf[1] = 'Z';
- WriteData(std::string(argv[1]) + ".dll", inbuf);
- }
- } while (FALSE);
-
- return 0;
- }
复制代码
总结综上所述,我们重点介绍了在构件中、内存中、通过威胁搜索和网络中检测Brute Ratel的技术。随着这个框架在威胁者中的普及,了解它的多种检测方式是很重要的。作为附带说明,我们还说明了该框架是如何从许多可用的开源社区工具中获得灵感的;对这些工具的了解可以帮助对该框架进行逆向工程,并更好地了解其能力。
|