QT程序监控不到拖拽事件如dragEnterEvent - Windows权限问题的解决方案

问题:当客户端已高完整性启动(例如启动客户端的进程是Bypass UAC启动的高完整性的进程,导致客户端继承了其高完整性),由于explorer.exe资源管理器是以中等Medium权限启动,客户端的权限较高,导致设置了qt编写的客户端设置了的setAcceptDrops(true)后依然无法触发dropEvent,导致无法接受其它程序或者资源管理器拖拽过来的文件。

解决方法与代码在文章的末尾。


完整性等级概念

强制完整性控制(Mandatory Integrity Control,MIC),它是对 discretionary access control list 的补充,并且是在 DACL 之前检查的
这是从 Windows Vista 新增的安全机制,在 Windows XP 中几乎所有的进程都是运行在管理员权限下的。
在官方文档中描述为 Windows 为其划分了四个完整性级别:低、中、高、系统;但从实际上看到的对应表中,有五个等级,多了一个不受信任等级。

具有低完整性级别的主体无法写入具有中等完整性级别的对象,这就相当于提供了一种在同一用户下,根据可信程度来限制不同进程之间的交互,低完整性等级的进程都无法对高完整性进程进行注入

虽然是限制了进程间的交互,但是高低完整性的进程还是可以通过其他的进程间通信的方式来进行交互:共享内存、Sockets、RPC、Windows 消息机制、命名管道,这些是不受限制的。

微软文档:https://learn.microsoft.com/zh-cn/previous-versions/dotnet/articles/bb625963(v=msdn.10)


完整性等级

Windows 直接使用了 SID 来定义完整性等级,这样就非常容易的将此机制集成到现有的结构当中,还不用修改代码
完整性等级所使用的 SID 格式是:S-1-16-xxxx(请与Session ID会话窗口ID区分开,并不是一个概念)

16 就是强制完整性的标识,后面的 xxxx,就是所对应的 RID,用来表示完整性等级的,这个值就是上面所提到的那几个十六进制值,它们以 0x1000 为间隔,也是为了将来能够再定义其他的等级
所以组合到一起以后完整性等级所对应的 SID 就变成了

  • System
    这是最高的完整性级别,由在本地服务、网络服务和系统账户下运行的进程和服务使用。此级别的目的是在管理员和系统之间提供一个安全层,即使以完全管理员身份运行的进程也无法与系统完整性级别进程交互
    唯一的例外情况是,如果管理员账户被授予 SE_DEBUG_NAME 权限,那么他就可以在 token 中启用这个权限,来进行交互
  • High
    分配给在管理员账户下运行的进程的默认完整性级别,如果启用了 UAC,则此级别将仅提供给提升过 UAC 权限的用户
  • Medium
    授予在非管理员用户账户下运行的进程或启用 UAC 的管理员账户上的进程。
    此完整性级别的进程只能修改 HKEY_CURRENT_USER、非受保护文件夹中的文件以及具有相同或更低完整性的进程。请注意explorer.exe是Medium等级
  • Low
    最低完整性级别默认不分配给进程,它要么通过继承,要么由父进程设置。
    以低完整性级别运行的进程只能在 HKEY_CURRENT_USER\Software\AppDataLow 下操作,或者将文件写入 %USERPROFILE%\AppData\LocalLow 目录下
    低完整性进程实际上不可能对系统进行任何更改,但仍然可以读取大部分的数据。
    在 Process Explorer 中可以查看到进程的完整性等级
    可以看到 Chrome 默认启动的是 Medium 等级的,其中还有 Low 等级的,这个可能就是沙盒用到的,给它们足够低的等级,能够最大限度的减少在出现问题时所带来的影响;而大量的不被信任的进程,有可能就是各个标签页所在的处理进程

其他注意:

1.进程是无法更改自己的完整性等级的
2.进程一旦运行,完整性等级是无法再修改了,即使是更高完整性等级的进程
3.进程只能够创建具有相同或者更低完整性等级的进程
4.进程不能修改或者写入具有更高完整性等级的进程或者文件

完整性等级的限制还有几个例外的情况
1.被授予 SE_DEBUG_NAME 权限的高完整性等级的进程可以修改更高完整性等级的进程
2.中等完整性的进程可以通过一些操作提升到高完整性等级,这就是平时的 Bypass UAC 的操作
3.进程可以请求从中等完整性提升到高完整性等级,这个只能在执行的时候发生,会弹出 UAC 的提示让用户来选择


使用 Process Explorer 查看进程完整性的等级:

点击View -> Select Columns -> Process Image -> 选择Integrity Level


解决方法:

方法1:启动客户端时,使用 CreateProcessAsUser 函数启动,复制父进程的Token,调用 SetTokenInformation 修改其中的完整性等级为Medium,再付给要启动的客户端进程,这样启动的客户端就具有Medium等级了。

修改方式也有两种,一种是使用 ConvertStringSidToSid 将文本转换成对应的SID,一种是调用 AllocateAndInitializeSid 直接创建初始化对应等级的SID

知名安全标识符 (SID)对应文本

S-1-16-0      不受信任的强制等级
S-1-16-4096    低强制级别
S-1-16-8192    中强制级别
S-1-16-12288    高强制级别
S-1-16-16384    系统强制级别

(1)使用ConvertStringSidToSid

#include <Windows.h>
#include <sddl.h>
  
static void launch_notepad_as_user(HANDLE token) {
  PROCESS_INFORMATION pi;
  STARTUPINFO si;
  
  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof( si );
  if (CreateProcessAsUser(token, TEXT("C:\\Windows\\Notepad.exe"), NULL, NULL,
                          NULL, FALSE, 0, NULL, NULL, &si, &pi )) {
    /* Process has been created; work with the process and wait for it to
       terminate. */
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
  }
}
  
