2、第二阶段(解密beacon中的PE文件)
如下是来到了返回的内容:其实这里就是cobaltstrike里面的beacon了:
在流量侧我们看到的是:MDwT是符合checkSum8算法(cs里面对uri资源判断的一个算法,详情可参考笔者另一篇文章)
接下来我们进入beacon的内容来看这里面干了什么:如下图:beacon里面上来就是实现了一个解密操作,将46偏移之后内容通过和一个固定的3d偏移的内容,以4字节为单位做异或,还原对应内容。
还原的长度是:0x33000
内存中没解密之前的数据是这样的:
解密之后:稍微留心就会发现这个解密之后的内容其实就是一个PE文件,开头是4D5A,下面的是Dos引导区,再后面13D偏移的位置还有PE头:
3、第三阶段(通过PE头部引导,运行beacon解密出来的PE文件中的reflectiveloader函数)
继续往下看,发现通过一个pop edi,jmp edi, 直接跳到了 刚刚还原出来的数据 45偏移的位置开始执行:
这里就比较奇怪了,明明是个PE文件结构的内容,为啥beacon直接把这个当shellcode去运行执行了,就强行将jmp过去,从pe的头开始执行。
嗯,笔者之前写过一篇文章是记录一次对Cobaltstrike powershell 上线的分析,当时笔者也是遇到了这个问题,powershell上线的时候也是莫名其妙的构造了一个shellcodeloader,然后把dll文件丢到loader里面,具体为什么这样做,感兴趣可以看下那篇文章。
这里也不绕圈子了,其实就是利用反射dll修复技术,一般我们称其为Reflective DLL Injection技术,是由研究员 Stephen Fewer 再2009年提出的,后续在15年
加入了一些shellcode 技巧和引导程序来完善,发展的较为成熟,应用到恶意软件的中,很多apt组织使用了这个技术,当时基本可以过所有的av,从而大火。
关于这个技术的参考项目:
https://github.com/stephenfewer/ReflectiveDLLInjection
之前了解这个技术原理的师傅们可以去看看。
言归正传,我们继续在od中看下这个dll怎么被“执行”的:如下图是跟进之后,发现指令混到一起了
为了方便我们接下来的调试,我们将这个PE文件的内容dump下来:从这个0x29F0045开始,一直到后面没有指令的位置,或者你实在不放心,就dump长度为33000的长度,这个长度是刚刚解密的时候解密内容的长度。
这里我们手动dump下来就行,选中想要dump的内容,右键——backup——savetofile 就完事了,选中对应内容的时候还有一个小技巧,直接CTRL+G来找头和尾的位置,然后按住Shift 点两下就行了,就把要dump的内容全选了。
dump下来之后如下:
然后我们简单和之前一样,做个简易的loader,做的内容上文有,这里我就不罗嗦了:
如下代码:
#include #include using namespace std; void run() { HANDLE hfile = CreateFileA("afterdecryptothingDLL1.mem", FILE\_ALL\_ACCESS, 0, NULL, OPEN\_EXISTING, FILE\_ATTRIBUTE\_NORMAL, NULL); LPVOID exec = VirtualAlloc(NULL, 0x4000000, MEM\_COMMIT, PAGE\_EXECUTE\_READWRITE); DWORD realRead = 0; ReadFile(hfile, exec, 0x4000000, &realRead, NULL); ((void(\*)())exec)(); } int main() { run(); }
最后生成exe:(运行的时候记得把afterdecryptothingDLL1.mem这个文件放过来)
首先我们测下,这个有问题没:运行下,如下图没问题,运行之后成功上线
接着我们使用od对其调试,看下里面干了什么:
前面loader的代码我就不说了,这里我们直接来到强行执行PE的位置:如下图,上来就是一个经典的call pop,找到当前的位置:
然后将位置放到edi里面存着,后面跟了个push edx,和inc ebp ,来还原前面”4d 5a(dec ebp, pop edx)“干的事情:
然后同push ebp 和 mov ebp,esp,另起一个栈,并将刚刚获取到的真实位置+8150的偏移,刚刚获取到的是这段分配到空间的0007偏移位置的真实地址,所以我们得到的就是这段分配到空间的8157偏移的位置,调用对应位置执行:
在分析到对应位置之前,我们回过头来看下这个afterdecryptothingDLL1.mem 这个文件,使用PE查看器打开:
可以看到给出的信息是一个dll文件:
导出表:存在一个导出函数:RVA是08d57,对应的文件偏移就是:0x08d57-0x1000+0x400=0x08157,正好就是我们上面要跳往的地址:
所以也就是说,接下来就是在执行这个ReflectiveLoader函数:如下图,进来,先通过以下手段,找到PE头部的真实位置
然后就是三环fs寄存器那套了:获取到加载模块的名称:
然后计算hash,和特征码作比较:如下图
紧接着,不匹配就继续遍历下一个模块,匹配上了的话就获取对应dll的基址:如下图,在struct _LDR_DATA_TABLE_ENTRY结构中,下面的eax在本身就是在0x08偏移位置,这里加0x10,就变成了0x18,这个位置就是对应模块的基址位置,
如下图,可以看到这个”6A4ABC5B“特征码找的是kernel32这个模块:
接着又是老套路,从基址找到导出表,找到以函数名称导出的函数的个数,找到函数名称导出表INT:
接着就是遍历函数名,计算特征码:
计算出来的特征码,和以下六个特征码比较:其实就是找到下面六个特征码对应的函数,并且这些函数都是在kernel32里面的:
如果特征码相等,那么就接着获取对应IAT表(导出地址表)对应的导出地址(、这个过程是由 从导出名称表(INT)中获取到循环的次序,从循环的次数到导出序号表获取导出序号,从导出序号到导出地址表(INT)中获取导出地址),存在栈中:
这些函数是:
GetProcessAddress、GetModuleHandleA、LoadLibraryA、LoadLibraryExA、VirtualAlloc、VirtualProtect
在Reflective dll inject 技术里面,这里的reflectiveloader方法是用来处理这个DLL的,因为我们解密还原出来的dll其实是以“文件形式”存在内存中的,所以这里
reflectiveloader这个函数的主要功能就是:将这个文件形式存在的dll,加载到内存里面,也就是把reflectiveloader这个函数自己所在的dll加载到内存里面。
这里面要经过的过程是:
1、从文件形式到内存形式的拉伸,因为内存对齐和文件对齐是不一样的。这个过程主要过程就是复制,先将文件头复制到新开辟的空间,然后再按区节一个个复制到对应的相对位置。
2、修复导入表,之前以文件格式存储的时候被我们直接复制过来了,此时这里的导入表还是双桥结构,也就是INT(导入名称表)和IAT(导入地址表)是一样的,所以当我们把其加载到内存中的时候,我们要修复IAT表。修复过程就是根据导入名称表遍历导入的模块名称和函数名称,然后利用上面获取到的LoadLibrary函数和GetProcessAddress函数来找到对应函数名称的真实地址。
3、如果有重定位表,修复重定位表(这里肯定是有的,因为后面我们再这个dll的oep 也就是dllmain中还要实现我们c2 client的逻辑,不可能全部用PIC(位置无关代码)来写把),而且这里我们这个dll新加载的位置,是我们新通过virtualalloc开辟的,肯定和这个dll本身的预期的基址(imageaddress)是不一样的,没有加载到指定的基址,那么这里有重定位表,我们就要对其进行修复:
修复重定位表,在PE结构中考虑到重定位表的空间占用问题,重定位表里面并不是,存的一个个需要重定位内容的地址,而是以基址+偏移的方式来存储,几个或者多个偏移位置共用一个基址,从而使空间利用率提高:如下图这种格式:
结构体:
存储内容的形式:
内存对齐单位使0x1000h,来分页,在重定位表里面也是这样来做的,相同的1页共用一个上面的块结构体,也就是说如果一个pe文件的有8页,并且每页里面多多少少都有需要重定位的内容,那么这里就会有8个上面的块结构。
这里我们简单计算下页里面的大小,0x1000h 也就是说有4096个单位,那么要找到4096个页内偏移的可能呢,需要用几位来标记呢,2的12次就是4096,所以我们只要有12位bit就可以表示这里页内的偏移,pe里面用的2byte 也就是16位bit,其中高4位固定位0011,后12位为偏移位置。
如下是这个dll的重定位表:
简单看下对应汇编里面对上述三步的实现:
1、复制数据过:
如下:用virtualAlloc开辟新空间,大小是sizeofimage:
接着,将整个文件pe头复制过去:
然后一些判断,如下:判断pe头中的characteristics最低位是否为1:这里就是判断重定位表
接下来就是循环复制每个节块过去:
如下图,先找到块表:
获取块表的相关属性,然后循环复制到新空间:
2、修复导入表:
如下图:这里先找到dll的name
然后调用前面获取到的LoadLibrary,加载这个模块:
然后通过上面获取到的GetProcAddress,找到我们这里在导出表里面遍历的函数的内存地址,然后将内存地址写到其IAT表里面,从而实现导入表的修复:
3、修复重定位表:
如下图,找到重定位表,根据格式对其每个块里面的内容进行遍历,每个块中存的重定位资源的个数: (块—>size -8 %2),-8是把前面的virtualAddress和size占8个字节干掉,除2,是因为这里我们每个相对偏移占2个字节。
如下图,对里面的偏移进行修复
最后,如下图:在reflectiveloader这个函数里面,找到这个我们加载进内存dll的EP(入口点),压入参数并调用:这里我们看那个压入的参数”1“,其实就是dllmain里面的fdwReason参数,如下图中第二个红色部分:(但是这里要注意一个小细节,这里的ep并不是dllmain,笔者之前一直以为dll的ep就是dllmain函数,在cs实现的这个dll里面这里调用的是:DllEntryPoint函数,根据这个函数间接调用dllmain函数,并且这两个方法的参数是一样的)
我们来看下这个DllEntryPoint的实现:如下图,其实就是对fdwReason进行判断,如果等于1,就执行一个A call,然后执行B call;如果不等于1,直接执行Bcall。其实这里的B call就是dllmain, A call是一个__securiyt_init_cookie()的函数(这个函数好像用来提供缓冲区溢出保护的,不知道也没关系)
接着我们来看下B Call ,也就是dllmain:
dllmain函数的参数:这里我们需要重点注意第二个参数fdwReason:
根据msdn上对这个参数的解释:如下图,这个参数为1的时候一般用来做初始化操作:
CS沿用了这一特性,在这里其实也是在做初始化。
我们先不着急跟进对应call 到dllmain中,如下图,可以看到reflectiveLoader函数在下图中的标记的3处中,call下面有一句:
这里将这个dll的ep,放到了eax里面,然后后续返回,所以这个dllmain方法就被带出去了,返回之后继续来到PE头部的引导区:如下图:可以看到这里利用eax带回去的地址,继续压入参数并调用,此时压入的fwdReason是4:也就是说又调用了一遍dllmain方法。
接下来就是研究这个dllmain方法里面干了啥了,为啥要一共调用两次(一次fdwReason参数是1,一次fdwReason参数是4),等于1的时候是我们上文说到的,第一次是在做初始化吗?