QQProtect驱动调用者签名校验漏洞分析
上次给大家带来一个“中级”的洞洞。或许大家感到有些失望,当然我也有些失望。由于时间过了快两个月,有些稍高点级别的漏洞可以放出来了。今天给大家带来一个“高级“的漏洞。即便如此,我确信有人还会失望的。因为仍然是”灰盒“的方法,而且是非内存破坏类型的。大牛们就飘过吧。
漏洞提交时候的原始资料如下(环境搭建,操作步骤的信息都在里边,我不会再过多描述)
漏洞名称:QQProtect驱动保护数字签名校验漏洞
漏洞类型:权限提升/绕过
危害等级:高
一、详细说明
默认情况下QQProtect.sys 会对打开驱动的进程映象文件进行检查。主要有两项:一是进程名称,必须为"qqprotect.exe",二是映象文件的数字签名。如果此两项的检查不能通过,则打开驱动会失败,无法获得驱动句柄。本漏洞正是因为检验签名的逻辑有缺陷,导致伪造的qqprotect.exe可以绕过数字签名检查打开驱动,从而获得对驱动的完全控制权限。
QQProtect.sys版本(2.6.0.4 ,2.8.0.5),32bit
这两个版本的问题函数地址分别为:0x1bbc0和0x1c6a0
二、漏洞证明
qqprotect.c 是漏洞演示源码。
qqprotect.exe.org是编译生成的二进制可执行程序。直接运行,此程序无法打开驱动,失败退出。
qqprotect.exe 是对qqprotext.exe.org进行加工的产物(二进制编辑器)。此程序具有绕过数字签名检查的能力。
演示过程:
本演示程序仅仅演示了关闭驱动监控,修改保护路径的效果。因为已经具有完全控制驱动的权限,此例子已经可以证明该漏洞的危害性。
1、正常运行附件中的qqprotect.exe程序,可以看到提示 hFile = xxx 的字样,如果xxx不是ffffffff 则证明驱动打开成功。
2、驱动打开后,程序会发送控制码给驱动,关闭监控。不出意外的话,可以看到屏幕上的提示(成功或失败)
3、控制码发送成功后,则监控已经关闭。此后要求输入新的保护路径字符串。输入后回车确认即可(此处可以不输入,直接关闭程序,仅演示关闭监控的功能)。
4、退出程序。此时,驱动监控已经关闭。可以到安装目录修改文件,删除文件,用注册表编辑器修改QQProtect驱动项键值等做测试。
注:第3步修改注册表的操作在win7下如果无管理员权限会失败。因为即使没驱动保护,修改关键的注册表项也需要管理员权限。这里的失败和驱动无关。
三、修复方案
在调试两个版本的漏洞过程中发现,此处曾经更新过。但还是可以绕过。建议开发人员继续改进数字签名验签算法。
备选方案:将入口点RVA转换为文件偏移后再判断其是否在哈希区域内。
下边我们从版本2.6.0.4开始分析。
对驱动有所了解的朋友都知道,Ring3打开驱动的方式首先是CreateFile,然后是DeviceIoControl或者Read/Write File一类的操作。而要校验调用者的合法性,方法无非是在IRP_MJ_CREATE 的派遣例程中进行验证,HOOK住ZwCreateFile对来自CreateFile的调用进行验证,或者是对来自DeviceIoControl,Read/WriteFile 的调用进行合法性验证等。而通过对QQP的研究发现,QQP采用的是最前边的方案,即在IRP_MJ_CREATE的派遣例程中对调用者进行签名检查的方案。先给出关键的伪代码。
代码:
NTSTATUS QQPDispatchCreateClose( PDEVICE_OBJECT DeviceObject,PIRP Irp ) { NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION IrpSp = NULL; KdPrint(("------------------------QQPDispatchCreateClose-------------------\n")); if( !DeviceObject || !Irp ) { status = STATUS_UNSUCCESSFUL; goto exit; } IrpSp = IoGetCurrentIrpStackLocation( Irp ); if( IRP_MJ_CREATE == IrpSp->MajorFunction ) { if( IsCallerAppImageAllowed() ) { if( !InterlockedCompareExchange( &g_bHookMonThreadBusy,TRUE,FALSE)) { MakeSureHookOk(); MakeSureLoadImageNotifyOk( &NotifyRoutine_LoadImage); MakeSureCreateThreadNotifyOk(&NotifyRoutine_CreateThread); MakeSureCreateProcessNotifyOk(&NotifyRoutine_CreateProcess); InterlockedExchange( &g_bHookMonThreadBusy,FALSE ); } } else { status = STATUS_ACCESS_DENIED; } } else if ( IRP_MJ_CLOSE == IrpSp->MajorFunction ) { CloseR3Object(); FreeList_0_1_2_3(); } Irp->IoStatus.Information = 0; Irp->IoStatus.Status = status; IoCompleteRequest( Irp,IO_NO_INCREMENT ); exit: KdPrint(("Ret value:%x\n",status )); KdPrint(("----------------------------------------------------------\n\n\n")); return status; }
在对IRP_MJ_CREATE的处理中,首先调用函数IsCallerAppImageAllowed 验证调用者可执行映象的合法性,接着进行恢复HOOK的操作。而看过标题的人应该都能明白。本漏洞的重点就在IsCallerAppImageAllowed函数中。该函数的伪码如下(注意看注释):
代码:
BOOLEAN NTAPI IsCallerAppImageAllowed() /*++ function description: Check whether current process is allowed to control driver Parameters: None return value: if allowed,return true,otherwise return false comment: this function is called in DispatchCreate Routine --*/ { WCHAR DosDeviceName[MAX_UNICODE_STRING_CHARS + 1] = {0}; WCHAR FileDosName[MAX_UNICODE_STRING_CHARS + 1] ={0}; //获取调用者可执行文件名称 GetProcessDevicePathName( (ULONG)PsGetCurrentProcessId(),DosDeviceName,0x100 ); GetFileDosName( DosDeviceName,FileDosName,0x100,FALSE ); wcsupr( FileDosName ); //统一为大写 if( wcsstr( FileDosName,L"\\QQPROTECT.EXE")) //判断文件全路径中是否含有QQPROTECT.EXE的字样 { BOOLEAN bResult = FALSE; __try{ bResult = IsFileDigitalSignatureOk( FileDosName ); //校验数字签名 }__except(EXCEPTION_EXECUTE_HANDLER ) { KdPrint(("exception occurred in IsCallerAppImageAllowed\n")); } return bResult; } return FALSE; }
后文着重看数字签名的验证算法。先上伪码:
代码:
BOOLEAN NTAPI IsFileDigitalSignatureOk( PCWSTR FileName ) /*++ function description: Check whether the digital signature( Tencent) in file is ok Parameters: FileName:the specific file name return value: if signature is ok,then return ture ,otherwise return false --*/ { BOOLEAN bResult = FALSE; PVOID FileData = NULL; ULONG FileSize = 0; struct _SIG_INFOR //签名信息的结构体 { ULONG StartFileOffset; ULONG Length; UCHAR MD5Value[0x10]; }sig_infor = {0},sig_infor_tmp = {0}; ULONG retBytes = 0; ULONG SigDataOffset = 0; FileData = GetFileData_6MB( FileName,&FileSize );// 从文件头提取不多于6MB的数据 if( !FileData ) goto exit; if( FileSize <= 0x40 ) goto exit; if( !IsValidPeImage( FileData )) //检查PE文件格式,对于版本2.6.0.4并不重要 goto exit; SigDataOffset = *(PULONG)((ULONG_PTR)FileData + 0x28);//获取RSA密文的文件偏移 if( !SigDataOffset || SigDataOffset > FileSize - 0x80 ) goto exit; retBytes = sizeof( sig_infor ); //RSA解密数据,此函数中包含TX的公钥。后边我会展开该函数。 if( !RSA_Encode( (unsigned char *)((ULONG_PTR)FileData + SigDataOffset), 0x80, (unsigned char *)&sig_infor,//解密后的数据存到sig_infor中 &retBytes )) goto exit; //验证签名数据是否有效。 sig_infor_tmp = sig_infor; if( sig_infor.Length && sig_infor.Length <= FileSize && sig_infor.StartFileOffset + sig_infor.Length <= FileSize ) { MD5Data( (unsigned char*)((ULONG_PTR)FileData + sig_infor.StartFileOffset), sig_infor.Length, sig_infor.MD5Value ); //对比文件从指定位置,指定长度的数据进行MD5运算,并和签名信息中的原始数据进行对比 if( RtlCompareMemory( sig_infor.MD5Value,sig_infor_tmp.MD5Value,0x10 ) == 0x10 ) bResult = TRUE; } exit: if( FileData ) ExFreePool( FileData ); return bResult; }
分析这个验证流程,我们可以知道,加密的数据是0x80字节(1024 bit)。解密得到的数据是一个结构。记录有一个文件偏移值,一个数据长度值以及一个MD5值。算法通过对调用者可执行映象文件中指定的数据进行MD5运算,然后与签名信息中的MD5值进行对比完成验答过程。就在这里的调试发现,签名信息中的文件偏移字段的值是从.text节开始的,并不包含可执行文件的PE文件头的验证。于是,猜想是不是可以通过PEDIY的方式绕过签名检查呢?答案显然是可以的。只要我们保证指定位置开始的指定长度的数据为原始数据即可。如何构造,对PE结构有所了解你就可以PEDIY了。很简单,我就少打点儿字。构造步骤放到附件里。
上边分析的是2.6.0.4版本。可能有白帽子找到类似的漏洞提交了,这个我不得而知。在2.8.0.5版本中,相应位置有所变化。(我的构造方法依然可用,可能前人提交的是别的利用方法)。下边是验证签名函数在2.8.0.5中的伪代码,大家注意看和2.6.0.4 不同的地方TX是怎么打补丁的以及这个补丁针对的是什么问题,我就不过多分析了(有代码有真相)。
代码:
BOOLEAN NTAPI IsFileDigitalSignatureOk( PCWSTR FileName ) /*++ function description: Check whether the digital signature( Tencent) in file is ok Parameters: FileName:the specific file name return value: if signature is ok,then return ture ,otherwise return false --*/ { BOOLEAN bResult = FALSE; PVOID FileData = NULL; ULONG FileSize = 0; struct _SIG_INFOR { ULONG StartFileOffset; ULONG Length; UCHAR MD5Value[0x10]; }sig_infor = {0},sig_infor_tmp = {0}; ULONG retBytes = 0; ULONG SigDataOffset = 0; ULONG RvaEntryPoint = 0; FileData = GetFileData_6MB( FileName,&FileSize ); if( !FileData ) goto exit; if( FileSize <= 0x40 ) goto exit; #if 0 //Version 2.6.0.4 if( !IsValidPeImage( FileData)) goto exit; #endif //Version 2.8.0.5 if( !IsValidPeImage( FileData,&RvaEntryPoint )) //返回了入口点的RVA goto exit; SigDataOffset = *(PULONG)((ULONG_PTR)FileData + 0x28); if( !SigDataOffset || SigDataOffset > FileSize - 0x80 ) goto exit; retBytes = sizeof( sig_infor ); if( !RSA_Encode( (unsigned char *)((ULONG_PTR)FileData + SigDataOffset), 0x80, (unsigned char *)&sig_infor, &retBytes )) goto exit; sig_infor_tmp = sig_infor; if( sig_infor.Length && sig_infor.Length <= FileSize && sig_infor.StartFileOffset + sig_infor.Length <= FileSize ) { MD5Data( (unsigned char*)((ULONG_PTR)FileData + sig_infor.StartFileOffset), sig_infor.Length, sig_infor.MD5Value ); if( RtlCompareMemory( sig_infor.MD5Value,sig_infor_tmp.MD5Value,0x10 ) == 0x10 ) { #if 0 //Version 2.6.0.4 bResult = TRUE; #endif //Version 2.8.0.5 //这里在验证MD5的同时,也检查入口点是否在签名的文件区域内。但程序员把Rva当成文偏移用,导致漏洞的产生。我给出的方案依然可以通过检查。 bResult = RvaEntryPoint >= sig_infor_tmp.StartFileOffset && RvaEntryPoint < sig_infor_tmp.StartFileOffset + sig_infor_tmp.Length; } } exit: if( FileData ) ExFreePool( FileData ); return bResult; }
分析到这里就大致结束了。技术含量虽然不大,但影响还是蛮大的。通过签名检查之后,从Ring3你可以很容易拿到QQP的驱动句柄,任意操作驱动(当然,需要一些控制码),过文件保护,注册表保护,关闭反注入,反劫持的功能都是很简单的事情了。
哈哈,如果你失望了,现在可以离开了。对密码学或者数学有兴趣的朋友可以继续往下看。上边曾经提到要展开RSA解密函数。函数名是:RSA_Encode 。在RSA中,其实加解密用的算法一样。只是key不同而已。所以,这里写个“Encode“,大家不要喷啊!下边是伪码:
代码:
unsigned char RSA_PKEY_N[0x80] = { 0xD5,0xB2,0xF4,0x66,0x33,0x0B,0xF3,0x2A,0xE9,0x6C,0x5A,0xDC,0x42,0x2E,0x4C,0x9F, 0xB8,0x38,0x6C,0x26,0xA0,0x0C,0x46,0x41,0xAA,0xEA,0xCD,0x2F,0x7B,0x09,0xB2,0x7C, 0x2D,0xB9,0x63,0x3D,0xDD,0xF8,0x4D,0xF0,0x9D,0x8C,0xC2,0x47,0x6E,0x43,0x4A,0x5B, 0x3F,0xA8,0x17,0xEA,0x5E,0x87,0x0C,0x3C,0x55,0x0D,0x99,0x7C,0x5D,0xB1,0xC1,0x46, 0x8D,0xE8,0x2B,0xA1,0x85,0x9B,0x42,0x9E,0x8D,0xBA,0x8F,0x94,0xF2,0x07,0x12,0xC2, 0xF9,0x2E,0x74,0xC4,0x2B,0x31,0x12,0x9C,0xF0,0x9D,0x59,0x7F,0xA9,0x91,0x3F,0xB2, 0x9B,0x22,0x67,0xBC,0xAA,0xE9,0x75,0x7D,0x1A,0x72,0x43,0xCB,0x47,0x85,0x62,0x72, 0xE9,0x0C,0x0E,0x7E,0xC3,0x8F,0xC1,0xA8,0xDC,0x6B,0x10,0x9B,0xB0,0xF5,0x89,0xA9 }; unsigned char RSA_PKEY_E[0x08] = //只用了前三个字节 { 0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00 }; bool RSA_Encode( unsigned char *in_buffer,unsigned int in_len,unsigned char *out_buffer,unsigned int *out_len ) /*++ function description:the rsa encode or decode function Parameters: in_buffer:the buffer to encode or decode in_len;the length of in_buffer in bytes out_buffer:the buffer to save result out_len:pointer to variable for get the length of out_buffer length and put the result length into it(optional ) return value:success return true,else return false --*/ { bignum_t indata,outdata,N,E; //从缓冲区中建立大数 if (!bignum_evaluate_buffer( &indata,in_buffer,in_len )) return false; if( !bignum_evaluate_buffer( &N,RSA_PKEY_N,0x80 )) return false; if( !bignum_evaluate_buffer( &E,RSA_PKEY_E,0x03)) return false; //执行RSA加解密操作,结果为一个大数,程序中定义为1024 bit if( !bignum_pow_mod( &outdata,&indata,&E,&N)) return false; //将大数数据复制到指定的长度的缓冲区中(这里可能有风险,后边说) if( !bignum_to_buffer( &outdata,out_buffer,*out_len)) return false; return true; }
在分析这个RSA加解密函数的时候,我发现了个小小的问题。也就是在RSA_Encode的注释中我提到的地方。在最后一处将大数数据复制到缓冲区的调用中,通过一个指针给定了缓冲区的长度。如果解密后的大数比这个长度大,则有截断操作。bignum_to_buffer 的逻辑是这样的:
代码:
bool bignum_api bignum_to_buffer( bignum_t *a,unsigned char *buf,unsigned int len ) /*++ Parameters: a:the bignum to convert to buf:pointer to output bignum len:bytes of buffer Return value: if success ,return true.otherwise,return false --*/ { unsigned int i = 0; unsigned char *p1,*p2; while( bignum_cmp_dword( a,0 ) && i < len ) //有截断 { buf[i++] = (unsigned char)a->bits[0]; bignum_r_shift( a,8 ); } //逆序操作 p2 = buf + len - 1; for( p1 = buf;p1 < p2;p1++,p2--) { unsigned char tmp = *p1; *p1 = *p2; *p2 = tmp; } return true; }
通过前边的分析我们不难发现,加密数据用公钥解密后的信息是一个SIG_INFOR 的结构,该结构的大小是0x18字节(192 bit )。也就是说,解密后的大数的低192位符合要求即可。高 832bit并不作要求(严格的设计中,这时高832bit也必须是全零才能保证解密后的数据是相对唯一的)。这是不是为伪造签名提供了一个突破口呢? 于是提出这样一个方案:
1、编写自己的程序。用该程序的可执行映象文件生成一个合法的SIG_INFOR 结构。该结构对应一个192bit的大数,记为S_192
2、寻找大数M,使得 (pow(M,E) mod N ) mod ( pow(2,192)) == S_192 ,其中E,N是我们已知的公钥。
如果可以找到符合条件的M,则把M附到自写的程序当中后,我们自制的签名就可以通过验证。然而,寻找这样的M难度如何呢?正常情况下,在获得S_192以后,通过私钥,可以得到一个M’,满足 pow(M’,E) mod N == S_192 。很显然,我们没有私钥,现在需要解方程。方程有两个:
(1):(pow(M,E) mod N ) mod ( pow(2,192)) == S_192
(2)pow(M’,E) mod N == S_192
解方程(2)等价于破解RSA,这个直接放弃。焦点问题是方程(1)是不是比方程(2)容易得到解。直观感觉,如果采用穷举的方法,(1)中搜索到解的概率要比(2)提高不少,有兴趣的同学可以自己算下。基于这个事实,可以断定TX的这个算法导致RSA的安全强度降低了。然而穷举算法是不可取的,是不是可以用专业的数学知识去解这个相对“容易“的方程(1)呢?当时很激动,去问数学大神,结果被无情鄙视了。结论是安全强度虽然有所下降,但仍然是相对安全的。下边是大神给我的答复,也拿出来和大家分享一下。在这里要特别感谢大神耐心的给我等菜鸟做解答。虽然被鄙视,但依然很高兴。
虽然这个缺陷最终没有被成功利用,但对于业界应该是有一定警示意义的。现实应用中,往往由于不合理的运用导致被证明是安全的加解密方案变得不再安全。当然,如果你不认为这是小题大作的话:-)
好了。分析到此就结束了。在此要特别感谢教主自从我进入论坛以来一直对我的帮助,感谢数学大神的不厌其烦地给我解惑。我会努力下去,绝不给你们丢脸!文章末尾,祝大家学习快乐。好好学习,天天向上!
注:QQ2013 Beta3 可以找到 QQProtect.sys 2.6.0.4