用户模式电能管理服务 ( User-Mode Power Service ) 在内存中处理对象时存在权限提升漏洞。攻击者成功利用此漏洞可以以高权限执行命令。
Windows 10 1903 umpo.dll 补丁对比
UmpoRpcLegacyEventRegisterNotification
和 UmpoNotifyRegister
:
第三个函数是 UmpoNotifyUnregister
,它没有被匹配到,在 umpo.dll 补丁版本中已经被移除。
改动函数分析
现在既然已经知道改动的函数了,我们就可以开始审计它们以找到 bug 所在。快速查看这两个改动过的函数就会发现它们并不长。为了知道这个函数是怎么被调用的,我们需要检查每一个函数的交叉引用。并没有交叉引用来自于其他任何函数到 UmpoRpcLegacyEventRegisterNotification
。根据名称推测它可能是通过 RPC 调用的。然而 UmpoNotifyRegister
有一个函数调用它
UmpoRpcLegacyEventRegisterNotification
UmpoRpcLegacyEventRegisterNotification
。只要可能,我更喜欢做动态分析,因此,我们将启动 WinDbg 并在目标函数上设置断点。__int64 UmpoRpcLegacyEventRegisterNotification(__int64 a1,
__int64 a2,
const wchar_t *a3,
int a4)
a3
是一个 wchar_t
类型的参数,在此处是一个服务的名字。a2
保存了一些值,但是这些值并没有指向有效的内存,所以很有可能被用作某些常量或者标识符。a4
的值是 0 。我们保持跟踪 a4
查看其他可能的值。第一段有趣的代码已经标记好备注了:
v14 = 0i64;
v4 = a4;
v5 = a3;
v6 = a2;
// Return if not local client (local RPC only)
if ( !(unsigned int)UmpoIsClientLocal() )
return 5i64;
// Get some object
v7 = UmpoGetSettingEntry();
// If not NULL
if ( v7 )
{
// Get another object at v7+32
v8 = (__int64 *)(v7 + 32);
// Walk circular linked list until back at head
for ( i = *v8; (__int64 *)i != v8; i = *(_QWORD *)i )
{
// Breaks if object offset 20 == 1 and
// if object offset 24 == a2
if ( *(_DWORD *)(i + 20) == 1 && *(_QWORD *)(i + 24) == v6 )
goto LABEL_11;
}
// If not found set i to 0
i = 0i64;
LABEL_11:
// If found within circular linked list set v14 to object pointer
v14 = i;
}
UmpoGetSettingEntry()
函数来获取一个对象的引用。这个对象包含了一个指针在偏移 32 处指向了其他的对象。这个对象看起来是一个循环遍历的循环列表。如果在偏移量 24 处的对象成员等于 a2
,并且在偏移量 20 处的对象成员等于 1 ,那么循环将中断。// if a4
if ( v4 )
{
// if section 1 code loop does not find anything i is 0
if ( !i )
return 0i64;
// otherwise unregister
result = UmpoNotifyUnregister(i);
}
// if a4 == 0 (our current case)
else
{
if ( i )
return 0i64;
// Get sessionID of service
v10 = WTSGetServiceSessionId();
// call UmpoNotifyRegister
result = UmpoNotifyRegister(v12, v11, v10, v6, v5, &v14);
}
a4
参数的作用。如果 a4
参数不为 0 ,UmpoNotifyUnregister
函数会被调用,并且传入第一部分代码循环返回的地址作为参数。如果 a4
为 0 ,UmpoNotifyRegister
函数会被调用。为了方便记录文档,我们将此函数简化为:__int64 UmpoRpcLegacyEventRegisterNotification(
// RPC Binding Handle
__int64 a1,
// Handle
__int64 a2,
// Service Name
const wchar_t *a3,
// 0 to Register 1 to Unregister
int a4)
UmpoNotifyRegister
UmpoNotifyRegister
。这个函数比之前的要稍微长一点,但是相对来说仍然很短。不太相关的部分将被忽略。在我们把最后一个函数逆向完成之后,我们已经对参数有了比较完整的理解:__int64 __fastcall UmpoNotifyRegister(
// Set to 0
STRSAFE_LPCWSTR pszSrc,
// Set to 0
__int64 a2,
// SessionID
int a3,
// Handle
__int64 a4,
// Service Name
const wchar_t *pszSrca,
// Reference to return to calling function?
__int64 *a6)
v6 = a4;
v7 = a3;
v8 = 0;
EnterCriticalSection(&UmpoNotification);
if ( service_name )
{
v9 = service_name;
v10 = 256i64;
// walk through string until null char is encountered
// or 256 characters are read
do
{
if ( !*v9 )
break;
++v9;
--v10;
}
while ( v10 );
// v11 set to error code if error if str len > 256
v11 = v10 == 0 ? 0x80070057 : 0;
if ( v10 )
// v12 set to length of string
v12 = 256 - v10;
else
v12 = 0i64;
}
else
{
v12 = 0i64;
v11 = -2147024809;
}
EnterCriticalSection
的调用是为了同步一些对象的共享访问。然后遍历 service_name
( 是我们的服务名称参数 ),直到遇到空字符,以确定字符串的长度。registrant
( 在备注中我们用 r
缩写来表示 )。事实证明,这个结构组成了循环链表 ( 实际上是一个双重循环链表 ),它在分析的第一个函数的第一节中遍历了 for
循环。struct registrant {
// pointer to next
next: usize,
// pointer to prev
prev: usize,
// set to 1 after alloc
count: u32,
// Flags
flags: u32,
// handle we pass
handle: usize,
// heap alloc which is size of service_name + 2 for null char
service_name: usize,
// sessionID
session_id: u32,
// unknown, might be two u16s
unknown: u32,
}
// if no error on service_name length check
if ( !v11 )
{
v13 = (_QWORD *)UmpoGetSettingEntry();
v11 = 8;
...
// allocate registrant struct memory 48 bytes
v14 = RtlAllocateHeap(UmpoHeapHandle, 8i64, 48i64);
v15 = v14;
// error case on alloc
if ( !v14 )
goto LABEL_20;
v16 = UmpoHeapHandle;
// r->count is set to 1
*(_DWORD *)(v14 + 16) = 1;
// r->prev is set to itself
*(_QWORD *)(v14 + 8) = v14;
// r->next is set to itself
*(_QWORD *)v14 = v14;
// allocate memory for r->service_name
v17 = (wchar_t *)RtlAllocateHeap(v16, 8i64, (unsigned int)(2 * v12 + 2));
// set r->service_name pointer to allocated memory
*(_QWORD *)(v15 + 32) = v17;
// error case on alloc
if ( !v17 )
{
LABEL_18:
if ( v15 )
UmpoDereferenceRegistrant((__int64 *)v15);
LABEL_20:
if ( v13 && v8 )
RtlFreeHeap(UmpoHeapHandle, 0i64);
goto LABEL_21;
}
// set r->flags set to 1
*(_DWORD *)(v15 + 20) = 1;
// set r->handle to a4 (handle)
*(_QWORD *)(v15 + 24) = v6;
// copy func arg service_name into r->service_name
StringCchCopyW(v17, v12 + 1, pszSrca);
// umpo!UmpoNotifyRegister+0x111: lea rax, [rsi+20h]
v18 = v13 + 4;
// set r->session_id
*(_DWORD *)(v15 + 40) = v7;
// v19 = settingEntry head->next
v19 = v13[4];
// check if linked list head->next->prev == head
if ( *(_QWORD **)(v19 + 8) == v13 + 4 )
{
// inserting at head of list
// set r->next to head->next
*(_QWORD *)v15 = v19;
// set r->prev to head
*(_QWORD *)(v15 + 8) = v18;
// set head->next->prev to r
*(_QWORD *)(v19 + 8) = v15;
// set head->next to r
*v18 = v15;
// set r+44 to 0
*(_BYTE *)(v15 + 44) = 0;
UmpoNotifyUnregister
UmpoNotifyUnregister
函数的移除。这个函数先调用 EnterCriticalSection
再调用了UmpoDereferenceRegistrant
。UmpoDereferenceRegistrant
执行了一些错误检查,从双重链表中删除了 registrant 结构并且释放在 UmpoNotifyRegister
函数分配的堆块。EnterCriticalSection
和 LeaveCriticalSection
已经从 UmpoNotifyRegister
移动到UmpoRpcLegacyEventRegisterNotification
。Critical sections 用于同步进程中对共享对象的同步访问。错误主要在同步访问共享数据会产生 bug。显然,EnterCriticalSection
被移动到调用 UmpoGetSettingEntry
( 分析的第一个函数的第一部分 ) 的上方是正确的。UmpoGetSettingEntry
返回一个全局变量,该变量包含一个指向 registrant 对象的双向循环链表头部的指针。未打补丁的代码是无法正确同步对该对象的访问。此错误将导致条件竞争。Thread 1 enters UmpoRpcLegacyEventRegisterNotication.
Thread 1 accesses linked list registrant shared object.
Thread 1 obtains pointer to target registrant struct.
Thread 1 enters UmpoNotifyUnregister.
Thread 2 enters UmpoRpcLegacyEventRegisterNotication.
Thread 1 enters UmpoDereferenceRegistrant.
Thread 2 accesses linked list registrant shared object.
Thread 2 obtains pointer to target registrant struct.
Thread 1 frees registrant struct memory.
利用 PoC 触发崩溃
UmpoRpcLegacyEventRegisterNotication
函数的名称我们猜测它是由 RPC 调用的。为了验证这一点,我们将使用由 James Forshaw 编写的非常有用的工具 NtObjectManagerHRESULT UmpoRpcLegacyEventRegisterNotification(
/* Stack Offset: 0 */ handle_t p0,
/* Stack Offset: 8 */ [In] UIntPtr p0,
/* Stack Offset: 16 */ [In] /* FC_SUPPLEMENT FC_C_WSTRING Range(0, 256) */
wchar_t* p1,
/* Stack Offset: 24 */ [In] int p2);
UmpoRpcLegacyEventRegisterNotication
的service_name
参数被设置为与 registrant 结构本身( 48 bytes )相同大小的堆分配用于填充被释放的内存。运行崩溃代码一段时间后,我们实现了崩溃!UmpoRpcLegacyEventRegisterNotication
, 引用了无效的内存 41414141`41414155 ( 这是我们的数据 )免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论