1.前言
Apologies for taking so long to get the initial response back from Engineering. When Customers upgrade to McAfee Agent 5.6.0 and later the Product Improvement Program Service is uninstalled. Should any Customers run into the problem that you’ve outlined our advice to them will be to install the latest version of McAfee Agent to force the uninstall. As PIPS is End of Life we don’t plan to make any code fixes for this issue. Thank you again for reporting this issue to us.
上面的一段英文摘自McAfee对我最近提交的一个漏洞的回复。大意是因为产品期结束,在最新版也就是5.6.0版本上移除了含有漏洞的功能,所以不会对漏洞进行任何的代码修复。要解决这个问题,自然是升级到最新版本上。要知道我们公司目前使用的是5.5.1版本!
2.机缘巧合
前几天项目产品有个障害要调查,因为长时间没有关机,分析过程有些卡顿。重启后竟然忘了dump文件所在的目录,只记得大概的名字,想要再次打开,却不能快速找到它,怎么办呢?(有人说,通过最近浏览记录打开,就是这么巧,没用它)。这里顺便安利一款Windows系统上的快速搜索工具Everything(主要支持NTFS文件系统),使用它立马定位到我要找的dump文件(至于有多快,谁用谁知道)。之后的分析工作,这里不做深入介绍。引起我强烈兴趣的是,一个名为mctelsvc.exe的进程在我的机器上产生了有10个dump文件,看日期也都是最近产生的。如下图1是所示:
图1
闲暇之余,通过查找资料,了解到这个进程是McAfee的一个服务组件(划重点,后面会考),属于McAfee Product Improvement Program产品,主要功能是收集客户端的必要情报信息(不涉及私密信息),供McAfee改善用。最重要的是它是我们公司目前使用的杀毒软件,作为客户,自然是可以把问题报告给McAfee,让他们去分析,可是作为程序员又觉得不能放过这样一个机会所以就对它展开了分析。
3.第一只拦路虎
因为不确定这个漏洞的发生条件,想要再现不是首选,所以最直接的办法是分析dump文件,看看是什么样的错误导致的。结果如图2所示:
图2
从上面的分析结果,我掌握2个重要的信息:1)根据异常代码,可知是在读取某一块内存地址上的内容,出现了访问违例。2)执行操作的函数是memcpy。凭借仅有的一点安全知识,能够想到的是搞不好这个漏洞能够被利用,一旦成功,那就是提权漏洞啊(活用考点)!看来这个问题不容忽视,继续深究。先看看这个程序运行在多少位上。
0:012> vertarget
Windows 7 Version 7601 (Service Pack 1) MP (4 procs) Free x86 compatible
很好,是32位的,分析工作应该会简单些(x64痛苦啊)。
想探明这个漏洞能不能被利用,先要确认memcpy函数的参数是什么,主要看来源是不是程序外部的输入,是否可控。遗憾的是当我确认源和目的地址上的内容的时候,oh, NO!结果是不可访问。
msvcr100!memcpy+0x1dc:
6d2b20fc f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:012> dc @esi L4
00000002 ???????? ???????? ???????? ???????? ????????????????
0:012> dc @edi L4
00000000 ???????? ???????? ???????? ???????? ????????????????
看来从dump中查明问题是不可能了,只能另辟他径。想通过动态调试跟踪,没想到又遇到一个难题,那就是这个服务进程不是启动后就执行出现问题的代码,根据它的策略机制(尝试得出),应该是差不多一天执行一次,太花时间!
4.一波三折
虽说调试跟踪花时间,可是这个工作还得做,将cdb挂上去之后就等着它执行了。利用暂时不能跟踪的这段时间,重新梳理下目前掌握的信息。异常发生在lua51模块,也就是说mctelsvc.exe这个进程在执行信息收集时,执行了特定的lua脚本文件,说不定收集信息的动作,就在这些脚本文件里面。哈哈,那马上行动起来!使用Everything很快找到了位于McAfee安装目录下的几个脚本文件。用编辑工具打开一个叫main.lua的文件,结果又是碰了一鼻子灰,这个文件被保护了,至少看起来不是明文的源码!如图3所示:
图3
不能就这么放弃啊!想着如果这是个加密的脚本文件,这可能意味着在调用lua51.dll的之前会进行解密,之后将解密后的内容交给lua51.dll执行。觉得可行,于是在怀疑的点上,下了断点(如lua51!luaL_loadfile),明天等着奇迹发生吧!然而现实又给我泼了盆冷水,在lua51!luaL_loadfile处下的断点倒是触发了,然而读取的就是保护的main.lua,并没有加载解密后临时文件。有的读者看到这,会跳出来说,lua开源的啊,结合它的源代码分析岂不是更容易!
是的,笔者也想到这个点,一番确认下来,再次被McAfee折服,lua51.dll这个模块的源码被McAfee改动过了!一次又一次,到底要不要就此作罢?!
5.无心插柳
在将这个问题搁置了2天后,想着还是从lua51!luaL_loadfile突破吧,看看McAfee究竟对这个函数做了什么改动。通过反汇编对比之后,发现McAfee在原来的代码开始部分添加了一个回调函数的调用,如下所示:
.text:10003D93 mov ecx, dword ptr mctelcre_callback
.text:10003D99 test ecx, ecx
.text:10003D9B jz loc_10003E76
.text:10003DA1 test ebx, ebx ; ebx is the script file pointer
.text:10003DA3 jz loc_10003E76
.text:10003DA9 lea eax, [esp+224h+var_210]
.text:10003DAD push eax ; _DWORD
.text:10003DAE push 0 ; _DWORD
.text:10003DB0 push ebx ; the script filename
.text:10003DB1 call ecx ; mctelcre_callback
上面的反汇编代码抽象后,换作C伪代码:
mctelcre_callback(script_filename, 0, eax);
继续跟踪进入这个函数内部看看它做了什么事情,没想到的是这一次跟踪下来竟有了意外收获!来看下面的反汇编代码,省略了其中不是很重要的部分。
[...]
.text:10004E56 push offset aRb ; "rb"
.text:10004E5B push esi ; char *
.text:10004E5C call ds:fopen
[...]
.text:10004F35 cmp eax, 1Bh
.text:10004F38 jz short loc_10004F6A
.text:10004F3A mov ecx, [ebp+file_size]
.text:10004F40 cmp ecx, 8 ; 8 is the magic string length
.text:10004F43 jge short loc_10004F6A
.text:10004F45 movsx edx, byte ptr ds:aMfetcont[ecx] ; "MFETCONT"
.text:10004F4C cmp eax, edx ; check each character "MFETCONT"
.text:10004F4E jnz loc_100050A6
.text:10004F54 inc ecx
.text:10004F55 push esi ; FILE *
.text:10004F56 mov [ebp+file_size], ecx
.text:10004F5C call ds:getc
.text:10004F62 add esp, 4
.text:10004F65 cmp eax, 0FFFFFFFFh
.text:10004F68 jnz short loc_10004F35
[...]
.text:10004F72 push esi ; FILE *
.text:10004F73 push 1 ; size_t
.text:10004F75 lea eax, [ebp+majorVersion]
.text:10004F7B push 4 ; size_t
.text:10004F7D push eax ; void *
.text:10004F7E call ds:fread
.text:10004F84 push esi ; FILE *
.text:10004F85 push 1 ; size_t
.text:10004F87 lea ecx, [ebp+minorVersion]
.text:10004F8D push 4 ; size_t
.text:10004F8F push ecx ; void *
.text:10004F90 call ds:fread
.text:10004F96 push esi ; FILE *
.text:10004F97 push 1 ; size_t
.text:10004F99 lea edx, [ebp+dsSize]
.text:10004F9F push 4 ; size_t
.text:10004FA1 push edx ; void *
.text:10004FA2 call ds:fread
[...]
.text:10005062 cmp [ebp+dsSize], 0
.text:10005069 jnz short loc_100050DA
[...]
.text:100050E5 mov ecx, [ebp+dsSize]
.text:100050EB push ecx ; size_t
.text:100050EC call ds:malloc
.text:100050F2 mov edx, [ebp+dsSize]
.text:100050F8 push esi ; FILE *
.text:100050F9 push edx ; size_t
.text:100050FA push 1 ; size_t
.text:100050FC push eax ; void *
.text:100050FD mov [ebp+dsBufferPtr], eax
.text:10005103 call ds:fread
[...]
与之对应的伪C代码:
char *pMagic = "MFETCONT";
fp = fopen(filename, "rb")
for (int i = 0; i < 8; i++)
{
if (getc(fp) != pMagic[i])
{
goto error;
}
}
fread(&majorVersion, 4, 1, fp); // Major Version
fread(&minorVersion, 4, 1, fp); // Minor Version
fread(&dsSize, 4, 1, fp); // Digit Signature Size
if (dsSize == 0)
{
goto error;
}
fread(malloc(dsSize), 1, dsSize, fp) // This place will crash
这部分代码逻辑简单,就不做深入介绍了,基本是按特定字节读取文件的内容进行校验。可以看出,对于dsSize的验证很简单,如果是0则出错,并没有考虑dsSize是负数的情况!如果是负数,那么malloc函数将返回值NULL,之后再进行fread函数读取文件内容,肯定是要崩溃的,0xc0000005异常怕是跑不了了!另外从以上代码也可以推导出文件的格式,如下图4所示:
图4
知道文件格式后,可以做个尝试,手动将Digit Signature Size域的值改成一个负数,如下图5所示的0x80000000:
图5
改动后,想验证结果,又遇到了之前的问题,这部分代码跟踪调试过了,想让它再次执行,怕是要再等上一段时间。无奈!可是不想等的愿望很强烈,等不了!再次把目光放到这个回调函数的原型上,可以看出目前只有第3个参数不太清楚做什么用,继续从反汇编代码上分析它是做什么的吧,经过一段时间的分析,确定这个值是函数内部传出的,大小4个字节。再次整理后的伪C代码:
int value = 0;
mctelcre_callback(script_filename, 0, &value);
这样一来我就可以写点代码调用它了,心中一阵窃喜,不过还是要等验证完再说。
HMODULE hModule = LoadLibrary("C:\\Program Files (x86)\\McAfee\\Telemetry\\mctelcre.dll");
if (hModule != nullptr)
{
typedef int(*TEST)(char* name, int, int *size);
auto ptr = ULONG(hModule) + 0x4DA0;
TEST testFunction = reinterpret_cast<TEST>(ptr);
int size = 0;
testFunction("C:\\Program Files (x86)\\McAfee\\Telemetry\\Content\\main.lua", 0, &size);
FreeLibrary(hModule);
}
将编译后的程序放置到mctelcre.dll的目录下,运行,程序崩溃,一切都那么完美!如下图6所示:
图6
有了这个验证结果,剩下的就是静静的等待McAfee的服务运行了……
6.时间线
7.总结
从上面的分析过程,我们可以看出,对于程序外部的输入不做严格的检查是多严重的一件事!虽然我没有挖出dump中出现的那个问题,但却在调查中,发现了另外一个问题。可以说作为安全厂商应该更严格的评审、测试自己的产品代码,一旦出现问题,极可能被黑客利用,导致不可挽回的损失!也许McAfee发现了自己产品的不足,所以在新版本中移除了这部分功能,也算是一定程度上保证了安全性吧!
后记
在发稿之前,McAfee关于这个问题,给我发了个认证,也算是对于以上问题分析的认可!
之后我又提交了两个McAfee驱动相关的漏洞,很遗憾因为一些原因并没有形成文档。时过境迁,有些资料已经找不到了,不过在McAfee的官网还是能看到一些信息以及相应的CVE编号(CVE-2019-3633以及CVE-2019-3634)。
https://kc.mcafee.com/corporate/index?page=content&id=SB10295(可能失效)