static BOOL adjust_token_integrity_level(HANDLE &token, const char *sid) {
  /* Convert the string SID to a SID *, then adjust the token's
     privileges. */
  BOOL ret;
  PSID psd = NULL;
  if (ConvertStringSidToSidA(sid, &psd)) {    // 将"S-1-16-8192"换算成SID
    TOKEN_MANDATORY_LABEL tml;
     
    ZeroMemory(&tml, sizeof(tml));
    tml.Label.Attributes = SE_GROUP_INTEGRITY;
    tml.Label.Sid = psd;
  
    ret = SetTokenInformation(token, TokenIntegrityLevel, &tml,
                              sizeof(tml) + GetLengthSid(psd));    // 修改Token的完整性等级
     
    LocalFree(psd);
  }
  return ret;
}
  
void launch_notepad(void) {
  /* Low level; see table for integrity level string names */
  const char *requested_sid = "S-1-16-8192";        // 中强制等级
  HANDLE token_cur, token_dup;
  /* Get the current process' security token as a starting point, then modify
     a duplicate so that it runs with a fixed integrity level. */
  if (OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE |
                                            TOKEN_ADJUST_DEFAULT |
                                            TOKEN_QUERY |
                                            TOKEN_ASSIGN_PRIMARY,
                                            &token_cur)) {
    if (DuplicateTokenEx(token_cur, 0, NULL, SecurityImpersonation,  // 复制父进程token
                         TokenPrimary, &token_dup)) {
      if (adjust_token_integrity_level(token_dup, requested_sid))    // 修改token的完整性等级
        launch_notepad_as_user(token_dup);
      CloseHandle(token_dup);
    }
    CloseHandle(token_cur);
  }
}


(2)使用AllocateAndInitializeSid

// 其他如上函数和调用方式,这里只写主要流程函数
static BOOL adjust_token_integrity_level(HANDLE &hToken) 
{
    SID_IDENTIFIER_AUTHORITY MLAuthority = SECURITY_MANDATORY_LABEL_AUTHORITY; PSID pIntegritySid = NULL;
    
    // 直接创建初始化对应等级的SID
    if (!AllocateAndInitializeSid(&MLAuthority, 1, SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &pIntegritySid))
    {
        return FALSE;
    }
    
    TOKEN_MANDATORY_LABEL tml = {0};
    tml.Label.Attributes = SE_GROUP_INTEGRITY;
    tml.Label.Sid = pIntegritySid;
    
    // 修改Token的完整性等级
    const BOOL bRet = SetTokenInformation(hToken, TokenIntegrityLevel, &tml, (sizeof(tml) + GetLengthSid(pIntegritySid)));
    
    if (pIntegritySid) 
    { 
        FreeSid(pIntegritySid); 
    } 
    
    return bRet;
}

 

方法2:通过windows原生的事件过滤器来解决,屏蔽掉qt自带的拖拽事件过滤器,创建主窗口后启动原生事件过滤
(没试过不适合博主的项目,不知道好不好用,但应该是没问题的)

void EnableDrag(QMainWindow& w) {
    ChangeWindowMessageFilter(WM_DROPFILES, 1);
    
    w.winId() << w.effectiveWinId();
    ChangeWindowMessageFilterEx((HWND)w.effectiveWinId(), WM_DROPFILES, MSGFLT_ALLOW, NULL);
    ChangeWindowMessageFilterEx((HWND)w.effectiveWinId(), WM_COPYDATA, MSGFLT_ALLOW, NULL);
    ChangeWindowMessageFilterEx((HWND)w.effectiveWinId(), 0x0049, MSGFLT_ALLOW, NULL);

    DragAcceptFiles((HWND)w.effectiveWinId(), true);
    RevokeDragDrop((HWND)w.winId());
}

{
    QMainWindow w;
    EnableDrag(w);    // 将窗口的qt拖拽过滤器用windows原生事件过滤器替换掉
    w.setAcceptDrops(true);    // 设置窗口接收拖拽
    w.show();
    a.exec();
}
// 此时窗口已经可以接受拖拽了,重载主窗口的nativeEvent即可
// 或者如果是控件,重写控件的dragEnterEvent等函数即可

bool QMainWindow::nativeEvent(const QByteArray& eventType, void* message, long* result) {
    if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") {
        MSG* pMsg = reinterpret_cast<MSG*>(message);
        if (pMsg->message == WM_DROPFILES) {
            HDROP hDropInfo = (HDROP)pMsg->wParam;
            wchar_t szFilePathName[_MAX_PATH] = { 0 };
            const UINT nNumOfFiles = DragQueryFile(hDropInfo, 0xFFFFFFFF, NULL, 0);
            if (nNumOfFiles > 0) {
                // DragQueryFile第二个参数为拖入文件的索引
                DragQueryFile(hDropInfo, 0, szFilePathName, _MAX_PATH); //直接取第一个 入参UINT iFile  = 0
                const QString currentfile = QString::fromWCharArray(szFilePathName);
                // currentfile 为当前拖拽文件
                // OnDragFinished(currentfile);
            }
            DragFinish(hDropInfo);
        }
    }
    return false;
}

 


参考文章链接:

https://www.cnblogs.com/SecSource/p/15949135.html

https://learn.microsoft.com/zh-cn/previous-versions/dotnet/articles/bb625963(v=msdn.10)

https://wiki.sei.cmu.edu/confluence/display/c/WIN02-C.+Restrict+privileges+when+spawning+child+processes

https://towriting.com/blog/2013/08/06/process-can-drag-drop/

 

posted @   人类观察者  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示