三探堆栈欺骗之Custom Call Stacks

news/2025/1/20 16:31:23/文章来源:https://www.cnblogs.com/fdxsec/p/18234881

本文首发阿里云先知社区:https://xz.aliyun.com/t/14592

背景知识

在之前的文章中,我们介绍了静态欺骗和动态欺骗堆栈,今天我们来一起学习一下另一种技术,它被它的作者称为Custom Call Stacks,即自定义堆栈调用。
关于堆栈欺骗的背景我们就不再说了,这里我们补充一下回调函数和 windows 线程池的知识。
回调函数是指向函数的指针,该函数可以传递给要在其中执行的其他函数,在常规的 shellcode loader 中回调函数也是一种常见的执行方式,并且 github 上有仓库详细的记录了各种各样的回调函数执行 shellcode:https://github.com/aahmad097/AlternativeShellcodeExec。
但是这种执行回调的方式都存在一个问题,回调方和调用方位于同一个线程中,假设当我们通过回调LoadLibrary 时,执行此时的堆栈就像是这样LoadLibrary returns to -> Callback Function returns to -> RX region,RX region 指的是我们的 shellcode 地址,所以我们的 shellcode 的内存空间很容易被发现。
为了解决这个问题,我们要用到 windows 线程池,官方介绍如下:Windows线程池是一种操作系统提供的机制,用于管理和调度多个工作线程,以提高多线程应用程序的性能和效率。线程池通过重用现有的线程来执行任务,避免了频繁创建和销毁线程的开销,从而提升系统资源利用率和应用程序的响应速度。
其实就是提前给我们创建好了很多线程,让我们可以方便的进行调度,当有任务需要执行时,我们提交给线程池就可以了。

参数如何传递

下面是一个小 demo

#include <windows.h>
#include <stdio.h>int main() {CHAR *libName = "wininet.dll";PTP_WORK WorkReturn = NULL;TpAllocWork(&WorkReturn, LoadLibraryA, libName, NULL); // pass `LoadLibraryA` as a callback to TpAllocWorkTpPostWork(WorkReturn);                                // request Allocated Worker Thread ExecutionTpReleaseWork(WorkReturn);                             // worker thread cleanupWaitForSingleObject((HANDLE)-1, 1000);printf("hWininet: %p\n", GetModuleHandleA(libName)); //check if library is loadedreturn 0;
}

让 gpt 来帮我们解释一下代码:

  1. TpAllocWork(&WorkReturn, LoadLibraryA, libName, NULL);:这行代码使用了TpAllocWork函数来分配一个工作项,将LoadLibraryA函数作为回调函数,以异步的方式加载指定名称的动态链接库。LoadLibraryA是用于加载ANSI字符串(即CHAR类型)的动态链接库函数。
  2. TpPostWork(WorkReturn);:这行代码将分配的工作项提交给线程池,请求线程池中的工作线程执行加载库的任务。
  3. TpReleaseWork(WorkReturn);:这行代码释放了分配的工作项,进行了工作线程的清理操作。

可以看到,通过这种方式确实帮助我们在一个新的线程执行了 LoadLibraryA 函数,但是能不能成功执行呢?
如果编译上面的代码,那么代码将会崩溃,因为他的参数传递并不正确。
TpAllocWork 的定义是:

NTSTATUS NTAPI TpAllocWork(PTP_WORK* ptpWrk,PTP_WORK_CALLBACK pfnwkCallback,PVOID OptionalArg,PTP_CALLBACK_ENVIRON CallbackEnvironment
);

这意味着我们的回调函数 LoadLibraryA 应该是 PTP_WORK_CALLBACK 类型。此类型扩展为:

VOID CALLBACK WorkCallback(
PTP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_WORK Work
);

从上图中可以看出,我们的 OptionalArg作为辅助参数转发到我们的 Callback ( PVOID Context )。因此,如果我们的假设是正确的,那么我们传递给 TpAllocWork 的参数 libName (wininet.dll) 最终将作为我们 LoadLibraryA 的第二个参数。在 x64dbg 中检查此项会导致下图:
image.png
还记得上篇文章中关于 64 位下传递参数的规则吗,rcx 应该存第一个参数,rdx 中应该存第二个参数。

直接在WorkCallback 中执行

但是不要放弃,还是有希望的,我们直接在上面的WorkCallback 中让它执行就可以了,如下面的代码所示:

