# Скрываясь на виду у всех. №2

**Это вторая часть блога о проксировании загрузки DLL для скрытия подозрительных следов стека, ведущих к выделенной пользователем области RX.** Я не буду углубляться в то, как работает стек, потому что я уже рассказал об этом в [предыдущей статье](https://szybnev.cc/skryvayas-na-vidu-u-vseh-1).

Ранее мы видели, что мы можем манипулировать инструкциями **call** и **jmp** для запроса callback windows в вызове **API LoadLibrary**. Однако обнаружение трассировки стека выходит далеко за рамки простого отслеживания загрузки DLL. Когда вы внедряете отражающую DLL в локальный или удаленный процесс, вам приходится вызывать вызовы API, такие как **VirtualAllocEx/VirtualProtectEx**, которые косвенно вызывают **NtAllocateVirtualMemory/NtProtectVirtualMemory**. Однако, когда вы проверите стек вызовов легитимных вызовов API, вы заметите, что **WINAPI**, такие как **VirtualAlloc/VirtualProtect**, в основном вызываются не-Windows DLL-функциями. Большинство windows DLL вызывают **NtAllocateVirtualMemory/NtProtectVirtualMemory** напрямую. Ниже приведен пример стека вызовов для **NtProtectVirtualMemory** при вызове **RtlAllocateHeap**.

![](https://0xdarkvortex.dev/assets/images/2023-01-29-Hiding-In-Plainsight/rtlprotectheap.png align="center")

Это означает, что поскольку ntdll.dll не зависит ни от какой другой DLL, все функции в ntdll, которые требуют игры с разрешениями для областей памяти, будут вызывать **NTAPI** напрямую. Таким образом, это означает, что если мы сможем перенаправить наш вызов **NtAllocateVirtualMemory** через чистый стек из самой **ntdll.dll**, нам вообще не придется беспокоиться об обнаружении. Большинство красных команд полагаются на косвенные системные вызовы, чтобы избежать обнаружения. В случае косвенных вызовов системы вы просто переходите к адресу инструкции вызова системы после тщательного создания стека, но проблема здесь в том, что косвенные вызовы системы изменят только адрес возврата инструкции вызова системы в **ntdll.dll**. Адрес возврата в данном случае - это место, куда должна вернуться инструкция syscall после завершения выполнения syscall. Но остальная часть стека ниже адреса возврата все еще будет подозрительной, поскольку она выходит из области **RX**. Если EDR проверит полный стек **NTAPI**, он может легко определить, что адрес возврата в конечном итоге возвращается в выделенную пользователем область **RX**. Это означает, что адрес возврата в регион ntdll.dll, но стек, исходящий из региона RX, является 100% аномалией с нулевой вероятностью ложного срабатывания. Это легкая победа для EDR, использующих **ETW** для трассировки системных вызовов в ядре.

Таким образом, чтобы обойти это, я потратил некоторое время на реверс нескольких функций **ntdll.dll** и обнаружил, что с небольшим знанием ассемблера и того, как работают callbacks windows, мы сможем манипулировать callback'ом для вызова любой **NTAPI-функции**. В этой статье мы рассмотрим пример **NtAllocateVirtualMemory**, возьмем код из первой части блога и изменим его. Мы возьмем пример того же **API TpAllocWork**, который может выполнить функцию обратного вызова. Но вместо того, чтобы передавать указатель на строку, как мы делали в случае с **Dll Proxying**, на этот раз мы передадим указатель на структуру. В этот раз мы также избежим глобальных переменных, убедившись, что вся необходимая информация находится в структуре, поскольку мы не можем иметь глобальные переменные при написании шеллкодов. Определение **NtAllocateVirtualMemory** согласно msdn следующее:

```apache
__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
  [in]      HANDLE    ProcessHandle,
  [in, out] PVOID     *BaseAddress,
  [in]      ULONG_PTR ZeroBits,
  [in, out] PSIZE_T   RegionSize,
  [in]      ULONG     AllocationType,
  [in]      ULONG     Protect
);
```

Это означает, что нам нужно передать указатель на **NtAllocateVirtualMemory** и его аргументы внутри структуры обратному вызову, чтобы наш обратный вызов мог извлечь эту информацию из структуры и выполнить ее. Мы проигнорируем аргументы, которые остаются статичными, такие как **ULONG\_PTR ZeroBits**, который всегда равен нулю, и **ULONG AllocationType**, который всегда равен **MEM\_RESERVE|MEM\_COMMIT**, что в шестнадцатеричном формате равно 0x3000. Таким образом, добавив оставшиеся аргументы, структура будет выглядеть следующим образом:

```apache
typedef struct _NTALLOCATEVIRTUALMEMORY_ARGS {
    UINT_PTR pNtAllocateVirtualMemory;   // pointer to NtAllocateVirtualMemory - rax
    HANDLE hProcess;                     // HANDLE ProcessHandle - rcx
    PVOID* address;                      // PVOID *BaseAddress - rdx; ULONG_PTR ZeroBits - 0 - r8
    PSIZE_T size;                        // PSIZE_T RegionSize - r9; ULONG AllocationType - MEM_RESERVE|MEM_COMMIT = 3000 - stack pointer
    ULONG permissions;                   // ULONG Protect - PAGE_EXECUTE_READ - 0x20 - stack pointer
} NTALLOCATEVIRTUALMEMORY_ARGS, *PNTALLOCATEVIRTUALMEMORY_ARGS;
```

Затем мы инициализируем структуру необходимыми аргументами, передадим ее в качестве указателя в **TpAllocWork** и вызовем нашу функцию **WorkCallback**, которая написана на ассемблере.

```c
#include <windows.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI* TPALLOCWORK)(PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment);
typedef VOID (NTAPI* TPPOSTWORK)(PTP_WORK);
typedef VOID (NTAPI* TPRELEASEWORK)(PTP_WORK);

typedef struct _NTALLOCATEVIRTUALMEMORY_ARGS {
    UINT_PTR pNtAllocateVirtualMemory;   // pointer to NtAllocateVirtualMemory - rax
    HANDLE hProcess;                     // HANDLE ProcessHandle - rcx
    PVOID* address;                      // PVOID *BaseAddress - rdx; ULONG_PTR ZeroBits - 0 - r8
    PSIZE_T size;                        // PSIZE_T RegionSize - r9; ULONG AllocationType - MEM_RESERVE|MEM_COMMIT = 3000 - stack pointer
    ULONG permissions;                   // ULONG Protect - PAGE_EXECUTE_READ - 0x20 - stack pointer
} NTALLOCATEVIRTUALMEMORY_ARGS, *PNTALLOCATEVIRTUALMEMORY_ARGS;

extern VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);

int main() {
    LPVOID allocatedAddress = NULL;
    SIZE_T allocatedsize = 0x1000;

    NTALLOCATEVIRTUALMEMORY_ARGS ntAllocateVirtualMemoryArgs = { 0 };
    ntAllocateVirtualMemoryArgs.pNtAllocateVirtualMemory = (UINT_PTR) GetProcAddress(GetModuleHandleA("ntdll"), "NtAllocateVirtualMemory");
    ntAllocateVirtualMemoryArgs.hProcess = (HANDLE)-1;
    ntAllocateVirtualMemoryArgs.address = &allocatedAddress;
    ntAllocateVirtualMemoryArgs.size = &allocatedsize;
    ntAllocateVirtualMemoryArgs.permissions = PAGE_EXECUTE_READ;

    FARPROC pTpAllocWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpAllocWork");
    FARPROC pTpPostWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpPostWork");
    FARPROC pTpReleaseWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpReleaseWork");

    PTP_WORK WorkReturn = NULL;
    ((TPALLOCWORK)pTpAllocWork)(&WorkReturn, (PTP_WORK_CALLBACK)WorkCallback, &ntAllocateVirtualMemoryArgs, NULL);
    ((TPPOSTWORK)pTpPostWork)(WorkReturn);
    ((TPRELEASEWORK)pTpReleaseWork)(WorkReturn);

    WaitForSingleObject((HANDLE)-1, 0x1000);
    printf("allocatedAddress: %p\n", allocatedAddress);
    getchar();

    return 0;
}
```

Вот здесь все становится интересным. В случае DLL-прокси мы выполняли **LoadLibrary** только с одним аргументом - именем загружаемой DLL, которое передается в регистр **RCX**. Но в случае с **NtAllocateVirtualMemory** у нас в общей сложности 6 аргументов. Это означает, что первые четыре аргумента идут в регистры быстрого вызова, т.е. **RCX, RDX, R8 и R9**. Однако оставшиеся два аргумента должны быть помещены в стек после выделения некоторого пространства для наших 4 регистров. Обратите внимание, что в верхней части нашего стека в настоящее время находится возвращаемое значение внутренней **NTAPI-функции** **TppWorkpExecuteCallback** по адресу **0ffset 0x130**. Вот как выглядит стек вызовов при вызове функции обратного вызова **WorkCallback**.

![](https://0xdarkvortex.dev/assets/images/2023-01-29-Hiding-In-Plainsight/TppStack_1.png align="center")

![](https://0xdarkvortex.dev/assets/images/2023-01-29-Hiding-In-Plainsight/TppStack_0.png align="center")

Теперь вот в чем загвоздка. Если вы измените вершину стека, где находится адрес возврата, добавите место для 4 регистров и добавите к ним аргументы, то вся стековая структура будет перепутана и испортит развертку стека. Таким образом, мы должны модифицировать стек, не изменяя сам фрейм стека, а изменяя только значения внутри фрейма стека. Каждый фрейм стека начинается и заканчивается у синей линии, показанной на рисунке выше. Наш стековый кадр для **TppWorkpExecuteCallback** имеет достаточно места внутри себя, чтобы вместить 6 аргументов. Поэтому следующим шагом будет извлечение данных из нашей структуры **NTALLOCATEVIRTUALMEMORY\_ARGS** и перемещение их в соответствующие регистры и стек. Когда мы вызываем TpAllocWork, мы передаем указатель на структуру **NTALLOCATEVIRTUALMEMORY\_ARGS** функции **WorkCallback**, это означает, что наш указатель на структуру должен сейчас находиться в регистре **RDX**. Каждое значение в нашей структуре состоит из 8 байт (для x64, для x86 это будет 4 байта). Итак, мы извлечем эти значения **QWORD** из структуры и переместим их в **RCX, RDX, R8, R9** и оставшиеся значения в стеке после корректировки пространства самонаведения. Соглашение о вызове функций x64 в windows согласно документации msdn будет выглядеть следующим образом:

```c
__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
  [in]      HANDLE    ProcessHandle,  // goes into rcx
  [in, out] PVOID     *BaseAddress,   // goes into rdx
  [in]      ULONG_PTR ZeroBits,       // goes into r8
  [in, out] PSIZE_T   RegionSize,     // goes into r9
  [in]      ULONG     AllocationType, // goes to stack after adjusting homing space for 4 arguments
  [in]      ULONG     Protect         // goes to stack below the 5th argument after adjusting homing space for 4 arguments
);
```

Преобразование этой логики в ассемблер будет выглядеть следующим образом:

```plaintext
section .text

global WorkCallback

WorkCallback:
    mov rbx, rdx                ; backing up the struct as we are going to stomp rdx
    mov rax, [rbx]              ; NtAllocateVirtualMemory
    mov rcx, [rbx + 0x8]        ; HANDLE ProcessHandle
    mov rdx, [rbx + 0x10]       ; PVOID *BaseAddress
    xor r8, r8                  ; ULONG_PTR ZeroBits
    mov r9, [rbx + 0x18]        ; PSIZE_T RegionSize
    mov r10, [rbx + 0x20]       ; ULONG Protect
    mov [rsp+0x30], r10         ; stack pointer for 6th arg
    mov r10, 0x3000             ; ULONG AllocationType
    mov [rsp+0x28], r10         ; stack pointer for 5th arg
    jmp rax
```

Чтобы объяснить приведенный выше код:

1. Сначала мы резервируем наш указатель на структуру, находящуюся в регистре **RDX**, в регистр **RBX**. Мы делаем это потому, что при вызове **NtAllocateVirtualMemory** мы будем использовать регистр **RDX** в качестве второго аргумента.
    
2. Мы перемещаем первые 8 байт из адреса в регистер **RBX** (**struct NTALLOCATEVIRTUALMEMORY\_ARGS**, т.е. **UINT\_PTR pNtAllocateVirtualMemory**) в регистр rax, куда мы перейдем позже после корректировки аргументов.
    
3. Перемещаем второй набор из 8 байт (**HANDLE hProcess**) из структуры в **RCX**
    
4. Третий набор из 8 байт, т.е. указатель на **NULL-указатель** (**PVOID\* адрес**), хранящийся в структуре, перемещаем в **RDX**. Именно сюда будет записан наш выделенный адрес с помощью **NtAllocateVirtualMemory**
    
5. Мы обнуляем регистр **R8** для аргумента **ULONG\_PTR ZeroBits**
    
6. Мы перемещаем 6-й аргумент, т.е. последний аргумент, который должен идти внизу всех аргументов (**ULONG Protect**, т.е. разрешения **PAGE**) в R10, а затем перемещаем его на смещение **0x30** от верхнего указателя стека.
    
    1. Указатель вершины стека = **RSP** = адрес возврата **TppWorkpExecuteCallback**, который составляет 8 байт.
        
    2. Размер пространства наведения для 4 аргументов = 4x8 = 32 байта
        
    3. Пространство для 5-го аргумента = 8 байт
        
    4. Таким образом, 32+8 = 40 = **0x28** (это место, куда будет помещен второй последний 5-й аргумент)
        
    5. Таким образом, 32+8+8 = 48 = **0x30** (сюда попадет последний 6-й аргумент).
        
7. Наконец, мы перемещаем значение 5-го аргумента (**ULONG AllocationType**), т.е. **0x3000** - **MEM\_COMMIT|MEM\_RESERVE** в регистр **R10**, а затем сдвигаем его на смещение 0x28 от RSP
    

Если собрать все вместе, вот как это выглядит перед переходом к **NtAllocateVirtualMemory**:

1. Разобранный код показывает инструкции asm, которые мы написали. Текущий указатель инструкции находится сразу после корректировки стека и перед переходом к **NtAllocateVirtualMemory** Регистры показывают аргументы для NtAllocateVirtualMemory Дамп показывает структуру **NTALLOCATEVIRTUALMEMORY\_ARGS** в памяти. Каждый 8-байтовый блок памяти является объектом, относящимся к содержимому структуры Стек показывает скорректированный стек для **NtAllocateVirtualMemory**
    
2. Регистры показывают аргументы для **NtAllocateVirtualMemory**
    
3. Дамп показывает структуру **NTALLOCATEVIRTUALMEMORY\_ARGS** в памяти. Каждый 8-байтовый блок памяти является объектом, относящимся к содержимому структуры
    
4. Стек показывает скорректированный стек для **NtAllocateVirtualMemory**
    

![](https://0xdarkvortex.dev/assets/images/2023-01-29-Hiding-In-Plainsight/finalStack.png align="center")

Быстрый взгляд на стек после выполнения **NtAllocateVirtualMemory** показывает правильный стек вызовов, который можно прекрасно размотать. Вы также можете увидеть, что вызов syscall для **NtAllocateVirtualMemory** вернул ноль, что означает, что вызов был успешным.

![](https://0xdarkvortex.dev/assets/images/2023-01-29-Hiding-In-Plainsight/stacktrace.png align="center")

Стек снова чист, как хрусталь, без признаков чего-либо вредоносного. Обратите внимание, что это не stacking spooing, потому что в нашем случае стек разворачивается полностью без сбоев. Существует еще много подобных вызовов API, которые можно использовать для проксирования различных функций; я оставлю это на усмотрение читателей, чтобы они использовали свои собственные творческие способности. Полный код для этого можно найти в [моем репозитории github](https://github.com/paranoidninja/Proxy-Function-Calls-For-ETwTI).
