Windows核心编程【22】小结

Windows核心编程(二十二)API拦截 一个模块的导入段包含一组DLL。为了让模块能够运行,这些DLL是必须的。导入段还包含一个符号表。它列出了该模块从各DLL中导入的符号。当模块调用这些导入符号的时候,系统实际上会调用转换函数,获得导入函数在导入表的地址,然后再跳到相应的位置。如果我们能将导入段中相应导入函数的地址替换成自定义的函数的地址,即可实现对该函数的拦截。在自定义的函数中,我们既可以调用拦截的函数,也可以执行其他工作。 阅读详情
第22章 DLL注入和API拦截


在Windows中,每个进程都有自己私有的地址空间。独立的地址空间对开发人员和用户都是非常有利的。对开发人员来说,系统更有可能捕获错误的内存读/写。对用户来说,OS变得更加健壮,因为一个应用程序的错误不会导致其他应用程序或OS崩溃。


APP需要跨越进程边界来访问另一个进程的地址空间的情况如下:
1、想要从另一个进程创建的窗口派生子类窗口。
(subclass a window created by another process, 一个subclass是一个窗口或一组具有相同窗口类的窗口,发往这个或这些窗口的消息在被送到窗口类的窗口过程处理之前,会先被另一个窗口过程截取并处理。这是对窗口的行为进行扩展和定制的一种方法。)
2、需要一些手段来辅助调试——例如,需要确定另一个进程正在使用哪些DLL。
3、想要给另一个进程安装挂钩。


DLL代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲,肆意妄为了。


一、DLL注入的一个例子
~~~~~~~~~~~~


二、使用注册表来注入DLL


整个系统的配置都保存在注册表中,可以通过调整其中的设置来改变系统的行为。要讨论的条目在下面这个注册表项中:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\


AppInit_Dlls键的值可能会包含一个DLL的文件名或一组DLL的文件名(通过空格或逗号分隔)。由于空格是用来分隔文件名的,因此我们必须避免在文件名中包含空格。第一个DLL的文件名可以包含路径,但其他DLL包含的路径则将被忽略。处于这个原因,最好是将自己的DLL放到Windows的系统目录中,这样就不必指定路径了。
LoadAppInit_Dlls,类型为DWORD的注册表项,值为1说明需要初始化。


当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知。当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL。当系统载入每个DLL的时候,会调用它们的DllMain函数并将参数fdwReason的值设为DLL_PROCESS_ATTACH,这样每个DLL就能够对自己进行初始化。


由于被注入的DLL是在进程生命期的早期被载入的,因此我们在调用函数的时候应该慎重。调用Kernel32.dll中的函数应该没问题,但是调用其他DLL中的函数可能会导致问题,甚至可能会导致蓝屏。User32.dll不会检查每个DLL的载入或初始化是否成功。


在用来注入DLL的所有方法中,这是最方便的一种。需要做的只是在注册表中添加两个值。但这种方法也有一些缺点,具体如下:
1、DLL只会被映射到那些使用了User32.dll的进程中。所有基于GUI的APP都使用了User32.dll,但大多数基于CUI的APP都不会使用它。
2、DLL会被映射到每个基于GUI的APP中,但我们可能只想把DLL注入到一个或少数几个APP中。
3、DLL会被映射到每个基于GUI的APP中,在APP终止之前,它将一直存在于进程的地址空间中。时间无法控制。


三、使用Windows挂钩来注入DLL


为了能让挂钩的工作方式与它们在16位Windows中的工作方式相同,MS被迫设计出一种机制,这种机制可以让我们将一个DLL注入到另一个进程的地址空间中。


看一个例子,进程A为了查看系统中各窗口处理了哪些消息,安装了一个WH_GETMESSAGE挂钩。该挂钩通过调用SetWindowsHookEx来安装:
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);
第一个参数表示要安装的挂钩的类型;第二个参数是一个函数的地址(在我们的地址空间中),在窗口即将处理一条消息的时候,系统应该调用这个函数;第三个参数标识一个DLL,这个DLL中包含了GetMsgProc函数。(在Windows中,hInstDll的值是进程地址空间中DLL被映射到的虚拟内存地址。)最后一个参数标识要给哪个线程安装挂钩。(一个线程可能会调用SetWindowsHookEx并传入系统中另一个线程的线程标识符)通过传0,告诉系统要给系统中的所有GUI线程安装挂钩。


