CobaltStrike ShellCode详解

VSole2022-08-04 16:51:50


一、前言

CobaltStrike大家应该知道,最近刚好遇上了一个CS的分段的Beacon样本,详细分析了下ShellCode,看它ShellCode是如何实现,给大家提供些混淆思路或者检测思路,如有错误欢迎指出。

二、目标样本

样本基本信息如下:

MD5: ad26a8c74596c32909923330eee2f6f7

SHA1: 775275f0debf7dce1fd86cc067f986d86884e1e0

SHA256: 627b6a48dbd4cd0c15a7cdd6ed125c2999ac0e6c6bbf8273481e5964a348cd28

未加壳

三、样本情况说明

这个样本主要是一个loader负责加载起shellcode,其主要功能就在函数sub_1400111D0其中。

该函数内的主要部分如下,申请内存写入shellcode并在EnumObjects的回调函数中运行shellcode,加载方式也是比较经典了,这里可以多说两句,此函数原来的功能是什么不重要,重要的是它可以指定一个回调函数就可以把shellcode的地址作为回调函数加载起来,还可以引申出一种免杀方式,那就是可以将shellcode转化为包括但不限于UUID/MAC地址后硬编码进样本,之后调用对应的转换函数转为二进制后调用有回调函数的API将其运行起来,Lazarus的一次攻击就用的这种方式具体的可以参考这篇文章:

https://research.nccgroup.com/2021/01/23/rift-analysing-a-lazarus-shellcode-execution-method/

 

接下来就是重点了,加载起来的这段shellcode开头先将DF标志位置0,这里为什么这样做后面会提到。

第一个call就是先将返回地址pop到rbp中将wininet的十六进制赋值给R14,之后压栈在取出字符,将后面要获取的LoadLibraryA的特征值赋给r10d,最后调用call rbp返回。

之后就是从PEB中获取文件名和文件名最大长度,具体解析见下:

GS+60h中存储的PEB

PEB+18h处存储的是_PEB_LDR_DATA

_PEB_LDR_DATA中+20h的地方就是LIST_ENTRY,这里有一个要注意的点,那就是这里的InMemoryOrderModuleList指向的其实是_LDR_DATA_TABLE_ENTRY中的InMemoryOrderLinks,所以在后面用这个地址的时候直接从InMemoryOrderLinks的地方开始取才是正确的,这里的知识点在下面就会有所用到。

LIST_ENTRY是一个双向链表,其内每个节点都存储着一个_LDR_DATA_TABLE_ENTRY结构。

InMemoryOrderLinks+48h处储存的模块名称信息的结构体。

注意:此处[rdx+50]并不是取_LDR_DATA_TABLE_ENTRY+50h而是InMemoryOrderLinks+50h,也就是_LDR_DATA_TABLE_ENTRY+60h的地方。

_UNICODE_STRING内+2h的地方存储着最大长度+8h的地方存储着模块名称,也就是[rdx+50]和[rdx+4A]。

之后就是我前面卖的关子了,shellcode在这块会调用lodsb指令读取模块名,这个指令就是根据DF标志位控制向前还是后读,这段shellcode功能就是循环读取模块名,判断是否大于61也就是小写的a,如果大于的话就减20,这里其实是在把小写的字母转化为大写然后用ror和add指令求一个特征值。

接下来就是保存指向InMemoryOrderLinks的rdx和先前求的特征值,在用InMemoryOrderLinks+20h获取到DllBase,也就是加载的模块基址,然后用这个基址+3Ch取到NT头的偏移,在判断NT头+18h的地方是否为20B,以此判断是否为64位文件,之后判断导出表是否为空,为空则直接跳转。

这里放上一张PE结构图供大家参考,也可直接010中对照。

在跳转处设置堆栈并获取下一个_LDR_DATA_TABLE_ENTRY,然后跳转回前面获取BaseDllName的地址开始下一个循环,直到找到有导出表的模块。

如果判断存在导出表的话就先用rax+rdx也就是导出表的RVA+基址获取到内存中导出表的地址,之后用该地址分别+18h和20h依次获取到名称导出函数个数和函数名称表,然后用函数名地址+函数名导出个数*4获取到具体函数名,这里*4是因为函数名是dword类型。

接下来就是对获取到的函数名取特征值,用存着LoadLibraryA特征值的r10d与r9d对比是否为LoadLibraryA,如果遍历完一个模块都没找到就跳转获取下一个模块地址然后跳转到获取BaseDllName的地址在下一个模块中开始下一个循环,直到找到LoadLibraryA。 

