首页
社区
课程
招聘
[翻译]DLL注入之SetThreadContext
发表于: 2020-9-18 22:13 25984

[翻译]DLL注入之SetThreadContext

2020-9-18 22:13
25984

现在的DLL注入技术有很多种,每种方法都有他的优点和缺点。

最简单的一种方式是通过使用CreateRemoteThread函数在目标进程创建一个线程,然后指定线程的入口函数为LoadLibrary。原因在于LoadLibrary和线程的起始函数从二进制的角度来看,他们有着相同的原型。(都接受一个指针)

这个方法比较简单明了。创建一个新的线程可以被其他很多方法检测到,比如说ETW event。如果存在一个驱动使用PsSetCreateThreadNotifyRoutine hook 了线程的创建,那么自然在创建远程线程时就会被检测到。

一个更隐秘的技术是使用已经存在的线程去完成这件事情。一种方法是通过QueueUserApc 向目标进程的线程插入APC对象。(异步过程调用的函数地址指定为LoadLibrary) 这种方法潜在的问题是目标线程必须进入 Alertable 状态,我们的函数才会被调用。不幸的是,我们没有办法保证一个线程将来会把自己置入 Alertable 状态。虽然我们可以尽可能的把目标进程的所有线程都插入APC,但是在某些情况下,都会注入失败。一个经典的例子就是cmd.exe, 据我所知,他就是一个单线程的进程,且从不会进入Alertable状态。

这篇博客讲述的是关于另外一种方法使得目标进程调用LoadLibrary。通过修改现有线程的上下文达到不创建线程,只改变现有线程的执行流来加载DLL的目的。

接下来让我们看看如何在x86和x64系统上实现这种方法。

首先,我们需要锁定目标进程的一个线程。技术原理上来说,选择目标进程的任意线程都是可以的。但是一个在等待的线程就不适合作为候选人,除非他马上准备去运行,所以最好选择一个正在运行的线程或者即将运行的线程,使得我们的DLL尽可能被及时加载。

一旦我们锁定目标进程和线程后,我们需要以适当的权限去打开它。

对于目标进程,我们需要 PROCESS_VM_OPERATIONPROCESS_VM_WRITE权限用于写入代码。对于线程,是因为我们需要能够改变线程上下文和挂起线程。

注入自身需要一系列的步骤。我们需要在目标进程申请一块可执行的内存,然后把我们的代码放进去。

这里我们申请了一页 RWX的内存。实际上我们并不需要那么多内存,但是Windows的内存管理在分配虚拟内存时最小是按照页来分配的,所以我们显示的申请一个完整的页面。

我们需要放置什么代码到目标进程呢?很明显,我们是想调用LoadLibrary,但是远不止于此。我们需要调用LoadLibrary后需要恢复执行原来线程离开时的上下文。因此我们先挂起线程,捕获他的执行上下文。

接下来,我们需要拷贝一些代码到目标进程。这段代码必须是用汇编写出来的,而且必须和目标进程的位数一样。(总不能拿x86汇编写入64bit进程跑吧)对于x86,我们可以使用下面裸函数和内联汇编的写法:

函数修饰为 __declspec(naked)属性表示告诉编译器不要给函数添加序言和结尾代码,我只要自己写的实际代码。代码中奇怪的数字仅仅是占位而已,当我们拷贝代码到目标进程时,需要进行修正。

在Demo源码中,我将上面的函数的机器码打包成了一个字节数组:

上面的字节对应上面的汇编指令。现在我们修正其中的哑值。

首先,我们获取到LoadLibraryA的函数地址,因为这个函数将用来加载我们的DLL到目标进程。LoadLibraryW也可行,但是ASCII版本的会使我们的工作简单些。DLL的路径地址被随意地设置在buffer里2KB的位置处。

接下来,我们将修改好的代码和DLL路径写入目标进程

最后一件事情就是设置新的指令指针到拷贝的代码处,然后恢复线程执行。

这就是32bit版本的注入方式。

这种情况下的调试是不容易的,因为我们需要附加到目标进程,并且跟踪我们写的代码。下面的例子我运行了一个32bit的notepad.exe,在\Windows\SysWow64 目录下(64-bit系统上)。demo项目的命令行允许我设置目标进程id和DLL路径注入。在调用SetThreadContext之前,我已经通过Visual Studio设置好了命令行参数以及断点。控制台程序显示了代码应该拷贝到的目标进程虚拟地址:

现在我们可以运行WinDbg附加到Notepad.exe ,查看控制台显示的虚拟地址的反汇编。

