QQProtect驱动保护目录跟踪漏洞详细分析
前个阶段一直在学习QQProtect的驱动设计,过程中难免会对其进行一些逆向操作,进而有机会接触到其中的一些细节。上个月运气不错,找到了几个可以用来突破QQ驱动保护的漏洞。在仙果的指引下,我知道了“TSRC”是神马意思。没想到无意间的发现能换来一堆QQ公仔,一个公仔可是价值500Kx啊!我这不是发财了么?嘿嘿,不谈财的事情了,有些俗气。还是早点进入主题吧。
找到本文要所描述的漏洞是8月初事情了,这也是我找到的第一个所谓的软件漏洞。虽然这个漏洞有些水,但我还是很兴奋。当时就想拿出来和大家分享,但迫于TSRC的压力(不给公仔),一直拖到了现在(9月份更新才能覆盖用户)。虽然漏洞已经修复,但9月份更新前的版本一直受此漏洞威胁,建议大家不要拿来做坏事,本文仅供技术交流。
下面这些文字是漏洞提交时候的原始资料,环境以及如何测试的描述都在里边,我就不重新写一遍了。请大家仔细阅读,后面我会着重分析漏洞成因。漏洞名称:QQProtect驱动保护目录跟踪漏洞
漏洞类型:权限提升/绕过
危害等级:中
一、详细说明
QQProtect.sys 在自己的注册表驱动服务项里保存QQ的安装目录以及QQProtect.exe的全路径,并在开机启动加载服务后读取并初始化保护路径。在设计中,有个功能是跟踪用户对影响QQ目录的父级目录的改名操作。并实时调整上边提到的注册表键值(QPath和Path)。而本漏洞,则是由于在挂钩函数ZwSetInformationFile中对RenameInformation的处理存在缺陷,导致驱动误修改保护路径,使得所有基于QQ安装目录的保护以及QQ核心文件的Prefetch操作功能完全失效!
QQProtect.sys版本(2.6.0.4 ,2.8.0.5)32bit
这两个版本的问题函数地址分别为:0x1c9a4和0x1d502
(9月份之前的版本均有效)
二、漏洞证明
设当前QQProtect.sys 的注册表服务项中Path和QPPath分别为:
Path:C:\Program Files\Tencent\QQ
QPPath:C:\Program Files\Tencent\QQ\QQProtect\Bin\QQProtect.exe
构造操作步骤如下:
1:新建文件(或者文件夹)C:\Pro
2:重合名1中建立的文件(文件夹)为 C:\Proo
3:再次查看注册表中相应键值如下:
Path:C:\Proogram Files\Tencent\QQ
QPPath:C:\Proogram Files\Tencent\QQ\QQProtect\Bin\QQProtect.exe
此时可以篡改或者删除QQ目录里多数可执行文件,驱动对于安装目录中文件的保护失效,Prefetch功能失效。
三、修复方案
修改挂钩函数ZwSetInformationFile 对RenameInformation处理的相关代码,使得对父级目录字符串的匹配不严格是子串匹配,而应当以"\"为单位一级一级匹配,确保用户所修改的目录确实是影响QQ安装目录的文件夹!
本漏洞中还有个缺陷是:如果修改的是字符串路径符合要求的文件(非文件夹)也会触发,原因是在句柄是否目录的判断部分代码有逻辑问题!
从提交的文字不难看出,这个漏洞确实是很水的一个。大家平时弄的,以前前人写的分析文章至少得有“溢出”二字吧?对于挖掘漏洞,FUZZ是个很眼熟的词汇吧?墨盒白盒大家都是很熟悉的。然而,这漏洞太水,以至于上边的提到的技术都用不上。只能苦逼地逆向分析代码逻辑,找到潜在的危险区域,这种方法介于白盒和黑盒之间,姑且就称之为“灰盒”吧。
对于本漏洞而言,很显然,ZwSetInformationFile的处理是最关键的地方。下边就从分析它开始一步一步深入下去,直到找到漏洞根源所在。
NTSTATUS
NTAPI
studZwSetInformationFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInformationClass
)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
BOOLEAN bProtect = FALSE;
PWSTR NewQQPathDir = NULL;
bProtect = CheckAndRecordRenameInfor(
FileHandle,
IoStatusBlock,
FileInformation,
Length,
FileInformationClass,
&NewQQPathDir );
if( bProtect )
{
status = STATUS_ACCESS_DENIED;
goto exit;
}
else
{
PZWSETINFORMATIONFILE OrgFuncAddr = (PZWSETINFORMATIONFILE)
g_BaseHookTbl[ BaseHookId_ZwSetInformationFile ].OriginalFuncAddress;
if( !OrgFuncAddr || !MmIsAddressValid( OrgFuncAddr))
{
if( g_Org_Ssdt_Func_table != NULL &&
(LONG)g_BaseHookTbl[ BaseHookId_ZwSetInformationFile ].IndexInSsdt >= 0 &&
g_BaseHookTbl[ BaseHookId_ZwSetInformationFile ].IndexInSsdt < KeServiceDescriptorTable.NumberOfServices)
{
OrgFuncAddr = (PZWSETINFORMATIONFILE)g_Org_Ssdt_Func_table[ g_BaseHookTbl[ BaseHookId_ZwSetInformationFile ].IndexInSsdt];
if( !OrgFuncAddr )
OrgFuncAddr = (PZWSETINFORMATIONFILE)g_BaseHookTbl[ BaseHookId_ZwSetInformationFile ].OriginalFuncAddress;
}
}
status = OrgFuncAddr( FileHandle,
IoStatusBlock,
FileInformation,
Length,
FileInformationClass);
if( FileInformationClass = FileRenameInformation && NT_SUCCESS( status ))
{
if( !NewQQPathDir )
return status;
SetNewQQPathAndProtectExePathInReg( NewQQPathDir );
}
}//end if bProtec else
exit:
if( NewQQPathDir )
ExFreePool( NewQQPathDir );
return status;
}
此函数的大致流程是:首先调用 CheckAndRecordRenameInfor 检测合法性。如果不合法,挂钩函数直接返回拒绝访问。否则调用系统的原始函数进行正常操作。如果足够细心,不难发现里边有个变量叫:NewQQPathDir。这个其实代表的是QQ安装目录(也是驱动的保护目录)。很明显,函数CheckAndRecordRenameInfor生成也这个值。而函数SetNewQQPathAndProtectExePathInReg 将这个值设置到了注册表中。从漏洞的说明文字中可以看出,一切都是围绕这个NewQQPathDir开始的。接下来,我们跟进CheckAndRecordRenameInfor,看看究竟新的QQ安装目录是怎么生成的!直接给出伪码如下:
BOOLEAN
NTAPI
CheckAndRecordRenameInfor(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInformationClass,
OUT PWSTR *NewQQPathDir)
/*++
NOTE:
the caller must call ExFreePool to free the memory that returned by NewQQPathDir
--*/
{
WCHAR FileName[MAX_UNICODE_STRING_CHARS + 1 ] = {0};
IO_STATUS_BLOCK ioStatusBlock = {0};
FILE_STANDARD_INFORMATION FileInfor = {0};
HANDLE hFile = 0;
BOOLEAN bResult = FALSE;
ULONG chars = 0,bytes = 0,current_pid = 0;
REGISTRY_DB_HELPER_DATA HelperData = {0};
if( !g_bFileMon ||
!(g_GlobalControlBlock.MonFlag & 0x10) ||
( FileInformationClass != FileRenameInformation &&
FileInformationClass != FileLinkInformation &&
FileInformationClass != FileDispositionInformation &&
FileInformationClass != FileAllocationInformation) ||
ExGetPreviousMode() == KernelMode ||
FileHandle == 0 ||
IsProcessInList5AndBelievable( current_pid = (ULONG)PsGetCurrentProcessId(),6,NULL )
)
{
goto exit;
}
if( !NT_SUCCESS( DuplicateHandle( FileHandle,*IoFileObjectType,&hFile)))
goto exit;
if( !NT_SUCCESS( ZwQueryInformationFile( hFile,
&ioStatusBlock,
&FileInfor,
sizeof(FileInfor),
FileStandardInformation)))
goto exit;
if( FileInfor.DeletePending == TRUE )
goto exit;
bResult = GetFileDosDeviceNameByHandle( FileHandle,FileName,MAX_UNICODE_STRING_CHARS);
if( !bResult )
goto exit;
AdjustPathString( FileName,MAX_UNICODE_STRING_CHARS );
chars = wcslen( FileName );
if( FileName[ chars - 1 ] == L'\\')
FileName[ chars - 1 ] = L'\0';
__try{
HelperData.Unknown2 = 4;
HelperData.Flags = FileInformationClass;
HelperData.Pid = current_pid;
bResult = IsFileNameProtectedFile( FileName,MAX_UNICODE_STRING_CHARS,0,&HelperData);
}__except( EXCEPTION_EXECUTE_HANDLER )
{
KdPrint(("Exception occurred in CheckAndRecordRenameInfor 1 \n"));
}
if( bResult ||
FileInformationClass != FileRenameInformation ||
FileInformation == NULL )
goto exit;
__try{
VerifyAndHashMemory( FileInformation,sizeof(FILE_RENAME_INFORMATION),4 );
bytes = ((PFILE_RENAME_INFORMATION)FileInformation)->FileNameLength;
if( bytes )
{
VerifyAndHashMemory( ((PFILE_RENAME_INFORMATION)FileInformation)->FileName,
bytes,
sizeof(WCHAR));
if( !IsFileDirectory( hFile ) ||
!(bResult = IsDirectoryInQQSubBinDirTbl( FileName,MAX_UNICODE_STRING_CHARS)) &&
!(bResult = IsDirectoryInMyProtectedDataFileDir(
((PFILE_RENAME_INFORMATION)FileInformation)->FileName,bytes / sizeof(WCHAR)))
)
{
AdjustPathString( ((PFILE_RENAME_INFORMATION)FileInformation)->FileName,
(USHORT)(bytes / sizeof(WCHAR)));
MakeNewQQPathDir( FileName,
((PFILE_RENAME_INFORMATION)FileInformation)->FileName,
bytes / sizeof(WCHAR),
NewQQPathDir );
}
}//end if bytes
}__except(EXCEPTION_EXECUTE_HANDLER )
{
KdPrint(("Exception occurred in CheckAndRecordRenameInfor 2 \n"));
}
exit:
if( hFile )
{
ZwClose( hFile );
hFile = 0;
}
if( bResult )
RecordRequestFileInfor( 6,current_pid,FileName,MAX_UNICODE_STRING_CHARS,4,FALSE );
return bResult;
}
此函数首先检测一些全局的控制标志,接着验证所请求操作的文件是否为被保护的可执行文件或者数据文件。最后验证对目录的重命名操作。而本漏洞的关键是最后一步。代码段如下:
if( !IsFileDirectory( hFile ) ||
!(bResult = IsDirectoryInQQSubBinDirTbl( FileName,MAX_UNICODE_STRING_CHARS)) &&
!(bResult = IsDirectoryInMyProtectedDataFileDir(
((PFILE_RENAME_INFORMATION)FileInformation)->FileName,bytes / sizeof(WCHAR)))
)
{
AdjustPathString( ((PFILE_RENAME_INFORMATION)FileInformation)->FileName,
(USHORT)(bytes / sizeof(WCHAR)));
MakeNewQQPathDir( FileName,
((PFILE_RENAME_INFORMATION)FileInformation)->FileName,
bytes / sizeof(WCHAR),
NewQQPathDir );
}
大家注意看这个if的判定条件在什么情况下会满足。
1:请求操作的对象不是目录(不是目录,那就是文件呗)
2:请求操作的对象是目录,但此目录不是QQ各种Bin目录的子目录
3:请求操作的对象是目录,但此目录不是数据文件所在目录的子目录。
以上3个条件有任意一个满足,则会执行if语句体。可见,想执行该if语句,构造的重命名操作还是很容易的。成功进入该if后,先执行AdjustPathString,将字符串中的所有 ‘/’ 字符替换为 ‘\’字符,统一目录分隔符。接着执行MakeNewQQPathDir通过原始目录名和新目录名生成新的QQ安装目录(保护目录)。我们跟进函数MakeNewQQPathDir,看看它的内部逻辑怎么导致了本漏洞的产生。分析过程我直接融合到了伪码中,大家注意看代码以及注释就行了。伪码如下:
VOID
NTAPI
MakeNewQQPathDir(
IN PWSTR OldDir,
IN PWSTR NewDir,
IN ULONG LengthOfNewDir,
OUT PWSTR *NewQQPathDir )
/*++
Function description:
Make new qq setup path directory by folder rename operation
Parameters:
OldDir:the original folder name for rename operation
NewDir:the result of rename operation
LengthOfNewDir:the length of NewDir in characters
NewQQPathDir:pointer to pointer for the result qq setup path dir
Return value:
None
NOTE:
The caller must call ExFreePool to free the memory that returned by NewQQPathDir
--*/
{
ULONG lengthOfOldDir = 0;
//消除目录名结尾的反斜杠
lengthOfOldDir = wcslen( OldDir );
if( OldDir[ lengthOfOldDir - 1 ] == L'\\' ||
OldDir[ lengthOfOldDir - 1 ] == L'/' )
{
lengthOfOldDir--;
}
//消除目录名结尾的反斜杠
if( NewDir[ LengthOfNewDir - 1 ] == L'\\' ||
NewDir[ LengthOfNewDir - 1 ] == L'/' )
{
LengthOfNewDir--;
}
//跳过字符串“\??\”
if( !wcsnicmp( NewDir,L"\\??\\",4 ))
{
LengthOfNewDir -= 4;
NewDir += 4;
}
//获取互斥量,保护全局变量 g_PathRegValue,g_PathRegValue 是一个WCHAR 类型的数组,用来保存当前QQ安装目录(驱动保护目录)。
ExAcquireFastMutex( &g_Mutex_for_PathRegValue);
__try{
if(lengthOfOldDir <= wcslen( g_PathRegValue ) &&
[COLOR=red]!wcsnicmp( OldDir,g_PathRegValue,lengthOfOldDir ))[/COLOR] //BUG 0day
{ //如果是当前安装目录的父目录,则执行该分支,正是由于此处简单的匹配导致本漏洞的产生。
PWCHAR pResult = (PWCHAR)MyAllocatePool( PagedPool,MAX_UNICODE_STRING_BUFFER_LENGTH);
*NewQQPathDir = pResult;
if( pResult )
{
//以下构造新的安装目录。
memset( pResult,0,MAX_UNICODE_STRING_BUFFER_LENGTH );
if( LengthOfNewDir >= MAX_UNICODE_STRING_CHARS + 1 )
LengthOfNewDir = MAX_UNICODE_STRING_CHARS;
//将新的目录名复制到pResult表示的缓冲区
wcsncpy( pResult,NewDir,LengthOfNewDir );
//拼接原始安装目录剩余的部分到pResult,产生新的安装目录
if( lengthOfOldDir < wcslen( g_PathRegValue ))
{
ULONG chars = wcslen( g_PathRegValue ) - lengthOfOldDir;
if( chars >= MAX_UNICODE_STRING_CHARS + 1 - LengthOfNewDir)
chars = MAX_UNICODE_STRING_CHARS - LengthOfNewDir;
wcsncat( pResult,g_PathRegValue + lengthOfOldDir,chars );
}
}
}
}__except( EXCEPTION_EXECUTE_HANDLER )
{
KdPrint(("Exception occurred in MakeNewQQPathDir!\n"));
}
//释放互斥量
ExReleaseFastMutex( &g_Mutex_for_PathRegValue );
}
先举一个程序正常工作的例子,设当前相关的目录信息如下:
g_PathRegValue: C:\Program Files\Tencent\QQ
OldDir: C:\Program Files\Tencent\
NewDir: C:\Program Files\Tencent123\
此时,OldDir即为g_PathRegValue 的父目录。它的改变将影响QQ的保护路径。程序首先消除字符串结尾的斜杠,接着进行长度比较(len(OldDir) <= len(g_PathRegValue) )以及父目录的匹配。g_PathRegVAlue 被分为两部分.1: C:\Program Files\Tencent 2:\QQ 。生成的新的安装目录名即为NewDir与g_PathRegValue的第2部分这种。结果为:C:\Program Files\Tencent123\QQ
由于代码中红色部分的匹配方法过于潦草,导致进行相应的构造可以使驱动误动作修改保护路径。有兴趣的话,大家可以模拟一下漏洞提交中给出的案例,也可以搭建环境自己体验一下。
到此,对于本漏洞的分析就完成了。漏洞是水了点儿,但此类漏洞如果没人去挖掘,或许永远是个0day吧,用户能触发并察觉的概率又有多少?程序员又有多少人会把注意力放在这里?没有溢出,价值太小,大牛们看不上,就给我等菜鸟留下了空间,呵呵。TSRC很绘力,这种低级别的都能换一堆公仔,新手们加油啦!
分析过程中难免有出错的地方,欢迎大家指正,一起学习进步。最后还是送上那一句不变的话:好好学习,天天向上!
补充一下:有个和本文提到漏洞相关的问题提交以后,被TSRC忽略了,大家讨论一下吧。欢迎大家积极发表观点儿。
一、详细说明
QQProtect.sys 在自己的注册表驱动服务项里保存QQ的安装目录以及QQProtect.exe的全路径,并在开机启动加载服务后读取并初始化保护路径。在设计中,有个功能是跟踪用户对影响QQ目录的父级目录的改名操作。并实时调整上边提到的注册表键值(QPath和Path)。正是由于此项功能的存在和不完善,导致本漏洞的产生。恶意攻击者可以利用此漏洞突破驱动基于安装目录的保护。
QQProtect.sys版本(2.6.0.4 ,2.8.0.5,2.9.0.3),32bit
二、漏洞证明
演示过程:
附件中 replace.bat 是用于演示的批处理文件(完全可以编程做同样的事情)。
1、直接运行replace.bat(如果是win7,且QQ安装目录在系统保护的目录中,请使用管理员权限),稍等片刻(主要时间是在复制文件)程序结束后进行下一步测试。
2、到QQ安装目录进行删除文件测试,如果可以正常删除被保护文件,则证明漏洞利用成功说明:具体操作步骤请参看批处理文件,主要工作原理是:改名转移驱动的保护路径,通过复制原始安装目录的内容制作不安全的QQ运行环境。由于QQ快捷方式从未变化过,运行目录也是原始的安装目录,所以此操作比较难以被察觉。
三、修复方案
1、驱动中需要判断当前运行的QQ实例是否在当前的保护路径下,这样可以防止不在保护目录中的“山寨”QQ的运行。
2、调整 QQProtect.exe ,通过对当前运行路径和注册表中驱动服务项的保护路径做对比决定是否正常运行QQ。
bat文件的内容如下:
cd c:\"Program Files"\Tencent
ren QQ QQ1
xcopy QQ1 QQ /E /I /Q
attrib +h +s QQ1
当时TSRC给出的忽略理由是:
该问题为产品策略所致,属于业务需求。 注:QQ2013 Beta3 可以找到 QQProtect.sys 2.6.0.4,漏洞说明中给出的地址是这个版本中的。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界