前言
监考程序需要对网络流量进行过滤,不允许除了考试网站以外的其他网站的访问。其实就是实现了一个小型的网关程序,一般地有驱动实现和非驱动实现两种方式。本文只针对一款简易的非驱动实现的监考程序进行分析。
注意:本文通过对某考试监考网关客户端程序的逆向分析研究,节选其中断网的实现过程进行讲解。所有内容均在本地计算机测试环境中完成,敏感标记均已加码处理,仅供参考,谢绝用于其他用途。
系列文章:
编号 | 文章标题 | ID |
1 | 非驱动考试网关程序(一):断网模块 | 136037076 |
2 | 非驱动考试网关程序(二):USB热拔插 | (暂未发布) |
一、初步分析调用树
初步分析时,分析软件是否依赖相应的 Windows 接口,如果有,那么问题就会非常简单。我们使用 API Monitor 程序实现对程序的监视。

在监视时,我们发现了关键的函数:GetIpForwardTable、DeleteIpForwardEntry、GetIpForwardTable2、DeleteIpForwardEntry2、CreateIpForwardEntry。
GetIpForwardTable 函数检索 IPv4 路由表。DeleteIpForwardEntry 函数删除本地计算机的 IPv4 路由表中的现有路由,而 DeleteIpForwardEntry2 则是 IPv6 版本。CreateIpForwardEntry 则用于恢复路由表。
由此,我们可以推测程序通过修改路由表实现断网的功能,这应该是非常简单了。
二、通过 IDA 深入分析
确定了关键的调用接口,我们接下来就要通过 IDA 去分析程序的具体实现。
我们从导入表很快定位到这个调用的具体实现,关键调用在 sub_4028E0 中(记住这个 IDA 标记的函数名)。
首先通过一次调用 GetIpForwardEntry 获取需要为 MIB_IPFORWARDROW 结构分配的内存的大小,随后,再次调用 GetIpForwardEntry 获取 IPv4 路由表。

随后就是遍历这个链表:

在遍历过程中删除路由:

最后通过全局变量 pRoute 将原始的路由表存储起来:

随后通过 sub_402690 检测并尝试删除默认的 IPv6 路由:

sub_402690 的实现:

随后分析 CreateIpForwardEntry,有两次需要用到该函数,一种情况是在删除所有路由后,恢复一部分路由,还有一种就是在考试完成时,完全恢复路由。
恢复部分:

全部恢复(通过全局变量 pRoute):

至此,我们已经了解了该程序是如何实现考试断网的功能,但是我们知道只修改一次肯定不能防止之后其他程序恢复系统默认路由的。下面我们分析他是如何在考试期间防止路由恢复的。
我们注意到 API 监视日志中的 SetTimer 调用,以及窗口的消息线程中 GetMessageW 以每秒一次的频率测试 WM_TIMER 消息,于是猜测到程序可能通过计时器不停地拉闸删除 IPv4 路由。
然后,我们通过查找 SetTimer 的交叉引用,锁定了来源,这是程序的窗口回调:
我们注意到 SetTimer 设置 ID == 1 的计时器,时间间隔为 1 秒。

在点击关闭客户端时,才会处理 KillTimer 删除计时器。

继续往下看,我们发现出现 WM_TIMER 消息时候,程序执行删除路由的例程 sub_4028E0,这个函数是前面分析过的:

