首页
社区
课程
招聘
[原创]病毒木马常用手段之Debug Blocker
2022-6-14 00:01 14305

[原创]病毒木马常用手段之Debug Blocker

2022-6-14 00:01
14305

Debug Blocker前置知识

约定

在Debug Blocker中,调试进程与被调试进程它们之间是一种父子关系
文章中提到的父进程子进程对应调试器被调试进程

什么是Debug Blocker

Debug Blocker是一种高级的反调试技术,而且这种技术常常被木马病毒利用

Debug Blocker原理

Debug Blocker是进程以调试方式运行自身或者其他可执行文件的技术。目的是增加逆向分析难度

Debug Blocker特点

1.父子关系
根据进程当时所扮演的不同角色(调试与被调试的关系),会执行不同的代码分支,并做出不同的动作
2.子进程不能再被其他调试器调试
在Windows中,一个进程无法同时被多个调试器调试,换言之,此时无法用OD进行调试(后面会讲如何调试)
3.父进程会影响子进程的执行
在Dbug Blocker技术中,调试器用来操纵被调试进程的执行分支,对子进程进行修改等操作,而且是一个连续的过程,换言之,在缺少调试进程的前提下,单独调试子进程无法正常运行
4.骨肉相连(这不是一道菜)
如果强制终止调试进程,那么子进程也会被终止,这也是Debug Blocker技术中非常高明的一点(仔细体会)
5.父进程处理子进程的异常
调试器和被调试者关系中,被调试进程发生的所有异常都由调试器处理,子进程故意触发异常(如内存访问异常),如果没有得到处理,程序将崩溃。子进程发生异常时,控制权转移到父进程,此时父进程修改被调试进程的执行分支,也可以对被调试进程进行加解密操作,或者修改寄存器、栈等特定值

小结

基于以上特点,Debug Blocker 是一种比较高级的反调试手段。而且必须对调试进程进行分析调试,确定调试进程是如何处理异常的(执行逻辑),这样才能准确获取被调试进程的代码

Debug Blocker完整代码

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
 
 
#define DEF_MUTEX_NAME      L"ReverseCore:DebugMe4"
 
 
void DoParentProcess();
void DoChildProcess();
 
 
void _tmain(int argc, TCHAR *argv[])
{
    HANDLE      hMutex = NULL;
 
    if( !(hMutex = CreateMutex(NULL, FALSE, DEF_MUTEX_NAME)) )
    {
        printf("CreateMutex() failed! [%d]\n", GetLastError());
        return;
    }
 
    // check mutex
    if( ERROR_ALREADY_EXISTS != GetLastError() )
        DoParentProcess();
    else
        DoChildProcess();
}
 
 
void DoChildProcess()
{
    // 8D C0 ("LEA EAX, EAX") 富档 救登绰 疙飞绢
    // 疙飞绢 辨捞 (0x17)
    __asm
    {
        nop
        nop
    }
 
    MessageBox(NULL, L"ChildProcess", L"DebugMe4", MB_OK);
}
 
 
void DoParentProcess()
{
    TCHAR                   szPath[MAX_PATH] = {0,};
    STARTUPINFO                si = {sizeof(STARTUPINFO),};
    PROCESS_INFORMATION        pi = {0,};
    DEBUG_EVENT             de = {0,};
    CONTEXT                 ctx = {0,};
    BYTE                    pBuf[0x20] = {0,};
    DWORD                   dwExcpAddr = 0, dwExcpCode = 0;
    const DWORD             DECODING_SIZE = 0x14;
    const DWORD             DECODING_KEY = 0x7F;
    const DWORD             EXCP_ADDR_1 = 0x0040103F;
    const DWORD             EXCP_ADDR_2 = 0x00401048;
 
    // create debug process
    GetModuleFileName(
        GetModuleHandle(NULL),
        szPath,
        MAX_PATH);
 
    if( !CreateProcess(
            NULL,
            szPath,
            NULL, NULL,
            FALSE,
            DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,
            NULL, NULL,
            &si,
            &pi) )
    {
        printf("CreateProcess() failed! [%d]\n", GetLastError());
        return;
    }
 
    printf("Parent Process\n");
 
    // debug loop
    while( TRUE )
    {
        ZeroMemory(&de, sizeof(DEBUG_EVENT));
 
        if( !WaitForDebugEvent(&de, INFINITE) )
        {
            printf("WaitForDebugEvent() failed! [%d]\n", GetLastError());
            break;
        }
 
        if( de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT )
        {
            dwExcpAddr = (DWORD)de.u.Exception.ExceptionRecord.ExceptionAddress;
            dwExcpCode = de.u.Exception.ExceptionRecord.ExceptionCode;
 
            if( dwExcpCode == EXCEPTION_ILLEGAL_INSTRUCTION )
            {
                if( dwExcpAddr == EXCP_ADDR_1 )
                {
                    // decoding
                    ReadProcessMemory(
                        pi.hProcess,
                        (LPCVOID)(dwExcpAddr + 2),
                        pBuf,
                        DECODING_SIZE,
                        NULL);
 
                    for(DWORD i = 0; i < DECODING_SIZE; i++)
                        pBuf[i] ^= DECODING_KEY;
 
                    WriteProcessMemory(
                        pi.hProcess,
                        (LPVOID)(dwExcpAddr + 2),
                        pBuf,
                        DECODING_SIZE,
                        NULL);
 
                    // change EIP
                    ctx.ContextFlags = CONTEXT_FULL;
                    GetThreadContext(pi.hThread, &ctx);
                    ctx.Eip += 2;
                    SetThreadContext(pi.hThread, &ctx);
                }
                else if( dwExcpAddr == EXCP_ADDR_2 )
                {
                    pBuf[0] = 0x68;
                    pBuf[1] = 0x1C;
                    WriteProcessMemory(
                        pi.hProcess,
                        (LPVOID)dwExcpAddr,
                        pBuf,
                        2,
                        NULL);
                }
            }
        }
        else if( de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT )
        {
            break;
        }
 
        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
    }
}