则会发生如下:
1、进程B中的一个线程准备向一个窗口派送一条消息。
2、系统检查该线程是否已经安装了WH_GETMESSAGE挂钩。
3、系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中。
4、如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器(lock count)递增。
5、由于DLL的hInstDll是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同。
如果hInstDll相同,那么在两个进程的地址空间中,GetMsgProc函数位于相同位置,系统可以直接在进程A的地址空间中调用GetMsgProc。
如果hInstDll不同,那么系统必须确定GetMsgProc函数在进程B的地址空间中的虚拟内存地址。
GetMsgProc B = hInstDll B + (GetMsgProc A - hInstDll A)
6、系统在进程B中递增该DLL的锁计数器。(为何再次递增???)
7、系统在进程B的地址空间中调用GetMsgProc函数。
8、当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器。


注意:当系统把挂钩过滤函数(hook filter function)所在的DLL注入或映射到地址空间时,会映射整个DLL,而不仅仅只是挂钩过滤函数。这意味着该DLL内的所有函数存在于进程B中,能够为进程B中的任何线程调用。


因此,为了从另一个进程的窗口来创建一个子类窗口,可以先给创建窗口的线程设置一个WH_GETMESSAGE挂钩,然后当GetMsgProc函数被调用的时候,就可以调用SetWindowLongPtr来派生子类窗口。当然,子类窗口的窗口过程必须和GetMsgProc函数在同一个DLL中。


~~~~~


四、使用远程线程来注入DLL


Windows提供了一些函数来让一个进程对另一个进程进行操作。虽然这些函数中的大多数一开始都是为调试器或其他工具设计的,但是任何应用程序都可以调用这些函数。


从根本上说,DLL注入技术要求目标进程中的一个线程调用LoadLibrary来载入我们想要的DLL。由于我们不能轻易地控制别人进程中的线程,因此这种方法要求我们在目标进程中创建一个新的线程。由于这个线程使我们自己创建的,因此我们可以对它执行的代码加以控制。
Windows提供了如下所示的CreateRemoteThread函数,使得在另一个进程中创建线程变得非常容易: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx
HANDLE WINAPI CreateRemoteThread(
  __in   HANDLE hProcess,
  __in   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in   SIZE_T dwStackSize,
  __in   LPTHREAD_START_ROUTINE lpStartAddress,
  __in   LPVOID lpParameter,
  __in   DWORD dwCreationFlags,
  __out  LPDWORD lpThreadId
);



除了有一个额外的参数hProcess之外,与CreateThread完全相同。这个参数用来表示新创建的线程归哪个进程所有。参数pfnStartAddr是线程函数的内存地址。当然,这个内存地址应该在远程进程(remote process)的地址空间中,因为线程函数的代码不能在我们自己进程的地址空间中。


再让我们创建的远程线程调用LoadLibrary函数,就可以实现DLL注入了。


巧合的是,LoadLibrary函数的原型和线程函数的原型基本相同。下面是线程函数的函数原型:
DWORD WINAPI ThreadFunc(PVOID pvParam);
因此我们可以直接创建这个新线程:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);
ANSI版本:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryA, "C:\\MyLib.dll", 0, NULL);


但是,有两个问题。


1、不能直接把LoadLibraryW或A作为第4个参数传给CreateRemoteThread。其原因不是那么明显。在编译和链接一个程序的时候,生成的二进制文件中包含一个导入段(19章中介绍)。这个段由一系列转换函数构成,这些转换函数用来跳转到导入的函数。因此,在代码调用诸如LoadLibraryW之类的函数,链接器会生成一个调用,来调用我们模块的导入段中的一个转换函数,这个转换函数然后会跳转到实际的函数。


如果在调用CreateRemoteThread的直接引用LoadLibraryW,该引用会被解析为我们模块的导入段中LoadLibraryW转换函数的地址。如果把这个转换函数的地址作为远程线程的起始地址传入,可能会访问违规。为了强制代码略过转换函数并直接调用LoadLibraryW函数,必须通过调用GetProcAddress来得到LoadLibraryW的确切地址。
(导入段怎么回事?)




对CreateRemoteThread的调用假定在本地进程(local process)和远程进程中,Kernel32.dll被映射到地址空间中的同一内存地址。每个应用程序都需要Kernel32.dll,而且根据的经验,系统在进程中都会将Kernel32.dll映射到同一个地址。即使这个地址在系统重启之后可能会改变,就像14章的地址空间布局随机化(Address Space Layout Randomization,ASLR)看到的那样,这个假定仍然成立。


