
作者:Bomb
起因
免杀这个东西,做到后面你会发现常规路子基本都死了。
CreateRemoteThread?EDR盯着呢。QueueUserAPC?行为检测等着你。SetWindowsHookEx?更别想了,这玩意儿被盯了十几年了。
所以安全圈开始往异常处理这条路走。思路很简单:不走常规API,故意搞一个异常出来,让系统调用你注册的异常处理器,在处理器里执行payload。EDR只盯着那些敏感API调用,你根本没调,它拿你没办法。
VEH,Vectored Exception Handler,就是这条路的主角。全局注册,不跟栈帧绑定,理论上很优雅。
理论上
handler和stub
先说原理,不啰嗦。
VEH注入就两个东西。一个叫handler,一个叫stub。
handler是你雇的保镖。你把他安排好了,他就站在那等。一旦出事——也就是异常发生了——他第一个冲上去。处理完了跟你说没事了,你就继续干你的。
stub是那个故意摔跤的人。他先雇好保镖,注册handler,然后故意搞一个异常出来。保镖冲上来处理,处理完他拍拍灰站起来继续走。
就这么简单
我当时也是这么理解的。然后我就开始写了。

handler是保镖,stub是那个故意摔跤的人
偏移计算,人类的敌人
shellcode写好之后,有些位置需要运行时动态修改。函数地址啊、参数啊这些,写代码的时候还不知道,得跑起来再填。
最直接的办法:算好偏移,直接写过去。
我开始手算。
然后发现x64指令编码是个深渊。
你以为某个指令的机器码是这个,反汇编一看是那个。你以为偏移0x88是正的,CPU说不好意思这是-120。你改了一个字节,后面所有偏移全乱。
从v1到v5,我一直在跟偏移较劲。每次觉得这次算对了,一跑就崩。改,跑,崩。改,跑,崩。
五个版本
最后我认输了。
改用哨兵值方案——在shellcode里埋一个特殊标记,运行时扫到就替换成实际数据。就像在地图上插旗子,你不需要知道具体坐标,认得旗子就行。
从此偏移问题消失。
五个版本换来的教训:能自动化的别手算。人类不是为这种活设计的。

v1到v5,与偏移计算的残酷損失
那个该死的返回值
偏移搞定之后我信心满满。
v6,跑了。handler被调用了。payload执行了。然后进程死了。
进程保护?没有保护。那是什么问题?
v7,改了点东西,崩。v8,换个思路,崩。v9,v10,v11......全崩。
我开始怀疑是不是Windows更新改了什么行为。甚至想过是不是目标环境有问题。
从v6到v15,我在各种方向上排查。每改一个地方就编译一次,每次都满怀希望,每次都进程崩溃。
最后发现,handler返回了0
0是什么意思?EXCEPTION_CONTINUE_SEARCH。翻译成人话就是“我没处理这个异常,帮我找下一个人”。
系统说好我找。找了一圈没人处理。那就杀了吧。
而正确答案是-1。EXCEPTION_CONTINUE_EXECUTION。“我处理好了,继续。”

一个0和一个-1,一个让进程活着,一个让进程去死
这是Windows异常处理入门第一页的东西。第一页。我翻到了第二页、第三页、第十页,唯独没看第一页。
v1到v27,十几个版本,一整天,败给了一个常量值。
说实话当时真的想砸键盘。
APC上下文,异常的禁区
返回值修好之后,下一个问题:怎么触发异常?
stub需要触发一个异常让handler被调用。常见的方式有几种,我选了一种,跑了,崩了。
换一种,崩了。
再换一种,活了
事后分析才知道,前两种方式在APC上下文中不能被VEH捕获。你的stub是通过APC注入到目标线程执行的,在这个上下文里,有些异常触发方式就是不管用,VEH看不见。
不是方式不对,是场景不对。
这个坑文档里不会告诉你。教程里不会提到。只有你亲自在APC上下文里跑一遍才会知道。
v27
v27通了。
那天晚上我看着屏幕上success的输出,没有兴奋,只有疲惫。十几个版本,一整天,最终败给了一个返回值写错。
但回头看,这些坑踩得也不算白踩。哨兵值方案是我最终留下来最有价值的东西,它彻底解决了偏移问题。APC上下文的限制让我对Windows异常分发机制有了更深的理解。handler返回值这个教训,我这辈子不会再犯。
如果你正在研究VEH注入,希望这篇文章能帮你省掉那一天。

从v1到v27,一条用代码铺成的路
记住三件事
1. handler返回-1,不是0。
2. APC上下文里不是所有异常都能被VEH捕获。
3. 能自动化的别手算。
就这三句,值一整天。你不用谢我,去写你的v1吧。
本文分享自 Ms08067安全实验室 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!