lonelyperson的文章

  • 使用ReadDirecotryChangesW函数实现文件监控

    背景在我没有了解 ReadDirecotryChangesW 这个目录监控函数之前,一直认为要想实现计算机上的文件监控,能够监控计算机上每个文件的改动,是一件极其困难的事情,曾经自己也细想过,但都没有什么好的思绪。不过,事实上,文件监控的确是一件比较复杂的事情。好在Windows为我们提供了一个功能强大,但是使用较为方便的函数接口,这边是我们这篇文章要讲解的 ReadDirecotryChangesW 函数。
    使用 ReadDirecotryChangesW 函数,可以实现对目录及其目录中文件的实时监控,可以有效地发现文件被改动的情况。现在,我就把这个文件监控小程序实现的原理及其过程整理成文档,分享给大家。
    函数声明ReadDirecotryChangesW 函数
    检索描述指定目录中更改的信息,但不会报告对指定目录本身的更改。
    函数声明
    BOOL WINAPI ReadDirectoryChangesW( _In_ HANDLE hDirectory, _Out_ LPVOID lpBuffer, _In_ DWORD nBufferLength, _In_ BOOL bWatchSubtree, _In_ DWORD dwNotifyFilter, _Out_opt_ LPDWORD lpBytesReturned, _Inout_opt_ LPOVERLAPPED lpOverlapped, _In_opt_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
    参数

    hDirectory [in]要监视的目录的句柄。必须使用FILE_LIST_DIRECTORY访问权限打开此目录。
    lpBuffer [out]指向要读取结果的DWORD对齐的格式化缓冲区的指针。该缓冲区的结构由FILE_NOTIFY_INFORMATION结构定义。这个缓冲区可以同步或异步地进行填充,这取决于目录的打开方式以及给予lpOverlapped参数的值。有关详细信息,请参阅备注部分。
    nBufferLength [in]lpBuffer参数指向的缓冲区的大小(以字节为单位)。
    bWatchSubtree [in]如果此参数为TRUE,则该函数将监视以指定目录为根的目录树。如果此参数为FALSE,则该功能仅监视hDirectory参数指定的目录。
    dwNotifyFilter [in]函数检查以确定等待操作是否已满足过滤条件。此参数可以是以下值中的一个或多个:




    VALUE
    MEANING




    FILE_NOTIFY_CHANGE_FILE_NAME
    监视目录或子树中的任何文件名更改导致更改通知等待操作返回。 更改包括重命名,创建或删除文件


    FILE_NOTIFY_CHANGE_DIR_NAME
    监视目录或子树中的任何目录名更改导致更改通知等待操作返回。 更改包括创建或删除目录


    FILE_NOTIFY_CHANGE_ATTRIBUTES
    监视目录或子树中的任何属性更改导致更改通知等待操作返回


    FILE_NOTIFY_CHANGE_SIZE
    被监视目录或子树中的任何文件大小更改导致更改通知等待操作返回。 仅当文件写入磁盘时,操作系统才能检测文件大小的更改。 对于使用大量缓存的操作系统,仅当高速缓存充分刷新时才会发生检测


    FILE_NOTIFY_CHANGE_LAST_WRITE
    对监视目录或子树中文件的上次写入时间的任何更改都会导致更改通知等待操作返回。 只有当文件写入磁盘时,操作系统才会检测到最后写入时间的更改。 对于使用大量缓存的操作系统,仅当高速缓存充分刷新时才会发生检测


    FILE_NOTIFY_CHANGE_LAST_ACCESS
    对监视目录或子树中文件的最后访问时间的任何更改都会导致更改通知等待操作返回


    FILE_NOTIFY_CHANGE_CREATION
    对被监视目录或子树中的文件的创建时间的任何更改都会导致更改通知等待操作返回


    FILE_NOTIFY_CHANGE_SECURITY
    监视目录或子树中的任何安全描述符更改导致更改通知等待操作返回




    lpBytesReturned [out,optional]对于同步调用,此参数接收传输到lpBuffer参数的字节数。 对于异步调用,此参数未定义。 您必须使用异步通知技术来检索传输的字节数。
    lpOverlapped [in,out,optional]指向OVERLAPPED结构的指针,提供在异步操作期间要使用的数据。 否则,此值为NULL。 该结构的Offset和OffsetHigh成员未被使用。
    lpCompletionRoutine [in,optional]指向完成例程的指针,当操作已经完成或取消并且调用线程处于可警告的等待状态时被调用。 有关此完成例程的更多信息,请参阅FileIOCompletionRoutine。

    返回值

    如果函数成功,则返回值不为零。 对于同步调用,这意味着操作成功。 对于异步调用,这表示操作成功排队。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现过程1. 打开目录,获取文件句柄首先,我们需要根据目录路径,调用 CreateFile 函数来打开目录,获取文件句柄,因为下面的调用的 ReadDirecotryChangesW 函数需要用到这个文件句柄。根据上面函数介绍,文件句柄必须要有 FILE_LIST_DIRECTORY 权限,所以要创建 FILE_LIST_DIRECTORY 权限的文件句柄。而且,要获取目录的句柄,需要以 FILE_FLAG_BACKUP_SEMANTICS 为标志调用 CreateFile 函数。同时要注意目录路径,最后是反斜杠\:
    // 打开目录, 获取文件句柄 HANDLE hDirectory = ::CreateFile( pszDirectory, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (INVALID_HANDLE_VALUE == hDirectory) { ShowError("CreateFile"); return 1; }
    2. 设置目录监控然后,我们可以调用 ReadDirecotryChangesW 函数设置目录监控。其中,第 1 个参数表示监控目录句柄;第 2 个参数表示输出缓冲区;第 3 个参数表示输出缓冲区大小;第 4 个参数表示是否监控指定目录下的文件及其子目录下的文件,TRUE,则监控,FALSE则表示只监控指定目录下的文件;第 5 个参数表示操作过滤,本文只监控文件名更改、属性更改以及最后一次写入更改操作;第 6 个参数表示返回缓冲区的字节数;第 7 、第 8 个参数为NULL。
    // 设置监控目录 bRet = ::ReadDirectoryChangesW(hDirectory, pFileNotifyInfo, dwBufferSize, TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_LAST_WRITE, &dwRet, NULL, NULL); if (FALSE == bRet) { ShowError("ReadDirectoryChangesW"); break; }
    3. 判断文件操作类型并将宽字节文件名字符串转成多字节字符串表示只要有满足设置条件的文件操作,ReadDirectoryChangesW 立马返回信息,并将返回的信息返回到输出缓冲区中,返回数据是按 FILE_NOTIFY_INFORMATION 结构返回的,所以,我们直接按照 FILE_NOTIFY_INFORMATION 结构解析数据。在 FILE_NOTIFY_INFORMATION 结构中,4字节整型变量 Action 表示操作类型,宽字节字符串变量 FileName 表示更改文件的文件名。
    // 将宽字符转换成窄字符 W2C((wchar_t *)(&pFileNotifyInfo->FileName), pFileNotifyInfo->FileNameLength, szTemp, MAX_PATH); // 判断操作类型并显示 switch (pFileNotifyInfo->Action) { case FILE_ACTION_ADDED: { // 新增文件 printf("[File Added Action]%s\n", szTemp); break; } default: { break; }
    其中,我们是调用 WideCharToMultiByte 函数,将宽字节字符串转为多字节字符串。
    获取了一个文件操作之后,还要继续循环设置。如此,才能获取下一个文件操作。到此,我们监控文件操作原理就结束了。
    由于目录监控是需要不停循环调用 ReadDirecotryChangesW 函数进行设置的,所以,如果把这段代码放在主线程,可能会导致程序卡住,所以,我们可以创建一个文件监控的多线程,把文件监控这部分的实现代码放到多线程中,就可以解决主线程阻塞的问题。
    // 创建文件监控多线程 ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MonitorFileThreadProc, pszDirectory, 0, NULL);
    编码实现宽字节文件名字符串转成多字节字符串// 宽字节字符串转多字节字符串void W2C(wchar_t *pwszSrc, int iSrcLen, char *pszDest, int iDestLen){ ::RtlZeroMemory(pszDest, iDestLen); // 宽字节字符串转多字节字符串 ::WideCharToMultiByte(CP_ACP, 0, pwszSrc, (iSrcLen / 2), pszDest, iDestLen, NULL, NULL);}
    文件监控// 目录监控多线程UINT MonitorFileThreadProc(LPVOID lpVoid){ char *pszDirectory = (char *)lpVoid; // 打开目录, 获取文件句柄 HANDLE hDirectory = ::CreateFile(pszDirectory, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (INVALID_HANDLE_VALUE == hDirectory) { ShowError("CreateFile"); return 1; } char szTemp[MAX_PATH] = { 0 }; BOOL bRet = FALSE; DWORD dwRet = 0; DWORD dwBufferSize = 2048; // 申请一个足够大的缓冲区 BYTE *pBuf = new BYTE[dwBufferSize]; if (NULL == pBuf) { ShowError("new"); return 2; } FILE_NOTIFY_INFORMATION *pFileNotifyInfo = (FILE_NOTIFY_INFORMATION *)pBuf; // 开始循环设置监控 do { ::RtlZeroMemory(pFileNotifyInfo, dwBufferSize); // 设置监控目录 bRet = ::ReadDirectoryChangesW(hDirectory, pFileNotifyInfo, dwBufferSize, TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_LAST_WRITE, &dwRet, NULL, NULL); if (FALSE == bRet) { ShowError("ReadDirectoryChangesW"); break; } // 将宽字符转换成窄字符 W2C((wchar_t *)(&pFileNotifyInfo->FileName), pFileNotifyInfo->FileNameLength, szTemp, MAX_PATH); // 判断操作类型并显示 switch (pFileNotifyInfo->Action) { case FILE_ACTION_ADDED: { // 新增文件 printf("[File Added Action]%s\n", szTemp); break; } default: { break; } } } while (bRet); // 关闭句柄, 释放内存 ::CloseHandle(hDirectory); delete[] pBuf; pBuf = NULL; return 0;}
    程序测试现在,根据上面的实现原理,我们将开发好的程序直接运行。本文的例子,是监控”C:\\Users\\DemonGan\\Desktop\\temp\\“目录的文件增加情况。所以,我们复制一个文件到在此目录下,程序成功实时显示目录中的变化信息:

    总结这个程序在普通权限下就可以顺利执行,监控指定目录的文件。但是,需要特别注意一点就是,在使用 CreateFile 函数打开监控目录获取文件句柄的时候,文件目录路径字符串必须要在末尾加上反斜杠‘\’,例如C盘下的Windows目录,要写成“C:\\Windows\\”,不要写成“C:\\Windows”,这样会出错的!所以,一定要注意是以反斜杠‘\’作为结尾。
    而且目录监控的时候,是循环调用 ReadDirectoryChangesW 函数进行设置监控的,所以如果把它放在主线程上,会导致进程卡住,我们可以创建一个监控多线程,把监控部分的代码放到多线程上面去。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-12-02 09:30:15
  • 判断指定进程是否具有管理员权限

    背景某天,一个小伙伴突然问我,用什么程序或者软件可以查看计算机上进程是否具有管理员或者管理员以上的权限。我一下子懵了,印象中好像还真没有遇到过可以判断进程是否有管理员权限的软件。我运行了我电脑上的 Process Explorer 进行查看,好像确实没有检测管理员权限的功能。
    但是,作为一个动手能力较强的程序员,没有的程序可以自己写。于是,自己上网搜索了判断进程是否具有管理员权限的实现原理。原来,就是通过获取进程令牌的令牌特权提升信息,令牌特权提升信息中就专门有一个标志表示进程权限是否提升。如果进程权限已经提升,则说明是具有管理员或者管理员以上的权限,否则,只是普通权限。
    现在,我就把程序实现的过程及其原理进行整理,形成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId // 进程标示符);
    参数

    dwDesiredAccess [in]访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。要获取扩展错误信息,请调用GetLastError。

    OpenProcessToken 函数
    打开与进程关联的访问令牌。
    函数声明
    BOOL WINAPI OpenProcessToken( _In_ HANDLE ProcessHandle, _In_ DWORD DesiredAccess, _Out_ PHANDLE TokenHandle);
    参数

    ProcessHandle [in]要打开访问令牌的进程的句柄。 该进程必须具有PROCESS_QUERY_INFORMATION访问权限。DesiredAccess [in]指定一个访问掩码,指定访问令牌的请求类型。 这些请求的访问类型与令牌的自由访问控制列表(DACL)进行比较,以确定哪些访问被授予或拒绝。TokenHandle [出]指向一个句柄的指针,用于标识当函数返回时新打开的访问令牌。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    GetTokenInformation 函数
    获取有关访问令牌的指定类型的信息。 调用进程必须具有获取信息的适当访问权限。
    函数声明
    BOOL WINAPI GetTokenInformation( _In_ HANDLE TokenHandle, _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, _Out_opt_ LPVOID TokenInformation, _In_ DWORD TokenInformationLength, _Out_ PDWORD ReturnLength);
    参数

    TokenHandle [in]检索信息的访问令牌的句柄。如果TokenInformationClass指定TokenSource,则该句柄必须具有TOKEN_QUERY_SOURCE访问权限。对于所有其他TokenInformationClass值,句柄必须具有TOKEN_QUERY权限。TokenInformationClass[in]指定来自TOKEN_INFORMATION_CLASS枚举类型的值,以识别函数检索的信息类型。检查TokenIsAppContainer并返回0的任何呼叫者也应该验证呼叫者令牌不是识别级别的模拟令牌。如果当前令牌不是应用程序容器,而是身份级别令牌,则应返回AccessDenied。TokenInformation[out]指向缓冲区的指针,该函数填充所请求的信息。放入此缓冲区的结构取决于由TokenInformationClass参数指定的信息类型。TokenInformationLength[in]指定由TokenInformation参数指向的缓冲区的大小(以字节为单位)。如果TokenInformation为NULL,则此参数必须为 0。ReturnLength [out]指向一个变量的指针,该变量接收TokenInformation参数指向的缓冲区所需的字节数。如果该值大于TokenInformationLength参数中指定的值,则该函数将失败,并且不将数据存储在缓冲区中。如果TokenInformationClass参数的值为TokenDefaultDacl,并且令牌没有默认DACL,则该函数将ReturnLength指向的变量设置为sizeof(TOKEN_DEFAULT_DACL),并将TOKEN_DEFAULT_DACL结构的DefaultDacl成员设置为NULL。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现原理判断一个进程是否具有管理员权限,可以分成 4个步骤:

    首先,使用 OpenProcess 打开进程,并获取 PROCESS_ALL_ACCESS 权限的进程句柄
    然后,根据进程句柄,可以调用 OpenProcessToken 函数打开进程令牌,并获取具有 TOKEN_QUERY 权限的进程令牌。因为下面我们需要查询进程令牌的权限提升信息,所以要求进程令牌必须具有 TOKEN_QUERY 权限
    接着,我们就可以调用 GetTokenInformation 函数,获取令牌权限提信息 TokenElevation,获取的信息存储在 TOKEN_ELEVATION 结构体缓冲区里。TOKEN_ELEVATION 结构体的成员 TokenIsElevated 就表示进程令牌权限是否提升。TRUE,表示已经提升,则是管理员或者管理员以上的权限;FALSE,表示没有提升,则表示普通权限
    最后,我们关机进程句柄,返回判断结果

    编码实现// 判断指定进程是否具有管理员或者管理员以上的权限BOOL IsProcessRunAsAdmin(DWORD dwProcessId){ HANDLE hProcess = NULL; HANDLE hToken = NULL; TOKEN_ELEVATION tokenEle = {0}; BOOL bElevated = FALSE; DWORD dwRet = 0; BOOL bRet = FALSE; // 打开进程, 获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 打开进程令牌, 获取进程令牌句柄 bRet = ::OpenProcessToken(hProcess, TOKEN_QUERY, &hToken); if (FALSE == bRet) { ShowError("OpenProcessToken"); return FALSE; } // 获取进程令牌特权提升信息 bRet = ::GetTokenInformation(hToken, TokenElevation, &tokenEle, sizeof(tokenEle), &dwRet); if (FALSE == bRet) { ShowError("GetTokenInformation"); return FALSE; } // 获取进程是否提升的结果 bElevated = tokenEle.TokenIsElevated; // 关闭进程句柄 ::CloseHandle(hToken); return bElevated;}
    程序测试我们以普通权限运行 520.exe 程序,然后使用我们的程序进行判断,结果判断正确:

    然后,我们以管理员权限运行 520.exe 程序,并使用我们的程序进行判断,结果正确:

    总结要注意一点就是,我们运行自己的判断程序的时候,建议使用管理员权限运行。因为我们判断一个程序是否具有管理员权限之前,先调用 OpenProcess 函数打开进程,那么,低权限程序打开高权限的程序,执行这个函数就会因为权限不足而报错。所以,如果一开始我们就以管理员权限运行我们的判断程序,就可以避免这种情况的发生。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-07 11:37:51

发送私信

曾经输掉的东西,只要你想,就一定可以再一点一点赢回来

15
文章数
25
评论数
eject