必须通过下面这样来调用CreateRemoteThread:(UNICODE版本)
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")),"LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);


2、与DLL陆军字符串有关,字符串"C:\\MyLib.dll"位于调用进程的地址空间中。把这个地址传给先创建的远程线程,远程线程再把它传入LoadLibraryW,当LoadLibraryW访问该内存地址的时候,DLL的路径字符串并不在那里,远程进程的线程很可能会引发访问违规。远程进程会被弄崩。


为解决这个问题,需要把DLL的路径字符串存放到远程进程的地址空间中去。然后再调用CreateRemoteThread的时候,传入(在远程进程的地址空间中)存放字符串的地址。碰巧的是,Windows提供了VirtualAllocEx函数,可以让一个进程在另一个进程的地址空间中分配一块内存。 http://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx
LPVOID WINAPI VirtualAllocEx(
  __in      HANDLE hProcess,
  __in_opt  LPVOID lpAddress,
  __in      SIZE_T dwSize,
  __in      DWORD flAllocationType,
  __in      DWORD flProtect
);


另一个函数可以让我们释放这块内存,VirtualFreeEx: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366894(v=vs.85).aspx
BOOL WINAPI VirtualFreeEx(
  __in  HANDLE hProcess,
  __in  LPVOID lpAddress,
  __in  SIZE_T dwSize,
  __in  DWORD dwFreeType
);



一旦为字符串分配了一块内存,还需要一种方法来把字符串从进程的地址空间中复制到远程进程的地址空间中去。Windows提供了一些函数,可以让一个进程对另一个进程的地址空间进程读写。
ReadProcessMemory( http://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx)和WriteProcessMemory( http://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx
BOOL WINAPI ReadProcessMemory(
  __in   HANDLE hProcess,
  __in   LPCVOID lpBaseAddress,
  __out  LPVOID lpBuffer,
  __in   SIZE_T nSize,
  __out  SIZE_T *lpNumberOfBytesRead
);
BOOL WINAPI WriteProcessMemory(
  __in   HANDLE hProcess,
  __in   LPVOID lpBaseAddress,
  __in   LPCVOID lpBuffer,
  __in   SIZE_T nSize,
  __out  SIZE_T *lpNumberOfBytesWritten
);



远程进程由hProcess参数来识别。参数pvAddressRemote表示远程进程中的地址,pvBufferLoacl是本地进程中的内存地址,dwSize是要传输的字节数,pdwNumBytesRead和pdwNumBytesWritten分别表示实际传输的字节数。可以在函数返回后查看这两个参数的值。


总结步骤:
1、用VirtualAllocEx函数在远程进程的地址空间中分配一块内存。
2、用WriteProcessMemory函数把DLL的路径名复制到第1步分配的内存中。
3、用GetProcAddress函数来得到LoadLibraryW或LoadLibraryA函数(在Kernel32.dll)的实际地址。
4、用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数并在参数中传入第1不分配的内存地址。
5、用VirtualFreeEx来释放第1步分配的内存。
6、用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll中)的实际地址。
7、用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE。


五、使用木马DLL来注入DLL


注入DLL的另一种方式是,把我们知道的进程比如会载入的一个DLL替换掉。在我们的DLL的内部,导出原来的DLL导出的所欲符号。这用20章介绍的函数转发器很容易实现,这样一来,给某些函数安装挂钩只不过是小事一桩。但是,不能自动适应版本变化,应该避免使用它。


如果只是想把这种方法用在一个应用程序中,则可以给我们的DLL起一个独一无二的名称,并修改APP的.exe模块的导入段。说得更具体点,导入段包含了一个模块所需的所有DLL的名称。可以在文件的导入段中找到那个要被替换的DLL的名称,对它进程修改。前提是要相当熟悉.exe和DLL的文件格式。