在找到LoadLibraryA后先弹出导出表地址然后+24h获取到导出序号表地址,接着获取序号表内存中位置,用名称表内的排序去序号表内找对应的序号,因为在导出表中名称表是与序号表一一对应的,获取到函数序号后我们可以直接用这个序号去函数地址表中找到对应的地址即可。

一切准备完成后设置堆栈push+jmp直接调用LoadladLibraryA,因为是x64的所以用x64调用约定也就是依次用rcx,rdx,r8,r9,多出部分使用栈进行传递,这里的rcx就是之前push的wininet。

这里贴上一段我用PEB找到Kernel32的基址后遍历导出表找到LoadladLibraryA的地址后调用的代码,这段代码最后加载的dll是我写的一个测试dll,功能是弹个窗。

#include #include #include  typedef void* (WINAPI* FnLoadLibraryA)(char*);FnLoadLibraryA MyLoadLibraryA; int main(){    UINT_PTR uiBaseAddress = 0;    UINT_PTR uiExportDir = 0;    UINT_PTR uiNameArray = 0;    UINT_PTR uiAddressArray = 0;    UINT_PTR uiNameOrdinals = 0;    DWORD dwCounter = 0;    void* hKernel32 = NULL;     //直接通过PEB获取到Kernel32的基址    __asm {        mov rdx, gs: [60h]        mov rdx, [rdx + 18h]        mov rdx, [rdx + 20h]        mov rdx, [rdx]        mov rdx, [rdx]        mov rdx, [rdx + 20h]        mov hKernel32, rdx    }    uiBaseAddress = (UINT_PTR)hKernel32;    //获取NT头    uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;    //获取数据目录表中的导出表RVA    uiNameArray = (UINT_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];    //获取导出表    uiExportDir = uiBaseAddress +((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress;    //获取名称表    uiNameArray = uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames;    //获取导出地址表    uiAddressArray = uiBaseAddress +((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions;    //获取导出序号表    uiNameOrdinals = uiBaseAddress +((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals;    //获取名称导出的个数    dwCounter = ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->NumberOfNames;    while (dwCounter--)    {         char* cpExportedFunctionName = (char*)(uiBaseAddress + *(DWORD*)(uiNameArray));        //找LoadLibrary        if (strstr(cpExportedFunctionName, "LoadLibraryA") != NULL)        {            // 用导出序号*dword是在获取函数在地址表内的位置+地址表获取到内存中的位置            uiAddressArray += (*(WORD*)(uiNameOrdinals) * sizeof(DWORD));            printf(" LoadLibraryA RVA: % d", *(DWORD*)(uiAddressArray));            // 返回函数地址RVA+基址                       MyLoadLibraryA = (FnLoadLibraryA)( uiBaseAddress+* (DWORD*)uiAddressArray);            //调用后加载            HMODULE hUser32 = (HMODULE)MyLoadLibraryA((char*)"testdll.dll");            //返回LoadLibraryA的rva             return *(DWORD*)(uiAddressArray);        }        // 名称表++        uiNameArray += sizeof(DWORD);        // 序号表++        uiNameOrdinals += sizeof(WORD);    }    return 0;}

OK,我们接着说,shellcode在调用LoadLibrary加载wininet后返回shellcode起始处,开始遍历加载模块找加载的wininet遍历其导出表依次找到并调用InternetOpenUrlA,InternetOpenA, InternetConnectA,HttpOpenRequestA, HttpSendRequestA,在调用HttpSendRequestA之后会判断返回值是否为NULL,如果返回失败的话就不停的尝试10次。

在尝试10次都失败后会跳转赋值r14而不是赋值r10,因为R10在这段shellcode中是存储函数特征值的,所以这样就会导致r10一直为0从而没有匹配到的函数,最后遍历完所有模块后lodsb指令读取模块名时导致异常。

如果返回成功的话就会调用VirtualAllocEx申请空间。

之后会调用InternetReadFile从c2分段读取文件到申请的空间内,InternetReadFile在文件读取完成后会将接收读取字节数变量的指针置为0,shellcode借此判断文件是否读取完成,读取完成后跳转执行。

跳转会有一个小循环解密后续的payload。

其连接c2在VT评论如下:

解密出一个dll文件并加载,可以将其dump下来。

其dll文件为CS的后门运行后会调用自身的反射加载函数,在内存中实现反射注入,这里反射如何实现与分析我们留待下次细说。

之后会回连c2执行c2指令。

根据获取指令执行后续操作。

通过上面的分析可以发现这是一个cs的分段Beacon,其特征就是会从C2下载并加载后续的payload,相对的也就是还有不分段的Beacon可以直接加载shellcode执行,这里我用cs4.4生成了一个默认的不分段的Beacon快速分析一下。

不分段的Beacon