三、编写测试工具
分析了具体的程序的实现,接下来,我们可以尝试编写一款修改器模拟对抗对吧?
这里的方法就比较多了,毕竟该程序完全依赖 Win32 API,并且不做防护。
提供两种思路如下:
思路1:在程序启动时挂钩 DeleteIpForwardEntry 和 DeleteIpForwardEntry2 ,不做任何操作并返回成功。
思路2:在程序启动前备份路由表,程序启动后任意时间均可挂钩 GetMessageW 并拦截 WM_TIMER 消息(wPararm == 1),在消息中 KillTimer,或者不做任何操作均可。随后通知挂钩程序恢复原始路由表。
提供一份挂钩例程的 Dll 代码(不包含编程恢复原始路由表的处理代码):
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"#include <Windows.h>
#include "detours.h"
#include <ipmib.h>
#include <string>
#include <iphlpapi.h>
#include <atlstr.h>#pragma comment(lib, "detours.lib")
#pragma comment( lib, "Iphlpapi.lib" )PVOID pFunGetMessageW = NULL;typedef BOOL(WINAPI* __GetMessageW)(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax
);BOOL WINAPI HookedGetMessageW(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax
);BOOL APIENTRY InstallHook();
BOOL APIENTRY UnInstallHook();BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:InstallHook();break;case DLL_THREAD_ATTACH:break;case DLL_THREAD_DETACH:break;case DLL_PROCESS_DETACH:UnInstallHook();break;}return TRUE;
}BOOL APIENTRY InstallHook()
{DetourTransactionBegin();DetourUpdateThread(GetCurrentThread());pFunGetMessageW = DetourFindFunction("User32.dll", "GetMessageW");DetourAttach(&pFunGetMessageW, HookedGetMessageW);LONG ret = DetourTransactionCommit();return ret == NO_ERROR;
}BOOL APIENTRY UnInstallHook()
{DetourTransactionBegin();DetourUpdateThread(GetCurrentThread());DetourDetach(&pFunGetMessageW, HookedGetMessageW);LONG ret = DetourTransactionCommit();return ret == NO_ERROR;
}BOOL WINAPI HookedGetMessageW(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax
)
{// 判断是否是我们要取消的计时器if (lpMsg->wParam == 1 && lpMsg->message == WM_TIMER){// 删除计时器,解除路由检查lpMsg->message = WM_USER;KillTimer(lpMsg->hwnd, lpMsg->wParam);return TRUE;}return ((__GetMessageW)pFunGetMessageW)(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
}
路由备份和检查的代码可以参考下面的代码:
CString SwitchStatusStr(int iSwitch)
{CString strSwitch;strSwitch.Format(_T("%d"), iSwitch);return strSwitch;
}BOOL GetIsRestoreDefaultRoute(DWORD bToDeleteorAdd)
{BOOL bRet = FALSE;// Declare and initialize variablesPMIB_IPFORWARDTABLE pIpForwardTable = NULL;PMIB_IPFORWARDROW pRow = NULL; // 默认网关,添加时使用DWORD dwSize = 0;BOOL bOrder = FALSE;DWORD dwStatus = 0;// 默认路由保存路径std::wstring wsFilePath = L"C:\\DefaultRoute.ini";// Identify the required size of the buffer.if (bToDeleteorAdd == 0){dwStatus = GetIpForwardTable(pIpForwardTable, &dwSize, bOrder);if (dwStatus == ERROR_INSUFFICIENT_BUFFER){// Allocate memory for the table.if (!(pIpForwardTable = (PMIB_IPFORWARDTABLE)malloc(dwSize))){printf(("pipForwardTable Malloc failed. Out of memory.\n"));goto _END_;}// Retrieve the table.dwStatus = GetIpForwardTable(pIpForwardTable, &dwSize, bOrder);}if (dwStatus != ERROR_SUCCESS){printf(("getIpForwardTable failed.\n"));if (pIpForwardTable){free(pIpForwardTable);}goto _END_;}// Search for the required row in the table. The default gateway has a destination// of 0.0.0.0. Be aware the table continues to be searched, but only// one row is copied. This is to ensure that, if multiple gateways exist, all of them are deleted.for (DWORD i = 0; i < pIpForwardTable->dwNumEntries; i++){if (pIpForwardTable->table[i].dwForwardDest == 0){// Delete the old default gateway entry.if (bToDeleteorAdd == 0){if (!pRow) {// Allocate some memory to store the row in; this is easier than filling// in the row structure ourselves, and we can be sure we change only the// gateway address.pRow = (PMIB_IPFORWARDROW)malloc(sizeof(MIB_IPFORWARDROW));if (!pRow) {printf("Malloc failed. Out of memory.\n");exit(1);}// Copy the rowmemcpy(pRow, &(pIpForwardTable->table[i]),sizeof(MIB_IPFORWARDROW));}// 先保存默认路由相关信息printf("开始保存默认路由\n");WritePrivateProfileString(L"RECOVER", L"dwForwardDest", SwitchStatusStr(pRow->dwForwardDest), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMask", SwitchStatusStr(pRow->dwForwardMask), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardPolicy", SwitchStatusStr(pRow->dwForwardPolicy), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardNextHop", SwitchStatusStr(pRow->dwForwardNextHop), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardIfIndex", SwitchStatusStr(pRow->dwForwardIfIndex), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardType", SwitchStatusStr(pRow->dwForwardType), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardProto", SwitchStatusStr(pRow->dwForwardProto), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardAge", SwitchStatusStr(pRow->dwForwardAge), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardNextHopAS", SwitchStatusStr(pRow->dwForwardNextHopAS), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMetric1", SwitchStatusStr(pRow->dwForwardMetric1), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMetric2", SwitchStatusStr(pRow->dwForwardMetric2), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMetric3", SwitchStatusStr(pRow->dwForwardMetric3), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMetric4", SwitchStatusStr(pRow->dwForwardMetric4), wsFilePath.c_str());WritePrivateProfileString(L"RECOVER", L"dwForwardMetric5", SwitchStatusStr(pRow->dwForwardMetric5), wsFilePath.c_str());// 删除默认路由printf("保存默认路由成功。\n");}bRet = TRUE;goto _END_;}}}else if (bToDeleteorAdd == 1) // 恢复默认路由{printf("正在恢复默认路由......\n");pRow = (PMIB_IPFORWARDROW)malloc(sizeof(MIB_IPFORWARDROW));pRow->dwForwardDest = GetPrivateProfileIntW(L"RECOVER", L"dwForwardDest", -1, wsFilePath.c_str());pRow->dwForwardMask = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMask", -1, wsFilePath.c_str());pRow->dwForwardPolicy = GetPrivateProfileIntW(L"RECOVER", L"dwForwardPolicy", -1, wsFilePath.c_str());pRow->dwForwardNextHop = GetPrivateProfileIntW(L"RECOVER", L"dwForwardNextHop", -1, wsFilePath.c_str());pRow->dwForwardIfIndex = GetPrivateProfileIntW(L"RECOVER", L"dwForwardIfIndex", -1, wsFilePath.c_str());pRow->dwForwardType = GetPrivateProfileIntW(L"RECOVER", L"dwForwardType", -1, wsFilePath.c_str());pRow->dwForwardProto = GetPrivateProfileIntW(L"RECOVER", L"dwForwardProto", -1, wsFilePath.c_str());pRow->dwForwardAge = GetPrivateProfileIntW(L"RECOVER", L"dwForwardAge", -1, wsFilePath.c_str());pRow->dwForwardNextHopAS = GetPrivateProfileIntW(L"RECOVER", L"dwForwardNextHopAS", -1, wsFilePath.c_str());pRow->dwForwardMetric1 = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMetric1", -1, wsFilePath.c_str());pRow->dwForwardMetric2 = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMetric2", -1, wsFilePath.c_str());pRow->dwForwardMetric3 = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMetric3", -1, wsFilePath.c_str());pRow->dwForwardMetric4 = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMetric4", -1, wsFilePath.c_str());pRow->dwForwardMetric5 = GetPrivateProfileIntW(L"RECOVER", L"dwForwardMetric5", -1, wsFilePath.c_str());// Create a new route entry for the default gateway.dwStatus = CreateIpForwardEntry(pRow);printf("恢复默认路由成功。\n");}_END_:if (pRow){free(pRow);}// Free the memory. if (pIpForwardTable){free(pIpForwardTable);}return bRet;
}
以上代码仅供学习参考使用,请勿用于其他用途。
四、总结&后续更新
其实没有什么好总结的。主要建议就是不要直接用微软接口,如果你做监考程序的话最好还是用驱动,用自己的实现,不然的话 R3 的挂钩就就全完了。当然,我们需要秉持着诚信考试的原则。
【保留备用】
发布于:2024.02.06;更新于:2024.02.06。