# 记录一次由于权限问题导致的程序卡顿问题

By [eip1571](https://paragraph.com/@eip1571) · 2022-07-14

---

问题描述：
=====

前段时间朋友给我发送了一个exe，程序在以管理员权限即high权限下运行时正常，而以受限管理员和普通用户权限即medium权限下运行时，在点击一个按钮时切换主页面，右侧工具栏页面加载会卡顿2-3s。本文描述了如何解决这个问题的思路。

朋友给我发送了一个带盾牌的exe，以及安装包，接下来进行测试。

　　1. 带盾牌的意思是双击exe时，会弹出UAC（User Account Control用户账户控制）框，需要点击确定才能提升到high权限，这个也跟系统设置有关，默认是有UAC控制的。

　　2. 当编译exe的时候，如果存在manifest资源文件且XML内容中level = "requireAdministrator"，那么就会带盾牌，程序就必须以High权限运行才可以，默认情况下我在windows下编译的程序都带有manifest文件，内容level = "asInvoker"，表明进程启动权限继承自父进程权限。

第一个想法是当我双击exe时会请求提权，那我怎么以中等权限运行这个程序呢，为了解决这个问题，我使用resource\_hacker工具打开exe，删除资源文件中的manifest信息，接着将exe另存一个文件，这个新的文件就不带盾牌了，双击时正常情况即可以普通权限运行，但是测试时发现这个删除了manifest信息的exe以medium权限运行时并没有卡顿问题，此时觉得朋友的描述有问题。

然后朋友另外给我发送了一个exe，以及一个安装包，并说明第一次发送的带盾牌的程序是错误的，需要使用第二次发送的安装包进行测试。

经过测试这个新发送的安装包里的exe运行时确实会卡顿，这个exe带manifest文件，其中的内容为level = "asInvoker"，而当双击时程序的权限继承父进程外壳程序explorer.exe的权限（一般为medium），双击时程序在本机卡顿。

**经过思考，找不到切入点，偶然情况将第一次发送过来的exe（开始带盾牌但是删掉manifest后不带盾牌了）放到安装目录下，再次运行时发现程序不卡顿了，暂时以此种方式来解决此问题。**

通过朋友查看源代码，很难找到切入点，且在部署代码的机器（下文称为代码机，我自己的机器称为本机）上程序以medium权限启动时并没有卡顿，查看代码机上用户为Administrator，这个用户很特殊，一般刚安装的Win10操作系统中自带此用户，此用户拥有很高权限，但是默认情况下此账户都被禁用了，在代码机上以此用户来启动各种程序，我怀疑是不是双击时默认以high权限运行（即此账户关闭了UAC控制），通过使用Process Explorer工具（Windows SysinternalsSuite套件中的工具）查看进程权限看到目标进程是medium权限，这就陷入僵局了，只能思考是不是两台机器的环境不一样，例如环境变量，例如注册表等内容，先把部署代码生成的exe及目录都拷贝一份到本机中（不拷贝代码），下面慢慢说出我尝试的各种方式，其中包含很多没成功的思路：

1.第一次还没有见到代码时猜测是不是由于程序中有对权限的判断，high权限与medium权限走不同的路径，但是通过一些调试手段（使用windbg）没有找到对一些关键api的调用，此方法暂告失败。

2.通过任务管理器工具查看程序缺页异常，结果没有找到什么问题，在本机上medium和high权限，缺页异常都差不多，还没看到是不一样的 此方法暂告失败。

3.会不会跟UAC有关呢？在任务管理器下查看medium权限和high权限下UAC虚拟化选项内容是不一样的，未删除manifest文件medium进程UAC虚拟化为已禁用，删除manifest文件medium进程UAC虚拟化为已启用，以high权限运行未删除manifest的exe文件UAC虚拟化为不允许，这个时候对于虚拟化还有疑惑，不太清楚是什么东西，此方法暂时不继续。

\*\*1.\*\*通过Process Explorer查看进程加载的模块及文件等内容，工具用法请自行百度，可以看到进程的各种模块之后，CTRL+A全选 CTRL+S保存到txt文件中，在代码机和本机上分别做这个操作，并且对txt文件中删除一些多余的文件，放在Beyond Compare软件中进行比较，比较的结果发现代码机中的模块带了好几个某个公司的模块，以及多加载了jsc文件以及qmlc文件，而本机模块中缺少这几个模块以及没有jsc文件，当时的想法是不是由于安装了了其他软件，或者是qt程序qml优化缓存的问题，暂时找不到突破口，此方法暂告失败。

\*\*2.\*\*想尝试通过逆向调试的方式来找到程序是不是有什么很重的代码,先定位到点击操作，代码比较多，并没有怎么查看，卡顿时程序的表现为当点击一个按钮时会切换页面，这个主页面切换很快，侧栏的工具也会跟着切换，但是切换的很慢，界面很卡，定位代码的话，不通过源代码查找，我们运行程序后，先启动windbg附加到此进程，然后点击按钮切换切页面（切换侧边工具栏需要2~3s），点击按钮后快速切换到windbg页面，之后中断此程序，因为程序很卡，此时程序应该还在处理加载右边侧边栏的逻辑，而侧边栏加载页面是属于UI线程，一般情况下是进程中0号线程来负责UI的，通过~0 s切换到0号线程，通过r寄存器查看接下来执行的执行，k查看调用堆栈，此时可能需要一些耐心单步调试来查看调用哪个函数时，消耗了大量的时间，经过查找，找到一处调用函数，单步步过此函数时大概需要2s的时间，但是函数内容较多，且由于对代码不太熟悉，无法对应到具体源代码，此方法暂告失败（或许应该监控下这2s的文件操作、网络操作等耗时工作）。

\*\*3.\*\*后来朋友告诉我说，在安装了某个软件的机器上运行此程序就不会出现卡顿问题，这与我之前的猜测有一点相同，即环境不太一样，暂时将此软件称为前置软件，以便行文。但是一般情况下如果安装了前置软件，要想影响到此软件，可能的方式有前置软件安装的dll或其他文件，必须通过路径被卡顿程序识别到，可能为环境变量，检查代码机上安装了前置软件且环境变量path中包含此目录，暂时删除此目录，发现在代码机上软件正常运行，说明不是通过环境变量来识别的，那么会不会是前置软件安装到系统目录了呢？通过Process Monitor（Windows SysinternalsSuite套件中的工具）工具监控安装软件写了哪些内容到系统目录下，如系统盘\\Windows\\system32、系统盘\\Windows\\SySWOW64等文件夹，没有监控到对这些特殊目录的写入，安装时看到前置软件会安装FLASH软件，应该是以运行FLASH安装包的方式运行的，因为监控时是监控前置软件exe的活动，所以重新监控一次FLASH安装包.exe的文件活动，没有找到对特殊目录的写入，此方法暂告失败。

这个时候挺沮丧的，继续寻找问题，再次考虑这个问题，为什么在本机和代码机上同样以medium权限运行进程，结果会不一样呢？两边可执行文件目录下的内容一模一样，也就是说其他的因素影响了程序运行状态，例如其他软件或文件。还有一个问题：为什么在本机上以medium权限带manifest的文件和不带manifest的文件结果会不一样呢，开始搜索UAC、manifest方面的内容。

**从以下链接中看到一句话：** [**https://blog.csdn.net/talking12391239/article/details/51694555**](https://blog.csdn.net/talking12391239/article/details/51694555) **对于没有manifest的32位程序，Windows将自动默认虚拟化，而含有manifest的程序，虚拟化是默认禁用的。 非常感谢这位博客的作者。**

假如按照这个思路的话，可以理解为运行删除了manifest的程序会自动开启虚拟化，而开启虚拟化会影响什么？会影响到一些关键路径下的文件的读写，假如程序需要操作某些特殊路径下如系统盘\\ProgramData\\目录下的文件、文件夹等内容，一般情况下普通用户是没有写权限的，较老版本的程序如windows xp中并没有对权限进行严格控制，程序可以读写这些文件夹，高版本Windows系统中加了权限控制等机制，如果程序要写这些目录下的内容时由于权限问题无法写入，如果开启了虚拟化，那么操作系统会自动将写入操作重定向到 系统盘\\Users\\用户名\\AppData\\Local\\VirtualStore\\ProgramData\\目录下还会新建一些目录用来区分不同的应用程序，程序对于这个目录下是有读写权限的，而程序以为自己操作的是ProgramData目录，其实已经被重定向了，但是不影响程序的正常运行，这样开启了虚拟化之后就兼容了Windows老版本的应用程序。

根据以上推测，是不是由于在本机上打开某些特殊文件权限不足导致失败呢？虽然不明白为什么程序不会失败还可以运行，但是这并不影响我们根据这个思路去尝试一些操作：

  

在本机和代码机上分别运行Process Monitor软件，监控程序运行时的文件操作，过滤Result为Access Denied的行为，Access Denied为拒绝访问，通过两边的对照比较，发现本机上很多访问同一个db文件的拒绝访问事件，但是同样的操作在代码机上并没有问题，接着在代码机上新建一个普通用户，再次运行程序，发现也卡顿了，这个时候去查看这个db文件，右键属性->安全，发现这个db文件拒绝普通用户的写权限，而Administrator用户拥有完全控制权限，在普通用户下编辑此文件的权限（编辑权限需要提权），让普通用户拥有对此文件的读写权限，再次在普通用户权限下运行此程序，程序正常不卡顿，大功告成。

正常情况下普通用户medium权限不具备对ProgramData目录下文件的读写操作，而受限管理员不一定拥有对此目录下文件的权限。当删除了manifest文件时程序运行时开启虚拟化自动重定向到VirtualStore文件夹，medium权限对此文件夹的文件具有读写权限，未删除manifest文件时程序以high权限运行，本身就拥有对ProgramData目录下文件的读写操作可以正常运行，而代码机上的管理员太过特殊，是Windows自带的Administrator 用户，可能是由于环境的原因受限管理员也拥有对ProgramData目录的权限，在这些情况下，程序的行为都正常不卡顿，虽然在内部读写的文件不一致，但是对于用户来说看起来行为是一致的。

**对前置软件进行了测试**，发现安装前置软件过程中对ProgramData这个目录添加了everyone用户完全控制的权限，所以安装前置软件后，不管是受限管理员还是普通用户都可以读写这个db文件，程序即可正常运行。 使用Process Monitor过滤文件事件，过滤路径为C:\\ProgramData，结果如下

1.  从1处可以看到icacls.exe 调用SetSecurityFile操作 C:\\ProgramData路径
    
2.  从2处看到icacls.exe的父进程id为4956
    
3.  从3处看到4956是临时exe后缀为tmp，应该是拷贝了一份可执行文件，是pid6676创建了pid4956
    
4.  从4处可以看到icacls.exe进程启动命令，是为C:\\ProgramData路径 添加everyone可以完全控制的权限。
    

解决方案：在代码中修改此db文件的创建路径，如果程序本身是针对所有用户安装的，那么放置在系统盘\\users\\public\\自定义目录下，这个目录所有用户everyone都拥有读写权限，如果安装程序仅对单个用户，那么安装在系统盘\\users\\当前用户名\\自定义目录下，当前用户拥有对目录下文件的读写权限，这样就不会出现Access Denied的结果了，修改代码中创建db文件的路径后，程序即可正常运行。

获取系统盘\\users\\当前用户名和系统盘\\users\\public的代码如下：

`#include <shlobj.h>`

`#pragma comment(lib, "Shell32.lib")//为了 链接SHGetKnownFolderPath`

`#pragma comment(lib, "Ole32.lib")//为了 链接 CoTaskMemFree`

  

`//使用环境变量的方式 Windows Vista 及更高版本`

`printf("第一种方法 环境变量\n");`

`TCHAR destCurrent[1024] =` `{0};`

`ExpandEnvironmentStrings(L"%USERPROFILE%\appname",     destCurrent, _countof(destCurrent));//最后一个参数为字符个数  获取路径  系统盘\users\当前用户名`

`printf("%ls\n", destCurrent);`

  

`TCHAR destPublic[1024] =` `{ 0` `};`

`ExpandEnvironmentStrings(L"%public%", destPublic, _countof(destPublic));//最后一个参数为字符个数  获取路径  系统盘\users\public`

`printf("%ls\n", destPublic);`

  

`printf("下面第二种方法 推荐第二种 调用API\n");`

`//FOLDERID_Profile`

`//FOLDERID_Public`

`//参考 https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid`

`//当前用户`

`REFKNOWNFOLDERID fidCurrent =` `FOLDERID_Profile;`

`TCHAR*` `pBuffCurrent =` `NULL;`

`if` `(S_OK ==` `SHGetKnownFolderPath(fidCurrent, 0, NULL, &pBuffCurrent))     //获取路径  系统盘\users\当前用户名`

`{`

    `printf("%ls\n", pBuffCurrent);`

`}`

`CoTaskMemFree(pBuffCurrent); //不论成功失败都要释放`

  

`//公共文件夹`

`REFKNOWNFOLDERID fidPublic =` `FOLDERID_Public;`

`TCHAR*` `pBuffPublic =` `NULL;`

`if` `(S_OK ==` `SHGetKnownFolderPath(fidPublic, 0, NULL, &pBuffPublic))　　//获取路径  系统盘\users\public`

`{`

    `printf("%ls\n", pBuffPublic);`

`}`

`CoTaskMemFree(pBuffPublic);//不论成功失败都要释放`

程序性能有问题，可能是文件磁盘或者网络等耗时操作，但是这次是打开数据库db文件，程序还可以运行，只不过是卡顿，程序本身是用QT开发的，使用的qt操作sqlite的数据库，**打开db文件应该判断是否可以成功，在测试阶段尽快暴露此问题。** 在此记录，以备后用。

转载自看雪论坛，原文链接如下

[https://bbs.pediy.com/thread-273610.htm](https://bbs.pediy.com/thread-273610.htm)

---

*Originally published on [eip1571](https://paragraph.com/@eip1571/jpKX3uP5wfFmw8Lsx9Rt)*