开始调试

第一次调试

进入main函数之后,先是调用CreateMutexW创建互斥体,然后根据返回值判断是以父进程运行还是以子进程运行,如果返回值是0xB7(0xB7说明已经有一个互斥体),就以子进程运行,此时程序是第一次运行,所以会执行父进程
图片描述

 

进入父进程函数之后,先调用GetMouduleHandleW和GetModuleFileNameW获取到文件路径,然后调用CreateProcessW通过传入DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS参数以调试方式创建子进程
图片描述

第二次调试

一般使用了Debug Blocker技术的程序,其核心代码基本上在子进程运行,所以我们重新运行程序,在判断CreateMutexW返回值的时候我们修改ZF标志位强制跳转到子进程函数。
可以看到将要执行的指令是LEA EAX,EAX ,很明显是一条非法指令,(而且后面的指令不是正常的指令),会触发异常,从而将程序执行流程转移到父进程,父进程可能对此处的代码执行解密操作或者修改EIP为其他地址。
图片描述

第三次调试

这是第一次异常,我们自己一共预设了两个异常

 

以调试方式创建子进程后,会调用WaiteForDebugEvent等待子进程发生Debug事件,(此处会涉及到Debug_EVENT结构体和调试相关的知识,不清楚的同学请自行补课)
图片描述
然后根据WaiteForDebugEvent返回的信息进行三次判断,第一次判断是否属于调试事件(EXCEPTION_DEBUG_EVENT),第二次判断发生异常的原因是否属于执行无效指令(EXCEPTION_ILLEGAL_INSTRUCTION),第三次判断发生异常的地址是否是我们提前预设好的地址(0x0040103F),如果这三个条件都成立,那么继续往下执行ReadProcessMemory,否则继续等待,直到条件成立
图片描述

 

通过在第三次判断的下一条指令下断点,我们直接进入到ReadProcessMemory这一步,通过ReadProcessMemory读取加密的代码到一个Buffer中,然后进行异或解密

 

图片描述
解密后的代码(大家仔细看解密后的代码)
图片描述
然后调用WriteProcessMemory将解密后的代码写入到子进程
图片描述
再调用GetThreadContext获取子进程的线程环境块
图片描述

 

修改子进程的EIP后,调用SetContextThread将修改后的值设置到子进程
图片描述

 

执行ContinueDebugEvent使子进程继续运行起来
图片描述

第四次调试

子程序继续运行后还会触发第二个异常
图片描述
第二次触发异常时,子进程仍然会把控制权交给父进程,通过一系列的判断之后,确定触发异常的地址是我们预设好的地址401048
图片描述
然后将触发异常的地址处的内容(原来是8DC0,也就是LEA EAX,EAX)修改两个字节,修改为681C
图片描述
接着调用ContinueDebugEvent使子进程继续执行
图片描述
可以看到子进程顺利执行
图片描述

第五次调试

如果我们要对一个使用了Debug Blocker技术的程序逆行分析,可以使用OD的编辑功能(Ctrl+E),直接修改401041-101054处的代码即可
图片描述
上面这种情况适合要修改的代码量和代码逻辑简单的情况下使用,一旦要修改的代码比较多,情况比较复杂,而且分布在程序各个地方,就不方便了。
接下来就介绍一种适合复杂情况下使用的调试方法

动态调试法

1.使用OD调试父进程,运行到关键的地方(例如子进程解密完成时的地方)

图片描述
图片描述
记住 hProcess ThreadId ProcessId 这三个值

2.在(被调试进程)子进程中要分析的代码处(比如入口点)设置无限循环

使用OD的编辑功能(Ctrl+E)在407EDF处写入2个字节的无限循环代码(EBFE),然后在4012D4-4012E4地址处写入汇编代码,实际上就是调用WriteProcessMemory给子进程写一个无限循环,如下图
图片描述
接着手动调用(将之前记下来的进程ID 和线程ID当作参数传进去)ContinueDebugEvent使子进程继续运行(这里注意要使用寄存器传参,否则可能会出错)
图片描述

3.将(调试进程)父进程和子进程进行分离(否则无法调试子进程)

调用DebugActiveProcessStop()函数将被调试进程从调试器中分离出来
图片描述

4.附加OD到子进程

直接F9运行
图片描述
然后F12暂停
图片描述
将401041处的无限循环代码还原为6A00
将401048处的代码修改为681C
图片描述

总结

Debug Blocker 技术调试起来非常繁琐,即使是简单的示例程序应用了此技术后调试起来也非常麻烦,其实原理并不复杂,主要是理清楚它们之间的关系,一般都是子进程故意触发异常,然后由父进程处理该异常,在日常分析中许多病毒木马会将Debug Blocker层层嵌套,并结合多种反调试技术,给逆向分析人员带来极大的挑战。最后,革命尚未成功,同志仍需努力!!


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-6-14 22:52 被寒江独钓_编辑 ,原因:
收藏
点赞12
打赏
分享
最新回复 (2)
雪    币: 619
活跃值: (1558)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
寒江独钓_ 2022-6-14 00:02
2
0
说明:在调试途中虚拟机出了点问题,所以换了一个OD调试,这也是图中出现两个OD的原因
雪    币: 23
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
confocus 2022-8-2 18:04
3
0
受益匪浅
游客
登录 | 注册 方可回帖
返回