Cobaltstrike4.0——记一次上头的powershell上线分析(三)

简介: Cobaltstrike4.0——记一次上头的powershell上线分析

5、总结:

上面便是远程进程注入实现dll的注入,实质上就是在宿主进程上创建了一个新的线程,新的线程执行了我们写的恶意代码。

整个过程最关键的一步也是最巧妙的一步

就是我们调用CreateRemoteThread这个api来在远程进程中创建线程的时候,要传入一个过程方法,创建的线程就会去执行这个过程方法(对这个方法是有两个限制的,返回值和参数类型要满足条件)。这里我们想一下,这个过程方法可以是我们自己在“注射器”程序中写好的函数方法吗?

答案是:必然不可以的,

因为这个函数方法是没办法再宿主程序中执行的。那怎么办呢?这里巧妙又巧合的是,LoadLibraryA这个方法是突破口:

1、LoadLibraryA这个api是系统api,再kernel32.dll里面,所以宿主程序中可以直接调用

2、LoadLibraryA这个api的返回参数和传入参数类型和CreateRemoteThread这个api需要传入的方法符合

3、LoadLibraryA这个api是一个模块的dllmain方法被调用的方法之一,也就是说,如果我们调用LoadLibraryA(my.dll),那么就会触发my.dll模块里面的dllmain方法。这里正是因为这个条件,我们可以直接再dllmain方法里面写要执行的恶意代码,这里可以直接写,不用像shellcode那么麻烦。

所以这里完美的解决了CreateRemoteThread使用方法的问题,除此之外,就是我们这里的dll是在磁盘上的,所以我们加载的时候要传入绝对路径才行。而这个绝对路径我们在“注射器“程序中写没用,要写到宿主程序里面才行,所以在调用LoadLibraryA之前,我们通过WriteProcessMemory写到远程进程里面去了。

这种通过船舰远程线程的方法来实现dll注入,是非常容易被查杀的,首先我们加载的dll是第三方的dll,其次我们的恶意dll是在文件系统上的,杀毒软件很容易发现并阻止。于是这种技术出现了一次更新升级:

Stephen Fewer这个大佬在2010年左右就提出了Reflective DLL Injection

二、Reflective DLL Injection

正如我们所了解的,上面注入的关键在于LoadLibrary方法,这个方法是系统win api,所以宿主程序能调用。(也正是因为如此,杀毒软件只要检测在通过CreateRemoteThreat方法传入LoadLibrary方法这种场景以及在某进程中通过LoadLibarary动态加载dll,并对dll文件位置进行检测,那么很容易被检测到了)

那么我们是不是能构造一个能和CreateRemoteThreat配合的方法,并且这个方法是在宿主进程中能被使用的呢,这样的话问题就解决了,所以现在的问题就是怎么在宿主进程中构造一个能够被CreateRemoteThreat所调用的方法呢(CreateRemoteThreat方法的第二个参数),这里我们可以通过在”注射器“进程中使用WriteProcessMemory这个api,来在宿主进程中写入想要的方法,但是有一个问题,这里的写的方法过程全程得用shellcode,那么就实现了在宿主进程中调用我们shellcode的这个思路。

那其实这整个过程和dll注入就没啥关系了,上面这种方法叫代码注入,并且我们又回到了要使用shellcode的要求来编写那个方法了。这当然不是我们所说的Reflective DLL Injection,但是这里的思路差不多,异曲同工。

