17

CVE-2021-1647复现

 2 years ago
source link: https://suyumen.github.io/2022/04/06/2022-04-06-CVE-2021-1647%E5%A4%8D%E7%8E%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

CVE-2021-1647复现

Word count: 1.5k | Reading time: 5min

二进制安全一窍不通啊,写逆向作业还费劲呢就要复现这个真是难为我啊呜呜。

  • 漏洞名称:Microsoft Defender缓冲区溢出漏洞
  • 漏洞编号:CVE-2021-1647
  • 漏洞类型:堆溢出
  • 漏洞影响:远程代码执行
  • CVSS:7.8 HIGH
  • 利用难度:中等
  • 基础权限:不需要

安装Win10-1903虚拟机环境。

登入win10虚拟机并断网(防止Windows Defener自动更新),复制样本压缩包放入虚拟机,解压。

弹出shell窗口。同时Windows Defender发生崩溃,并在数十秒后自动重新启动。

将虚拟机中Windows Defener的文件MpEngine.dll加载入ida,找到函数CAsprotectDLLAndVersion::RetrieveVersionInfoAndCreateObjects反汇编一下,来到漏洞代码段。

不知道是不是因为ida版本的原因,我反汇编得到的结果和在网上参考的别人得到的反汇编的结果有很大的差别。个人经过一些分析处理后,简化为如下伪代码段。

unsigned int section[4];
max_rva = 0;
max_size = 0;
a = 0;
index = 0i64;
i = (unsigned int *)((char *)this + 28);
while (1)
{
if (i[2] < *i)
goto LABEL_110;
section[index] = i[2] - *i;
if (i[1] > section[index])
break;
++index;
i += 2;
if (index >= 3)
{
section[3] = (*((_DWORD *)this + 14) + 4095) & 0xFFFFF000;
section_index = 0i64;
asp_enc_table = (_DWORD *)((char *)this + 32);
while ((*(asp_enc_table - 1) & 0xFFF) == 0)
{
if (*(asp_enc_table - 1) > max_rva)
{
max_rva = *(asp_enc_table - 1);
max_rva_size = (*asp_enc_table + 4095) & 0xFFFFF000;
}
if (section[section_index] <= a)
section[section_index] = a;
a = section[section_index];
++section_index;
asp_enc_table += 2;
v30 = malloc(max_rva_size + max_rva);
*((_QWORD *)this + 10) = v30;
if ( v30 )
{
memset(v30, 0, *((unsigned int *)this + 23));

this + 28section数组的开始,section数组包含四个元素,每个元素是8字节,前4字节代表section的虚拟地址,后四个字节表示section的大小。

遍历section数组,最后获得一个max_rva最大相对虚拟地址,和max_rva_size存储大小,在后面malloc申请一片大小为 max_rva + max_rva的内存用于存储解压后的section内容。

在判断语句if (*(asp_enc_table - 1) > max_rva),由于这里没有考虑等于的情况,导致如果给数组的赋值中虚拟地址相同,而在大小上后面的数比前面的数大(如[0,0],[0,0],[0x2000,0],[0x2000,0x3000])得到的结果会是max_rva = 0x2000max_rva_size = 0,从而在解压最后一个section的时候产生堆溢出问题。

漏洞利用样本分析

将漏洞利用样本拖入ida分析。

首先call sub_409BC0,跟进,反汇编一下。

看到这个函数最终是call 7C96C654h这个地址且第一个参数是3,Windows Defender在扫描样本时模拟执行样本的代码,因此内存地址并不是真实的地址,而是模拟的内存空间中的一个地址,即ntdll.dll的地址。而7C96C654h这个地址对应于ntdll.dll中的这一段代码(我自己没找到,这是参考的代码):

函数sub_7C96C654尾部两个字节0xff,0xff表明函数是native调用;函数尾部的0x9E9EFDF0是标识native api函数的crc校验码;sub_7C96C654函数第一个参数为3时表示获取defender版本信息;根据不同defender的版本信息可以硬编码关键偏移;call sub_7C96C654最终会调用 mpengine!NTDLL_DLL_NtControlChannel。也就是说,样本开头就获得了偏移信息


start中,RegSetValueExW(phkResult, L"test", 0, 3u, lpData, dwSize);修改的是 vmmcontrol中的一个关键字段,这个关键字段描述的是索引数组一共有多少个元素。大量的ResumeThread的调用代码用来进行内存布局和占位的,在堆溢出发生后,会修改布局在堆内存后的lfind对象的两个关键字段,将分别由107e107f修改为2f9b2f9c,在在模拟执行ResumeThread函数时调用lfind_switch::switch_in函数中被引用。由于被修改之后的值比正常的大,造成越界写入

Windows Defender在模拟内存空间时会用到如下所示结构体(在内存中以数组形式存在),这个结构体用于维持模拟内存空间与真实内存地址的映射关系。

Struct EmuVaddrNode{
PVOID Vaddr //机器上真实的虚拟地址
Int32 EmuPageNum //Windows Defener模拟内存空间地址的页号
Int32 EmuProtect //virtualalloc这类函数中的vprotect属性对应于模拟内存空间中的属性
……
……
}

EmuVaddrNode数组和它的索引数组在真实内存中的布局:

索引数组---EmuVaddrNode数组---Page模拟内存对应的真实内存

vmmcontrol中的关键字段描述索引数组有多少元素。因为使其变大,所以索引数组元素变多,这时在Page中伪造索引数组和EmuVaddrNode结构,若Windows Defender模拟执行*p=value,会先从伪造的索引数组中取出伪造索引,然后根据伪造索引
访问伪造的EmuVaddrNode结构,最后访问攻击者构造的Vadder,实现任意地址写入的功能。同理,若Windows Defender模拟执行value=*p,就可以实现任意地址读取的功能。


Windows Defener中会将常用的代码片段进行jit(即时编译)处理,在取得任意地址读写能力后,样本中先将通过硬编码偏移获取到了jit部分的真实地址,将EmuVaddrNodevaddr设置为jit的真实地址,并且利用模拟执行 memcpy(EmuPageNum,shellcode, sizeof (shellocde))jit地址写入了shellcode,最终只要jit功能一使用便会执行shellcode。从而实现了shellcode的布置。

  • MpEngineasprotect壳解压后的内容校验如下:

    可类似*(memory)=0x8d;*(memory+1)=0x85;*(memory+6)=0x50; *(memory+7)=0xc3进行特征匹配。
  • 样本通过NtControlChannel获得版本信息以确定偏移信息,可以把NtControlChannel函数的调用特征作为匹配依据。

https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/reference/lfind?view=msvc-170

https://www.anquanke.com/post/id/231625


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK