记录一次由于权限问题导致的程序卡顿问题
问题描述:
前段时间朋友给我发送了一个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
对于没有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处可以看到icacls.exe 调用SetSecurityFile操作 C:\ProgramData路径
- 从2处看到icacls.exe的父进程id为4956
- 从3处看到4956是临时exe后缀为tmp,应该是拷贝了一份可执行文件,是pid6676创建了pid4956
- 从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文件应该判断是否可以成功,在测试阶段尽快暴露此问题。
在此记录,以备后用。
转载自看雪论坛,原文链接如下