六、把DLL作为调试器来注入
PEB及PEB_LDR_DATA结构 PEB结构如下:(比MSDN上详细多了http://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx) 这个进程环境块就不罗嗦了,直接贴代码。 typedef struct _PEB { // Size: 0x1D8 /*000*/ UCHAR InheritedAddressSpace; /*001 阅读详情

相关推荐

栈溢出防御之——Windows安全机制GS编译选项

安全漏洞中有个重灾区:栈溢出。利用类似memset之类的字符串修改函数,输入超出正常长度的字符串,导致栈溢出,从而影响其它数据(返回地址、标志变量等)。 维基百科给出的资料http://zh.wikipedia.org/wiki/%E5%A0%86%E6%A0%88%E6%BA%A2%E5%87%BA主要是函数无限调用导致的堆栈溢出,下面给出个0day2里面的例子温习下栈溢出: #includ

BetaBin 8550

安装wsl2和ubuntu22.04时报错0x8007019e...如何解决?

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。

**My Coding Family** 1373

vs2008加载dll深入

之前简单写过如何创建lib和dll文件及简单的使用(http://blog.csdn.net/betabin/article/details/7239200)。现在先再深入点写写dll的加载方式。 dll的加载方式主要分为两大类,显式和隐式链接。具体名词解释如下: 隐式链接有时称为静态加载或加载时动态链接。 显式链接有时称为动态加载或运行时动态链接。 这样我们就大概理解了这两种链接方式了,

BetaBin 8260

Windows核心编程》之22远程线程注入DLL

起源 贵铎·范·罗萨姆(Guido van Rossum)于 1989 年底始创了 Python 1991 年初,Python 发布了第一个公开发行版。 特点C 或者 C++最大的弊病在于内存管理是由开发者负责的。在 Python 中,由于内存管理是由 Python 解释器负责的,所以开发人员就可以从内存事务中解放出来,全神贯注于最直接的目标,仅仅致力于开发计划中首要的应用程序。这会使错误更少

qingmoshu的专栏 414

Windows核心编程的官方网站

http://wintellect.com/books.asps http://blogs.technet.com/b/markrussinovich/archive/2008/07/21/3092070.aspx

daojin505的专栏 338

学习小结(2005-2-22

学习小结(2005-2-22)Author: Kendiv ( fcczj@263.net )Last Update:Tuesday, February 22, 2005 终于翻译完了《Undocumented Windows 2000 Secrets》的第四章,有些累了。这本书的第四章写的非常不错。介绍了X86架构下的内存管理机制同时,还对应讲解了Windows 2000的实现方

Kendiv's Box 2701

Windows 下从 Prodkeys.net 下载并安装 Ryujinx Keys 与 Firmware v22.5

Windows 下从 Prodkeys.net 下载并安装 Ryujinx Keys 与 Firmware v22.5

一起coding,一起嗨。 1803

(万字长文)从零开始在windows下配置Ubuntu22.04+VS code

本文详细介绍了在Windows系统下通过WSL2安装Ubuntu22.04并配置CUDA开发环境的完整流程。主要内容包括:1)对比虚拟机、WSL和双系统三种Linux安装方式的优缺点;2)WSL2的详细安装步骤,包括系统迁移、用户创建等;3)Ubuntu环境配置,如换源、Anaconda安装;4)CUDA和cuDNN的安装与验证;5)PyTorch的安装配置;6)VSCode连接WSL的方法。文章特别针对CUDA13.0和PyTorch2.8.0的兼容性问题提供了解决方案,并给出了详细的错误排查方法,为开发

qq_45053161的博客 1348

多核应用编程实战

《多核应用编程实战》 基本信息 原书名:Multicore application programming:for windows,linux,and Oracle Solaris 作者: (美)戈夫(Darryl Gove) 译者: 郭晴霞 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115317506 上架时间:2013-5-22 出版日期:2013...

weixin_33739541的博客 248

C++核心编程

变量类型存储区域生命周期地址特征全局变量全局区程序启动→结束地址小,固定静态变量全局区程序启动→结束地址小,固定局部变量栈区函数开始→结束地址大,每次运行可能不同局部常量栈区函数开始→结束地址大,和局部变量相邻全局常量常量区程序启动→结束地址中,只读字符串常量常量区程序启动→结束地址中,只读1.2 程序运行后​ **栈区:**​ 由编译器自动分配释放, 存放函数的参数值,局部变量等​ 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。

丰锋的博客 331

OpenClaw 2026.3.22 更新了什么?一文看懂这次“大改版”为何值得重视

摘要 OpenClaw 2026.3.22版本是一次重大更新,涉及插件安装逻辑、浏览器接入方式、图片生成路径等核心功能的调整。本次更新包含多项Breaking Changes,包括插件安装优先转向ClawHub、移除旧的Chrome扩展relay路径、统一图片生成路径、迁移插件SDK入口等。同时新增了技能市场、沙箱后端支持等特性。官方建议用户在升级前备份配置,检查插件来源及浏览器兼容性。这一版本标志着OpenClaw向更统一、安全的架构演进,但升级需谨慎评估对现有工作流的影响。 (字数:149)

