(改进的代码、POC也打包一并发布在了圈子内部)
进程中的每个线程都有自己的调用栈。当一个函数被调用时,会为其创建一个frame;这个frame是一个用于存储该函数的一些数据(例如局部变量)的空间。frame基址的正下方包含一个返回地址;即frame调用者的位置。这样,当函数执行完毕后,它就可以返回到调用者。
EDR除了Hook之外还有其他很多功能,它可以利用函数调用的调用堆栈来判断函数是否是恶意的。Elastic已经实现了这个功能,大致猜测这些数据是通过为进程和线程创建事件注册的内核回调、以及ETWTI获取的,甚至可能还有其他方式。
内存查杀、堆栈扫描是一个需要大量关注的点,Hunt-Beacon-Sleep开源工具的发布(它可以主动检查休眠的线程),使得这些未备份区域的调用堆栈有了可疑活动的信息。
所以,产生了堆栈欺骗,可是呢?这个Trampoline Gadget特征、前一个指令是否为call特征也是很明显,下面两张图分别是①正常构造两帧+Trampoline Gadget。②DEFCON会议上提到的StackMoonWalk。这两种欺骗方式使用Hunt-Sleep-Beacon的扫描:
对线程堆栈的扫描不仅仅是在Beacon“睡眠”时期,任何敏感操作如Beacon回连等等都很有可能触发堆栈扫描,无论是主动还是被动。下面提出的思路以Beacon“睡眠时期”为例,根据Ekko、DeathSleep的代码、思路进行了优化、改造。
计时器对象
项目地址:https://github.com/Cracked5pider/Ekko,它的思路是使用计时器队列,计时器队列是一种轻量级对象,用于在指定时间内调用回调函数,通过使用CreateTimerQueue、CreateTimerQueueTimer函数,可以创建并管理计时器队列以及计时器。
代码就不全部贴出来了,这里的xxx.Rsp -= 8和RtlCaptureContext的细节在于,NtContinue切换执行流之后还需要正常返回,所以使用 RtlCaptureContext() 在工作线程中复制上下文,并将获取的上下文的堆栈指针增加 8,这样它就会指向通过调用 RtlCaptureContext() 在堆栈中引入的地址,也就是最后一个函数的返回地址,我们可以将它用作所有函数的返回地址。
使用这个方法确实是一种不太一样的思路,但是这种思路也已经存在有检测,Hunt-Sleep-Beacon、TickTock对于计时器也已经有了检测:
并且这种方式还是使用到了WaitForSingleObject等一些等待的函数,正所谓计时器就计时器,直接使用计时器代替计时器对象队列中要执行的WaitForSingleObject,所以直接修改CreateTimerQueueTimer中的DueTime参数就行。另外,Beacon自己的线程也有一个WaitForSingleObject,这是为了等待计时器的队列(修改内存属性、加密、解密...)全部执行完成,看到Beacon会产生线程等待就有点不爽,替换方法是使用while循环即可:
虽然扫描出计时器进程,但是并不会仅仅因为这个就会把进程直接列为恶意进程。
堆栈不在,何必欺骗?
接下来进入这一篇文章的主题,《堆栈不在,何必欺骗?》,这个思路一开始是DeathSleep项目提出的,缺点就是不同机器需要不同处理,它使用了新的线程池API函数,并且对他们进行了逆向,与CreateTimerQueueTimer做了对比,解决了参数位置处理上的问题。另外,在我对他使用Hunt-Beacon-Sleep进行扫描时候,发现它并不会触发计时器对象的一些行为。
在睡眠时期非常短,且你疯狂Hunt-Sleep-Beacon扫描就会有一个报“使用NtContinue”,无伤大雅。
大致思路就是:
保存DeathSleep之前的堆栈,并且使用RtlCaptureContext获取一个CONTEXT(threadCtxBackup),将它RIP设为Call DeathSleep的下一条地址。如何获取RIP呢?看下面,其中initialRsp是调用DeathSleep的函数的Rsp,保存多少堆栈呢?翻翻正常的beacon.exe就知道保存大小了,断点一下Sleep函数就行了。
threadCtxBackup.Rip = *(PDWORD64)(initialRsp - 0x8);stackBackup = malloc(0x150);memcpy(stackBackup, (PVOID)initialRsp, 0x150);
关于新的线程池API也就是InitializeThreadpoolEnvironment、CreateThreadpool、CreateThreadpoolCleanupGroup、SetThreadpoolTimer哪一些。我们beacon当前线程退出之后,剩下的就交给线程池去做了,我的使用方式是一个ROP链执行修改RW属性、加密操作,之后再调用rebirth函数去恢复。(注意Loader中对于WaitForSingleObject的写法就行,主线程不能退出。)
VirtualProtect -> ROP Gadgets -> NtContinue For encrytionPermCtx -> encrytionPermCtx.Rsp -= 8 (helperCtx) to exit
memcpy(&encrytionPermCtx, &helperCtx, sizeof(CONTEXT));encrytionPermCtx.Rsp -= 8;encrytionPermCtx.Rip = (ULONG_PTR)winApi.pfnSystemFunction032;encrytionPermCtx.Rcx = (ULONG_PTR)(&pData);encrytionPermCtx.Rdx = (ULONG_PTR)(&pKey);ropMemBlock = malloc(0x1000);// 【+】 ROP: VirtualProtect -> ROP Gadgets -> NtContinue For encrytionPermCtx -> encrytionPermCtx.Rsp -= 8 (helperCtx) to exitPVOID ropStackPtr = InitilizeRopStack(ropMemBlock, 0x1000, winApi.pfnNtContinue, &encrytionPermCtx, rcxGadgetAddr, shadowFixerGadgetAddr);RtlCaptureContext(&changePermRwCtx);changePermRwCtx.Rsp = ropStackPtr;changePermRwCtx.Rip = (DWORD_PTR)VirtualProtect;changePermRwCtx.Rcx = (DWORD_PTR)pRdiAddress;changePermRwCtx.Rdx = dwRdiSize;changePermRwCtx.R8 = PAGE_READWRITE;changePermRwCtx.R9 = (DWORD_PTR)&OldProtect;
在恢复beacon的函数中,原作者的创新点是将当前的栈挪一个位置(具体挪多少,作者有一个py脚本,用来定位标记以及使用Unwind Info执行替换),如图。当然你不挪Awake也可以,你可以把要恢复的beacon栈区离Awake Rsp远远的,确保不要覆盖到就行。之后调用NtContinue对先前保存的线程上下文threadCtxBackup恢复就好。
项目地址:https://github.com/janoglezcampos/DeathSleep,网上可以搜得到。我也将这个项目的代码改了一下,放在了内部圈子之中,更容易懂代码没有这么冗余。对于嵌入UDRL中还是比较容易的。
结合一下?
看到这里,已经知道Ekko的计时器会被扫描出来,DeathSleep的并不会,所以,结合一下?
圈子介绍
圈子内部致力于红蓝对抗,武器免杀与二开,不定期分享前沿技术文章,经验总结,学习笔记以及自研工具与插件,进圈联系~
圈子已满200余人,目前价格199,学生优惠30
后续升价
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...