前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅析syscall

浅析syscall

作者头像
鸿鹄实验室
发布2022-02-17 13:54:42
3.2K0
发布2022-02-17 13:54:42
举报
文章被收录于专栏:鸿鹄实验室鸿鹄实验室

最近在面试一些人的免杀问题时总会谈到syscall,但对于一些检测、细节、绕过检测反而没有说的很清楚,本文简单总结一些syscall的方式,来帮你唬过面试官。

简介

目前syscall已经成为了绕过AV/EDR所使用的主流方式,可以用它绕过一些敏感函数的调用监控(R3)。主流的AV/EDR都会对敏感函数进行HOOK,而syscall则可以用来绕过该类检测。

Hook一般放置在kernel32.dll、kernelbase.dl、ntdll.dll之中,在EDR环境中,如果调用的函数被 HOOK,则跳转到EDR的dll之中,该dll一般在进程启动时被加载。这里拿Bitdefender Antivirus 进行测 试。

这是一个简单的测试代码:

代码语言:javascript
复制
#include <Windows.h> 
#include <iostream>
int main() {  
  STARTUPINFO            sinfo;    
  PROCESS_INFORMATION    pinfo;    
  memset(&sinfo, 0, sizeof(STARTUPINFO));    
  memset(&pinfo, 0, sizeof(PROCESS_INFORMATION));    
  sinfo.dwFlags = STARTF_USESHOWWINDOW;    s
  info.wShowWindow = SW_SHOWMAXIMIZED;   
   BOOL bSucess = CreateProcess(L"C:\\Windows\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &sinfo, &pinfo);    
   std::cout << "Hello World!\n"; 
   }

当进程启动时会加载Bitdefender的dll,即atcuf64.dll

如果进行调试则可以看到atcuf64的调用:

EDR一般会对多个函数进行HOOK,常见的Hook手段有JMP、EAT、GPA,Bitdefender的HOOK列表 可以在这里找到:https://github.com/Mr-Un1k0d3r/EDRs/blob/main/bitdefender.txt,而针对性的 Unhook可以在此处找到:https://github.com/plackyhacker/Unhook-BitDefender 除了这种手动检测 之外可以编写代码进行批量测试,主要测试逻辑如下

代码语言:javascript
复制
 if (correct_bytes[0] == assemblyBytes[0] && correct_bytes[1] == assemblyBytes[1] && correct_bytes[2] == assemblyBytes[2] && correct_bytes[3] == assemblyBytes[3])            {                printf( "\t[*]%s has NOT been hooked!\n", szExportedFunctionName );                nClean++;            }            else            {                printf("\t[*] %s HAS been hooked!\n", szExportedFunctionName);                printf("\t\t");             }

效果

windows下函数的调用流程是

代码语言:javascript
复制
OpenProcess() [Kernel32] -> OpenProcess() [Kernelbase] -> NtOpenProcess() [Ntdll] -> Direct syscall to the kernel -> | Kernel Mode |

在执行流程中以Nt和Zw开头的Ntdll函数进行执行,他们后的代码都在内核中运行。

所以直接系统调用是R3执行的后一步,将函数执行转发给R0。整个过程 唯一的区别就是EAX中的数字,也就是syscall number,不同操作系统版本之间syscall number不同。可以参考https://j00ru.vexillium.org/syscalls/nt/64/

即下面这种形式:

代码语言:javascript
复制
0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00

所以为了绕过HOOK,我们可以使用Syscall。使用前提为:

  • 不使用GetModuleHandle找到ntdll 的基址
  • 解析DLL的导出表
  • 查找syscall number
  • 执行syscall

查找DLL地址

此类操作我们需要用到PEB_LDR_DATA中的InMemoryOrderModuleList,说白了还是PEB、TEB的使 用。

代码语言:javascript
复制
typedef struct _LIST_ENTRY {   struct _LIST_ENTRY *Flink;   struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

PEB_LDR_DATA的InMemoryOrderModuleList.Flink指向第一个模块加载的InMemoryOrderLinks, InMemoryOrderLinks在LDR_DATA_TABLE_ENTRY结构体之中,而模块加载信息都在 _LDR_DATA_TABLE_ENTRY中。

代码语言:javascript
复制
typedef struct _LDR_DATA_TABLE_ENTRY {    PVOID Reserved1[2];    LIST_ENTRY InMemoryOrderLinks;    PVOID Reserved2[2];    PVOID DllBase; // Base address of the module in memory     PVOID EntryPoint;    PVOID Reserved3;    UNICODE_STRING FullDllName; // Full path + name of the dll    BYTE Reserved4[8];    PVOID Reserved5[3];    union {        ULONG CheckSum;        PVOID Reserved6;    };    ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

如果我们想浏览每个模块,我们只需要跳转到每个InMemoryOrderLinks.Flink。在 _LDR_DATA_TABLE_ENTRY结构中,我们可以检索到模块的基本地址和它的名称等。更详细的信息可以 查看:https://mohamed-fakroud.gitbook.io/red-teamings-dojo/shellcoding/leveraging-from-pe-pa rsing-technique-to-write-x86-shellcode

解析导出地址表 (EAT)

一旦我们检索到 Dll 基地址,我们需要找到目标函数的地址。为此,我们必须解析 DLL 的导出部分以 找到Export Address Table(EAT)。包含 DLL的EAT所有函数地址。这个工作可以交给_IMAGE_EXPORT_DIRECTORY,其架构体如下:

代码语言:javascript
复制
typedef struct _IMAGE_EXPORT_DIRECTORY {                  DWORD   Characteristics;                DWORD   TimeDateStamp;                    WORD    MajorVersion;                 WORD    MinorVersion;                 DWORD   Name;                   // The name of the Dll    DWORD   Base;                   // Number to add to the values found in AddressOfNameOrdinals to retrieve the "real" Ordinal number of the function (by real I mean used to call it by ordinals).    DWORD   NumberOfFunctions;      // Number of all exported functions          DWORD   NumberOfNames;          // Number of functions exported by name          DWORD   AddressOfFunctions;     // Export Address Table. Address of the functions addresses array.       DWORD   AddressOfNames;         // Export Name table. Address of the functions names array.            DWORD   AddressOfNameOrdinals;  // Export sequence number table.  Address of the Ordinals (minus the value of Base) array.             } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

调用Syscall

有了上面的内容,下面就是进行动态查找syscall number和调用syscall ,下面是一些常见的Syscall技 术

(https://github.com/jthuraisamy/SysWhispers不在其中)。

Hell’s Gate:地狱之门

地址之门,项目地址为:https://github.com/am0nsec/HellsGate、项目简介地址:

https://vxug.fak edoma.in/papers/VXUG/Exclusive/HellsGate.pdf 利用代码动态查找0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00:

也就是函数从 RCX 寄存器移入 R10 寄存器,然后将系统调用移入 EAX,与自带的asm文件对应:

测试结果,未能绕过Bitdefender (shellcode为手写的shellcode不存在被杀的问题):

Halo’s Gate:光环之门

光环之门主要是为了防止当mov r10,rcx被HOOK时地狱之门失效的问题。项目地址为:https://blog. sektor7.net/#!res/2021/halosgate.md 成熟的项目为:https://github.com/boku7/AsmHalosGate 其 思路为syscall stub中的 syscall ID 是彼此递增的!这意味着,例如,如果你被钩住了,但下面的下一个 函数没有,你只需要检索它的系统调用并减去 1 即可获得当前函数的系统调用。

代码如下:

Tartarus Gate:塔尔塔罗斯之门

在光环之门的基础上增加了一些asm混淆。从:

变成了:

项目地址为:https://github.com/trickster0/TartarusGate

FreshyCalls

mdsec新出的一种syscall的方式,项目地址:https://github.com/crummie5/FreshyCalls 文章地 址:https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-syst em-calls-for-red-teams/ 主要思路为从函数地址中获取Syscall ID。在调试器中按照地址排序,ID号递增

所以我们可以将Nt函数的地址进行排序,便可以在不解析syscall stub 的情况下获取syscall id。实现 代码如下:

代码语言:javascript
复制
for (size_t i = 0; i < export_dir->NumberOfNames; i++) {    function_name = reinterpret_cast<const char *>(ntdll_base + names_table[i]);
    // If the name of the function start with "Nt" and don't start with "Ntdll"    // we retrieve the info    if (function_name.rfind("Nt", 0) == 0 && function_name.rfind("Ntdll", 0) == std::string::npos) {      stub_ordinal = names_ordinals_table[i];      stub_address = ntdll_base + functions_table[stub_ordinal];            // We put the RVA as a key and the function name as the value.      // This is a sorted map.      // The elements are automatically sorted using the key value.      // This means that when all the Nt function will be loaded,       // the first element will be the Nt function with the lowest address       // and the last the one with the biggest address.      stub_map.insert({stub_address, function_name});    }  }
  // `stub_map` is ordered from lowest to highest using the stub address. Syscalls numbers are  // assigned using this ordering too. The lowest stub address will be the stub with the lowest  // syscall number (0 in this case). We just need to iterate `stub_map` and iterate the syscall  // number on every iteration.
  static inline void ExtractSyscallsNumbers() noexcept {    uint32_t syscall_no = 0;    // The stub_map filled previously in the code presented above    for (const auto &pair: stub_map)  {      //Creation of a map associating function name and syscall identifier.      syscall_map.insert({pair.second, syscall_no});      syscall_no++;    }  };

Syswhispers2

与FreshyCalls基本相同,项目地址如下:https://github.com/jthuraisamy/SysWhispers2 只是把Nt 换成了zw,nt与zw的区别参考:https://docs.microsoft.com/en-us/windows-hardware/drivers/kern el/using-nt-and-zw-versions-of-the-native-system-services-routines

实现代码如下:

代码语言:javascript
复制
  DWORD i = 0;
  /*
  //For info. It's in the header file.
  typedef struct _SW2_SYSCALL_ENTRY
  {
    DWORD Hash;
    DWORD Address;
  } SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;
  
  #define SW2_MAX_ENTRIES 500
  */
  
    PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
    do
    {
        // Retrieve function name from the Dll.
        PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);

        // Check if the function name starts with "Zw"
        if (*(USHORT*)FunctionName == 'wZ')
        {
      // If yes, hash the name (for AV/EDR/Malware Analyst evasion reasons) and put it in an Entries element
            Entries[i].Hash = SW2_HashSyscall(FunctionName);
      // Put also the address of the function
            Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];

            i++;
            if (i == SW2_MAX_ENTRIES) break;
        }
    } while (--NumberOfNames);

    // Save total number of system calls found.
    SW2_SyscallList.Count = i;

    // Sort the list by address in ascending order.
    for (i = 0; i < SW2_SyscallList.Count - 1; i++)
    {
        for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++)
        {
            if (Entries[j].Address > Entries[j + 1].Address)
            {
                // Swap entries.
                SW2_SYSCALL_ENTRY TempEntry;

                TempEntry.Hash = Entries[j].Hash;
                TempEntry.Address = Entries[j].Address;

                Entries[j].Hash = Entries[j + 1].Hash;
                Entries[j].Address = Entries[j + 1].Address;

                Entries[j + 1].Hash = TempEntry.Hash;
                Entries[j + 1].Address = TempEntry.Address;
            }
        }
    }

    return TRUE;
}

ParallelSyscalls

也是一种由mdsec提出的方式,项目地址:https://github.com/mdsecactivebreach/ParallelSyscalls 文章地址:https://www.mdsec.co.uk/2022/01/edr-parallel-asis-through-analysis/ 主要思路为利用 LdrpThunkSignature 恢复系统调用。实现代码如下

代码语言:javascript
复制
BOOL InitSyscallsFromLdrpThunkSignature()
{
    PPEB Peb = (PPEB)__readgsqword(0x60);
    PPEB_LDR_DATA Ldr = Peb->Ldr;
    PLDR_DATA_TABLE_ENTRY NtdllLdrEntry = NULL;

    for (PLDR_DATA_TABLE_ENTRY LdrEntry = (PLDR_DATA_TABLE_ENTRY)Ldr->InLoadOrderModuleList.Flink;
        LdrEntry->DllBase != NULL;
        LdrEntry = (PLDR_DATA_TABLE_ENTRY)LdrEntry->InLoadOrderLinks.Flink)
    {
        if (_wcsnicmp(LdrEntry->BaseDllName.Buffer, L"ntdll.dll", 9) == 0)
        {
            // got ntdll
            NtdllLdrEntry = LdrEntry;
            break;
        }
    }

    if (NtdllLdrEntry == NULL)
    {
        return FALSE;
    }

    PIMAGE_NT_HEADERS ImageNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)NtdllLdrEntry->DllBase + ((PIMAGE_DOS_HEADER)NtdllLdrEntry->DllBase)->e_lfanew);
    PIMAGE_SECTION_HEADER SectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)&ImageNtHeaders->OptionalHeader + ImageNtHeaders->FileHeader.SizeOfOptionalHeader);

    ULONG_PTR DataSectionAddress = NULL;
    DWORD DataSectionSize;

    for (WORD i = 0; i < ImageNtHeaders->FileHeader.NumberOfSections; i++)
    {
        if (!strcmp((char*)SectionHeader[i].Name, ".data"))
        {
            DataSectionAddress = (ULONG_PTR)NtdllLdrEntry->DllBase + SectionHeader[i].VirtualAddress;
            DataSectionSize = SectionHeader[i].Misc.VirtualSize;
            break;
        }
    }

    DWORD dwSyscallNo_NtOpenFile = 0, dwSyscallNo_NtCreateSection = 0, dwSyscallNo_NtMapViewOfSection = 0;

    if (!DataSectionAddress || DataSectionSize < 16 * 5)
    {
        return FALSE;
    }

    for (UINT uiOffset = 0; uiOffset < DataSectionSize - (16 * 5); uiOffset++)
    {
        if (*(DWORD*)(DataSectionAddress + uiOffset) == 0xb8d18b4c &&
            *(DWORD*)(DataSectionAddress + uiOffset + 16) == 0xb8d18b4c &&
            *(DWORD*)(DataSectionAddress + uiOffset + 32) == 0xb8d18b4c &&
            *(DWORD*)(DataSectionAddress + uiOffset + 48) == 0xb8d18b4c &&
            *(DWORD*)(DataSectionAddress + uiOffset + 64) == 0xb8d18b4c)
        {
            dwSyscallNo_NtOpenFile = *(DWORD*)(DataSectionAddress + uiOffset + 4);
            dwSyscallNo_NtCreateSection = *(DWORD*)(DataSectionAddress + uiOffset + 16 + 4);
            dwSyscallNo_NtMapViewOfSection = *(DWORD*)(DataSectionAddress + uiOffset + 64 + 4);
            break;
        }
    }

    if (!dwSyscallNo_NtOpenFile)
    {
        return FALSE;
    }

    ULONG_PTR SyscallRegion = (ULONG_PTR)VirtualAlloc(NULL, 3 * MAX_SYSCALL_STUB_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    if (!SyscallRegion)
    {
        return FALSE;
    }

    NtOpenFile = (FUNC_NTOPENFILE)BuildSyscallStub(SyscallRegion, dwSyscallNo_NtOpenFile);
    NtCreateSection = (FUNC_NTCREATESECTION)BuildSyscallStub(SyscallRegion + MAX_SYSCALL_STUB_SIZE, dwSyscallNo_NtCreateSection);
    NtMapViewOfSection = (FUNC_NTMAPVIEWOFSECTION)BuildSyscallStub(SyscallRegion + (2* MAX_SYSCALL_STUB_SIZE), dwSyscallNo_NtMapViewOfSection);

    return TRUE;
}

缺点是会在加载处显示两个ntdll。

绕过syscall检测

下面是一些针对syscall检测的绕过方法。

int2Eh法

来源:https://captmeelo.com/redteam/maldev/2021/11/18/av-evasion-syswhisper.html 就是把 syscall关键字换成了int 2eh

Egg Hunting

因为调用syscall的过程基本都是固定的,所以我们可以更改其行为逻辑。

在汇编中我们可以使用DB进行字节插入,比如“Hello”,我们便可以:

代码语言:javascript
复制
DB 77h ; 'H' DB 0h  ; 'e' DB 0h  ; 'l' DB 74h ; 'l' DB 0h  ; 'o'

使用这个技巧,我们可以放置一系列已知字节(egg)作为syscall指令的占位符,并在运行时替换 它。比如这样:

结果

增加syscall混淆后,成功绕过bitdefender

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 鸿鹄实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档