Stephen Fewer这个大佬提出的方法是:在”注射器“进程中通过WriteProcessMemory来将一个dll文件写到宿主进程空间里面(注意这里就是直接写dll文件内容进去,不是映射进去,所以这个dll是没有办法正常使用的),这个dll模块存在一个导出函数ReflectiveLoader,在CreateRemoteThreat方法的参数里面传入ReflectiveLoader函数的“真实地址(开辟空间的基址+ReflectiveLoader这个函数的文件偏移地址)”,这里的ReflectiveLoader函数的参数和返回类型是dll中构造好的,和LoadLibrary一样,也符合CreateRemoteThreat对方法参数的要求,那么关键点来了,这个ReflectiveLoader里面做了什么呢,这个也是这个技术最关键的,在ReflectiveLoader中干了一个LoadLibrary差不多的事情,将写到宿主进程中的dll文件内容展开,“加载”到宿主进程中(这里的加载其实有很多步,下文我们详细来看看一个dll加载到进程中要干些啥),最终使dll成为正常的模块被使用,然后再ReflectiveLoader的最后调用Dllmain方法(dll中ep所在的点就是Dllmain函数的起始地址)。所以通过这样一系列操作,我们就可以再dllmian方法中实现我们想要写的任意代码了,可以随意调用win api,不用使用复杂的shellcode。

以上是理论的推演,技术由来和技术实现的描述,接下来我们来看看其实现的步骤:

这里笔者参考Stephen Fewer这个研究院的开源代码:https://github.com/stephenfewer/ReflectiveDLLInjection

写下大致的实现步骤,其实最关键的就是不使用LoadLibrary,自己使用c实现LoadLibrary要干的活:

这个开源项目有两部分代码:

  • 1、inject是“注射器”进程的实现。
  • 2、Reflective_dll是待注入的dll的实现。

笔者看到国内网上很少有写详细分析的文章,基本都是大佬一笔带过。

所以下面是笔者看Stephen Fewer的ReflectiveDllInject项目之后总结的:

从下面两个方面的实现来分析下思路和过程:

1、“注射器“进程的实现思路:

这里的思路和上面我们普通dll注入思路没啥区别,就不结合代码来看了,和上面的普通dll注入差不多,唯一的区别在于下面第3步:

1.1、使用OpenProcess拿到宿主进程的句柄。

1.2、再宿主进程中使用Virtualalloc开辟一个空间,使用WriteProcessMemory写入构造好的reflective_dll文件

1.3、拿到reflective_dll文件内容中ReflectiveLoader函数的真实地址(ReflectiveLoader的文件偏移地址+开辟空间的基地址)

1.4、调用CreateRemoteThread再宿主进程中开辟线程,其中参数lpStartAddress,传入dll中ReflectiveLoader函数在宿主进程中的真实地址。

1.5、执行ReflectiveLoader函数里面的内容。

2、待注入的dll的实现思路:

这里的核心就是ReflectiveLoader的实现:

2.1、找到被(以文件形式)写到宿主进程中dll的基址(思路就是从ReflectiveLoader这个函数的开始地址一直往上找,找"MZ 4D5A"—>找3C偏移-->找”PE 5045“),这个基址在后续都要使用,用来找一些位置,比如下面2.3中的可选pe头中的SizeOfImage等,这里有一个小细节,其实这个基址我们是有的,在注射器进程中我们通过VirtualAlloc开辟空间的时候返回的就是这个基址,但是这里并没有选择在调用ReflectiveLoader的时候传入这个参数,这个作者传入的是另一个字符串指针,最后被dllmain所使用,笔者理解作者为啥要这么做就是为了增加这个dllmain的”可玩性“;

2.2、通过fs寄存器的方法来在宿主进程中找到,我们之后要用的几个函数地址,如:LoadLibraryA && GetProcAddress && VirtualAlloc && NtFlushInstructionCache(这里的方法在0x05shellcode的编写里面讲过了就不细说了);

2.3、利用在2.2中找到的VirtualAlloc方法,在宿主进程中申请一块(dll文件中的可选pe头中SizeOfImage属性大小)空间;(从这里开始到下面的2.7 就都是在实现将宿主进程中文件格式的dll,加载成正常的模块了,简单理解就是在实现LoadLibrary的内容,只不过这里特殊一点,不是load的一个磁盘上的文件,而是进程空间的dll文件)

