在准备恶意软件分析中反调试课程内容,提到了使用直接系统调用,绕过hook函数,这个在软件保护的方向是ok的,不需要考虑栈的样子,但是想到在恶意软件方向,就是一个指标,现在edr都会监控栈的数据,使用unwind展开栈,然后判断栈是否合理,下面是我随便截的一张。为了解决这个问题的话,堆栈欺骗是一个很有趣的一个技术,建议学习,可以对栈有一个充分的认识,在学习之前有一些基础知识要学习,比如栈的定义用途,push和ret的一些对栈的操作,直接调用和间接调用的理解,还有Windows x64 调用约定的理解。
此文章我首先分析SilentMoonwalk的原理,研究他怎么做到的堆栈欺骗,该https://github.com/klezVirus/SilentMoonwalk作者说使用了两种模式,一种是合成堆栈,一种是同步堆栈也就是实时的,作者说正解是同步堆栈的话那就只看同步堆栈。
在准备分析之前,我使用agent简单的学习了一下,然后在搜集文章的时候,发现关于该项目没有好的中文文章,甚至关于栈方面的我也只看到了几篇翻译的外文文章,估计是我信息收集能力不行,于是打算动手做实验,记录一下对该项目的研究,供给各位终端安全爱好者,希望能帮助大家更好的理解栈,未来做出更精彩的技术对抗。
接下来我会先拆解几个小部分循序渐进的学习。
比如下图是一个简单的系统调用,这个大家是一定能看懂的。
在反汇编中,调用函数使用call指令,如下图所示在call之前做了一些给寄存器赋值和给栈赋值的一些操作,那么这个就引出了下下图的概念,前四个参数给了寄存器,之后的参数放在栈上。
此外, call 指令把下一条指令地址压栈,之后进入函数执行之后结束的地方ret 指令稍后会检索这个保存的地址,以便程序可以返回到上次执行的位置,如下图下一条就是18D9,在ret是做了一个pop,直接rip变为当前rsp的值。
这是一个函数我们对他的理解,那么函数里面嵌套一个函数呢,也是同样继续call和ret打配合,但最终都是堆栈平衡的,这部分的话,在学习滴水逆向课程的时候,通过画图能更好的理解,2018年的课程,但是依旧强推。
一个正常调用函数时
我们使用直接调用,直接使用asm文件,里面写好eax,然后直接syscall过去(这边补一下系统调用的原理,X64使用syscall调用参数rax值的函数,路线就是kernelbase->ntdll,然后找对应的mov eax,xx syscall,建议调试一遍即可明白),此时栈的情况
jmp到ntdll地址里面进行执行syscall
那么这里其实引出痛点,如何绕过hook的监控,并且堆栈也是正常的呢,也引出需要学习的东西
1.这个栈回溯到底是什么
2.edr如何做到监控到syscall执行的
简单说一下,其实这部分也是我学习的见解,可能也有一点问题,我输出自己的看法,首先就是离我们之前介绍调用约定,提到了子函数,其实就是继续调用别的函数,这样的话,流程是一样的,继续call,然后下一个rip地址压栈,然后进入函数时候sub栈,就是从高地址往低地址越来越高的栈,那么在这里我们可以看到,返回地址在哪里放着,是不是就是在sub rsp的时候,返回地址==rsp,如果sub rsp,0x20,那么返回地址==sub后rsp+0x20
那么这是在函数中的情况,也就是说这个message使用的栈的size就是0x38,如果现在有一个东西,我本来就提前知道message函数需要的栈的size是0x38,然后我还可以根据当前状态跟上下文作比较,比如上图rip在sub rsp,0x38下面,那么我就知道该函数对栈产生了变化,要知道返回地址的话,就要把sub删除的补回来,也就是要补0x38就拿到返回地址。
这里留一个问题,如果是我们当前rip之前还有一个push rax,的操作呢。如果push压栈了,rsp又小一行,为了保证返回地址,那么是不是还要加一个0x8。
那么上面引出了这个东西,windows系统里面有吗,我们不想自己做啊,有的兄弟有的。AI说明如下
1 | wind(缠绕)= 函数调用的正过程:每进一层函数,push 这 push 那、sub rsp——把栈一层层缠起来。 unwind(解开)= 倒放电影:从当前停留的这层出发,按说明书算出“返回地址在哪、上一层从哪开始”,跳到上一层,再查上一层的说明书,继续解……直到栈底。 |
实际写个简单脚本进行unwind的话,然后加一些说明,我在简单说一下,意思就是pdata/xdata里面已经给我们存好了一些基础信息了,比如反汇编中的push,sub等等各种命令,都有记录,图中所说的序言就是存放在异常目录中的数据,然后根据地址做比对,看是否执行了这些会影响栈的动作,然后该补就补,图中所说是撤销,就这么一直回溯就完事了,是不是这么说还挺简单的。
基础其实我们学的差不多了,到了真正对抗博弈的时候了,moonwalk怎么利用这些东西玩的这么花的,一句话说就是moonwalk在汇编执行代码的时候,就已经开辟一个大栈,里面存放很多很多东西,精准的构造了一个完美的rop链,并且比较考验数学计算工地,人工构造的话我时常算错,更恐怖的是这位大佬使用动态填写信息让构造栈达到恐怖的平衡,真正数学高手,利用这些基础原理,搞出这么有趣的东西,当我看到的时候其实想狠狠深入他的大脑皮层了。
这里我们可以先说出他使用的关键手法
1.利用栈回溯的时候,遇到如下两种这种类型的,会对RBP做手脚,一个是直接把当前栈的值给了rbp,另一个就会使用rbp去反推RSP的值,那么就是一个可控的变量,我们如果在栈上构造,让展开器首先进去设置rbp的地方,然后他计算出返回值,然后进入使用RBP计算出的RSP的栈,这样就合理了,然后让展开器使用这个rsp进行计算出的返回值为main进程所在线程返回值,然后就进入到开头的线程初始化栈的那些函数,栈就完整了。
2.如何确定RBP的值,并且RBP应该放在哪里。最终计算出返回地址肯定是要在main函数的返回地址槽,那么在F1的话,就是要进行set fpreg的这个阶段,展开器计算的时刚开始是,rsp=rbp-0x20,然后补了很多其他杂七杂八的,那么最后就是返回地址=rbp-0x20+0xF8,那么想要返回地址等于main函数返回地址,那么rbp就要等于(main函数返回地址-0xD8),这样RSP==main函数返回地址了。RBP应该放在哪里呢,是应该放在set fpreg这个阶段上面那个阶段,就是push nonyol那个阶段,也就是F2,我们看到F2是在栈底的时候才进行了Push RBP,那么就是这个栈上的地址给了RBP,也就是刚进入F2的时候,计算返回地址=rsp+0x148,然后直接进入F1的,然后F1拿着RBP就进行使用了,所以要保证F2的PUSH RBP是最后被展开器进行直接把栈上数据装进RBP,那么就是在F2的时候RSP+0x140.
3,为什么控制流和展开器不一样。罪魁之首JMP RBX,正常执行这个的时候,是直接跳走了啊,但是展开器,看到这个,首先确认你在哪个模块中的哪个函数,找了半天pdata/xdata数据,然后计算返回值,就被我们栈上的一些诱导过去走向rop链条了。
4.如何做到动态堆栈加密的。每次启动都会去find这些可以用的链条,然后把需要用的一些参数给到结构体,然后汇编调用结构体里面的数据,进行使用,RCX就是结构体。然后看这些结构体参数都是有用的,G1和G2好理解一些,就是进行ADD或者JMP的,只有F1和F2,在RBP上做了手脚,把RBP放到了固定的位置,然后就是栈帧大小的话,我觉得这个无所谓,不需要在意,因为我们是在最外层函数的话,这些图中的函数肯定该运行的都运行完了,不用在意里面栈有什么变化要计算,所以直接理解为制作一个假栈,但是这些函数地址用的是真的函数地址