#include <windows.h>
#include <stdio.h>VOID CALLBACK WorkCallback(
_Inout_     PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID                 Context,
_Inout_     PTP_WORK              Work
) {LoadLibraryA(Context);
}int main() {CHAR *libName = "wininet.dll";PTP_WORK WorkReturn = NULL;TpAllocWork(&WorkReturn, WorkerCallback, libName, NULL); // pass `LoadLibraryA` as a callback to TpAllocWorkTpPostWork(WorkReturn);                                // request Allocated Worker Thread ExecutionTpReleaseWork(WorkReturn);                             // worker thread cleanupWaitForSingleObject((HANDLE)-1, 1000);printf("hWininet: %p\n", GetModuleHandleA(libName)); //check if library is loadedreturn 0;
}

但是这样的话,回调相当于在我们 shellcode 的内存区域执行的,我们的堆栈变成了LoadLibraryA returns to -> Callback in RX Region returns to -> RtlUserThreadStart -> TpPostWork,这样并不好,因为归根结底还是出现了 shellcode 的内存区域。

借助汇编跳转执行

但是不要放弃,我们还有别的机会来进行尝试,我们可以通过汇编来帮助我们调整堆栈结构,只需要在汇编里面 mov rdx,rcx,再 jmp 到 LoadLibraryA 的位置就可以了,注意这里是 jmp 而不是 call,如果 call 的话我们会先将此时的地址压入堆栈,再去执行,这样堆栈中还是会出现 shellcode 的内存区域,但是我们 jmp 的话,就直接过去了,我们的汇编函数并没有在堆栈留下任何痕迹,这也是一个小技巧。
代码如下:

#include <windows.h>
#include <stdio.h>typedef NTSTATUS (NTAPI* TPALLOCWORK)(PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment);
typedef VOID (NTAPI* TPPOSTWORK)(PTP_WORK);
typedef VOID (NTAPI* TPRELEASEWORK)(PTP_WORK);FARPROC pLoadLibraryA;UINT_PTR getLoadLibraryA() {return (UINT_PTR)pLoadLibraryA;
}extern VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);int main() {pLoadLibraryA = GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA");FARPROC pTpAllocWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpAllocWork");FARPROC pTpPostWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpPostWork");FARPROC pTpReleaseWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpReleaseWork");CHAR *libName = "wininet.dll";PTP_WORK WorkReturn = NULL;((TPALLOCWORK)pTpAllocWork)(&WorkReturn, (PTP_WORK_CALLBACK)WorkCallback, libName, NULL);((TPPOSTWORK)pTpPostWork)(WorkReturn);((TPRELEASEWORK)pTpReleaseWork)(WorkReturn);WaitForSingleObject((HANDLE)-1, 0x1000);printf("hWininet: %p\n", GetModuleHandleA(libName));return 0;
}

我们的汇编函数如下:

section .textextern getLoadLibraryAglobal WorkCallbackWorkCallback:mov rcx, rdxxor rdx, rdxcall getLoadLibraryAjmp rax

触发回调时执行WorkCallback 函数,然后在WorkCallback 我们手动调整参数位置,然后call getLoadLibraryA,获得LoadLibraryA 的内存地址,然后直接 jmp 过去,这就是我们所完成的事情。
现在看一下我们的堆栈,十分完美:
image.png

多参数调用

现在我们要考虑一些其他的问题了,比如参数个数,如果参数个数超过 4 个我们是要存放在堆栈中的,以NtAllocateVirtualMemory 为例,它的定义是:

__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory([in]      HANDLE    ProcessHandle,[in, out] PVOID     *BaseAddress,[in]      ULONG_PTR ZeroBits,[in, out] PSIZE_T   RegionSize,[in]      ULONG     AllocationType,[in]      ULONG     Protect
);

我们现在需要将 NtAllocateVirtualMemory 的指针及其结构内的参数传递给回调,以便我们的回调可以从结构中提取这些信息并执行它。忽略掉 ZeroBits (值恒为 0)和 AllocationType(值为0x3000),我们可以得到一个新的结构体,定义如下

typedef struct _NTALLOCATEVIRTUALMEMORY_ARGS {UINT_PTR pNtAllocateVirtualMemory;   // pointer to NtAllocateVirtualMemory - raxHANDLE hProcess;                     // HANDLE ProcessHandle - rcxPVOID* address;                      // PVOID *BaseAddress - rdx; ULONG_PTR ZeroBits - 0 - r8PSIZE_T size;                        // PSIZE_T RegionSize - r9; ULONG AllocationType - MEM_RESERVE|MEM_COMMIT = 3000 - stack pointerULONG permissions;                   // ULONG Protect - PAGE_EXECUTE_READ - 0x20 - stack pointer
} NTALLOCATEVIRTUALMEMORY_ARGS, *PNTALLOCATEVIRTUALMEMORY_ARGS;