2.4、将dll的头复制到开辟空间的头部

2.5、将各节区,从文件格式拓宽成内存格式写到开辟空间里面

2.6、根据我们的从2.2中获取的 GetProcAddress来还原导入表,因为我们内存中展开的dll其导入表里面仍然是双桥结构(IAT=INT),所以我们要修复IAT,这里其实就是便利导入表,将要用到的函数每个地址找到,然后赋值给IAT。

2.7、修复重定向表

2.8、调用通过上面3-7所加载的模块的dllmain方法(这里就是跳转到dll的ep,就是dllmain)

2.9、在dllmian中根据不同场景来实现我们想要实现的代码,因为这里传入参数都是我们可控的,想怎么写怎么写

看完上面的思路,应该就明白为啥这个叫反射dll注入了,自己还原(从文件格式映射成内存并且完成修复导入表重定向表加载之类操作)自己

如下图是笔者借鉴稀土掘金技术社区的图片,构画的一张ReflectiveDllInjection思路的总图:

其中绿色的是第一部分”注射器进程“代码完成的:

其中蓝色的是第二部分”宿主“进程中新建线程的来完成的:

其中最为经典的就是ReflectiveLoader的实现,感觉读下这个的源码还是很有必要的。

3、总结:

对于这个Reflective DLL Injection这个技术最直观的就是,宿主进程在整个过程中没有使用落地的DLL文件,是直接在内存中开展的操作,所以可以绕过之前的普通dll注入场景的检测方法,还是比较巧妙的。

好了到这我们技术铺垫就差不多,言归正传,我们回到cs的powershell上线的研究,上文通过“0x04中的分析代码逻辑”我们发现是里面其实就是实现了一个powershell的shellcodeLoder,也就是shellcode加载器,但是加载的内容不是shellcode,而是一个dll文件。

powershell中的DLL分析

一、dll分析

shellcode是一串机器码,所以这里的dll也一样会被当成机器码来执行,所以这里我们来看下这个dll文件头,看看转成机器码有没有什么说法:

注意:这里我们下面都是拿32位的payload来分析的,因为在实战中32是能被64位兼容的:

即在生成powershell上线的时候不勾选x64:

我们将获取到的dll重命名位final32.dll,使用ida打开,选择以binary方式打开:

将开头都转成机器码(按快捷键c就可以了):

转化后:如下图

这里可以看到开头有两个call,我们来逐句分析下对应机器码:

开头两句,没啥好说的是DLL的dos头”MZ“得来的,ebp-1,出栈esp指向的值给edx;

dec ebp            ; ”M“  
pop edx            ; ”Z“

接着如下,两句,call $+5,这里就是调用当前call指令开始位置往下偏移5的位置,call命令本身就是5个字节,所以就是调用下一条语句,但是call执行的时候会有压栈操作,会把下一条待执行代码(eip+1)地址压栈,函数返回使用,所以这里和下面pop ebx,连在一起就是把pop ebx这条指令的地址给到ebx

call $+5            ;跳转到下一条语句,将下一条语句的地址入栈  
pop ebx             ;栈顶的地址赋值给ebx

接着,如下,注意观察的话会发现这两条和开头MZ那两条其实就是相反的操作,从而恢复了ebp和栈堆的值

push edx            ; 恢复栈堆,将edx丢回去  
inc ebp             ; 恢复ebp

接着,如下,push ebp;mov ebp, esp ; 这两句就很熟悉了,就是进入函数方法之后的刚开始的堆栈平衡了,开新的栈

push ebp            ; 保存ebp  
mov ebp, esp        ; 切换堆栈

接着如下,ebx+0x8150,然后call ebx

add     ebx, 8150h  
call    ebx

之前edx的文件偏移位置是0x0007:

加上0x8150之后,对应函数地址的文件偏移就是0x8157了,我们来看看0x8157偏移位置的这个方法:如下图

