首页 > 其他分享 >三探堆栈欺骗之Custom Call Stacks

三探堆栈欺骗之Custom Call Stacks

时间:2024-06-08 19:34:08浏览次数:21  
标签:WORK Custom CALLBACK Call WorkCallback WorkReturn 堆栈 三探 PTP

本文首发阿里云先知社区: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 TpAllocWork
    TpPostWork(WorkReturn);                                // request Allocated Worker Thread Execution
    TpReleaseWork(WorkReturn);                             // worker thread cleanup

    WaitForSingleObject((HANDLE)-1, 1000);
    printf("hWininet: %p\n", GetModuleHandleA(libName)); //check if library is loaded

    return 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 TpAllocWork
    TpPostWork(WorkReturn);                                // request Allocated Worker Thread Execution
    TpReleaseWork(WorkReturn);                             // worker thread cleanup

    WaitForSingleObject((HANDLE)-1, 1000);
    printf("hWininet: %p\n", GetModuleHandleA(libName)); //check if library is loaded

    return 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 .text

extern getLoadLibraryA

global WorkCallback

WorkCallback:
    mov rcx, rdx
    xor rdx, rdx
    call getLoadLibraryA
    jmp 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 - 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;

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

#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 .text

global WorkCallback

WorkCallback:
    mov rbx, rdx                ; backing up the struct as we are going to stomp rdx
    mov rax, [rbx]              ; NtAllocateVirtualMemory
    mov rcx, [rbx + 0x8]        ; HANDLE ProcessHandle
    mov rdx, [rbx + 0x10]       ; PVOID *BaseAddress
    xor r8, r8                  ; ULONG_PTR ZeroBits
    mov r9, [rbx + 0x18]        ; PSIZE_T RegionSize
    mov r10, [rbx + 0x20]       ; ULONG Protect
    mov [rsp+0x30], r10         ; stack pointer for 6th arg
    mov r10, 0x3000             ; ULONG AllocationType
    mov [rsp+0x28], r10         ; stack pointer for 5th arg
    jmp rax

堆栈也是非常干净

总结

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

标签:WORK,Custom,CALLBACK,Call,WorkCallback,WorkReturn,堆栈,三探,PTP
From: https://www.cnblogs.com/fdxsec/p/18234881

相关文章

  • 265 Custom Exceptions(更容易定位报错内容具体是什么)
    优势CustomExceptions相比于ArgumentException/ArgumentNullException,更容易定位报错内容具体是什么,报错内容与具体业务相关。示例新建类库项目Exceptions,为Services/CRUDExample项目添加Exceptions项目引用Exceptions项目中添加InvalidPersonIdException.csnamespac......
  • VS下QT使用QCustomplot报错QPainter::HighQualityAntialiasing': Use Antialiasing in
    @Time:2024-06-07@Error:VS+QT+QCustomplot编译时报错ERROR4995QPainter::HighQualityAntialiasing':UseAntialiasinginstead@原因:使用标记有 deprecated 的函数。参见:/sdl(启用附加安全检查)|MicrosoftLearn @解决办法:关闭编译报错或编译警告;参见:编译器警告(级别3)C4......
  • GLM-4-9B领先!伯克利函数调用榜单BFCL的Function Calling评测方法解析与梳理
    智谱公布的GLM-4-9B基于BFCL榜单的工具调用能力测试结果©作者|格林来源|神州问学在智谱最新开源的GLM-4-9B-Chat中,其工具调用能力在BFCL(伯克利函数调用排行榜)榜上获得了超高的总BFCL分,和gpt-4-turbo-2024-04-09几乎不相上下。在榜单中,还提到了AST总分以及Exec总分两个......
  • cmake的add_custom_command如何处理多输出+多依赖
    intro在一个复杂的项目中,免不了需要动态生成文件,此时可能就需要用到cmake的add_custom_command命令,这个命令可以生成cmake识别的输出文件,并作为构建过程中其它命令的依赖和输出。add_custom_command(OUTPUToutput1[output2...]COMMANDcommand1[ARGS][args1...][COMMAND......
  • 神话的呼唤/Call of Myth游玩延迟高、联机延迟高的解决办法
    神话的呼唤/CallofMyth是一款架空历史题材的卡牌类游戏,以恐怖神话为背景,玩家将进入到一个充满危险的漆黑世界中,玩家需要凭借自己的智慧,挑战古老的旧神和它们狂热的信徒。近期很多玩家在游戏时遇到延迟高的情况,影响游戏体验,下面分享几种解决高延迟的方法,帮助大家流畅游玩。......
  • 连接 Dynamics 365 Customer Engagement (on-premises)
    AuthType=AD创建项目模板是.NETframework4.6.2的控制台程序添加nuget包Microsoft.CrmSdk.CoreAssemblies,Microsoft.CrmSdk.XrmTooling.CoreAssemblyProgram类添加以下代码usingSystem;usingSystem.Configuration;usingMicrosoft.Crm.Sdk.Messages;usingMic......
  • 中断卡在configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
    今天在调试以太网驱动的时候遇到一个问题,当程序执行到这个/*addthenetworkinterface(IPv4/IPv6)withRTOS*/netif_add(&gnetif,&ipaddr,&netmask,&gw,NULL,&ethernetif_init,&tcpip_input);函数里面的netif_invoke_ext_callback(netif,LWIP_NSC_NETIF_ADDED,N......
  • 微软的 Copilot+PC 如何关闭 Recall 功能?3 种方法教会你
    关闭Recall功能的方法在Windows1124H2更新之后,我们可以在系统中禁止或者关闭Recall功能。禁止Recall功能我们可以在设置中直接禁止使用Recall功能,在创建新用户账户的时候我们可以选择关闭Recall功能,如果在创建时未关闭也可以在设置面板中关闭该功能。首先我......
  • 深入探讨Function Calling:在Semantic Kernel中的应用实践
    引言上一章我们熟悉了OpenAI的functioncalling的执行原理,这一章节我们讲解一下functioncalling在SemanticKernel的应用。在OpenAIPromptExecutionSettings跟LLM交互过程中,ToolCallBehavior的属性之前我们的章节有介绍过ToolCallBehavior:属性用于获取或设置如何......
  • SAP:观察I_CALLBACK_USER_COMMAND 参数(按钮点击事件)
    1、从函数级SLVC_FULLSCREEN里复制 一个在”GUI状态“ 下的“STANDARD_FULLSCREEN”标准全屏幕到 程序Z16_04里。 标准工具添加 定义按钮(关闭、保存)主程序代码:*&---------------------------------------------------------------------**&ReportZ16_04*&利用......