我们可以清晰地看到我们修改过后的代码,当恢复线程执行时,LoadLibraryA就会调用起来。我们可以再这里下一个断点。

接下来我们可以让notepad运行起来,然后再运行起注入进程。果不其然,我们命中了这个断点。这是断点和调用栈:

如果我们有需要的话,可以进行单步调试。现在让notepad跑起来,会发现将会加载我们的DLL。我们可以在DllMain里做我们想做的事情。

64bit的代码我写的十分简易,不能保证在任何情况下都能奏效,需要一些更多的测试。

下面是代码:

这里我不会再进行讨论细节。但是这里的代码看起来和x86版本不同,原因在于x64的代码调用约定不是x86 的 __stdcall。比如说,x64上对于四个整数的参数将会被传入RCX,RDX,R8和R9 而不是在栈上。在我们的例子中,RCX足以使得LoadLibraryA正常工作。

修改哑值的地方自然要使用不同的偏移值

到这里,你应该基本掌握了如果通过目标进程现有线程进行DLL注入的方法。这个方法相对于创建线程要难于检测。一种可能的方式是定位可执行页面与已知模块的地址进行比较。但是检测都存在一个窗口期,这里可以考虑注入进程在注入完成后(通过事件对象)来释放用于注入分配的内存,概率性规避。

完整的项目代码在Github上。

 
 
 
 
 
 
 
//
// 打开进程句柄
//
auto hProcess = ::OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE,FALSE,pid);
if(!hProcess)
    return Error("Failed to open process handle");
 
//
// 打开线程局部
//
auto hThread = ::OpenThread(THREAD_SET_CONTEXT|THREAD_SUSPEND_RESUME|
THREAD_GET_CONTEXT,FALSE,tid);
if(!hThread)
    return Error("Failed to open thread handle");
//
// 打开进程句柄
//
auto hProcess = ::OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE,FALSE,pid);
if(!hProcess)
    return Error("Failed to open process handle");
 
//
// 打开线程局部
//
auto hThread = ::OpenThread(THREAD_SET_CONTEXT|THREAD_SUSPEND_RESUME|
THREAD_GET_CONTEXT,FALSE,tid);
if(!hThread)
    return Error("Failed to open thread handle");
 
const auto page_size = 1 << 12;
 
auto buffer = static_cast<char*>(::VirtualAllocEx(hProcess,nullptr,page_size,
    MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE));
const auto page_size = 1 << 12;
 
auto buffer = static_cast<char*>(::VirtualAllocEx(hProcess,nullptr,page_size,
    MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE));
 
if(::SuspendThread(hThread)==-1)
    return false;
 
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
if(!::GetThreadContext(hThread,&context))
    return false;
if(::SuspendThread(hThread)==-1)
    return false;
 
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
if(!::GetThreadContext(hThread,&context))
    return false;
void __declspec(naked) InjectedFunction(){
    __asm{
        pushad
        push     11111111h    ; DLL路径参数
        mov     eax, 22222222h ; LoadLibrary 函数地址
        call eax
        popad
        push     33333333h    ; 代码返回地址
    }
}
void __declspec(naked) InjectedFunction(){
    __asm{
        pushad
        push     11111111h    ; DLL路径参数
        mov     eax, 22222222h ; LoadLibrary 函数地址
        call eax
        popad
        push     33333333h    ; 代码返回地址
    }
}
 
BYTE code[]={
    0x60,
    0x68,0x11,0x11,0x11,0x11,
    0xb8,0x22,0x22,0x22,0x22,
    0xff,0xd0,
    0x61,
    0x68,0x33,0x33,0x33,0x33,
    0xc3
};
BYTE code[]={
    0x60,
    0x68,0x11,0x11,0x11,0x11,
    0xb8,0x22,0x22,0x22,0x22,
    0xff,0xd0,
    0x61,
    0x68,0x33,0x33,0x33,0x33,
    0xc3
};
auto loadLibraryAddress = ::GetProcAddress(::GetModuleHandle(L"Kernel32.dll"),
"LoadLibraryA");
 
// 设置DLL路径
*reinterpret_cast<PVOID*>(code+2) = static_cast<void*>(buffer+page_size/2);
// 设置 LoadLibraryA 函数地址
*reinterpret_cast<PVOID*>(code+7) = static_cast<void*>(loadLibraryAddress);
// 跳转地址 (返回原来的代码)
*reinterpret_cast<unsigned*>(code+0xf) = context.Eip;
auto loadLibraryAddress = ::GetProcAddress(::GetModuleHandle(L"Kernel32.dll"),
"LoadLibraryA");
 
