4、第四阶段(调用dllmain方法,dllmain方法研究)
来到dllmain方法:如下图,进来就判断fdwReason参数是否等于1,这里也就是第一次调用dllmain,cs其实是在做初始化操作,接下来我们跳转过去看看:
来到跳转后的位置:如下图
跟进到这个call里面:如下图,这个call里面对某块数据进行解密还原:循环的次数是0x1000:
还原之后的数据如下,其中红色的部分我们是能看出来端倪的,其中包括c2,ua,URI,CT,以及相关心跳传输内容的传输字段(下面这个是Cookie字段)
所以这里这个fwdReason为1的时候的call,其实就是在初始化,将一些被加密的要用的信息还原出来(这个可能是因为beacon生成的时候会受c2profile的配置影响,总不能修改一个c2profile里面的配置,我们就要大费周章的去beacon里面找对应位置做修改把,所以cs对beacon的修改接口就是对这个资源段的修改)。
回过头来,我们来看看dllmain中fwdReason等于4的时候:
一堆逻辑,这里的逻辑代码就不是位置无关代码了,所以我们这里直接反汇编来分析会更加直观简单些,这里我们直接用ida来打开这个dll文件看下:
找到dllmain函数:
如下图,也就是在8cdf偏移的位置(ida默认基址是10000000):
直接f5大法:如下图,可以看到其实就是我们分析的对fdwReason进行判断:进行如下逻辑:
上面通过VirtualQuery获取到这个dll的虚拟空间的一系列页面信息,对获取到的buffer(MEMORY_BASIC_INFORMATION 结构),判断其type属性:
type这个属性在msdn上是这么解释的:
所以这里对这个属性做了个判断,为20000的时候释放8000的空间,为40000的时候取消对应dll的映射,笔者这里也不太清除这是要干啥,是检查dll被分配到空间的权限问题吗?
无碍,这个逻辑最后不管这么说,都是去到0x1388这个偏移对应的函数,这个函数就是我们要分析的点,我们简单来看下主要的一些关键位置:
如下是这个偏移函数反汇编后开始的内容:首先获取到和c2通信要用的一些资源和配置,如c2IP、端口、心跳时间,ua等等
最关键的点是,beacon段要定时发送心跳,并接收返回,对返回的内容进行判断处理,然后做出对应的指令。这里其实就是下面这个死循环:其中1A69偏移的这个函数是在和c2建立连接:(里面就是Wininet里面的InternetOpen,InternetSetOption、InternetConnect)
发送心跳请求,如果有就获取响应体内容(通过InternetReadFile获取,cs通信中,如果心跳请求有响应体了,就说明这里在下发任务了):
然后根据返回内容来执行对应命令:如下图,当某个响应内容的值大于0的时候调用8831这个偏移的函数:
这个函数的实现如下:,其中有个8305的偏移的函数,这个函数就是在处理执行操作的类型,更具类型执行不同的命令:
如下图,可以看到这个cobaltstrike4.0的beacon里面内置了100个任务类型:
根据响应内容来判断调用不同形式命令,常见的什么截图、弹窗、代理等相关命令之类的
switch ( a3 ) { case 1: v3 = 1; goto LABEL\_3; case 3: return (void \*)sub\_100021AE(Src); case 4: return (void \*)sub\_10002231(Src, result); case 5: return (void \*)sub\_100021C2(result); case 9: return (void \*)sub\_100043D4(1); case 10: return (void \*)sub\_100026BA((int)result, Src, "wb"); case 11: return (void \*)sub\_10003ACE(result, Src); case 12: return (void \*)sub\_10002269(result); case 13: return (void \*)sub\_10009D53(result, Src, 1); case 14: return (void \*)sub\_10006FC6(result, Src); case 15: return (void \*)sub\_100071D2(Src); case 16: return (void \*)sub\_10007214(Src); case 17: return (void \*)sub\_10006F72(result); case 18: return (void \*)sub\_1000477E(Src, 1); case 19: return (void \*)sub\_10003D0C(Src); case 22: return (void \*)sub\_100062FE(Src); case 23: return (void \*)sub\_10006445(Src); case 24: return (void \*)sub\_100062BD(Src); case 27: return (void \*)sub\_1000A2A2(Src); case 28: return (void \*)sub\_1000A16F(Src); case 29: return (void \*)sub\_1000274D(result, Src); case 31: return (void \*)sub\_1000A371(result, Src); case 32: return (void \*)sub\_1000894E(result, Src); case 33: return (void \*)sub\_10008886(Src, result); case 37: return (void \*)sub\_10007714(result); case 38: return (void \*)sub\_1000243C(result, Src); case 39: return (void \*)sub\_100028D4(Src); case 40: case 62: return (void \*)sub\_10005F7A(result, Src); case 41: return (void \*)sub\_100060AA(Src); case 42: return (void \*)sub\_10006112(Src, result); case 43: return (void \*)sub\_100043D4(0); case 44: v4 = 1; goto LABEL\_40; case 45: return (void \*)sub\_100047C9(Src, 1); case 46: return (void \*)sub\_100047C9(Src, 0); case 47: return (void \*)sub\_10002938(Src, result); case 48: return (void \*)sub\_1000589B(result, Src); case 49: return (void \*)sub\_1000A5D8(result, Src); case 50: return (void \*)sub\_10007616(Src, result); case 51: return (void \*)sub\_100076C0(Src, result); case 52: return (void \*)sub\_100029DA(result, Src); case 53: return (void \*)sub\_10003FD1(result, Src); case 54: return (void \*)sub\_10003EAA(Src, result); case 55: return (void \*)sub\_10003DA1(Src, result); case 56: return (void \*)sub\_10003E68(Src, result); case 57: return (void \*)sub\_10002A92(result, Src); case 58: return (void \*)sub\_10002C10(result, Src); case 59: return (void \*)sub\_1000A8BD(Src, result); case 60: return (void \*)sub\_10002FE8(result); case 61: return (void \*)sub\_10003075(Src); case 67: return (void \*)sub\_100026BA((int)result, Src, "ab"); case 68: return (void \*)sub\_1000660C((LPCSTR)result); case 69: return (void \*)sub\_10009D53(result, Src, 0); case 70: v5 = 1; goto LABEL\_63; case 71: v6 = 1; goto LABEL\_65; case 72: return (void \*)sub\_10002212(result); case 73: return (void \*)sub\_10003ED3(result, Src); case 74: return (void \*)sub\_10003F53(result, Src); case 75: return (void \*)sub\_10007A4A(Src, result); case 76: return (void \*)sub\_1000233F(result, Src); case 77: return (void \*)sub\_10003154(result, Src); case 78: return (void \*)sub\_100025C9(result, Src); case 79: return (void \*)sub\_1000774D(Src, result); case 80: return (void \*)sub\_1000499E(Src, result); case 81: return (void \*)sub\_100097FC(result, Src); case 82: return (void \*)sub\_1000766B(Src, result); case 83: return (void \*)sub\_10001092(result, Src); case 84: return (void \*)sub\_10001130(Src); case 85: return (void \*)sub\_100011D5(Src); case 86: return (void \*)sub\_1000695C(result, Src); case 87: v5 = 0; LABEL\_63: result = (void \*)sub\_10004903(result, Src, 1, v5); break; case 88: v6 = 0; LABEL\_65: result = (void \*)sub\_10004903(result, Src, 0, v6); break; case 89: v3 = 0; LABEL\_3: result = (void \*)sub\_10004475(result, Src, 1, v3); break; case 90: v4 = 0; LABEL\_40: result = (void \*)sub\_10004475(result, Src, 0, v4); break; case 91: result = (void \*)sub\_1000477E(Src, 0); break; case 92: result = (void \*)sub\_10007966(Src, result); break; case 93: result = (void \*)sub\_100046AD(result, Src, 1); break; case 94: result = (void \*)sub\_100046AD(result, Src, 0); break; case 95: result = (void \*)sub\_1000585E(Src, result); break; case 96: result = (void \*)sub\_100045C7(1); break; case 97: result = (void \*)sub\_100045C7(0); break; case 98: result = (void \*)sub\_10004520(1); break; case 99: result = (void \*)sub\_10004520(0); break; default: return result; }
相关
一、上文中用来提炼函数名和模块名生成特征码使用的算法
笔者理解这里之所以要存在这个算法,主要是两个作用:
1、缩短shellcode的长度,有些函数名以及模块名比较长,如果直接写到shellcode里面我们要使用像下面这种位置无关代码,这样就会占比较长位置(shellcode越短越好,因为在一些系统溢出漏洞中对内存空间的大小限制是非常严格的)。
char szMessageBoxA\[\] = { 'M','e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'A', 0 };
机器码的存储形式是,如下图:
2、对抗静态分析把,如果直接出现一些函数名,模块名在里面,特征太明显了。(但是其实算法后的特定值之后也会被作为静态分析的特征)
二、关于windows在rang3,如何从fs寄存器中拿到模块基址的这个过程
上文这里没有详细写,如果想具体了解,可以参考笔者之前写的文章:
https://forum.butian.net/share/1934
中的shellcode编写部分的内容,如下图,这里面有详细讲:
三、检测思路:
笔者了解到目前已有的对上述shellcode加载过程的检测:
1、流量侧
1、拉取beacon文件的时候,可以检测到发起的请求,这个请求满足算法checksum8(),返回文件很大,20000个字节左右。
2、我们可以去对beacon文件里面的内容做检测,因为这里beacon文件加密方式就是和一个密钥异或,并且密钥也是在beacon里面的,解密出来之后我们就可以看到pe文件的全貌了,这是一种检测思路
3、执行反射加载的dll里面dllmain方法的时候,触发c2客户端逻辑,发起心跳流量和命令执行流量,心跳流量请求的uri(http/https隧道的)、元数据传输字段,主要由生成shellcode的时候对应cs的c2profile文件控制。
4、当是https隧道的时候,因为TLS在建立连接的时候要发送证书,这里可以通过一些cs默认的证书去检测(当然如果更换证书了,这里就检测不了了);除此之外,笔者之前看了一篇文章,说这个c2server在和beacon利用TLS协议建立连接的时候,c2server端发送的一些内容是有特征的,能检测。
2、主机侧
笔者看github上有个beaconeyes的项目,这个项目检测的是主机内存空间,将进程的内存dump下来,去判断我们上面说的dllmain初始化逻辑里面还原出来的一些数据,通过这些数据的通用形式来匹配。
还有一些查杀技术,比如专门针对shellcode的检测,在r3检测是否存在通过fs获取模块基址的行为;再比如专门对抗检测反射dll加载的查杀技术,对开辟空间进行检查,检测是否存在动态加载dll的过程等
总结
一、过程
简单总结下cs的shellcode的思路:
1、执行shellcode,shellcode会通过fs寄存器获取内存模块加载表,从而从kernel32模块里面获取loadlibrary的地址,来加载wininet这个模块,加载之后从这个模块里面找到一些网络连接要用的函数(如,InternetConnectA,HttpSendRequestA等等),通过调用这些函数,向cobaltstrike的c2 server拉取beacon文件,并执行。
2、执行beacon,对其中部分数据进行解密,还原出来一个pe文件,并执行。
3、执行pe文件头部,通过dos头引导,执行pe文件里面的reflectiveloader函数,reflectiveloader函数里面主要实现:
- 通过3环fs寄存器那套,找到kernel模块里面我们要用的几个函数(GetProcessAddress、GetModuleHandleA、LoadLibraryA、LoadLibraryExA、VirtualAlloc、VirtualProtect)
- 通过找到的函数,实现对dll本身的加载(1、将pe从文件格式映射到内存格式;2、修复导入表 3、修复重定位表 4、运行dllmian(初始化))
4、dos引导的最后也是会调用从reflectiveLoader函数里面返回的dllmain函数,dllmain里面实现c2客户端的通信逻辑,发送心跳,执行命令等
对cs的shellcode进行研究分析还是比较有价值的,能为后续我们对免杀技术手段的研究打一个夯实的基础。