分享 Windows 系统部署、镜像封装、终端管理、故障排查、Sysinternals 工具、PowerShell 自动化及 AI 工具应用。通过真实案例记录 IT 运维实践,探索更高效的系统管理方法。 2万+

Windows 10 LTSC系统解析:官方精简方案如何让老电脑重获新生

在操作系统领域,轻量化与兼容性的平衡一直是技术优化的核心课题。其原理在于通过精简非必要组件、减少后台服务负载来释放硬件资源,这对于提升老旧设备的运行效率具有显著技术价值。特别是在企业级应用场景中,长期服务版本(LTSC)通过移除微软商店、Cortana等现代化应用,实现了极致的系统稳定性与纯净度。这种设计思路恰好解决了老机器因硬件性能不足而面临的卡顿问题,为Windows 10 IoT企业版LTSC在老设备上的流畅运行提供了基础。本文聚焦于这一官方精简方案,深入探讨其如何通过做减法而非删减核心功能,为老爷机

weixin_34023982的博客 377

5个核心功能助力开发者高效配置Windows安卓子系统完整环境

在当前跨平台开发与应用测试场景中,开发者与高级用户常面临三大痛点:传统安卓模拟器性能损耗高达30%以上,无法真实模拟移动设备环境;官方Windows安卓子系统(WSA)功能受限,缺乏Google服务框架与系统级定制能力;第三方解决方案配置流程复杂,需手动处理驱动兼容性、权限管理等底层问题。这些挑战直接导致开发效率降低、测试环境不一致、应用兼容性问题频发等实际困难。 以企业级应用测试为例,某团队在

gitblog_00710的博客 262

解决问题:开启Wireshark之NPF驱动问题

关于新装系统之后,由于我们一系列的开机优化,很多服务都被停掉了,NPE也是一样。 针对以上问题(The NPF driver isn't running...),解决方案如下: (首先确认自己先安装了Winpcap) 命令行运行命令“net start npf”即可,当然,这是Windows Vista之前的。Windows Vista和Windows 7的用户需要自己进入到系统盘:C:\W

BetaBin 1万+

Windows提高进程权限至Debug

关于提权,需要了解知道下面三个函数吧, OpenProcessToken 得到进程的令牌句柄 LookupPrivilegeValue 查询进程的权限 AdjustTokenPrivileges 判断令牌权限 在Windows下,每个进程不是说有管理员权限就可以为所欲为。因为每个进程还有个令牌,访问令牌吧。而令牌代表了一些权限使用,如果需要某些特殊权限,需要自己去使能或除能。

BetaBin 1万+

获取系统运行进程信息——PSAPI介绍使用

网上资料显示,有这么三种方法可以用来获取系统运行进程信息: 方法 平台 备注 PSAPI Windows NT,Windows2000,Windows XP 获取进程,驱动器,模块,内存和工作集信息 性能计数器 Windows NT,Windows2000,Windows XP 提供除进程清单以外的关于进程的更多

BetaBin 8094

VS2008中LIB和DLL的创建及调用

(这个年有点冷,元宵刚过,也得继续开始学习了。) LIB和DLL的知识就懒得敲了,直接从如何建立生成LIB或DLL开始。 创建项目→Win32项目→下一步之后按照需求选择DLL或者静态库(再视需求是否空项目,一般我都空项目),然后就OK了。 一、LIB生成、及使用 1、新建betabinlib.h文件 #ifndef BETABINLIB_H #define BETABINLIB_H

BetaBin 6301

VS2008无法调试,报错MSVCR90.DLL丢失

(非常之郁闷的一个下午,睡完午觉,发现早上搭好的代码没办法调试了。顿时抓狂了下,后来还是想了下早上干过什么。隐隐约约记得把一个MSVCR90.DLL拖到虚拟机去的感觉,然后还改过shared的项目属性,不过只是个预编译头开关。) —————— 更新,最近发现这个问题可能是由于我的VS2008出问题,有个项目包含stdlib.h就报丢失MSVCR90.DLL。 ——2012/04/16 ——

BetaBin 5631
上一篇: Windows核心编程【21】小结
下一篇: API-HOOK and ANTI-API-HOOK For Ring3
BetaBin
博客等级 码龄15年 155粉丝 121原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值