本文所载网络安全技术仅为技术研究与学习之用,严禁用于非法攻击、侵入他人系统等违法违规行为。所有内容均需遵守国家相关法律法规,滥用所产生的一切法律责任,由行为人自行承担,与本文作者、发布者无关。
黑客外传之 shellcode 传奇堂堂复活了,本集来说一下反调试这方面,属于 “对抗技术” 的范畴
鄙人技术浅陋,经网上四处观看浏览总结得此文,如有不足希望师傅们多包容⊙﹏⊙
下面正文开始喵
前言
稍微扯一点(
其实我对逆向的这方面就是一坨,因为毕竟是属于技术对抗嘛,可能同样的反调试能困住一些新手逆向师傅,但是一些高级的大手子就不太行了,不能有效的防止我们的样本被分析
成功的反调试(包括之前的反沙箱)都是有效防止我们的样本被分析、研究,有效的延长我们🐎的生命周期
当然其实是有手段能够做到无文件残留的 c2 上线,之后有机会的话找一集说一下,对抗技术也是红队成员想要进一步突破瓶颈拉高自身评级的关键技术点,能够完全免杀完全不残留文件和痕迹的 c2 上线,也算是迈入了 apt 的(算是吧
最后还是那句话,本人技术浅陋,文章的不足之处请师傅们多多包涵
逆向与调试/反调试
这里不是来专门讲逆向,只是做一个基础介绍(
所谓逆向,都是通过 “调试技术” 对程序进行动态的分析诸如内存堆栈api调用等行为,所以我们写代码,就会有“反调试技术”,对抗因素由此产生
对于普通的软件开发,反调试可以保护我们的知识产权不被滥用,保护合法权益;
对于红队技术人员,反调试可以保护我们的恶意程序,让其不被防守方安服团队以及厂商研究人员分析我们的技术手法,使我们的 bypass 技巧长久有效
可以粗略的分为两种情况,静态以及动态:
静态
静态的反调试技术包括加壳和混淆
加壳
很形象的字面意思,壳和原始 exe 的关系很类似 loader 和 shellcode 的关系
当一个程序加了壳之后,运行时先启动的是壳,之后再将原始的 exe 解密加载入内存,比较经典的壳会有这些特征行为:
一个典型的 “壳” 会做这些:
- 压缩 把原本很大的 exe 变小,方便下载、传输。
- 加密 把原始的代码、数据全部加密,直接用 IDA 、反汇编工具打开的话会只能看到乱码
- 加一层启动器(外壳程序) 你双击运行时
- 先运行壳代码
- 壳在内存里解密、解压出真正的程序
- 再跳过去执行真正的 exe
- 反调试、反逆向 阻止调试器(如 OllyDbg、x64dbg、IDA) attach
但是值得一说的是,加壳虽然可以算是一手不错的反调试,但是 “有壳” 这个特征本身就会加大杀软对程序的 “怀疑”,需要根据情况来灵活使用
混淆
混淆就很好理解了
将原本很好读的一些函数名,比如把创建线程申请内存的行为封装成 loadShellcode 函数,混淆的话就是先把名字做成 Gu9c7cveI8R6VI 这种意义不明的
改名其实还不够(甚至说用处很小)真正的混淆其实是打乱代码逻辑,能够让分析人员看的晕头转向
就比如刚刚说的这个申请内存加载线程的行为,不直接这么做,先申请一段内存,然后突然间转到一个别的其实并没有用的函数,对他写点啥,然后改点啥然后读一下传个参什么的,最后再在一个别的地方读取shellcode覆盖上去(甚至可以再分两次写入),让整个程序的行为很复杂很多,一眼看去不像是在加载 c2
整体的战略头脑很像草船借箭,看似是要攻打你,最后不敌而撤退,实际上是来拿你箭的,当年要是不在撤退的时候告诉一声的话,可能曹丞相都不知道自己中l 草船借箭之计了(
总之就是混淆的意义就是让代码功能混乱,行为轨迹混乱,看不懂就对了
动态
也就是本文重点要说的,真正的“反调试”,如果被调试直接自杀,报错,程序完全不运行直接退出,防止自身被分析
正式引出核心议题:“检测自己是否被调试 或者有没有调试器存在”
下面另起一小节正式开始喵
动态反调试
Windows API – isDebuggerPresent
这个 API 是属于 kernel32.dll 库的,核心作用:
检测当前进程是否正在被调试器(比如 Visual Studio 调试器、x64dbg、OllyDbg 等)附加调试
函数原型:
#include <windows.h>
BOOL IsDebuggerPresent(void);
可以看到,返回值是 BOOL 类型,并且不需要传入参数 ,可以直接调用
下面是一个调用的 demo:
#include <windows.h>
#include <stdio.h>
int main() {
// 调用 IsDebuggerPresent 检测调试器
if (IsDebuggerPresent()) {
printf("检测到调试器正在附加当前进程!\n");
} else {
printf("未检测到调试器附加\n");
}
system("pause");
return 0;
}
实际添加到我们的 loader 里面使用的话可以直接套个 if 用:
if (IsDebuggerPresent()) {
return 0;
}
提一句,这个 API 仅限 XP 以及之后的版本可以用,不过应该很少有比 XP 还老的了(
缺点:最基本的检测 API,很容易被 hook 的
TEB->PEB->BeingDebugged
上述手法的进阶一层操作
从 TEB 到 PEB 的这块内容在 shellcode 传奇的第 (2) 集说过了,此处不展开过多
PEB 里有一个成员 BeingDebugged,作用就是标记当前进程是否正在被调试器附加:
- 如果进程正在被调试,这个值为 1,反之则为 0
上面说的 API :isDebuggerPresent 其实就是通过读取这个值来实现的(
如果我们直接读取这个值,相当于手动实现了一下这个 API 的功能,就能成功 bypass 掉这个 API 的 hook
isDebuggerPresent 的汇编实现(32位 Windows 系统):
mov eax, dword ptr fs:[00000018h] ; 从 FS:[0x18] 读取 TEB 地址
mov eax, dword ptr [eax+30h] ; 从 TEB+0x30 读取 PEB 地址
movzx eax, byte ptr [eax+2] ; 从 PEB+0x2 读取 BeingDebugged 字节
ret ; 返回结果
64 位Windows系统的区别:GS 段寄存器代替了 FS 指向 TEB(对编写loader的影响不大),至于内部的偏移量都是一样喵
既然说到了 hook,那为什么需要绕过 hook 呢?
ps:其实前些天就想挑一节来讲 hook 了,但是一直想不出来写点什么,能想到的素材内容不太能撑起来一节笔记的量(逃
大概说就是如果这个 API 被下了 hook,就相当于被布置了陷阱,去调用的线程就会触发陷阱,没有进入到原本的 API ,而是进入 hook 的执行逻辑,调试人员的调试器如果对这个 API 打了 hook,当程序去调用,中招,随后由 hook 直接返回 0 值,欺骗线程这里没有调试器,从而绕过我们的反调试
直接读取这个 beingDebugged 的值就可以绕过这种 hook
写个 cpp 的 demo:
BOOL CheckBeingDebugged()
{
BOOL isDebugged = FALSE;
#if defined(_WIN64) // 64位系统
__asm {
mov rax, gs:[0x18]
mov rax, [rax + 0x30]
mov al, [rax + 0x2]
mov isDebugged, al
}
#elif defined(_WIN32) // 32位系统
__asm {
mov eax, fs:[0x18]
mov eax, [eax + 0x30]
movzx eax, byte ptr [eax + 0x2]
mov isDebugged, eax
}
#endif
return isDebugged;
}
//通过NtTerminateProcess隐蔽退出(32/64位适配)
VOID HiddenExit(UINT exitCode = 0)
{
#if defined(_WIN64) // 64位Windows
__asm {
// 64位下调用NtTerminateProcess(通过SSDT/系统调用号)
mov r10, rcx // 64位系统调用要求参数放入r10
mov eax, 0x00000026 // NtTerminateProcess的系统调用号
mov rcx, (UINT_PTR)-1 // 参数1:ProcessHandle = -1(当前进程)
mov rdx, exitCode // 参数2:ExitStatus = 退出码
syscall // 触发64位系统调用
ret
}
#elif defined(_WIN32) // 32位Windows
__asm {
// 32位下调用NtTerminateProcess
mov eax, 0x00000018 // NtTerminateProcess的系统调用号(32位固定)
mov ebx, (UINT_PTR)-1 // 参数1:ProcessHandle = -1(当前进程)
mov ecx, exitCode // 参数2:ExitStatus = 退出码
int 0x2e // 触发32位系统调用
ret
}
#endif
}
//反调试逻辑
VOID AntiDebugLoader()
{
if (CheckBeingDebugged())
{
HiddenExit(0);
}
}
int main()
{
AntiDebugLoader();
return 0;
}
写的蛮清楚的,不再解释了
内容已经不少了喵,这集先到这里吧,作为反调试内容的第一部分