// 设置DLL路径
*reinterpret_cast<PVOID*>(code+2) = static_cast<void*>(buffer+page_size/2);
// 设置 LoadLibraryA 函数地址
*reinterpret_cast<PVOID*>(code+7) = static_cast<void*>(loadLibraryAddress);
// 跳转地址 (返回原来的代码)
*reinterpret_cast<unsigned*>(code+0xf) = context.Eip;
 
//
// 拷贝机器码到buffer
//
 
if(!::WriteProcessMemory(hProcess,buffer,code,sizeof(code),nullptr))
    return false;
 
//
// 拷贝dll路径到buffer
//
if(!::WriteProcessMemory(hProcess,buffer+page_size/2,dllPath,::strlen(dllPath)+1,nullptr))
    return false;
//
// 拷贝机器码到buffer
//
 
if(!::WriteProcessMemory(hProcess,buffer,code,sizeof(code),nullptr))

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 3
支持
分享
最新回复 (12)
雪    币: 57
活跃值: (796)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
配合内存加载效果更好
2020-9-18 22:45
0
雪    币: 12352
活跃值: (5874)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
3
缺陷是后续清理内存的时机无法直接得知,除非代码里自行call NtSetEvent。如果把ETHREAD的FreezeCount和SuspendApc.Inserted分别设为1和0,这方法照样抓瞎
2020-9-19 09:56
0
雪    币: 12352
活跃值: (5874)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
4
“创建一个新的线程可以被其他很多方法检测到,比如说ETW event”
从Win10 1507到1803,NtSetContextThread必定被ETW记录(WOW64除外),1809到2004,Usermode的NtSetContextThread、PsSetContextThread、PspWow64SetContextThread都会被ETW记录(包括x64和WOW64)。这老外想当然地认为SetThreadContext很隐蔽,其实和CreateThread半斤八两
2020-9-19 11:05
2
雪    币: 22
活跃值: (443)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
2020-9-19 20:46
0
雪    币: 1055
活跃值: (412)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
曾哥,永远的神!
2020-9-22 22:11
0
雪    币: 302
活跃值: (246)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
7
hhkqqs “创建一个新的线程可以被其他很多方法检测到,比如说ETW event” 从Win10 1507到1803,NtSetContextThread必定被ETW记录(WOW64除外),1809到2004, ...
你好 你说的这个ETW event有R3的API接口吗
2020-9-29 17:56
0
雪    币: 12352
活跃值: (5874)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
8
一二三六 你好 你说的这个ETW event有R3的API接口吗
论坛自行搜索StartTrace EnableTrace OpenTrace…
2020-9-29 20:13
0
雪    币: 302
活跃值: (246)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
9
hhkqqs 论坛自行搜索StartTrace EnableTrace OpenTrace…
我看了下ETW Flags  发现除了EVENT_TRACE_FLAG_CSWITCH跟线程切换相关  并没有发现哪个Flags能记录SetContextThread啊
2020-10-11 16:57
0
雪    币: 12352
活跃值: (5874)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
10
一二三六 我看了下ETW Flags 发现除了EVENT_TRACE_FLAG_CSWITCH跟线程切换相关 并没有发现哪个Flags能记录SetContextThread啊
我指的是系统本身etw也会记录setcontextthread,win10每个版本NtSetContextThread都被内嵌了一句EtwWrite,虽然没记录太多信息,但也实打实地记录了这个API的调用
2020-10-11 19:02
0
雪    币: 12352
活跃值: (5874)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
11
一二三六 我看了下ETW Flags 发现除了EVENT_TRACE_FLAG_CSWITCH跟线程切换相关 并没有发现哪个Flags能记录SetContextThread啊
应用层的ETW阉割了太多东西,你在ntos跟一下KiSystemServiceCopyEnd的一个跳转分支PerfInfoLogSysCallEntry就知道了,InfinityHook的灵感就是从这里来的,就算不Hook也可以记录系统调用
2020-10-13 15:46
0
雪    币: 302
活跃值: (246)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
12
hhkqqs 应用层的ETW阉割了太多东西,你在ntos跟一下KiSystemServiceCopyEnd的一个跳转分支PerfInfoLogSysCallEntry就知道了,InfinityHook的灵感就是从这 ...
enen...好的  谢谢  我这就去看看
2020-10-16 15:15
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
2022-11-30 02:14
0
游客
登录 | 注册 方可回帖
返回
//