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

问题描述:

前段时间朋友给我发送了一个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. 从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文件应该判断是否可以成功,在测试阶段尽快暴露此问题。
在此记录,以备后用。

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

Subscribe to eip1571
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.