利用APC队列隐藏线程

admin 2023年1月7日12:55:02评论38 views字数 2787阅读9分17秒阅读模式

一. 风起

先来看一段信标加载的代码

利用APC队列隐藏线程

这份代码通过加载文件形式的payload上线,看起来并没有什么问题,而且分离式免杀效果也还可以,但是当面对众多AV软件时,在运行时还是有可能会被pass掉。

根据我为数不多的知识,得出如下几点结论:

  1. 可能是读文件的时候被 minifilter 搜索到特征码了

  2. 可能是分配内存的函数被 Hook 了

  3. 创建线程被监控了

可是我的payload明明经过 shikata_ga_nai编码器混淆过,怎么就被扫到特征了,怎么肥四,不可能!!!

那Hook呢?

你仔细看代码,我明明在第一时间重映射了 ntdll 模块,Hook 不存在的!!!

那线程监控呢?

二. 知己知彼

经过我的一番思考

我们来模拟一波AV软件的操作,尝试写一个驱动程序,来检测系统中的所有线程创建,看看能否正确识别出beacon线程的创建。


首先我们对 beacon 的 shellcode 进行分析,得出如下3个特征:

  1. beacon 的地址空间都是动态申请的内存,正常代码的地址空间在某个模块中。

  2. beacon 首部有一个 cld 指令

  3. beacon 内存属性为 RWX ,第一阶段内存大小为 0x1000,第二阶段内存大小为 0x84000


有了以上逻辑,我们开始写代码,先来查看一下设置系统线程回调PsSetCreateThreadNotifyRoutine 函数,参数是一个回调函数地址,当系统中线程创建时或退出时会触发回调。回调函数第一个参数为进程ID,第二个参数为线程ID,第三个参数为线程创建或线程退出。

MSDN定义如下:

VOID(*PCREATE_THREAD_NOTIFY_ROUTINE) (    IN HANDLE  ProcessId,    IN HANDLE  ThreadId,    IN BOOLEAN  Create    );
NTSTATUS PsSetCreateThreadNotifyRoutine( [in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine);

这里只需要获取创建线程回调,所以直接 !Create return 掉线程退出的通知。

利用APC队列隐藏线程

我们需要判断线程创建地址是否在模块内,来检测线程地址是否属于 shellcode 代码。

那么我们需要先获取创建线程的起始地址,这个地址在回调参数中是没有的,我们可以通过windbgdt _ETHREAD 命令查看 ETHREAD结构, 发现 Win32StartAddress 字段就是我们需要的属性。


利用APC队列隐藏线程

通过线程ID读取线程起始地址

利用APC队列隐藏线程

通过进程ID获取 Peb 指针

利用APC队列隐藏线程

遍历 LDR 链表,来判断当前线程起始地址是否在模块内。

利用APC队列隐藏线程

此时我们随便运行一段 shellcode 测试一下,无模块的 shellcode 运行已经被识别了。

利用APC队列隐藏线程

接下来需要判断 shellcode 首部的汇编指令是否存在 beacon 特征,这里仅仅是简单判断特征码,仅作为演示逻辑使用,如果要想完美检测,那就是AV软件需要干的事了。

利用APC队列隐藏线程

此时我们加载payload测试,结果如下

利用APC队列隐藏线程

可以看到CS上的PID与上面输出一致,也是 5756

利用APC队列隐藏线程

so,难怪说之前死活上线不了,它指定是在beacon线程头部写了个 ret,导致线程直接退出了。

这关如何过?

莫急,各位师傅拿好小板凳,且听我慢慢道来。

三. 暗度陈仓

面对这种不讲武德的操作,是不是只需要将线程地址改为正常模块内就可以绕过了,就比如说:创建一个线程,指定线程入口点为 MessageBoxA ,且以暂停的方式启动,这样线程起始地址在 User32模块 内,线程回调肯定就扫不到了啊。

利用APC队列隐藏线程

那这样 shellcode 又该如何运行呢,我们需要执行payload 啊

是时候请我们的主角出场了,没错就是:APC(Asyncroneus Procedure Call,异步过程调用)

先来看一下 QueueUserAPC 函数,MSDN上的说明为:将用户模式 (APC) 对象添加到指定线程的 APC 队列。

DWORD QueueUserAPC(  [in] PAPCFUNC  pfnAPC,  [in] HANDLE    hThread,  [in] ULONG_PTR dwData);

根据文档 QueueUserAPC 函数第一个参数是apc 函数的指针,第二个参数是一个线程句柄(需要具有THREAD_SET_CONTEXT访问权限),第三个参数是传递给APC函数的参数。

NtContinue 属于未公开的函数,这个函数的功能就是跳转到指定context执行。

NTSTATUS NTAPI NtContinue(    PCONTEXT Context,     BOOL TestAlert);

fastcall 调用约定

微软x64调用约定使用RCX, RDX, R8, R9这四个寄存器传递头四个整型或指针变量(从左到右),使用XMM0, XMM1, XMM2, XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。少于64位的参数并没有做零扩展,此时高位数随机。在Windows x64环境下编译代码时,只有一种调用约定----就是上面描述的约定,也就是说,32位下的各种约定在64位下统一成一种了。在微软x64调用约定中,调用者的一个职责是在调用函数之前(无论实际的传参使用多大空间),在栈上的函数返回地址之上(靠近栈顶)分配一个32字节的“影子空间”;并且在调用结束后从栈上弹掉此空间。影子空间是用来给RCX, RDX, R8和R9提供保存值的空间,即使是对于少于四个参数的函数也要分配这32个字节。

利用APC队列隐藏线程

QueueUserAPCNtContinue 配合使用时效果就会比较明显了,先获取上面线程的 context 并且 copy 3份,分别执行不同的功能

利用APC队列隐藏线程

根据上面的知识,构造一份NtProtectVirtualMemory 函数的X64调用栈,将返回地址设置为NtTestAlert,调用 QueueUserAPC 将APC插入到线程APC队列中,APC回调地址为 NtContinue 参数为 context ,当APC执行时会调用 NtContinue 执行指定 context , 当 NtContinue 执行完毕时,返回到NtTestAlert 继续触发下一个APC执行。

利用APC队列隐藏线程

第二个APC ,调用 GetGadget 函数在NTDLL的代码段中查找 jmp [rsi] 指令,将shellcode 设置到 Rsi ,将RIP设置为 jmp 指令,完成shellcode 调用。

利用APC队列隐藏线程

最后一个APC调用 RtlExitUserThread 退出当前线程。

利用APC队列隐藏线程

最后调用 ResumeThread 函数恢复线程执行,WaitForSingleObject 触发APC调用

利用APC队列隐藏线程

经过APC配合NtContinue的时空轮转,线程入口的 MessageBoxA 并没执行,而是优先执行了3个APC,然后线程就退出了。

利用APC队列隐藏线程

此时我们重新测试,可有看到线程回调扫到的是模块内地址,但实际执行的确是payload,所以我们这一条阴阳线程就成功执行了。

利用APC队列隐藏线程

利用APC队列隐藏线程


原文始发于微信公众号(刨洞技术交流):利用APC队列隐藏线程

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月7日12:55:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用APC队列隐藏线程https://cn-sec.com/archives/1504159.html

发表评论

匿名网友 填写信息