本文主要内容
本文主要对cobaltstrike4.0中shellcode的运作原理的分析。
Cobaltstirke 4.0 shellcode分析
一、shellcode生成
Cobaltstrike启动服务端,然后打开aggressor 端,如下图生成payload:
打开生成的payload:
长度是1600个byte
创建一个加载这个段机器码的加载器,在cobaltstrike中这段机器码我们一般叫stager,所以我们简单写一个stagerloader:
二、shellcodeloader
这里实现的方法很多,可以直接通过c++内联汇编,获取shellcode的存储地址,然后直接跳转过去;也可以分配内存空间,将对应payload当成一个返回类型为void的函数来执行。如下图,也是比较常见的c实现的loader的形式:
#include int main(void) { unsigned char buf\[\] = 上面那串payload;; //创建一个堆(这里看个人习惯,不建堆也可以直接分配) HANDLE myHeap = HeapCreate(HEAP\_CREATE\_ENABLE\_EXECUTE, 0, 0); //从堆上分配一块内存 void\* exec = HeapAlloc(myHeap, HEAP\_ZERO\_MEMORY, sizeof(buf)); //payload复制过去 memcpy(exec, buf, sizeof(buf)); //将exec强制转换成返回类型为void的函数指针,并调用这个函数 ((void(\*)())exec)(); return 0; }
编译链接生成exe,(使用vs编译链接的时候建议把什么优化,安全检查,随机基址,以及相关清单信息啥的都关掉,方便后面我们对exe进行调试)
通过x86 的release生成的exe如下:
完成之后我们先简单测试下,丢到虚拟机运行下,看下上线啥的是否正常:
如下图,没啥问题,成功上线:
接下来我们使用ollydbg来调试下这个exe,来看看这个payload在执行什么内容:
od运行之后,一个call和一个jmp:使用这个编译器生成好像都是这样,不重要,我们要找的代码在jmp里面,跟过去就行了
过来之后又是一堆操作,调用啥的,代码还挺长:
一个个过肯定是能找到执行我们payload的调用的,过的方法呢,就看过完对应call之后,aggressor端是否新增上线设备就行。但是这里有简单方法,比如给heapAlloc上调试断点,f9,就直接跳过去了。
最后是在这个call里面调用了对应的payoad:
f7进去看看,如下图,就是我们自己写的代码了:其中的四步内容如下:
三、shellcode分析
1、第一阶段(stager)
直接跟进去调用的payload:如下图,就是我们生成的payload:(从这里开始往下,因为笔者调试的是多是多次进行运行,所以每次分配的堆的内存空间不一样,比如下图是9805c8开始,下下下下张图(从下开始第四张)是9c05c8开始,所以这里我们主要看代码逻辑即可,地址可能对不上,笔者尽量在完整的一段逻辑里面分析的时候一次过)
第一个call:
和上图call配合,这里pop ebp,获取到eip,如下图然后压入特征码和参数返回eip:
上图中,其中参数有,“wininet”,和一个特征码 726774c,然后通过call ebp 返回。
如下图,返回之后通过fs寄存器,找打TEB——>PEB——>PEB_LDR_DATA——>内存模块加载List(InMemoryOrderLinks)——_LDR_DATA_TABLE_ENTRY
——>获取模块的名称和基址,为后续遍历模块和函数名做准备。
然后对获取的模块名,对其进行特定的一个hash算法,如下图:第一个获取到的模块名是,exe本身这个模块,所以模块名是:“shellcodeloader.exe”
对其进行特定的求特征算法,(算法逻辑就是:对每个字符判断,如果大于0x60,就减0x20(其实就是小写转大写),然后累加,在累加前将上一次的累加结果循环右移14位)
最后得出来的值是在edi中,将其压入栈中,后面会使用到。
然后找到edx+10 对应位置:如下图
这个位置其实就是,_LDR_DATA_TABLE_ENTRY中的0x18偏移的内容(edx是头指针,在里面的0x08偏移的位置),下表是该结构体的内容,所以这里其实就是拿到对应的模块的基址
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 .... };
拿到基址之后,如下,寻找3c偏移,这里是在DOS头3c偏移找PE起始位置,然后找pe头中的78偏移。
PE头中的0x78偏移:如下,可以看到这个位置其实就是可选头里面的导出表
找到导出表之后,接下来的test命令,判断是否为0,也就是判断这里是否有导出表:
等于0的话,来到如下,这里我们第一次运行的时候也的确等于0,因为我们的exe里面没有导出表,下面的对导出表为空的处理其实就是接着遍历下一个模块,然后跳回到获取模块名的位置:
接着我们去看看当某个模块的导入表不为空的时候,怎么处理的,其实第二个模块就是ntdll.dll (一般程序第三个是kernel32.dll),此时就满足导出表不为空,
然后其操作是:找到导出表里面的0x18偏移和0x20偏移的地方,这个表的结构如下:
然后如下图,遍历名称,计算特征值(这里的计算算法和之前对模块的特征算法有点不一样,区别就是没有判断是否是大于0x60,然后小写转大写),和之前栈中的特征码比较:
如下图:拿计算出来模块的特征值加上函数的特征值,最后和我们在开头压入栈中的特征值做比较:
如果不相等:就遍历下一个函数,一直到这个模块的全部以名称形式导出的函数被遍历完,然后重复遍历下一个模块:
这里我们来看下,当找到对应的函数,其通过这种hash算法计算出来的值和压入栈中的相等时的情况,
1、这个函数是什么函数
2、对这个函数干什么
如下图,可以看到,当我们遍历到,kernel32.dll模块里面的LoadLibraryExA函数的时候,特征码就相等了:
如下图是干了什么,这里面一顿操作,通过循环次数获取在其导出序号表里面获取到导出序号,然后利用导出序号在导出地址表里面获取到其导出地址,之后就可以随便调用LoadLibraryExA:
如下是,获取到之后的动作,利用这个函数,来加载wininet这个dll:
试想为什么要加载这个dll,其实大概能猜出来,肯定是后面用的函数载这个wininet这个dll模块里面,而本身的程序里面没有加载这个模块。
我们继续往下看:之后就是使用相同的方式,传入对应的特征码,然后循环遍历模块去找函数,这里其实大概率下面的这些函数都载wininet这个dll里面:
大概看了下是如下的9个函数,根据对应特征码,使用同样的方法获取到对应函数的地址,并通过压入堆栈的数据作为参数并调用对应函数:
简单看下这些函数是什么:
第一个:A779563A对应的是wininet里面的InternetOpenA函数
第二个:C69F8957对应的wininet里面的InternetConnectA
第三个:3B2E55EB对应的wininet里面的HttpOpenrequestA
第四个:7B18062D对应的是wininet里面的HttpSendRequestA
第五个:315E2145对应的是user32里面的GetDesktopWindow
第六个:0BE057B7对应的是wininet里面的InternetErrorDlg
第七个:E553A458对应的是kernel32里面的VirtualAlloc
第八个:E2899612对应的是wininet里面的InternetReadFile
这里面的逻辑,就是建立和cobaltstrike server的链接,然后发送get请求,开辟内容空降将返回的内容存起来,最后运行对应返回的内容,(上述这个步骤就是我们说的通过stager拉取beacon的操作)