WinRAR 目录穿越漏洞 漏洞详情
该漏洞是由于 WinRAR 所使用的一个陈旧的动态链接库 UNACEV2.dll 所造成的,该动态链接库在 2006 年被编译,没有任何的基础保护机制 ( ASLR,DEP 等)。该动态链接库的作用是处理 ACE 格式文件。而在解压处理过程中存在一处目录穿越漏洞,允许解压过程写入文件至开机启动项,导致代码执行。
简单来说,在早期,存在一种压缩文件格式,称为 Ace 格式。unacev2.dll 提供了对 ace 格式的压缩文件进行解压缩的接口,但是由于过于陈旧,一直都没有更新。安全分析人员通过 fuzz 技术发现了
漏洞复现 复现环境 操作系统:Windows XP SP3
软件版本:WinRAR 5.50(32 位)
EXP:
复现流程 从 github 下载 exp 目录如下:
exp.py
中:
1 2 3 4 ... target_filename = r"C:\C:C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\hi.exe" ...
指定了将目录中的 calc.exe 复制到指定目录下,重命名为 hi.exe。
执行 exp.py,生成 test.rar 如下:
在 Windows XP 中打开 test.rar 如下:
可以看到,有一项不正常的内容。解压这个 rar(实际上是 ace 格式)之后,hi.exe 将被解压到某个目录:(因为用的是 XP 系统,所以 AppData 及后续目录不是正确的启动项。但是无所谓,这已经足以表明解压的时候在异常的位置创建了文件,并可能在启动时运行。)
漏洞分析 根据漏洞提交团队的博客 来看,他们是使用了 fuzz 来挖掘出该漏洞的。所以后面我也想尝试使用 winafl 来测试。使用 winafl fuzz 需要编写一个 harness,可以参考 【模糊测试】如何编写一个Harness? 。总而言之编写了一个 harness:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 #include <stdio.h> #include <stdlib.h> #include <Windows.h> #include "STRUCS.H" #include "UNACEFNC.H" #define NOP(x) \ int _nop##x(){ \ printf("==>nop%d called\n" , x ); \ return (int)x; \ } \ int (*nop##x)(void) = _nop##x; NOP(0 ) NOP(1 ) NOP(2 ) NOP(3 ) HMODULE module; char unknowstr1[1024 ];char unknowstr2[1024 ];char passwd[1024 ];char comment[0x2000 ];int (*ACEInitDll_addr)(pACEInitDllStruc) = NULL ;int (*ACEExtract_addr)(LPSTR, pACEExtractStruc) = NULL ;void fuzzfunction (char *rarpath, char *extractpath) ;BOOL isEmptyDirectory (const char * dir) ; wchar_t * charToWChar (const char * text) ;int main (int argc, char **argv) { if (argc < 2 ) { printf ("Usage: %s <ace-format-file>\n" , argv[0 ]); return 1 ; } wchar_t * PathName = charToWChar(argv[1 ]); module = LoadLibrary("unacev2.dll" ); if (module == NULL ) { return -1 ; } ACEInitDll_addr = (int (*)(pACEInitDllStruc))GetProcAddress(module, "ACEInitDll" ); ACEExtract_addr = (int (*)(LPSTR, pACEExtractStruc))GetProcAddress(module, "ACEExtract" ); char filename[100 ] = { 0 , }; _splitpath(argv[1 ], NULL , NULL , filename, NULL ); char extractpath[1024 ]; sprintf (extractpath, "C:\\tmp\\%s" , filename); puts (extractpath); int i = CreateDirectory(extractpath, NULL ); printf ("%d" , &i); fuzzfunction(argv[1 ], extractpath); RemoveDirectory(extractpath); FreeLibrary(module); module = NULL ; return 0 ; } void fuzzfunction (char *rarpath, char *extractpath) { memset (unknowstr1, 0 , 1024 ); memset (passwd, 0 , 1024 ); memset (unknowstr2, 0 , 1024 ); memset (comment, 0 , 0x2000 ); char tmpdir[] = "C:\\tmp" ; tACEInitDllStruc struc = {0 ,}; struc.GlobalData.MaxArchiveTestBytes = 0x3FFFF ; struc.GlobalData.MaxFileBufSize = 0x2FFFF ; struc.GlobalData.Comment.Buf = comment; struc.GlobalData.Comment.BufSize = 0x2000 ; struc.GlobalData.TempDir = tmpdir; struc.GlobalData.InfoCallbackProc = (INT (__stdcall *)(pACEInfoCallbackProcStruc))nop0; struc.GlobalData.ErrorCallbackProc = (INT (__stdcall *)(pACEErrorCallbackProcStruc))nop1; struc.GlobalData.RequestCallbackProc = (INT (__stdcall *)(pACERequestCallbackProcStruc))nop2; struc.GlobalData.StateCallbackProc = (INT (__stdcall *)(pACEStateCallbackProcStruc))nop3; ACEInitDll_addr(&struc); tACEExtractStruc extract; char fl[] = "*" ; extract.Files.FileList = fl; extract.ExcludePath = NULL ; extract.Files.SourceDir = (LPSTR)unknowstr1; extract.Files.ExcludeList = (LPSTR)unknowstr2; extract.Files.FullMatch = 1 ; extract.DestinationDir = extractpath; extract.DecryptPassword = passwd; ACEExtract_addr(rarpath, &extract); return ; } wchar_t * charToWChar (const char * text) { size_t size = strlen (text) + 1 ; wchar_t * wa = new wchar_t [size]; mbstowcs(wa, text, size); return wa; }
用 harness 来调试至少看起来更简洁,虽然漏洞位于 unacev2.dll 里,跟 harness 也没关系。
IDA + WinDBG 动态调试配置准备 需要调试的程序是 unacev2.dll,但不能直接触发,需要通过执行编写的 harness 来调用 dll 中的函数。因此 IDA 中的设置如下:
Application:设置成入口程序,就是我们编写的 harness;
Input file:需要被调试的程序,设置成 dll。
调试的时候我的 xp 虚拟机崩掉了,执行的时候总有 bug,所以后面就放到我的 Windows 7 上调试了。配置项都是一样的。
动静结合分析 unacev2.dll harness 的主要的调用流程如下:
1 2 3 4 ... ... ACEInitDll_addr(&struc); ... ... ACEExtract_addr(rarpath, &extract);
所以解压过程主要发生在函数 ACEExtract_addr
中。因此在该函数内下断点,主要分析这个函数:
进行了简单的逆向。
GetAceFileName_guess
:猜测是用于获取要解压的 ace 文件路径;
DealWithPaths
:处理文件路径,解压路径等;
DealWithReg
:进行注册表相关的操作,具体功能未知;
Maybe_CheckAndExtract
:对 ace 文件进行检查,然后解压。
所以然后进入 Maybe_CheckAndExtract()
函数。
可以看到第 8 行,Maybe_Check()
进行检查,检查通过之后执行内部的解压和检查操作。这里主要关注 Maybe_Extract()
函数。
通过追踪 Maybe_Extract()
函数的调用流程,找到位于 0038BF0F 的函数(重命名为 maybe_extract_all_files()
),该函数中的 for 循环会依次提取压缩包中的每个文件:
所以接下来分析 do_extract()
函数(地址是 0035BADC),总体逻辑如下:
1 2 3 4 ... ... maybe_create_extract_file(); ... ... maybe_write_extract_file();
原则上,maybe_create_extract_file()
应该只能在我们指定的目录创建文件,但是恶意构造的 ace 文件可以向其他路径创建文件。所以漏洞应该发生在这个函数中。
我主要关注了解压 calc.exe 这个隐藏的恶意程序的时候,do_extract()
函数和 maybe_create_extract_file()
函数的执行流程。
在执行 do_extract()
函数之前,调用了 strcpy()
函数来获取当前要提取的文件名。解压 hello.txt 时内容正常,如下:
但解压隐藏的恶意程序时,文件名如下:
maybe_create_extract_file()
逻辑如下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 if (file_name[0 ] == '\\' && file_name[1 ] == '\\' ) { ... ... } if (file_name[1 ] == ':' && file_name[2 ] == '\\' ) { strcpy (file_name, &filename[3 ]); } char *name = NULL ;if (file_name[0 ] != ':' || file_name[1 ] == '\\' ){ .loop: char *fn = file_name; char *substr = NULL ; while (substr = guess_strstr(fn, "..\\" )){ if (substr == file_name || substr[-1 ] == '\\' ){ strcpy (substr, &substr[3 ]); } else { fn = substr + 1 ; } } } else { file_name = &file_name[2 ]; goto .loop; } char *path;if is_path (file_name) { path = "" ; } else { path = extract_dir; } sprintf (final_path, "%s%s" , extract_dir, file_name);
最终创建目录的函数:0065CA78 final_create_path_ttttttt
经过上述处理之后,file_name
被处理为下面的字符串:(其实就是截断了两个 C:\
)
通过逆向分析程序,可以看出,原程序的各种检查过滤了所有的 ..\
,我认为是为了避免目录逃逸。但是从后面的逻辑来看,似乎又没有禁止使用绝对路径。(虽然前面也对盘符等绝对路径做了过滤)。错综复杂的逻辑最后得到了这样一个绝对路径。
接下来,调用了 final_create_path()
函数 (地址:0034CA78)来创建文件。里面又对目录做了处理(?为什么不能在一个函数处理完目录呢??),该函数逻辑如下:
本质上 GetAceFileName_guess()
函数是调用了 GetFullPathNameA()
函数,可以从某个相对路径得到绝对路径。处理完之后的结果如下:
到这里就基本搞清楚是怎么实现目录穿越的了。进一步地进行分析,包括进行 exp 编写的话,就需要搞清楚 ace 的格式了。先挖个坑。