这里我们从文件偏移来计算下内存偏移,0x8157-0x400+0x1000=8d57 ;然后我们再重新打开ida,选择以PE文件打开:

找到10008d57(ida的默认加载基址为10000000):如下图,可以看到这里是个叫ReflectiveLoader的导出函数

代码还原下(f5大法):代码挺多的这里我们就先不往下看了

二、结论:

其实分析到这我们就差不多明白了些什么了,这里我们先不着急去读这个ReflectiveLoader的实现。

我们通过分析CS powershell中还原出来的代码,对该DLL DOS头部分的代码进行分析发现,这里的头部其实就是在调用后面的ReflectiveLoader方法,通过之前在DLL注入中的ReflectiveDllInjective项目的学习,我们知道这个ReflectiveLoader其实就是在加载自己,将这个DLL加载到进程中(就相当于LoaderLibrary),并且最后调用DllMain方法。所以上面一堆的ReflectiveLoader的机器码其实就是和我们在0x06dll注入学习里面ReflectiveDllInject里面ReflectiveLoader是一样的。最后在DllMain方法里面执行自己想执行的代码逻辑。至于逻辑是什么怎么执行这就是和cs配套的了,我们后续再说。(整个过程宿主进程就是本身执行powershell这个进程,就变成了宿主进程自己加载自己。。。。。。)

这种技术在2015年就被Dan Staples这个研究员提出来了,通过Dos头中的引导程序来实现这一点自加载。

这里我们在ida里面f5下DllMain方法:如下,这里代码还原出来是通过if来判断传入的fdwReason参数,来调用不同的方法,(其实可能应该是switch,代码还原器这个可能有些bug,之前看b站上看一个up主逆向老钱的一场直播的时候,他直播通过读机器码还原投稿的exe代码,也翻车了,把while变成了还原成了if,虽然逻辑上没啥区别,但是这两者在本质上肯定是有区别的,包括这里的switch),大概就是两条路,一个是当调用该方法传入的fdwReason参数为1的时 候,调用sub_10009C43;下面的fdwReason为4的情况,简单分析下是对后续的处理,判断对内存的权限从而来释放之前使用virtualAlloc开辟的空间之类的,具体其DllMain中的代码实现逻辑是怎样的,之后我们结合cs的teamserver端源码看,从beacon和teamserver之间的详细通信过程来看会更好理解些。

三、结合teamserve和beacon直接的通信协议,深入分析final.dll DllMain的实现

笔者之前在奇安信攻防社区写过一篇分析cs httpbeacon上线流量分析的文章《Cobaltstrike4.0 学习——http分阶段stagebeacon上线流量刨根问底》,此文中有写到,beacon和teamserver之间的通信过程,本文我们分析切入的点,如下图就是那片文章中第七步开始的点:

所以我们接下来分析dllmian方法里面的时候,就参考这个来,很明显这个dllmain方法里面要调用网络通信的api,接下来大概就是两种分析思路:

1、利用还原出来的dllmain方法,去分析分析追追,看看能不能找到调用网络通信的点。

2、动态调试下,去追追dllmain调用过程,但是这里有一个问题,笔者的思路就是将dll丢到od里面,我们强制修改eip,使其执行dllmain方法,然后去分析,那么问题就出现了,dllmain方法里面如果使用了一些变量是在重定向表里面的话就不太行,还有就是这里强行改eip,我们还要注意堆栈,笔者试了下很麻烦,老是蹦。

所以我们接下来还是以分析dllmain的伪代码实现为主:

其实就两条路,关键的两个函数如下:

最后我们不停的追就会分析发现,这里是sub_10001388,就是回连后续通信的实现,我们来看看:

如下图是10001388函数的实现,这里伪代码里面我们差不多就看出有点猫腻了,agent、source、serverPort之类的

下面我们来看看在哪调用了:

如下图,我们不分析逻辑,我们只看调用点就行:可以看到是通过一个sub_10001A69来实现对上面那些参数调用的:

跟进去看看:如下两图,果然,这里面调用了wininet.dll里面的网络通信api,InternetOpenA之类的,所以基本就是在这里实现回连的。

这里我们不去详细分析实现,我们来分析下逻辑,如下图种的第七步心跳请求,这种心跳请求肯定是要重复的,重复的依据就是time,默认设置是60秒,这里我们在10001388是能看到这个循环的过程的:大概就是下面这里:

这里我们没法动态分析,并且变量逻辑啥的都挺乱的,所以就不进一步分析了,比如beacon端接受到任务的时候是怎么解密,判断执行的任务的之类的,后续的话我们直接分析cs的beacon.exe,动态调试的时候再来详细看。

到这我们这个cs 的powershell上线实现的分析就差不多结束了。

总结

笔者本来只是简单想分析下cs的powershell的实现,然后分析归纳下流量侧特征,后来发现流量侧没啥好分析的,和之前笔者在cs流量分析 写的一样,就是将前面分阶段拉取beacon的流量变成了一个http请求获取到要的beacon(shellcodeloader+shellcode),然后就发现这里的shellcode奇奇怪怪的是个dll,从而有了后文,通过这个过程的分析和思考,学到了挺多的东西,同时也挺感慨。

后续的话会结合cs的相关通信协议来分析beacon.exe(无阶段的上线exe),具体分析下上文中没有讲到的dllmain的详细逻辑之类的。

同时之后也准备分析下最近比较火的CS爆出来的漏洞,这个xss造成rce的漏洞怎么说呢,实质上就是参数没有检查的问题,如下图,借用下网上别人复现的截图:

这里的user参数可控,结合cs aggressor端界面渲染相关的漏洞,从而造成了这个cve,那这个user参数是哪来的呢,还是cs流量分析 此文中提到的第七步(这里面就是cs里面传元数据),这个流量是可以伪造的,只要知道公钥和C2profile,通过伪造心跳上线的元数据流量,user字段是在元数据里面的,从而user就可控了,最终串一起就造成了《Cobalt Strike 远程代码执行漏洞 (CVE-2022-39197)》。

这里简单分析下要做到伪造心跳流量的难度:

核心就是公钥和c2profile,c2profile决定了明文心跳流量如何伪造,公钥是用来加密伪造好的流量。攻击者获取公钥和c2profile的途径只能是从样本分析中提取出来,但是cs的样本在内存里面是有一个反分析手段的,所以这个还是不简单的,但是你要是使用默认配置的c2profile,那就没啥好说的了。

所以在笔者看来这个漏洞的利用条件是比较高的,cs官方给出的意见是《升级至 Cobalt Strike 4.7.1或更高版本》,哈哈哈。要不是这个洞是个中国人发现的,我都怀疑是cs自己的py操作。

相关文章
|
安全 API
Powershell脚本分析
Powershell脚本分析
CS-Powershell免杀-过卡巴等杀软上线
CS-Powershell免杀-过卡巴等杀软上线
515 0
|
PHP
Powershell写入文件问题简要分析
Powershell写入文件问题简要分析
91 1
|
安全 Shell API
powershell红队免杀上线小Tips
powershell红队免杀上线小Tips
powershell红队免杀上线小Tips
|
Python
PowerShell随机免杀结合ps2exe上线
PowerShell随机免杀结合ps2exe上线
325 0
|
存储 安全 API
Cobaltstrike4.0——记一次上头的powershell上线分析(二)
Cobaltstrike4.0——记一次上头的powershell上线分析
380 0
|
Java API C#
Cobaltstrike4.0——记一次上头的powershell上线分析(一)
Cobaltstrike4.0——记一次上头的powershell上线分析
543 0
|
30天前
|
监控 关系型数据库 MySQL
PowerShell 脚本编写 :自动化Windows 开发工作流程
PowerShell 脚本编写 :自动化Windows 开发工作流程
26 0