MaliciousCode-FileInfect
MaliciousCode-FileInfect
本篇博客的主要目的在于记录学习,由于内容涉及不安全因素,不做特别详细的解释,学习需要的可以私信。
这是一篇对一个实验项目的记录,实验的目的和关键如下:
- 实验目的:编写shellcode,通过利用系统动态链接库调用API从而实现对目标文件夹内文件的感染。
- 相关知识:需要熟知PE文件结构
- 涉及到的API有:
CreateFileA
、GetFileSize
、CreateFileMapping
、MapViewOfFile
、UnmapViewOfFile
、CloseHandle
、FindFirstFileA
、FindNextFileA
、FindClose
、LoadLibraryA
、GetProcAddress
主要思路
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() |
这里我自己写了一个通过比对DLL名称来查找目标DLL的函数,以增强不同Windows系统版本的适用性。
2、GetAllAPIAddress
函数的功能是查找所有目标API的地址,代码如下:
void GetAllAPIAddress(DWORD DllBase,char * ApiNameBase) |
该函数调用GetAPIAddress
函数逐个查找ApiName
数组中存放的函数名,让后将地址保存到160个字节的数组空间的后面,通过判断字符串尾部\x0
判断字符串的结束,以这种方式逐个查找函数地址。
3、GetAPIAddress
我们从GetAllAPIAddress
的代码中看到,它是通过调用GetAPIAddress
来查找到每一个API的地址的,主要也是利用PE文件的结果实现的:
DWORD GetAPIAddress(unsigned char * pDllBase,char * ApiName) |
该函数通过传入的pDllBase(其实就是kernel32.dll在内存中的基址),找到dll文件的PE头位置,然后利用PE头结构找到其中可选头里的导出表,kernel32.dll的导出表里AddressOfNameOrdinals存储了函数名序号表的RVA,这里利用index索引进行比较,比对成功后,最后返回的时候加上镜像基址ImageBase就拿到了函数地址.
- 这里我们可以参考导出表结构:
4、SearchDirectory
代码如下:
void SearchDirectory(DWORD * ApiAddressArray) |
该函数主要利用FindFisrtFileA
、FindNextFileA
、FindClose
三个API完成打开文件、对指定文件夹进行遍历、最后关闭句柄内存的过程。,每成功找到一个文件便调用MyInfect
函数对该文件进行感染。
5、MyInfect
代码如下:
BOOL MyInfect(DWORD * ApiAddressArray, char * FileName) |
MyInfect
函数里对写入shellcode的起始位置进行了设置:VirusStart
,同时设置了写入shellcode的大小的VirusSize
,这个大小由项目生成的可执行程序里的text节
的尺寸决定,因为shellcode都在此节内存着。
然后调用MapFileToMemory
函数得到映射视图文件的开始地址值并赋值给pBase
,然后调用PutShellcodeToLastSection_0
函数对目标文件进行感染,成功感染之后,调用WriteToFile
函数停止当前程序的一个内存映射,将映像到内存中的文件写回磁盘。
6、MapFileToMemory
代码如下:
void * MapFileToMemory(DWORD * ApiAddressArray, char * FileName ) |
此函数通过调用CreateFileA
、GetFileSize
、CreateFileMappingA
、MapViewOfFile
、CloseHandle
等API,将指定的文件打开,如果成功打开文件,则获取文件的大小,然后将文件映射到内存并获取映射到内存中的首地址,并返回给pBase
。
7、PutShellcodeToLastSection_0
代码如下:
BOOL PutShellcodeToLastSection_0(void * pBase,char * pVirus,DWORD VirusSize) |
该函数利用上一个函数传回的pBase
,以及VirusStart
和VirusSize
,将指定位置和长度的shellcode
写入pBase
指向的文件内存映射中。
由函数的前半部分代码逻辑我们可知,该函数先通过pBase
得到待感染文件的DOS头
,然后利用如图的结构找到PE头
然后利用IfLastSectionCanBeInfected
函数判断文件的最后一节是否可以被感染,函数主要判断最后一节是否时可丢弃的或者该块的大小是否超过了物理大小而判断是否可以被感染。某一节块是否能被感染的属性如下图:
判断了是否能被感染之后,如果可以被感染,则调用NeedLastSectionEnlargeSize
函数判断最后一节的大小是否足够写入shellcode
,函数返回零则不必扩大,返回大于0的值即为需要扩大的尺寸,便继续执行代码利用AlignToFile
和AlignToSection
进行扩大节的操作。
此过程结束后,便可以正式进行shellcode写入了,代码:
//修改节属性,使之属于可执行节 |
该部分代码先对节的属性进行修改,使之成为可执行的,然后记录旧的入口点到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() |
主函数利用call/pop组合
将_MYDATA
中存放的函数名压入栈,然后调用RealBegin
函数实现恶意代码的注入,RealBegin
函数中都是利用前面提到的函数来实现功能,不过的赘述。
这里,通过比对魔数MZ
后四个字节的值是否为xxxx
来判断程序是我们写的恶意程序本身还是被感染的函数,从而根据判断结果决定退出执行还是写入89898989h
这个存放入口点的位置。
其他关键内容
在编写和完善代码的过程中,很多细节需要注意,否则就会出现难以预期的问题,我在实验过程中遇到的几个关键问题如下:
1、设置VirusSize的大小
对VirusSize
进行设置,这个地方我们先设置个适当大点的值0x2000
,调试之后查看生成可执行文件的text节
的大小,然后应当用扩大之后替换VirusSize
之前的值。
由图可知可以将VirusSize
设置为比0x838
稍微大的值,以免感染文件写入shellcode时写不全。不过设置的再大些也没有问题,前提是能存下shellcode。
2、偏移EntryPointPos的设置
代码中对EntryPointPos的定义如下:
|
前面提到了这是记录写入原入口点地址偏移的地方,比如当读取10个函数的地址的时候,该偏移大小为0xea
恰好能偏移到89898989h
的地方,然后覆盖成为原入口点地址,而当我们又多读取了两个函数的地址,那么89898989h
也将会向后移动8个字节(这一点是不一定的,因为我们加入两个函数名,160个字节大小的数组空间足够放下函数名,故API_ADDRESS_OFFSET的值160不用变,而当需要加入其它API名的时候,160个字节存不下函数名时,这个地方也要扩大,向后偏移的大小会更大,所以需要根据实际情况来分析),我们可以通过观察文件结构来分析,更为直观:
- 1、首先,我们写入新的两个函数之后,不对
EntryPointPos
进行更改,那原程序的入口点(实验中用到的calc.exe
的原始入口点的VA
为01012475h
)将会仍然被写到偏移为0xea
的地方,而89898989h
将会存在于后面几个字节的地方,如图:
红色框内是入口点写入到0xea
个偏移的位置,黄色标记是89898989h
被写入的地方,没有成功被覆盖,因此被感染的程序最后只能蜂鸣而不能正常运行。 - 2、修改程序,将
EntryPointPos
改为0xf2(242)
,然后再次感染,看一下效果:
我们发现01012475h
被写入到原来的89898989h
位置,经过测试,成功感染。此时便能利用LoadLibraryA
和GetProcAddress
函数找到MessageBoxA
进行弹窗。
总结
自己在本次实验中深刻学习了感染目标程序的机制,关键点就在于:写入shellcode、更改入口点等主要作用的实现都离不开对PE文件结构的操作。基础不牢、地动山摇呀!
最后再次声明:本篇博客只用于记录学习!
- Post Title: MaliciousCode-FileInfect
- Post Author: ggb0n
- Post Link: http://ggb0n.cool/2020/02/03/MaliciousCode-FileInfect/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
1.TCTF2020部分题解
2.第五空间pwn题练习
3.堆溢出-Tcache_Attack
4.堆溢出-Housese_Of_XXX
5.堆溢出基础
6.入坑二进制