本文为看雪论坛优秀文章
看雪论坛作者ID:mb_wiyiprvz
CobaltStrike大家应该知道,最近刚好遇上了一个CS的分段的Beacon样本,详细分析了下ShellCode,看它ShellCode是如何实现,给大家提供些混淆思路或者检测思路,如有错误欢迎指出。MD5: ad26a8c74596c32909923330eee2f6f7SHA1: 775275f0debf7dce1fd86cc067f986d86884e1e0SHA256: 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中获取文件名和文件名最大长度,具体解析见下: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 <iostream>
#include <stdio.h>
#include <windows.h>
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借此判断文件是否读取完成,读取完成后跳转执行。解密出一个dll文件并加载,可以将其dump下来。其dll文件为CS的后门运行后会调用自身的反射加载函数,在内存中实现反射注入,这里反射如何实现与分析我们留待下次细说。通过上面的分析可以发现这是一个cs的分段Beacon,其特征就是会从C2下载并加载后续的payload,相对的也就是还有不分段的Beacon可以直接加载shellcode执行,这里我用cs4.4生成了一个默认的不分段的Beacon快速分析一下。不分段的Beacon
样本会在解密出shellcode后在线程回调中调用。其解密出的shellcdoe就是类似于前面分析的分段Beacon连接cc获取的CS的dll文件。将其dump下来后会发现其用于接受指令执行的流程图基本一致。可以看出这两种cs的样本最终的目的都是用外层的loader加载起来内层的shellcode,后面的后渗透操作都是由加载起来的dll所执行。分析下来后看这段ShellCode其实也是比较经典的,PEB找系统模块,遍历导出表调用,最后获取payload解密调用。https://www.yuque.com/p1ut0/qtmgyx/aneyo7#1RQls
https://mp.weixin.qq.com/s?__biz=MzIyMjkzMzY4Ng==&mid=2247487765&idx=1&sn=4aa17ec86305ebc3832becc1ed057144&chksm=e824b6ccdf533fdaaecb93f7e9a7b7f0992c745918f07670ebf1e33cb1fcadd7746d5bdeb8a0&scene=21#wechat_redirect
https://bbs.pediy.com/thread-264470.htm#msg_header_h3_1
看雪ID:mb_wiyiprvz
https://bbs.pediy.com/user-home-874115.htm
*本文由看雪论坛 mb_wiyiprvz 原创,转载请注明来自看雪社区
文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458460264&idx=1&sn=52e25757888a3df53e8c6d01c7a0229d&chksm=b18e10e286f999f4d1377a09747bfcb8f2ec5dc8ee757c1e69d5b198403571dd9b5386531ad9#rd
如有侵权请联系:admin#unsafe.sh