然后我们的代码和上面也差不多

#include <windows.h>
#include <stdio.h>typedef NTSTATUS (NTAPI* TPALLOCWORK)(PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment);
typedef VOID (NTAPI* TPPOSTWORK)(PTP_WORK);
typedef VOID (NTAPI* TPRELEASEWORK)(PTP_WORK);typedef struct _NTALLOCATEVIRTUALMEMORY_ARGS {
UINT_PTR pNtAllocateVirtualMemory;   // pointer to NtAllocateVirtualMemory - rax
HANDLE hProcess;                     // HANDLE ProcessHandle - rcx
PVOID* address;                      // PVOID *BaseAddress - rdx; ULONG_PTR ZeroBits - 0 - r8
PSIZE_T size;                        // PSIZE_T RegionSize - r9; ULONG AllocationType - MEM_RESERVE|MEM_COMMIT = 3000 - stack pointer
ULONG permissions;                   // ULONG Protect - PAGE_EXECUTE_READ - 0x20 - stack pointer
} NTALLOCATEVIRTUALMEMORY_ARGS, *PNTALLOCATEVIRTUALMEMORY_ARGS;extern VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);int main() {LPVOID allocatedAddress = NULL;SIZE_T allocatedsize = 0x1000;NTALLOCATEVIRTUALMEMORY_ARGS ntAllocateVirtualMemoryArgs = { 0 };ntAllocateVirtualMemoryArgs.pNtAllocateVirtualMemory = (UINT_PTR) GetProcAddress(GetModuleHandleA("ntdll"), "NtAllocateVirtualMemory");ntAllocateVirtualMemoryArgs.hProcess = (HANDLE)-1;ntAllocateVirtualMemoryArgs.address = &allocatedAddress;ntAllocateVirtualMemoryArgs.size = &allocatedsize;ntAllocateVirtualMemoryArgs.permissions = PAGE_EXECUTE_READ;FARPROC pTpAllocWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpAllocWork");FARPROC pTpPostWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpPostWork");FARPROC pTpReleaseWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpReleaseWork");PTP_WORK WorkReturn = NULL;((TPALLOCWORK)pTpAllocWork)(&WorkReturn, (PTP_WORK_CALLBACK)WorkCallback, &ntAllocateVirtualMemoryArgs, NULL);((TPPOSTWORK)pTpPostWork)(WorkReturn);((TPRELEASEWORK)pTpReleaseWork)(WorkReturn);WaitForSingleObject((HANDLE)-1, 0x1000);printf("allocatedAddress: %p\n", allocatedAddress);getchar();return 0;
}

重点是我们汇编传递参数的部分参数,调用回调函数 WorkCallback 时,我们的堆栈顶部是 TppWorkpExecuteCallback 的返回值。

如果在的堆栈顶部修改返回地址,并向其添加参数,则整个堆栈帧将发生混乱,从而导致 WorkCallback 函数无法正常返回。因此,我们必须在不更改堆栈帧本身的情况下修改堆栈。因此我们只能直接修改堆栈的值,TppWorkpExecuteCallback 的堆栈是可以容下我们参数所需要的栈的,下面是作者给的汇编代码:

section .textglobal WorkCallbackWorkCallback:mov rbx, rdx                ; backing up the struct as we are going to stomp rdxmov rax, [rbx]              ; NtAllocateVirtualMemorymov rcx, [rbx + 0x8]        ; HANDLE ProcessHandlemov rdx, [rbx + 0x10]       ; PVOID *BaseAddressxor r8, r8                  ; ULONG_PTR ZeroBitsmov r9, [rbx + 0x18]        ; PSIZE_T RegionSizemov r10, [rbx + 0x20]       ; ULONG Protectmov [rsp+0x30], r10         ; stack pointer for 6th argmov r10, 0x3000             ; ULONG AllocationTypemov [rsp+0x28], r10         ; stack pointer for 5th argjmp rax

堆栈也是非常干净

总结

当然还有其他的利用方式,这里也不再一一列举,我们还需要思考的问题是除了TpAllocWork TpPostWork TpReleaseWork这一组 api,还有没有其他的 api 可以利用,这里推荐一个项目:
https://github.com/fin3ss3g0d/IoDllProxyLoad
另外这种方式可不可以和 syscall 结合到一起,推荐项目:
https://github.com/pard0p/CallstackSpoofingPOC/tree/main

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/721928.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

