在本文中,您可以了解如何拦截HTTP流量,以便将自定义代码注入Windows HTML标记。为此,有两种完全不同的方法:一种采用内核模式,另一种采用用户模式。为简单起见,不会覆盖HTTPS流量。

本文用作详细示例,其中包含用户模式与内核模式实现比较的更广泛主题的代码插图。

HTTP响应数据注入算法

在我们开始讨论不同的方法之前,让我们看一下将数据注入HTTP响应的算法。

该算法包括以下数据:

响应的初始HTTP响应体(不应为空)

所有数据都通过qzip压缩算法压缩并位于单个存档中。首先,我们需要分析响应的标头并搜索以下字段:

转移编码:分块

如果此标头存在,那么我们需要处理第一个块。

如果找不到此标头,那么我们需要查找Content-Length。 Content-Length标头的值是确认已收到所有数据并将该数据与新值挂钩所必需的。

内容编码:gzip - 显示数据是否已存档。

"\ r \ n \ r \ n"字符串 - 显示标题结束和数据开始的位置。

搜索完这些字段后,我们会提取数据。接下来,我们搜索<!DOCTYPE HTML>标记并在其后注入以下脚本:

  1. <script language = \"JavaScript \">
  2. if(确认("你想在https://www.apriorit.com/dev-blog上阅读更多内容吗?")
  3. {
  4. window.open(\ "HTTPS://www.apriorit.com/dev-blog \");
  5. }
  6. </script>已修改的响应已存档。

接下来,使用旧响应中的标头形成新的HTTP响应。如果未找到Content-Length标头,则我们将根据新响应将其值替换为新值。不应更改其他标头字段。如果找到Transfer-Encoding:chunked字段,则第一个块的长度将替换为新块。在分隔行之后插入新数据。

发送带有新数据的HTTP响应代替原始数据,并删除原始响应。

如果响应不包含所有必需的数据,则在注入之前调用获取所有数据的函数。

用户模式

在本节中,我们将介绍如何在用户模式下将自定义JavaScript代码注入HTML。

这个怎么运作

要在用户模式下注入自定义JavaScript,我们需要访问进程地址空间。这可以通过将自定义动态库(DLL)注入进程来完成。当获得对进程地址空间的访问时,我们可以修改内存以便将标准函数与我们自己的函数挂钩。这允许我们修改包含HTML代码的HTTP流量。

注入DLL的方法

注入DLL有几种方法:

  • 通过AppInit注册表值
  • 通过SetWindowsHookEx函数
  • 通过远程线程
  • 通过特洛伊木马

每种方法都有其优点和缺点。

使用寄存器(AppInit)

此方法要求您使用AppInit注册表值,该值存储加载User32.dll库所需的DLL列表,其中包含用于呈现Windows应用程序的图形界面的函数。通过将我们自己的自定义DLL的路径添加到Applnit,我们可以保证我们的DLL将被加载到Windows中的每个图形应用程序中。您可以在此处查看有关此方法的更多详细信息。

这种方法的优点:

  • 这是实施方面最简单的方法
  • 无需指定需要注入DLL的进程
  • AppInit只需修改一次,之后DLL将被加载到所有图形应用程序中

这种方法的缺点:

  • 不影响控制台应用程序,因为它们不使用User32.dll
  • 管理员权限是修改注册表所必需的

使用SetWindowsHookEx函数

SetWindowsHookEx函数允许您通过DLL注入为窗口化应用程序设置钩子过程。当一个消息发送到进程窗口时,就会发生注入

已应用的SetWindowsHookEx函数被截获。调用SetWindowsHookEx时决定消息类型,这允许我们为所有图形应用程序设置挂钩过程。

这种方法的优点:

  • 可以涵盖单个图形应用程序或所有应用程序

这种方法的缺点:

  • 只有在拦截特定消息时才会发生注入
  • 执行此功能的应用程序需要由用户启动
  • 控制台应用程序不受影响,因为它们不使用单独的窗口

使用远程流

此方法基于使用CreateRemoteThread函数,该函数允许您在另一个进程中创建远程线程。通过CreateRemoteThread传输的函数的签名应如下所示:

  1. DWORD WINAPI ThreadFunc(PVOID pvParam);这允许我们使用LoadLibrary函数(或更准确地说,LoadLibraryA或LoadLibraryW,因为LoadLibrary是一个宏),它将DLL加载到进程中。这种方法很难实现,原因有两个:

  • 将链接传输到LoadLibraryA / LoadLibraryW可能会导致内存访问冲突,因为CreateRemoteThread调用中与LoadLibraryA / LoadLibraryW的直接链接将转换为对模块导入部分中的LoadLibraryA网关的调用。
  • 将链接(带有DLL路径的参数)传递给字符串也会产生不确定的行为,因为链接将被投射到另一个进程的内存中,此地址不包含字符串。

这种方法的优点:

  • 注入DLL最灵活的方法

这种方法的缺点:

  • 最难实施的方法
  • 调用该函数的应用程序需要运行
  • 我们想要注入DLL的过程需要明确指定

特洛伊木马DLL

有一种方法可以使用自定义DLL挂钩现有DLL。为此,自定义DLL需要导出与初始函数相同的函数。如果使用DLL函数的地址修改,这并不难。

如果您只想为一个应用程序使用木马DLL,那么您可以为自定义DLL提供唯一的名称,并将其添加到应用程序的可执行模块的导入部分。但是,这需要先进的可移植可执行(PE)格式知识。

您可以在此处找到有关类似解决方案的信息。

这种方法的优点:

  • 木马DLL只需要挂钩一次,之后它就会自行运行
  • 无需管理权限
  • 木马DLL可以执行两个任务:DLL注入和函数挂钩

这种方法的缺点:

  • 必须具备PE格式的高级知识
  • 由于数字签名,可能会出现系统DLL挂钩问题

选择如何注入DLL

我们的任务需要支持尽可能多的接收和显示HTML内容的应用程序。因此,我们不能使用CreateRemoteThread函数。木马DLL注入需要PE格式的知识。因此,我们可以在SetWindowsHookEx函数和通过AppInit注册表值注入之间进行选择。 SetWindowsHookEx方法的唯一优点是它不需要管理权限。同时,AppInit提供了注入DLL的最简单方法,并将自动与所有图形应用程序一起使用。由于我们需要使用HTML和JavaScript捕获网络流量,因此我们需要涵盖所有使用图形shell的浏览器。因此,在本文中,我们将通过AppInit注册表值介绍DLL注入。

挂钩功能的方法

出于我们的任务目的,有两种方法来挂钩函数:

  • 更改PE文件导入表
  • 更改功能的开头

更改PE文件导入表

每个PE文件都有一个导入表,用于存储从DLL导入的函数的虚拟内存地址,并由DLL文件中的PE文件使用。通过访问地址空间,可以通过将函数指针更改为指向我们自己的自定义函数来修改导入表。这种方法需要广泛的PE格式知识,因为没有现成的解决方案,如库或WinAPI功能。但是,可以在网上找到很多这种方法的概念实现证明。

更改功能的开头

这种方法基于修改进程地址空间(确切地说是函数的开头),我们需要将其更改为函数的JMP。此方法在主动支持的开源MHook库中实现。而且,通过这种方法,您仍然可以使用原始功能。

选择如何注入函数

更改函数的开头依赖于方便且有效支持的库,因此它是我们选择使用的方法。

实现方法概述

我们将从Ws2_32.dll挂钩recv函数,因为它被所有浏览器用来从网络接收数据。首先,我们需要在全局变量中接收并保存指向原始函数的指针。这可以通过以下方式完成:

  1. typedef int(WINAPI * _recv)(

  2. SOCKET中,

  3. char * buf,

  4. int len,

  5. int标志

  6. );

  7. static recv TrueRecv =( recv)GetProcAddress(GetModuleHandle(L"Ws2_32.dll"),"recv");在此之后,在将DLL加载到进程中的过程中,我们需要在DllMain中挂钩recv并在卸载期间取消挂钩。因此,DllMain函数如下所示:

  8. BOOL APIENTRY DllMain(HMODULE hModule,

  9. DWORD ul_reason_for_call,

  10. LPVOID lpReserved

  11. {

  12. 开关(ul_reason_for_call)

  13. {

  14. case DLL_PROCESS_ATTACH:

  15. {

  16. if(TrueRecv)

  17. Mhook_SetHook((PVOID *)(&TrueRecv),InjectedRecv); //用InjectRecv替换recv

  18. 打破;

  19. }

  20. 案例DLL_THREAD_ATTACH:

  21. 打破;

  22. 案例DLL_THREAD_DETACH:

  23. 打破;

  24. case DLL_PROCESS_DETACH:

  25. if(TrueRecv)

  26. Mhook_Unhook((PVOID *)(和TrueRecv)); //取消recv函数

  27. 打破;

  28. }

  29. 返回TRUE;

  30. 在InjectedRecv函数中,通过TrueRecv调用真正的recv函数并处理结果。

让我们再看一下recv函数签名:

  1. int recv(
  2. SOCKET中,
  3. char * buf,
  4. int len,
  5. int标志
  6. );在recv函数的所有参数中,buf和len对我们来说是最有趣的。 buf是指len大小的缓冲区,在调用recv函数之后,包含一个大小为len或更小的响应,通过套接字接收。确切的大小可以由返回的值确定,这可能导致许多相关的问题:

  1. 将自定义JavaScript代码添加到HTML后,数据大小可能会超过buf缓冲区的大小。
  2. recv不需要立即返回整个响应,特别是如果缓冲区没有足够的内存。
  3. 许多服务以qzip格式返回响应,因为它减少了服务器的大小和等待时间。在这种情况下,需要解压缩qzip,将JavaScript注入HTML,然后再次将HTML压缩回qzip。这种情况可以与上述两个问题相结合。在第一种情况下,将超过buf缓冲区的大小。在第二种情况下,解压缩不完整的qzip会在客户端代码中产生不确定的行为,因为在压缩之后,qzip的单个部分将成为整个qzip,并且所有剩余部分将作为二进制文件出现在客户端代码中,并且不会处理。

为了解决这个问题,在客户端代码的recv调用期间,我们需要调用一个或几个recv函数来收集完整的数据并将其保存在容器中。在此之后,可以使用在HTML中注入JavaScript的算法。修改后的缓冲区存储在静态容器中。此容器是包含以下内容的std :: map:

  1. typedef std :: pair <ByteBuffer,int> BufferInfo; static std :: map <SOCKET,BufferInfo> g_bufferedData; ByteBuffer是一个typedef std :: vector <unsigned char>。 BufferInfo是一对存储修改后的答案以及前一个recv已读取的字节数。因此,我们的std :: map将套接字存储为密钥,并将缓冲区信息存储为需要作为值传输到客户端的缓冲区。

对于每个InjectedRecv调用,首先执行检查以确定g_bufferedData中当前套接字中是否存在数据。如果存在数据,则返回大小为len或更小的部分,具体取决于剩余的字节数。将所有数据传输到客户端代码后,将清除读取字节的缓冲区和计数器。

如果当前套接字中没有数据,则执行对真实recv的调用。我们假设对于带有HTML的HTTP请求,答案的第一部分具有确定答案正文大小的所有必要信息。根据这些信息,我们可以调用真实的recv,直到我们获得所有数据或发生错误。如果我们收到错误,我们可以保存接收的数据而不进行修改,然后在g_bufferedData中有数据时按照前一种情况的步骤操作。然后我们可以修改HTML,之后将完全或部分地返回数据,具体取决于数据大小和len参数。

实际的例子

在本文下面,您可以找到Visual Studio 2013与C ++ 11的DLL项目的链接。这意味着您需要Visual Studio 2013或更高版本才能查看它。

您需要使用两个静态库(zlib和mHook)构建DLL。 mHook是项目的一部分并自动链接到DLL,但您需要手动添加zlib以进行正确的配置。

如果您有x64操作系统,则需要查看您尝试拦截其流量的浏览器是使用x64还是x84指令集。浏览器和DLL使用的指令集是相同的,否则DLL将无法正常工作。

AppInit

AppInit的路径可能因系统架构而异(64位与32位操作系统)

赢得x64

对于x64应用程序,注册表路径如下:

"HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Windows"

这里我们需要将LoadAppInit_DLLs参数设置为1并在AppInit中设置DLL的路径。

对于x86应用程序,AppInit的注册表路径如下:

"HKEY_LOCAL_MACHINE \ SOFTWARE \ Wow6432Node \ Microsoft \ Windows NT \ CurrentVersion \ Windows"

您还需要将LoadAppInit_DLLs参数设置为true(1)。

赢得x86

"HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows NT \ CurrentVersion \ Windows"

您需要将LoadAppInit_DLLs参数设置为1并在AppInit中设置DLL的路径。

为了测试DLL,我们选择了网站http://www.unit-conversion.info/。注入DLL后,当加载网页时(仅当它未加密时),将显示该消息,如下面的图1所示。

用户模式HTTP流量修改的实际示例

图1. DLL注入后打开网站时显示的消息

内核模式

在本节中,我们将介绍如何通过内核驱动程序插入广告横幅。

这个怎么运作

Windows允许将驱动程序嵌入到数据传输协议的每个级别。在创建过滤器驱动程序时可以利用这一点。过滤器驱动程序的主要平台是:

  1. NDIS
  2. 世界粮食计划署

实施方法

您还可以查看我们关于网络监控的文章,了解有关此主题的更多信息。

NDIS

网络驱动程序接口规范(NDIS)是由Microsoft和3Com开发的接口规范,用于将网络适配器驱动程序嵌入到操作系统中。

驱动程序初始化是相当标准的 - 传输有关驱动程序的信息和调用NDIS的函数指针。

流量修改发生在FILTER_SEND_NET_BUFFER_LISTS回调函数内。每次从协议驱动程序调用NdisSendNetBufferLists函数时,都会从驱动程序调用此函数。修改方案需要由开发人员设置。需要手动指定所有过滤和修改。

驱动程序将从NET_BUFFER和NET_BUFFER_LIST结构接收数据,修改此数据,并在必要时将这些结构中的数据发送到其他驱动程序。

世界粮食计划署

Windows过滤平台(WFP)是一种通用网络过滤技术,涵盖从传输层(TCP / UDP)到数据链路层(以太网)的所有主要网络层,并为开发人员提供了许多有趣的功能。

在驱动程序初始化期间,传送关于驱动程序的信息,指向过滤器功能的指针以及用于调用所述功能的条件。这些条件包括将用于处理数据的数据传输协议层,连接方向,数据包方向,IP地址,端口等。由于系统调用了驱动程序,因此将其命名为callout驱动程序。驱动程序与之交互的请求和响应的数据位于NET_BUFFER和NET_BUFFER_LIST结构中。过滤所需的数据位于FWPS_FILTER和FWPS_CLASSIFY_OUT结构中。通过编辑FWPS_CLASSIFY_OUT结构来完成过滤。

我们为什么选择WFP

  • WFP旨在开发过滤和修改驱动程序。
  • 世界粮食计划署有充分的文

实施概述

线程层最适合修改HTTP响应。因此,我们需要在线程层注册callout驱动程序。

要访问所有流量,已添加条件:

  1. 用于传入连接
  2. 用于传出连接

为了分析和修改数据,我们只选择来自服务器的响应(传入的数据包)。我们跳过所有其他流量,让其他过滤器驱动程序处理它。

接下来,我们需要按照上一节中的描述分析和注入数据。

驱动程序处理数据时的常见情况是需要从多个单独的包中收集所有数据。为了避免任何问题,我们设置了一个标志,告诉WFP在这种情况下收集更多数据。

要在线程中注入新数据,我们创建一个新的NET_BUFFER_LIST和MDL。将MDL注入到线程中以代替旧数据,并阻止旧数据。临时资源是免费的。

如果在提取的数据中找不到搜索到的对象,则数据将在链中进一步发送。

实际的例子

要使驱动程序正常工作,您需要构建它,安装它,运行它并打开浏览器。

在构建过程中,将创建驱动程序包,其中包含以下内容:

  • 驱动程序安全目录(* .cat)
  • * inf文件
  • 驱动程序文件(* sys)
  • WdfCoinstallerXXXXX.dll
  • 安装驱动程序很简单:右键单击* inf文件并选择"安装"。

您可以通过两种方式启动驱动程序:

  1. 使用管理权限从控制台调用"net start driverName"。
  2. 在设备管理器中选择"查看→显示隐藏的设备",然后在"非即插即用驱动程序"类别中找到具有正确名称的驱动程序并启动它。

为了测试我们的驱动程序,我们使用了http://msn.com

网站。

图2显示了驱动程序启动之前的网站。

启动驱动程序后,从头开始加载页面(而不是简单刷新)时,会在显示页面之前显示带有广告的对话窗口,如图3所示。

使用自定义驱动程序修改HTTP流量之前的网站页面

图2:驱动程序启动之前的MSN页面

使用自定义驱动程序修改HTTP流量后的网站页面

图3:驱动程序启动后的MSN页面

内核模式与用户模式修改方法的比较

HTTP流量

用户模式

好处:

  • 与内核模式相比,需要更少的Windows编程知识
  • 用户模式中可能发生的最糟糕的事情是应用程序停止工作

缺点:

  • 与内核模式方法相比,需要更多内存来保存修改后的响应
  • 此方法仅涵盖图形应用程序
  • DLL注入所有应用程序,而不仅仅是基于网络的
  • DLL中的错误可能会影响它注入的所有应用程序
  • 某些防病毒软件和其他安全软件可以防止DLL注入或功能挂钩

内核模式

好处:

  • 捕获网络请求的文档化和合法方式
  • 需要更少的内存:只有在线程中插入新数据之后才需要额外的缓冲区内存

缺点:

  • 开发人员承担更大的责任:任何单个错误都可能导致BSOD
  • 要获得所有必要的知识,您需要阅读大量文档
  • 调试很困难,需要另一台计算机或虚拟机
  • 通常的静态库不适用,需要在内核模式下重建
  • 内核模式的第三方库较少

最后,用户模式和内核模式都用于在HTTP响应中注入数据,因此您的选择应基于对每种方法的优缺点的分析以及它们与手头的特定任务的关系。

查看英文原文

查看更多文章

公众号:银河系1号

联系邮箱:public@space-explore.com

(未经同意,请勿转载)