样本会在解密出shellcode后在线程回调中调用。

其解密出的shellcdoe就是类似于前面分析的分段Beacon连接cc获取的CS的dll文件。

将其dump下来后会发现其用于接受指令执行的流程图基本一致。

可以看出这两种cs的样本最终的目的都是用外层的loader加载起来内层的shellcode,后面的后渗透操作都是由加载起来的dll所执行。分析下来后看这段ShellCode其实也是比较经典的,PEB找系统模块,遍历导出表调用,最后获取payload解密调用。

函数调用dword
本作品采用《CC 协议》,转载必须注明作者和本文链接
近期有使用手机投屏的需求,用过几个小工具感觉效果不是很理想,所以想着着手分析下。
概述在windows系统上,涉及到内核对象的功能函数,都需要从应用层权限转换到内核层权限,然后再执行想要的内核函数,最终将函数结果返回给应用层。本文就是用OpenProcess函数来观察函数从应用层到内核层的整体调用流程。OpenProcess函数,根据指定的进程ID,返回进程句柄。NTSTATUS Status; //保存函数执行状态。OBJECT_ATTRIBUTES Obja; //待打开对象的对象属性。HANDLE Handle; //存储打开的句柄。CLIENT_ID ClientId; //进程、线程ID. dwDesiredAccess, //预打开进程并获取对应的权限。ObjectNamePresent = ARGUMENT_PRESENT ; //判断对象名称是否为空
关于堆栈ShellCode操作:基础理论002-利用fs寄存器寻找当前程序dll的入口:从动态运行的程序中定位所需dll003-寻找大兵LoadLibraryA:从定位到的dll中寻找所需函数地址004-被截断的shellCode:加解密,解决shellCode的零字截断问题
EXP编写学习之绕过GS
2023-02-20 09:58:16
栈中的守护天使 :GSGS原理向栈内压入一个随机的DWORD值,这个随机数被称为canary ,IDA称为 Security Cookie。Security Cookie 放入 ebp前,并且data节中存放一个 Security Cookie的副本。栈中发生溢出时,Security Cookie首先被淹没,之后才是ebp和返回地址。函数返回之前,会添加一个Security Cookie验证操作,称为Security Check。检测到溢出时,系统将进入异常处理流程,函数不会正常返回,ret也不会被执行。函数使用无保护的关键字标记。缓冲区不是8字节类型 且 大小不大于4个字节。可以为函数强制启用GS。
该漏洞发生的位置是在驱动文件Win32k.sys中的xxxHandleMenuMessage函数,产生的原因是没有对该函数中调用的xxxMNFindWindowFromPoint函数的返回值进行合法性验证,直接将其作为参数传递给后面的xxxSendMessage函数调用,从而造成了提权漏洞。
反射式DLL注入实现
2022-05-13 15:59:21
反射式dll注入与常规dll注入类似,而不同的地方在于反射式dll注入技术自己实现了一个reflective loader()函数来代替LoadLibaryA()函数去加载dll,示意图如下图所示。蓝色的线表示与用常规dll注入相同的步骤,红框中的是reflective loader()函数行为,也是下面重点描述的地方。
在所有函数调用发生时,向栈帧内压入一个额外的随机 DWORD,随机数标注为“SecurityCookie”。在函数返回之前,系统将执行一个额外的安全验证操作,被称做 Security check。
MITM Fuzz下图是用户层与内核层实现通信的过程,可以看到,最后是通过NtDeviceIoControlFile来分发给相应驱动对象的派遣函数的,因此,可以通过对该函数进行HOOK操作。如果将修改以后的数据发送给NtDeviceIoControlFile函数以后,发生了内核崩溃或蓝屏,往往预示着该驱动程序可能存在内核漏洞。
当线程从等待状态苏醒后,会自动检测自己得APC队列中是否存在APC过程。所以只需要将目标进程的线程的APC队列里面添加APC过程,当然为了提高命中率可以向进程的所有线程中添加APC过程。然后促使线程从休眠中恢复就可以实现APC注入。往线程APC队列添加APC,系统会产生一个软中断。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT访问权限。第三个参数表示传递给执行函数的参数。如果直接传入shellcode不设置第三个函数,可以直接执行shellcode。
shellcode编写探究
2022-06-09 15:34:57
前言shellcode是不依赖环境,放到任何地方都可以执行的机器码。shellcode的应用场景很多,本文不研究shellcode的具体应用,而只是研究编写一个shellcode需要掌握哪些知识。要使用字符串,需要使用字符数组。所以我们需要用到 LoadLibrary 和 GetProcAddress 这两个函数,来动态获取系统API的函数指针。
VSole
网络安全专家