esphome esp8266刷写遇到的问题

问题描述: 在尝试打开串口时出现以下错误信息: Failed to execute open on SerialPort: Failed to open serial port.起因: 莫名其妙地安装了一个乐鑫的新版 CH340 驱动。解决方案:找到正确的驱动程序,如下图所示:卸载安装的新驱动,并等待系统自动安装正确的驱动。

数字滤波器和模拟滤波器(一)

下面介绍模拟滤波器和数字滤波器的频率响应的异同,以及如何使用python地`scipy.signal`来绘制其频谱响应和冲激阶跃响应。在第二期将谈到如何设计模拟滤波器和数字滤波器。模拟滤波器和数字滤波器(一) 下面介绍模拟滤波器和数字滤波器的频率响应的异同,以及如何使用python地…

茗鹤 | 模具如何在APS高级计划排程系统中,配置共模、互换镶件等有限约束条件?

在高度依赖模具的生产环境中,模具管理与APS生产排程的有效整合是提升效率、确保订单交期的关键。以下是针对模具管理的几个核心策略,通过APS高级计划排程系统的辅助,实现产能优化与生产调度的智能化升级在高度依赖模具的生产环境中,模具管理与APS生产排程的有效整合是提升效…

靶机练习:Gitroot

信息收集 扫描全端口以发现服务访问80端口,有hint尝试绑定域名到/etc/vuln,同时提示中存在用户名jen,可以尝试爆破ssh 绑定后能访问站点了用wpscan能扫出wordpress的用户名接下来没爆破出密码 尝试使用wfuzz扫描子域名 wfuzz -c -u http://gitroot.vuln -H "HOST:FUZZ.…

Redis-10-分布式锁.md

参考: 分布式锁介绍 1.概念 额,为什么的话,建议先了解下我这篇文章。 Java-并发-并发的基本概念 我们在并发场景下,区分一个场景是否有并发问题,个人理解,锁的场景需要考虑:共享:是否共享某个资源 竞态:如何构建竞态关系首先,我们得拎清楚它到底会不会共享,不是说多…

一文了解 - - SpringMVC

一、SpringMVC概述Spring MVC 是由Spring官方提供的基于MVC设计理念的web框架。 SpringMVC是基于Servlet封装的用于实现MVC控制的框架,实现前端和服务端的交互。1.1 SpringMVC优势严格遵守了MVC分层思想采用了松耦合、插件式结构;相比较于我们封装的BaseServlet以及其他的一些…

php-status监控流程

1.开启php的状态页功能 #基于php-fpm进程做的实验yum install php-fpm -y修改配置文件,开启php,status功能即可,打开如下参数即可 要求你访问php状态页面的入口就是/status_php[root@web-7 ~]#grep status_ /etc/php-fpm.d/www.conf pm.status_path = /status_phpphp-fpm,…

南昌航空大学软件学院23201823第二次blog

一、前言: 这是第二次的blog,接下来关于这最近三次的PTA大作业,只有第一次是上次答题判题程序的延续,接下来则是一个全新的关于电路的设计,最新的电路设计相较于之前的答题判题程序来说的话,难度确实有所下降。前两次中都含有三道题,而最后一次的PTA则是删去了其余两道题…

BUUCTF-Misc(121-130)

[UTCTF2020]sstv 参考: [UTCTF2020]QSSTV - cuihua- - 博客园 (cnblogs.com) qsstv解密一下flag{6bdfeac1e2baa12d6ac5384cdfd166b0}voip 参考: buuctf VoIP-CSDN博客 voip就是语音通话技术然后wireshark可以直接播放这个语音然后播放一下flag就是考听力的,加油吧,我太垃圾…

Paxos Made Simple

1 IntroductionPaxos算法是莱斯利兰伯特(Leslie Lamport)于1990年提出的一种基于消息传递且具有高度容错特性的共识(consensus)算法。《The Part-Time Parliament》最早发表于1998年,Paxos岛上有一个议会,这个议会来决定岛上的法律,而法律是由议会通过的一系列的法令定义…

题目集4~6的总结

目录一.前言 nchu-software-oop-2024-上-4 ~知识点 nchu-software-oop-2024-上-5 ~知识点 nchu-software-oop-2024-上-6 ~知识点二.设计与分析一.答题判题程序-41.继承2.多态二.家居强电电路模拟程序-11.类的设计2.抽象类二.家居强电电路模拟程序-21.面向对象设计原则——单…