在本文中,您可以学习如何拦截HTTP流量,以便将自定义代码注入Windows HTML标记。 为此,有两种完全不同的方法:一种采用内核模式,另一种采用用户模式。 为了简单起见,将没有掩盖的HTTPS流量。
本文用作代码示例的详细示例,以更广泛地讨论用户模式与内核模式实现比较。
HTTP响应数据注入算法在开始讨论不同的方法之前,让我们看一下将数据注入HTTP响应的算法。
该算法包括以下数据:
响应的初始HTTP响应正文(不应为空)
所有数据均通过qzip压缩算法压缩,并位于单个存档中。 首先,我们需要分析响应的标题并搜索以下字段:
传输编码:分块如果存在此标头,则需要处理第一个块。
如果找不到此标头,则需要查找Content-Length。 必须使用Content-Length标头的值来确认已接收到所有数据并将该数据与新值挂钩。
内容编码:gzip –显示数据是否已存档。
“ \ r \ n \ r \ n”字符串–显示标题结束处和数据开始处。
搜索完这些字段后,我们将提取数据。 接下来,我们搜索<!DOCTYPE HTML>标记,并在其后插入以下脚本:
<script language=\"JavaScript\">
if (confirm(\"Do you want to read more at https:⁄⁄www.apriorit.com⁄dev-blog ?\"))
{
window.open(\"https:⁄⁄www.apriorit.com⁄dev-blog\");
}
<⁄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函数,您可以通过DLL注入为窗口应用程序设置挂钩过程。 当向过程窗口发送消息时,注入发生
SetWindowsHookEx函数已被应用拦截。 消息的类型是在调用SetWindowsHookEx时确定的,这使我们可以为所有图形应用程序设置挂钩过程。
这种方法的优点:- 可以涵盖单个图形应用程序或所有应用程序
- 仅当截获特定消息时才会进行注入
- 执行此功能的应用程序需要由用户启动
- 控制台应用程序不使用单独的窗口,因此不受影响
此方法基于使用CreateRemoteThread函数的功能,该函数允许您在另一个进程中创建远程线程。 通过CreateRemoteThread传输的函数的签名应如下所示:
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一个唯一的名称,并将其添加到该应用程序的可执行模块的import部分。 但是,这需要可移植可执行(PE)格式的高级知识。
您可以在此处找到有关类似解决方案的信息。
这种方法的优点:
- 特洛伊木马DLL仅需挂接一次,之后即可自行运行
- 无需管理权限
- 木马DLL可以执行两项任务:DLL注入和函数挂钩
这种方法的缺点:
- 必须具备PE格式的高级知识
- 数字签名可能会导致系统DLL挂钩问题
我们的任务需要尽可能多的接收和显示HTML内容的应用程序的支持。 因此,我们不能使用CreateRemoteThread函数。 木马DLL注入需要PE格式的知识。 因此,我们可以在SetWindowsHookEx函数和通过AppInit注册表值注入之间进行选择。 SetWindowsHookEx方法的唯一优点是它不需要管理权限。 同时,AppInit提供了注入DLL的最简单方法,并将自动与所有图形应用程序一起使用。 由于我们需要使用HTML和JavaScript捕获网络流量,因此我们需要涵盖所有使用图形外壳的浏览器。 因此,在本文中,我们将通过AppInit注册表值介绍DLL注入。
挂钩功能的方法就我们的任务而言,有两种方法可以钩住一个函数:
- 更改PE文件导入表
- 更改功能的开头
每个PE文件都有一个导入表,该表存储用于从DLL导入并由DLL的PE文件使用的功能的虚拟内存地址。 通过访问地址空间,可以通过更改函数指针以指向我们自己的自定义函数来修改导入表。 这种方法需要广泛的PE格式知识,因为那里没有现成的解决方案,例如库或WinAPI函数。 但是,可以在网上找到许多这种方法的概念证明。
更改功能的开始这种方法基于修改进程地址空间(确切地说是函数的开头),我们需要将其更改为函数的JMP。 这种方法在积极支持的开源MHook库中实现。 而且,通过这种方法,您仍然可以使用原始功能。
选择如何注入功能更改函数的开头依赖于方便且受积极支持的库,因此这是我们选择使用的方法。
实施方法概述
我们将从Ws2_32.dll中获取recv函数,因为所有浏览器都使用该函数从网络接收数据。 首先,我们需要接收指向原始函数的指针并将其保存在全局变量中。 可以通过以下方式完成:
typedef int(WINAPI* _recv)(
_In_ SOCKET s,
_Out_ char *buf,
_In_ int len,
_In_ int flags
);
static _recv TrueRecv = (_recv)GetProcAddress(GetModuleHandle(L"Ws2_32.dll"), "recv");
此后,在将DLL加载到进程中的过程中,我们需要在DllMain中钩住recv并在卸载过程中将其解钩。
因此,DllMain函数将如下所示:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
if (TrueRecv)
Mhook_SetHook((PVOID*)(&TrueRecv), InjectedRecv);// replacing recv with InjectRecv
break;
}
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
if (TrueRecv)
Mhook_Unhook((PVOID*)(&TrueRecv));// unhooking recv function
break;
}
return TRUE;
}
在InjectedRecv函数中,真实的recv函数通过TrueRecv调用并处理结果。
让我们再来看一下recv函数签名:
int recv(
_In_ SOCKET s,
_Out_ char *buf,
_In_ int len,
_In_ int flags
);
在recv函数的所有参数中,buf和len对我们来说最有趣。
buf表示len大小的缓冲区,在调用recv函数之后,该缓冲区包含通过套接字接收的len或更小的响应。
确切的大小可以由返回的值确定,这可能导致许多相关的问题:
- 将自定义JavaScript代码添加到HTML后,数据大小可能会超过buf缓冲区的大小。
- recv不需要立即返回整个响应,尤其是在缓冲区没有足够的内存的情况下。
- 许多服务以qzip格式返回响应,因为它减小了服务器的大小和等待时间。 在这种情况下,需要解压缩qzip,将JavaScript注入HTML,然后再将HTML压缩回qzip。 这种情况可以与上述两个问题结合在一起。 在第一种情况下,将超出buf缓冲区的大小。 在第二种情况下,不完整的qzip的解压缩会在客户端代码中产生不确定的行为,因为压缩后qzip的单个部分将变成整个qzip,而所有其余部分将在客户端代码中显示为二进制文件,而不会处理。
为了解决这个问题,在客户端代码调用recv期间,我们需要在后台调用一个或几个recv函数以收集完整的数据并将其保存在容器中。 此后,可以使用将JavaScript注入HTML的算法。 修改后的缓冲区存储在静态容器中。 该容器是一个std :: map,其中包含以下内容:
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或更小的部分。 将所有数据传输到客户端代码后,将清除读取字节的缓冲区和计数器。
如果当前套接字中没有数据,则执行对real recv的调用。 我们假定对于带有HTML的HTTP请求,答案的第一部分具有确定答案正文大小的所有必要信息。 基于此信息,我们可以调用真实的recv,直到获得所有数据或发生错误为止。 如果出现错误,我们可以保存接收的数据而无需修改,然后在g_bufferedData中有数据的情况下,按照前面情况的步骤进行操作。 然后,我们可以修改HTML,然后根据数据的大小和len参数完全或部分返回数据。
实际例子
在本文下面,您可以找到带有C ++ 11的Visual Studio 2013的DLL项目的链接。 这意味着您需要Visual Studio 2013或更高版本才能查看它。
您需要使用两个静态库– zlib和mHook构建DLL。 mHook是项目的一部分,它会自动链接到DLL,但是您需要手动添加zlib才能进行正确的配置。
如果您使用的是x64操作系统,则需要查看您要拦截其访问量的浏览器是使用x64还是x84指令集。 浏览器和DLL使用的指令集必须相同,否则DLL将无法工作,这一点很重要。
AppInitAppInit的路径可能因系统架构(64位和32位OS)而异。
赢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允许将驱动程序嵌入到数据传输协议的每个级别。 创建过滤器驱动程序时可以利用它。 筛选器驱动程序的主要平台是:
- NDIS
- 世界粮食计划署
您也可以查看我们关于以下内容的文章
网络监视以获取有关此主题的更多信息。 NDIS网络驱动程序接口规范(NDIS)是由Microsoft和3Com开发的接口规范,用于将网络适配器驱动程序嵌入到操作系统中。
驱动程序初始化是相当标准的-有关驱动程序的信息以及指向将调用NDIS的函数的指针。
流量修改发生在FILTER_SEND_NET_BUFFER_LISTS回调函数中。 每次从协议驱动程序调用NdisSendNetBufferLists函数时,都会从驱动程序中调用此函数。 修改方案需要由开发人员设置。 所有过滤和修改都需要手动指定。
驱动程序将从NET_BUFFER和NET_BUFFER_LIST结构接收数据,修改此数据,并在必要时将这些结构中的数据发送给其他驱动程序。
世界粮食计划署Windows筛选平台(WFP)是一种通用的网络筛选技术,涵盖了从传输层(TCP / UDP)到数据链路层(以太网)的所有主要网络层,并为开发人员提供了许多有趣的功能。
在驱动程序初始化期间,将传输有关驱动程序,指向过滤器函数的指针以及调用该函数的条件的信息。 这些条件包括将用于处理数据的数据传输协议层,连接方向,数据包方向,IP地址,端口等。 由于驱动程序是由系统调用的,因此它被称为标注驱动程序。 与驱动程序交互的请求和响应的数据位于NET_BUFFER和NET_BUFFER_LIST结构中。 过滤所需的数据位于FWPS_FILTER和FWPS_CLASSIFY_OUT结构中。 通过编辑FWPS_CLASSIFY_OUT结构来完成过滤。
我们为什么选择世界粮食计划署- WFP专为开发筛选和修改驱动程序而设计。
- 粮食计划署有据可查。
线程层最适合用于修改HTTP响应。 因此,我们需要在线程层注册标注驱动程序。
为了访问所有流量,已添加条件:
- 对于传入连接
- 对于传出连接
为了分析和修改数据,我们仅选择来自服务器的响应(传入数据包)。 我们跳过所有其他流量,让其他过滤器驱动程序处理它。
接下来,我们需要按照上述部分中的说明分析和注入数据。
驱动程序使用数据时,通常的情况是需要从几个单独的程序包中收集所有数据。 为了避免出现任何问题,我们设置了标志来告知WFP在这种情况下收集更多数据。
为了在线程中注入新数据,我们创建了一个新的NET_BUFFER_LIST和MDL。 MDL代替旧数据注入线程中,并且旧数据被阻止。 临时资源是免费的。
如果在提取的数据中找不到要搜索的对象,则将数据进一步发送到链下。
实际例子为了使驱动程序正常工作,您需要对其进行构建,安装,运行并打开浏览器。
在构建过程中,将创建驱动程序包,其中包含以下内容:
- 驱动程序安全目录(* .cat)
- * inf文件
- 驱动程序文件(* sys)
- WdfCoinstallerXXXXX.dll
- 安装驱动程序很简单:右键单击* inf文件,然后选择“安装”。
您可以通过两种方式启动驱动程序:
- 从具有管理权限的控制台中调用“ net start driverName”。
- 在设备管理器中选择“查看→显示隐藏的设备”,然后在“非即插即用驱动程序”类别中找到具有正确名称的驱动程序并启动它。
为了测试我们的驱动程序,我们使用了
http://msn.com网站。图2显示了启动驱动程序之前的网站。
驱动程序启动后,从头开始加载页面(而不是简单地刷新)时,在显示页面之前会显示一个带有广告的对话框窗口,如图3所示。
使用自定义驱动程序修改HTTP流量之前的网站页面
图2:启动驱动程序之前的MSN页面
使用自定义驱动程序修改HTTP流量后的网站页面
图3:启动驱动程序后的MSN页面
内核模式和用户模式修改 HTTP流量用户模式的方法比较
优点:
- 与内核模式相比,所需的Windows编程知识更少
- 用户模式下最糟糕的事情是应用程序停止工作
缺点:
- 与内核模式方法相比,需要更多的内存来保存修改后的响应
- 此方法仅涵盖图形应用程序
- DLL被注入所有应用程序中,不仅基于网络
- DLL中的错误可能会影响注入它的所有应用程序
- 某些防病毒软件和其他安全软件可以阻止DLL注入或功能挂钩
优点:
- 记录网络请求的合法方式
- 需要更少的内存:仅在线程中插入新数据之前需要额外的缓冲内存
缺点:
- 开发人员的责任更大:任何单个错误都可能导致BSOD
- 为了获得所有必要的知识,您需要阅读很多文档。
- 调试很困难,需要另一台计算机或虚拟机
- 常规静态库不适用,需要在内核模式下重建
- 内核模式的第三方库较少
最后,用户模式和内核模式都可以在HTTP响应中注入数据,因此,您的选择应基于对每种方法的优缺点以及它们与当前特定任务的关系的分析。