实战|记一次攻防样本——shellcode分析(下)
sub19456
其逻辑实现如下:
如果a4为4,也就是之前隐藏在shellcode中的文件格式pe的pe头其Characteristics**IMAGE_FILE_BYTES_REVERSED_HI**
值为0,那么这里就会调用VirtualProtect函数将,现在加载到内存的pe的可执行段权限修改为可读可执行,这里攻击者这么做的目的也非常明显,因为上面我们分析sub_19396
的时候,如何a4==4,其将申请的空间的权限,利用VirtualProtect将其修改成可读可写了,没有执行权限,那么这里攻击者经过上面一堆操作,差不多就是手动的把文件格式pe以内存格式(也就是我们常说的拉升)加载到了内存的一个地址,并且做了一些还原,对可执行段代码进行解密,对导入函数表解密还原,对重定向表进行修复等相关操作;做完这些操作自然就是要开始运行了,所以给可执行节区的内存空间要加个可执行权限;如下图:a1+0x28,其实就是v13里面的第六个数组,v13[5],也就是VirtualPortect;
然后回到主逻辑sub_188E6
,对v13里面的进行清空,如下图:
一个条件判断,获取PE_head_Characteristic的值,判断第13位是否为1,这个位一般是用来标志映像是系统文件的;如果该标志位为1,v2等于pehead的文件可选头里面的SizeOfInitializedData的值,反之,v2等于pehead的文件头里面的TimeDateStamp和PointerToSymbolTable的组合部分值,前者的后两个字节+后者的前两个字节,不难看出这v2的值应该也是受攻击者的配置项影响的:
调用DLL主函数初始化
然后继续看,拿到这个v2用来做什么了,v10=v5+v2,v5是映射到内存pe的基址,那么这里应该是基址加相对偏移拿到的拿到一个绝对地址;
最后直接把v10强行转化成指针函数执行了,传入了三个函数,v5,1,a1分别的实际含义是:映射到内存的恶意pe的基址、1、最先传入的参数(没有:0);
最后返回的rax,就是v10的值(这个大概率就是一个内置好的函数地址,攻击者通过是可以通过配置更改,调用的函数的,这里应该是有两个选择,因为v2的取值有两种可能,是看攻击者怎么配置的):
简单总结sub_188e6
这部分逻辑,就是找到隐藏在shellcode中的恶意pe文件,该pe文件被做了很多手脚,于是做了很多操作还原,并申请空间,将其拉伸到内存中,中间做了一些免杀手段,最后调用拉伸pe中的某个函数的地址; 返回的地址就是调用的地址;
到这,我们就需要动态调试了,可以直接把开辟的用来加载恶意pe的空间,直接dump下来,我们再分析对应调用的函数实现;
如下图是最后调用代码段中v10对应的函数的时候,开辟空间里面的相关内容,
拿到这个v10 call调用的地址:
ida里面直接找到对应偏移的函数:sub_20b48
(v10 对应的函数):
分析逻辑,上来一个条件判断第二个参数是否为1,是则调用sub_282d0
,(上面我们分析引导加载的函数最后调用v10传入的第二个参数就是1):
里面越界内存分别调用的函数是GetSystemTimeAsFileTime、GetCurrentThreadId 、GetCurrentProcessId 、QueryPerformanceCounter
这里逻辑大致就是通过GetSystemTimeAsFileTime获取当前时间戳,异或线程ID和进程ID得到一个值v2;然后QueryPerformanceCounter获取另一个时间戳v4,通过位移运算以及和v2进行一些异或运算得到v0,对v0做了一个判断是否等于指定值:0x2B992DDFA232,等于就赋值0x2B992DDFA233,最后使用v0给qw_47ef0
赋值,并返回;(乍一看感觉是攻击者在做什么校验,其实不然,这里是在计算64位的security_cookie,就是我们编译的时候开GS生成的代码)
分析到这,我们不难发现其实这个v10call,也就是sub_20B48
其实就是恶意加载的pe文件(dll)的dllmain或dllentrypoint,我们看其逻辑和参数和dllmain都是一样的,这里的第二个参数就是fdwReason,为1一般是首次加载dll触发,再加上上面的security_cookies计算;参考cobaltstrike,反射调用引导函数修复加载dll之后也是直接跳去dll主逻辑;
所以我们基本可以把重点定位到如下函数sub_209e8
:
sub_209e8
实现逻辑如下,接收两个参数,这里我们可以看到a2为1的时候,先调用的是上面那次,此时传入的v6是被加载恶意pe的基址,a2是1
case1情况下,先是两个条件判断,sub_20d88
和sub_249B0
:
第一个if,sub_20d88实现如下,可以看到调用了一个函数,然后根据函数是否调用成功返回eax,函数如果返回是0,sub_20d88
也返回0,反之返回1;此外还把函数的返回值存到qw_4c930
:
调用的函数是:GetProcessHeap,这里是获取进程的堆句柄;
然后sub_249B0
这个判断,其实现如下,没有接收参数:
先调用sub_1f1a8
函数,该函数实现如下:先调用函数RtlEncodePointer获取返回值,然后作为参数调入下面几个函数
RtlEncodePointer不是windows公开的api,看名称像是获取一个“被编码/被加密”指针;
在如下的几个函数里面,传入刚刚RtlEncodePointer的返回,这些函数都是赋值函数,把返回值赋值给一些指定处;
如:sub_20de8
最后调用sub_2312c
并返回,其实现如下:
如上图,sub_2312c
也是一个赋值函数,这个函数先调用GetModuleHandle()拿到kernel32.dll的基址,然后调用GetPorcAddress,获取上述的函数的地址并保存到指定处(qw_525a0
),存的时候还做了一个异或加密,key是:2b992ddfa232
;最后返回GetCurrentPackageId的地址;
简单总结:sub_1F1A8
,就是做了很多初始化的赋值操作,获取函数地址值等;
然后我们回到sub_249B0
,一个判断,判断条件sub_22f14
,做赋值操作,将sub_23070
函数结果返回dw_479c8
,然后调用另一个函数sub_24a30
:
这些函数的逻辑有些晦涩难懂,不太好分析;
四、恍然大悟跳出来
到这其实我们可以看出,shellcode中的引导加载后调用v10(dllmain),这里就是在做初始化;关键call相关逻辑大概率是初始化做完之后:
跳回shellcode开始处,我们可以看到sub_188e6
返回的rax,接下来被调用了,所以sub_188e6
里面自己调用一次是在做初始化;(这和cs一模一样)
此时传入的三个参数:shellcode中隐藏pe开始位置、4、一个类似特征码的56a2b5f0
此时再调用dllmian ,我们来到sub_183e0
函数:
其实现如下,三个参数和dllmian传入的一样;a2等于4;
带入a2=4,来到如下逻辑:
五、终是CobaltStrike没跑了
到这差不多我们就可以定性了,就是cobaltstrike,如下是之前之前笔者22年一文章里面分析的 ;Cobaltstrike4.0——记一次上头的powershell上线分析,可以看到上图逻辑和之前笔者分析的一样的:
按照cs逻辑,这里后续主要的逻辑在:sub_BA74()
里面;
回连c2、接收指令
指令解析执行流:
到这shellcode基本内容差不多就这些了,其实主要就是过了一遍反射加载函数和相关逻辑实现;
0x03 该shellcode利用的技术点
抛开上面的shellcode不谈,其是cobaltstrike的“shellcode”(严格意义说就是stage,只不过我们切入的时候是把他当shellcode切入),那么我们简单总结下分析出来的cs的反射加载函数里面兼容使用一些技术:
1、shellcode头部插入垃圾代码;
2、shellcode去call pop特征;
3、shellcode中隐藏pe文件(dll),去dos头特征、去pe头特征;
4、利用反射加载的dll里面的微软弃用字段来存储一些配置项(如密钥、是否开启内存权限管控等);
5、利用CreateFileMapping+MapViewOfFile来替换Virtual相关的内存空间申请;
6、反射加载函数里面找到要加载的pe之后,完全抹除pe文件的dos头、pe头特征;
7、反射加载的pe文件的导入表中的导入模块名称、导入函数名单以及可执行节区代码都是被加密的,反射加载函数加载的时候动态解密加载
通过分析我们也间接的学习了cobaltstrike对一些配置项落地实现的原理;
通过对比对应的关键profile配置项如下(这里我们根据分析出来的逻辑回去对比cs的配置profile里面的配置项,注意不是说上面的shellcode都做了)
stage{
set userwx “true” # 避免申请rwx权限内存
set allocator "MapViewOFFile" #利用MapViewOfFile来申请匿名映射内存空间,用于加载dll
set cleanup “true”
set magic\_mz\_x64 "YA" # 修改反射加载dll的头部特征,dos头和pe头
set magic\_pe "JQ"
set obuscate "true" # 混淆导入表
set stomppe "true" # 消除加载内存后的pe头中的dos头和pe头特征
transform-x64{
\` prepend "xxxxxx" #上述shellcode头部的多余字段内容
}
}
最后我们回过头来,直接使用脚本提取下这个beacon的配置:
这个样本的上线方式和之前的文章核对下,没有问题,可以看到如下图两个标记的地方,可以明显看到回连的c2,以及其使用了域名前置的手段;
0x04 总结
其实攻防中,红队使用的主流c2框架基本还都是CobaltStrike,因为cs 可以生成跨平台payload、可选远控形式的隧道多、强大的后渗透能力、互联网上有丰富的现成功能插件、和其他远控工具的兼容及其优秀的团队协作设计;所以很多红队都是使用cs;
但是享受其优点的同时也要接纳其缺点,比如会被检测设备重点照顾,你可能会说那cs不是可以修改profile来修改流量侧和端侧的特征吗,但是防守方法做检测的同学会想不到吗,所以在一定程度上你修改了profile的样本还是会被检测到(这个需要大量测试,因为不同厂商的检测点可能不尽相同,甚至对相同特征不同厂商的检测点也可能不同,你煞费苦心调制的profile可能因为没有做一个点,直接被秒了,如上文,拿几年前Didier Stevens师傅写的配置提取样本都可以直接把这个beacon秒了,那厂商端侧检测就更容易了,当然肯定是有一堆能绕过检测的配置能满足实际的攻防需求,因为实际攻防场景里我们不是要过所有安全设备,而是知道了目标环境之后,把目标环境里面的杀软以及相关安全设备过了就行); 那么在这种场景下,我们不妨思考下,红队如何破圈呢;这是一个值得思考的问题!
笔者才疏学浅,文章如有错误欢迎指出;
一方面是通过修改profiles

- 点赞
- 收藏
- 关注作者
评论(0)