这是用户在 2024-10-10 11:42 为 https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Qualys Security Advisory Qualys 安全公告

regreSSHion: RCE in OpenSSH's server, on glibc-based Linux systems (CVE-2024-6387)
regreSSHion:基于 glibc 的 Linux 系统上的 OpenSSH 服务器中存在 RCE (CVE-2024-6387)


======================================================================== Contents ========================================================================
======================================================================== 目录 ========================================================================


Summary SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, from 2005) - Theory - Practice - Timing SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, from 2006) - Theory, take one - Theory, take two - Practice - Timing SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, from 2024) - Theory - Practice - Timing Towards an amd64 exploit Patches and mitigation Acknowledgments Timeline
总结 SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3(Debian 3.0r6,从 2005 年开始) - 理论 - 实践 - 计时 SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3(Ubuntu 6.06.1,从 2006 年开始) - 理论,采取一 - 理论,采取二 - 实践 - 计时 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2(Debian 12.5.0,从 2024 年开始) - 理论 - 实践 - 实现 amd64 漏洞的计时 补丁和缓解 致谢 时间表


======================================================================== Summary ========================================================================
======================================================================== 摘要 ========================================================================


All it takes is a leap of faith -- The Interrupters, "Leap of Faith"
所需要的只是信念的飞跃 -- The Interrupters,“Leap of Faith”


Preliminary note: OpenSSH is one of the most secure software in the world; this vulnerability is one slip-up in an otherwise near-flawless implementation. Its defense-in-depth design and code are a model and an inspiration, and we thank OpenSSH's developers for their exemplary work.
初步说明:OpenSSH 是世界上最安全的软件之一;这个漏洞是原本几乎完美无缺的实现中的一个失误。它的深度防御设计和代码是一个模型和灵感,我们感谢 OpenSSH 的开发人员的模范工作。


We discovered a vulnerability (a signal handler race condition) in OpenSSH's server (sshd): if a client does not authenticate within LoginGraceTime seconds (120 by default, 600 in old OpenSSH versions), then sshd's SIGALRM handler is called asynchronously, but this signal handler calls various functions that are not async-signal-safe (for example, syslog()). This race condition affects sshd in its default configuration.
我们在 OpenSSH 的服务器 (sshd) 中发现了一个漏洞(信号处理程序竞争条件):如果客户端没有在 LoginGraceTime 秒数内进行身份验证(默认为 120 秒,在旧 OpenSSH 版本中为 600 秒),则 sshd 的 SIGALRM 处理程序将被异步调用,但此信号处理程序调用了各种非异步信号安全的函数(例如,syslog())。此争用条件会影响其默认配置中的 sshd。


On investigation, we realized that this vulnerability is in fact a regression of CVE-2006-5051 ("Signal handler race condition in OpenSSH before 4.4 allows remote attackers to cause a denial of service (crash), and possibly execute arbitrary code"), which was reported in 2006 by Mark Dowd.
经过调查,我们意识到此漏洞实际上是 CVE-2006-5051 的回归(“4.4 之前的 OpenSSH 中的信号处理程序争用条件允许远程攻击者造成拒绝服务(崩溃),并可能执行任意代码”),Mark Dowd 于 2006 年报告了该漏洞。


This regression was introduced in October 2020 (OpenSSH 8.5p1) by commit 752250c ("revised log infrastructure for OpenSSH"), which accidentally removed an "#ifdef DO_LOG_SAFE_IN_SIGHAND" from sigdie(), a function that is directly called by sshd's SIGALRM handler. In other words:
这种回归是在 2020 年 10 月 (OpenSSH 8.5p1) 由提交 752250c(“修订后的 OpenSSH 日志基础设施”)引入的,它意外地从 sigdie() 中删除了一个“#ifdef DO_LOG_SAFE_IN_SIGHAND”,该函数由 sshd 的 SIGALRM 处理程序直接调用。换句话说:


- OpenSSH < 4.4p1 is vulnerable to this signal handler race condition, if not backport-patched against CVE-2006-5051, or not patched against CVE-2008-4109, which was an incorrect fix for CVE-2006-5051;
- OpenSSH < 4.4p1 容易受到此信号处理程序争用条件的影响,如果未针对 CVE-2006-5051 进行向后移植修补,或未针对 CVE-2008-4109 进行修补,这是对 CVE-2006-5051 的错误修复;


- 4.4p1 <= OpenSSH < 8.5p1 is not vulnerable to this signal handler race condition (because the "#ifdef DO_LOG_SAFE_IN_SIGHAND" that was added to sigdie() by the patch for CVE-2006-5051 transformed this unsafe function into a safe _exit(1) call);
- 4.4p1 <= OpenSSH < 8.5p1 不易受到此信号处理程序竞争条件的影响(因为 CVE-2006-5051 补丁添加到 sigdie() 的“#ifdef DO_LOG_SAFE_IN_SIGHAND”将此不安全函数转换为安全的 _exit(1) 调用);


- 8.5p1 <= OpenSSH < 9.8p1 is vulnerable again to this signal handler race condition (because the "#ifdef DO_LOG_SAFE_IN_SIGHAND" was accidentally removed from sigdie()).
- 8.5p1 <= OpenSSH < 9.8p1 再次容易受到此信号处理程序竞争条件的影响(因为 “#ifdef DO_LOG_SAFE_IN_SIGHAND” 被意外地从 sigdie() 中删除了)。


This vulnerability is exploitable remotely on glibc-based Linux systems, where syslog() itself calls async-signal-unsafe functions (for example, malloc() and free()): an unauthenticated remote code execution as root, because it affects sshd's privileged code, which is not sandboxed and runs with full privileges. We have not investigated any other libc or operating system; but OpenBSD is notably not vulnerable, because its SIGALRM handler calls syslog_r(), an async-signal-safer version of syslog() that was invented by OpenBSD in 2001.
此漏洞可在基于 glibc 的 Linux 系统上远程利用,其中 syslog() 本身调用 async-signal-unsafe 函数(例如 malloc() 和 free()):以 root 身份执行未经身份验证的远程代码,因为它会影响 sshd 的特权代码,该代码未沙盒化并以完全权限运行。我们尚未调查任何其他 libc 或操作系统;但 OpenBSD 显然不易受到攻击,因为它的 SIGALRM 处理程序调用 syslog_r(),这是 OpenBSD 在 2001 年发明的 syslog() 的异步信号安全版本。


To exploit this vulnerability remotely (to the best of our knowledge, CVE-2006-5051 has never been successfully exploited before), we drew inspiration from a visionary paper, "Delivering Signals for Fun and Profit", which was published in 2001 by Michal Zalewski:
为了远程利用此漏洞(据我们所知,CVE-2006-5051 以前从未成功利用过),我们从 Michal Zalewski 于 2001 年发表的一篇富有远见的论文“Delivering Signals for Fun and Profit”中汲取了灵感:


https://lcamtuf.coredump.cx/signals.txt

Nevertheless, we immediately faced three major problems:
然而,我们立即面临三个主要问题:


- From a theoretical point of view, we must find a useful code path that, if interrupted at the right time by SIGALRM, leaves sshd in an inconsistent state, and we must then exploit this inconsistent state inside the SIGALRM handler.
- 从理论的角度来看,我们必须找到一个有用的代码路径,如果在正确的时间被 SIGALRM 中断,就会使 sshd 处于不一致的状态,然后我们必须在 SIGALRM 处理程序中利用这种不一致的状态。


- From a practical point of view, we must find a way to reach this useful code path in sshd, and maximize our chances of interrupting it at the right time.
- 从实际的角度来看,我们必须想办法在 sshd 中达到这条有用的代码路径,并最大限度地提高我们在适当的时候中断它的机会。


- From a timing point of view, we must find a way to further increase our chances of interrupting this useful code path at the right time, remotely.
- 从时间的角度来看,我们必须找到一种方法来进一步增加在正确的时间远程中断这个有用的代码路径的机会。


To focus on these three problems without having to immediately fight against all the modern operating system protections (in particular, ASLR and NX), we decided to exploit old OpenSSH versions first, on i386, and then, based on this experience, recent versions:
为了专注于这三个问题,而不必立即与所有现代操作系统保护(特别是 ASLR 和 NX)作斗争,我们决定首先在 i386 上利用旧的 OpenSSH 版本,然后根据这些经验,利用最新版本:


- First, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", from "debian-30r6-dvd-i386-binary-1_NONUS.iso": this is the first Debian version that has privilege separation enabled by default and that is patched against all the critical vulnerabilities of that era (in particular, CVE-2003-0693 and CVE-2002-0640).
- 首先是“SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3”,来自 “debian-30r6-dvd-i386-binary-1_NONUS.iso”:这是第一个默认启用权限分离的 Debian 版本,并针对那个时代的所有关键漏洞(特别是 CVE-2003-0693 和 CVE-2002-0640)进行了修补。


To remotely exploit this version, we interrupt a call to free() with SIGALRM (inside sshd's public-key parsing code), leave the heap in an inconsistent state, and exploit this inconsistent state during another call to free(), inside the SIGALRM handler.
为了远程利用这个版本,我们使用 SIGALRM 中断对 free() 的调用(在 sshd 的公钥解析代码中),使堆处于不一致状态,并在 SIGALRM 处理程序内对 free() 的另一次调用中利用这种不一致状态。


In our experiments, it takes ~10,000 tries on average to win this race condition; i.e., with 10 connections (MaxStartups) accepted per 600 seconds (LoginGraceTime), it takes ~1 week on average to obtain a remote root shell.
在我们的实验中,平均需要 ~10,000 次尝试才能赢得这个比赛条件;即,每 600 秒接受 10 个连接 (MaxStartups) (LoginGraceTime),平均需要 ~1 周才能获得远程根 shell。


- Second, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", from "ubuntu-6.06.1-server-i386.iso": this is the last Ubuntu version that is still vulnerable to CVE-2006-5051 ("Signal handler race condition in OpenSSH before 4.4").
- 其次,“SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3”,来自“ubuntu-6.06.1-server-i386.iso”:这是最后一个仍然容易受到 CVE-2006-5051 攻击的 Ubuntu 版本(“4.4 之前的 OpenSSH 中的信号处理程序竞争条件”)。


To remotely exploit this version, we interrupt a call to pam_start() with SIGALRM, leave one of PAM's structures in an inconsistent state, and exploit this inconsistent state during a call to pam_end(), inside the SIGALRM handler.
为了远程利用此版本,我们使用 SIGALRM 中断对 pam_start() 的调用,使 PAM 的一个结构处于不一致状态,并在 SIGALRM 处理程序内调用 pam_end() 期间利用这种不一致状态。


In our experiments, it takes ~10,000 tries on average to win this race condition; i.e., with 10 connections (MaxStartups) accepted per 120 seconds (LoginGraceTime), it takes ~1-2 days on average to obtain a remote root shell.
在我们的实验中,平均需要 ~10,000 次尝试才能赢得这个比赛条件;即,每 120 秒接受 10 个连接 (MaxStartups) (LoginGraceTime),平均需要 ~1-2 天才能获得远程根 shell。


- Finally, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", from "debian-12.5.0-i386-DVD-1.iso": this is the current Debian stable version, and it is vulnerable to the regression of CVE-2006-5051.
- 最后,“SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2”,来自“debian-12.5.0-i386-DVD-1.iso”:这是当前的 Debian 稳定版本,容易受到 CVE-2006-5051 回归的影响。


To remotely exploit this version, we interrupt a call to malloc() with SIGALRM (inside sshd's public-key parsing code), leave the heap in an inconsistent state, and exploit this inconsistent state during another call to malloc(), inside the SIGALRM handler (more precisely, inside syslog()).
为了远程利用此版本,我们使用 SIGALRM 中断对 malloc() 的调用(在 sshd 的公钥解析代码中),使堆处于不一致状态,并在 SIGALRM 处理程序内(更准确地说,在 syslog() 中)对 malloc() 的另一次调用中利用这种不一致状态。


In our experiments, it takes ~10,000 tries on average to win this race condition, so ~3-4 hours with 100 connections (MaxStartups) accepted per 120 seconds (LoginGraceTime). Ultimately, it takes ~6-8 hours on average to obtain a remote root shell, because we can only guess the glibc's address correctly half of the time (because of ASLR).
在我们的实验中,平均需要 ~10000 次尝试才能赢得此争用条件,因此每 120 秒 (LoginGraceTime) 接受 100 个连接 (MaxStartups) 需要 ~3-4 小时。最终,平均需要 ~6-8 小时才能获得远程 root shell,因为我们只能正确猜测 glibc 的地址有一半的时间(因为 ASLR)。


This research is still a work in progress:
这项研究仍在进行中:


- we have targeted virtual machines only, not bare-metal servers, on a mostly stable network link (~10ms packet jitter);
- 我们只将虚拟机(而不是裸机服务器)定位在基本稳定的网络链路上(~10 毫秒数据包抖动);


- we are convinced that various aspects of our exploits can be greatly improved;
- 我们确信,我们漏洞利用的各个方面都可以得到极大的改进;


- we have started to work on an amd64 exploit, which is much harder because of the stronger ASLR.
- 我们已经开始研究 AMD64 漏洞,由于 ASLR 更强,这要困难得多。


A few days after we started our work on amd64, we noticed the following bug report (in OpenSSH's public Bugzilla), about a deadlock in sshd's SIGALRM handler:
在我们开始 amd64 工作几天后,我们注意到以下错误报告(在 OpenSSH 的公共 Bugzilla 中),关于 sshd 的 SIGALRM 处理程序中的死锁:


https://bugzilla.mindrot.org/show_bug.cgi?id=3690

We therefore decided to contact OpenSSH's developers immediately (to let them know that this deadlock is caused by an exploitable vulnerability), we put our amd64 work on hold, and we started to write this advisory.
因此,我们决定立即联系 OpenSSH 的开发人员(让他们知道此死锁是由可利用的漏洞引起的),我们暂停了 amd64 的工作,并开始编写此公告。


======================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, from 2005) ========================================================================
======================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1 3.4p1-1.woody.3(Debian 3.0r6,2005 年起)========================================================================


------------------------------------------------------------------------ Theory ------------------------------------------------------------------------
------------------------------------------------------------------------理论------------------------------------------------------------------------


But that's not like me, I'm breaking free -- The Interrupters, "Haven't Seen the Last of Me"
但那不像我,我正在挣脱束缚 -- The Interrupters,“Haven't Seen the Last of Me”


The SIGALRM handler of this OpenSSH version calls packet_close(), which calls buffer_free(), which calls xfree() and hence free(), which is not async-signal-safe:
此 OpenSSH 版本的 SIGALRM 处理程序调用 packet_close(),后者调用 buffer_free(),后者调用 xfree(),因此调用 free(),后者不是异步信号安全的:


------------------------------------------------------------------------ 302 grace_alarm_handler(int sig) 303 { ... 307 packet_close(); ------------------------------------------------------------------------ 329 packet_close(void) 330 { ... 341 buffer_free(&input); 342 buffer_free(&output); 343 buffer_free(&outgoing_packet); 344 buffer_free(&incoming_packet); ------------------------------------------------------------------------ 35 buffer_free(Buffer *buffer) 36 { 37 memset(buffer->buf, 0, buffer->alloc); 38 xfree(buffer->buf); 39 } ------------------------------------------------------------------------ 51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal("xfree: NULL pointer given as argument"); 55 free(ptr); 56 } ------------------------------------------------------------------------
------------------------------------------------------------------------ 302 grace_alarm_handler(int sig) 303 { ...307 packet_close();------------------------------------------------------------------------ 329 packet_close(无效) 330 { ...341 buffer_free(&输入);342 buffer_free(&输出);343 buffer_free(&outgoing_packet);344 buffer_free(&incoming_packet);------------------------------------------------------------------------ 35 buffer_free(缓冲区 *缓冲区) 36 { 37 memset(缓冲区->buf, 0, 缓冲区->alloc); 38 xfree(缓冲区->buf); 39 } ------------------------------------------------------------------------ 51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal(“xfree: NULL 指针作为参数给出”);55 free(ptr); 56 } ------------------------------------------------------------------------


Consequently, we started to read the malloc code of this Debian's glibc (2.2.5), to see if a first call to free() can be interrupted by SIGALRM and exploited during a second call to free() inside the SIGALRM handler (at lines 341-344, above). Because this glibc's malloc is not hardened against the unlink() technique pioneered by Solar Designer in 2000, we quickly spotted an interesting code path in chunk_free() (which is called internally by free()):
因此,我们开始读取这个 Debian 的 glibc (2.2.5) 的 malloc 代码,看看对 free() 的第一次调用是否可以被 SIGALRM 中断,并在 SIGALRM 处理程序中第二次调用 free() 时被利用(在上面的第 341-344 行)。由于此 glibc 的 malloc 没有针对 Solar Designer 在 2000 年开创的 unlink() 技术进行强化,因此我们很快在 chunk_free() 中发现了一个有趣的代码路径(由free()内部调用):


------------------------------------------------------------------------ 1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ 1031 INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ 1032 struct malloc_chunk* fd; /* double links -- used only if free. */ 1033 struct malloc_chunk* bk; 1034 }; ------------------------------------------------------------------------ 2516 #define unlink(P, BK, FD) \ 2517 { \ 2518 BK = P->bk; \ 2519 FD = P->fd; \ 2520 FD->bk = BK; \ 2521 BK->fd = FD; \ 2522 } \ ------------------------------------------------------------------------ 3160 chunk_free(arena *ar_ptr, mchunkptr p) .... 3164 { 3165 INTERNAL_SIZE_T hd = p->size; /* its head field */ .... 3177 sz = hd & ~PREV_INUSE; 3178 next = chunk_at_offset(p, sz); 3179 nextsz = chunksize(next); .... 3230 if (!(inuse_bit_at_offset(next, nextsz))) /* consolidate forward */ 3231 { .... 3241 unlink(next, bck, fwd); .... 3244 } 3245 else 3246 set_head(next, nextsz); /* clear inuse bit */ .... 3251 frontlink(ar_ptr, p, sz, idx, bck, fwd); ------------------------------------------------------------------------
------------------------------------------------------------------------ 1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* 前一个块的大小(如果空闲。*/ 1031 INTERNAL_SIZE_T大小; /* 以字节为单位的大小,包括开销。*/ 1032 struct malloc_chunk* fd; /* 双链接 -- 仅在空闲时使用。 */ 1033 struct malloc_chunk* bk; 1034 };------------------------------------------------------------------------ 2516 #define unlink(P, BK, FD) \ 2517 { \ 2518 BK = P->bk; \ 2519 FD = P->fd; \ 2520 FD->bk = BK; \ 2521 BK->fd = FD; \ 2522 } \ ------------------------------------------------------------------------ 3160 chunk_free(arena *ar_ptr, mchunkptr p) ....3164 { 3165 INTERNAL_SIZE_T hd = p->size; /* 它的头部字段 */ ....3177 sz = HD & ~PREV_INUSE;3178 下一个 = chunk_at_offset(p, sz);3179 nextsz = chunksize(下一个);....3230 如果 (!(inuse_bit_at_offset(next, nextsz))) /* 合并前进 */ 3231 { ....3241 取消链接(下一个、返回、前方);....3244 } 3245 else 3246 set_head(next, nextsz);/* 清除 inuse bit */ ....3251 frontlink(ar_ptr、p、sz、idx、back、fwd);------------------------------------------------------------------------


To exploit this code path, we arrange for sshd's heap to have the following layout (chunk_X, chunk_Y, and chunk_Z are malloc()ated chunks of memory, and p, s, f, b are their prev_size, size, fd, and bk fields):
为了利用此代码路径,我们将 sshd 的堆安排为以下布局(chunk_X、chunk_Y 和 chunk_Z 是 malloc() 化的内存块,p、s、f、b 是它们的 prev_size、size、fd 和 bk 字段):


-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| user data
-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b|chunk_X |p|s|f|b|chunk_Y |p|s|f|b|chunk_Z |... -----|---+---------------|---+---------------|---+---------------|----- |<------------->|用户数据


- First, if a call to free(chunk_Y) is interrupted by SIGALRM *after* line 3246 but *before* line 3251, then chunk_Y is already marked as free (because chunk_Z's PREV_INUSE bit is cleared at line 3246) but it is not yet linked into its doubly-linked list (at line 3251): in other words, chunk_Y's fd and bk pointers still contain user data (attacker- controlled data).
- 首先,如果对 free(chunk_Y) 的调用被 SIGALRM 在第 3246 行之后但第 3251 行之前中断,那么chunk_Y已经被标记为 free(因为chunk_Z的 PREV_INUSE 位在第 3246 行被清除),但它尚未链接到其双向链表中(在第 3251 行):换句话说,chunk_Y的 fd 和 bk 指针仍然包含用户数据(攻击者控制的数据)。


- Second, if (inside the SIGALRM handler) packet_close() calls free(chunk_X), then the code block at lines 3230-3244 is entered (because chunk_Y is marked as free) and chunk_Y is unlink()ed (at line 3241): a so-called aa4bmo primitive (almost arbitrary 4 bytes mirrored overwrite), because chunk_Y's fd and bk pointers are still attacker- controlled. For more information on the unlink() technique and the aa4bmo primitive:
- 其次,如果(在 SIGALRM 处理程序中)packet_close() 调用 free(chunk_X),则输入第 3230-3244 行的代码块(因为 chunk_Y 被标记为 free)并chunk_Y被取消链接()(在第 3241 行):所谓的 aa4bmo 原语(几乎任意的 4 字节镜像覆盖),因为 chunk_Y 的 fd 和 bk 指针仍然由攻击者控制。有关 unlink() 技术和 aa4bmo 原语的更多信息:


https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article

- Last, with this aa4bmo primitive we overwrite the glibc's __free_hook function pointer (this old Debian version does not have ASLR, nor NX) with the address of our shellcode in the heap, thus achieving remote code execution during the next call to free() in packet_close().
- 最后,使用这个 aa4bmo 原语,我们用堆中 shellcode 的地址覆盖 glibc 的 __free_hook 函数指针(这个旧的 Debian 版本没有 ASLR,也没有 NX),从而在下次调用 packet_close() 中的 free() 时实现远程代码执行。


------------------------------------------------------------------------ Practice ------------------------------------------------------------------------
------------------------------------------------------------------------实践------------------------------------------------------------------------


Now they're taking over and they got complete control -- The Interrupters, "Liberty"
现在他们接管了,他们获得了完全的控制权 -- The Interrupters, “Liberty”


To mount this attack against sshd, we interrupt a call to free() inside sshd's parsing code of a DSA public key (i.e., line 144 below is our free(chunk_Y)) and exploit it during one of the free() calls in packet_close() (i.e., one of the lines 341-344 above is our free(chunk_X)):
为了对 sshd 发起攻击,我们中断了 sshd 的 DSA 公钥解析代码中对 free() 的调用(例如,下面的第 144 行是我们的 free(chunk_Y)),并在 packet_close() 中的一次 free() 调用中利用它(例如,上面的第 341-344 行之一就是我们的 free(chunk_X)):


------------------------------------------------------------------------ 136 buffer_get_bignum2(Buffer *buffer, BIGNUM *value) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(buffer, &len); ... 143 BN_bin2bn(bin, len, value); 144 xfree(bin); 145 } ------------------------------------------------------------------------
------------------------------------------------------------------------ 136 buffer_get_bignum2(缓冲区 *缓冲区, BIGNUM *值) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(缓冲区, &len); ...143 BN_bin2bn(bin, len, value);144 个 xfree(bin);145 } ------------------------------------------------------------------------


Initially, however, we were never able to win this race condition (i.e., interrupt the free() call at line 144 at the right time). Eventually, we realized that we could greatly improve our chances of winning this race: the DSA public-key parsing code allows us to call free() four times (at lines 704-707 below), and furthermore sshd allows us to attempt six user authentications (AUTH_FAIL_MAX); if any one of these 24 free() calls is interrupted at the right time, then we later achieve remote code execution inside the SIGALRM handler.
然而,最初,我们始终无法赢得这个竞争条件(即,在正确的时间中断第 144 行的 free() 调用)。最终,我们意识到我们可以大大提高赢得这场竞赛的机会:DSA 公钥解析代码允许我们调用 free() 四次(在下面的第 704-707 行),此外,sshd 允许我们尝试六次用户身份验证 (AUTH_FAIL_MAX);如果这 24 个 free() 调用中的任何一个在正确的时间被中断,那么我们稍后在 SIGALRM 处理程序中实现远程代码执行。


------------------------------------------------------------------------ 678 key_from_blob(u_char *blob, int blen) 679 { ... 693 switch (type) { ... 702 case KEY_DSA: 703 key = key_new(type); 704 buffer_get_bignum2(&b, key->dsa->p); 705 buffer_get_bignum2(&b, key->dsa->q); 706 buffer_get_bignum2(&b, key->dsa->g); 707 buffer_get_bignum2(&b, key->dsa->pub_key); ------------------------------------------------------------------------
------------------------------------------------------------------------ 678 key_from_blob(u_char *blob, int blen) 679 { ...693 开关 (type) { ...702 案例KEY_DSA:703 键 = key_new(类型);704 buffer_get_bignum2(&b, key->dsa->p);705 buffer_get_bignum2(&b, key->dsa->q);706 buffer_get_bignum2(&b, key->dsa->g);707 buffer_get_bignum2(&b, key->dsa->pub_key);------------------------------------------------------------------------


With this improvement, we finally won the race condition after ~1 month: we were happy (and did a root-shell dance), but we also felt that there was still room for improvement.
凭借这一改进,我们终于在 ~1 个月后赢得了比赛条件:我们很开心(并跳了根壳舞),但我们也觉得还有改进的余地。


------------------------------------------------------------------------ Timing ------------------------------------------------------------------------
------------------------------------------------------------------------定时------------------------------------------------------------------------


Don't worry, just wait and see -- The Interrupters, "Haven't Seen the Last of Me"
别担心,等着看 -- The Interrupters,“Haven't Seen the Last of Me”


We therefore implemented the following threefold timing strategy:
因此,我们实施了以下三重计时策略:


- We do not wait until the last moment to send our (rather large) DSA public-key packet to sshd: instead, we send the entire packet minus one byte (the last byte) long before the LoginGraceTime, and send the very last byte at the very last moment, to minimize the effects of network delays. (And we disable the Nagle algorithm.)
- 我们不会等到最后一刻才将我们的(相当大的)DSA 公钥数据包发送到 sshd:相反,我们在 LoginGraceTime 之前很久就发送整个数据包减去一个字节(最后一个字节),并在最后一刻发送最后一个字节,以尽量减少网络延迟的影响。(并且我们禁用了 Nagle 算法。


- We keep track of the median round-trip time (by regularly sending packets that produce a response from sshd), and keep track of the difference between the moment we are expecting our connection to be closed by sshd (essentially the moment we receive the first byte of sshd's banner, plus LoginGraceTime) and the moment our connection is really closed by sshd, and accordingly adjust our timing (i.e., the moment when we send the last byte of our DSA packet).
- 我们跟踪中位往返时间(通过定期发送从 sshd 产生响应的数据包),并跟踪我们预期连接被 sshd 关闭的时刻(本质上是我们收到 sshd 横幅的第一个字节加上 LoginGraceTime 的时刻)与我们的连接真正被 sshd 关闭的时刻之间的差异。 并相应地调整我们的时间(即我们发送 DSA 数据包的最后一个字节的时刻)。


These time differences allow us to track clock skews and network delays, which show predictable patterns over time: we experimented with linear and spline regressions, but in the end, nothing worked better than simply re-using the most recent measurement. Possibly, deep learning might yield even better results; this is left as an exercise for the interested reader.
这些时间差异使我们能够跟踪时钟偏差和网络延迟,它们随着时间的推移显示出可预测的模式:我们尝试了线性和样条回归,但最终,没有什么比简单地重复使用最新的测量结果更好的了。深度学习可能会产生更好的结果;这留给感兴趣的读者作为练习。


- More importantly, we further increase our chances of winning this race condition by slowly adjusting our timing through involuntary feedback from sshd:
- 更重要的是,我们通过 sshd 的非自愿反馈慢慢调整我们的时间,从而进一步增加了我们赢得这个比赛条件的机会:


- if we receive a response (SSH2_MSG_USERAUTH_FAILURE) to our DSA public-key packet, then we sent it too early (sshd had the time to receive our packet in the unprivileged child, parse it, send it to the privileged child, parse it there, and send a response all the way back to us);
- 如果我们收到对 DSA 公钥数据包的响应 (SSH2_MSG_USERAUTH_FAILURE),那么我们发送得太早了(SSHD 有时间在非特权子节点中接收我们的数据包,解析它,将其发送给特权子节点,在那里解析它,并将响应一直发送回给我们);


- if we cannot even send the last byte of our DSA packet, then we waited too long (sshd already received the SIGALRM and closed our connection);
- 如果我们甚至无法发送 DSA 数据包的最后一个字节,那么我们等待的时间太长了(sshd 已经收到了 SIGALRM 并关闭了我们的连接);


- if we can send the last byte of our DSA packet, and receive no response before sshd closes our connection, then our timing was reasonably accurate.
- 如果我们可以发送 DSA 数据包的最后一个字节,并且在 sshd 关闭连接之前没有收到任何响应,那么我们的时间是相当准确的。


This feedback allows us to target what we call the "large" race window: hitting it does not guarantee that we win the race condition, but inside this large window are the 24 "small" race windows (inside the 24 free() calls) that, if hit, guarantee that we do win the race condition.
此反馈允许我们针对我们所谓的“大”比赛窗口:击中它并不能保证我们赢得比赛条件,但在这个大窗口内是 24 个“小”比赛窗口(在 24 个 free() 调用内),如果被击中,则保证我们确实赢得了比赛条件。


With these improvements, it takes ~10,000 tries on average to win this race condition; i.e., with 10 connections (MaxStartups) accepted per 600 seconds (LoginGraceTime), it takes ~1 week on average to obtain a remote root shell.
通过这些改进,平均需要 ~10,000 次尝试才能赢得此比赛条件;即,每 600 秒接受 10 个连接 (MaxStartups) (LoginGraceTime),平均需要 ~1 周才能获得远程根 shell。


======================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, from 2006) ========================================================================
======================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3(Ubuntu 6.06.1,2006 年起)========================================================================


------------------------------------------------------------------------ Theory, take one ------------------------------------------------------------------------
------------------------------------------------------------------------ 理论,请看一------------------------------------------------------------------------


I sleep when the sun starts to rise -- The Interrupters, "Alien"
当太阳开始升起时,我睡着了 -- The Interrupters, “Alien”


The SIGALRM handler of this OpenSSH version does not call packet_close() anymore; moreover, this Ubuntu's glibc (2.3.6) always takes a mandatory lock when entering the functions of the malloc family (even if single- threaded like sshd), which prevents us from interrupting a call to one of the malloc functions and later exploiting it during another call to these functions (they would always deadlock). We must find another solution.
此 OpenSSH 版本的 SIGALRM 处理程序不再调用 packet_close();此外,这个 Ubuntu 的 glibc (2.3.6) 在进入 malloc 系列的函数时总是强制锁(即使是像 sshd 这样的单线程),这可以防止我们中断对其中一个 malloc 函数的调用,并在随后对这些函数的另一次调用中利用它(它们总是会死锁)。我们必须找到另一种解决方案。


CVE-2006-5051 mentions a double-free in GSSAPI, but GSSAPI (or Kerberos) is not enabled by default, so this does not sound very appealing. On the other hand, PAM is enabled by default, and pam_end() is called by sshd's SIGALRM handler (and is, of course, not async-signal-safe). We therefore searched for a PAM function that, if interrupted by SIGALRM at the right time, would leave PAM's internal structures in an inconsistent state, exploitable during pam_end() in the SIGALRM handler. We found pam_set_data():
CVE-2006-5051 提到了 GSSAPI 中的双重释放,但 GSSAPI(或 Kerberos)默认未启用,因此这听起来不是很吸引人。另一方面,PAM 默认启用,pam_end() 由 sshd 的 SIGALRM 处理程序调用(当然,它不是异步信号安全的)。因此,我们寻找了一个 PAM 函数,如果在正确的时间被 SIGALRM 中断,将使 PAM 的内部结构处于不一致的状态,在 SIGALRM 处理程序的 pam_end() 期间可被利用。我们找到了 pam_set_data():


------------------------------------------------------------------------ 33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; ------------------------------------------------------------------------
------------------------------------------------------------------------ 33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; ------------------------------------------------------------------------


If this function is interrupted by SIGALRM *after* line 66 but *before* line 74, then data_entry is already linked into PAM's structures (pamh), but its cleanup field (a function pointer) is not yet initialized (since the malloc() at line 57 does not initialize its memory). If we are able to control cleanup (through leftovers from previous heap allocations), then we can execute arbitrary code when pam_end() (inside the SIGALRM handler) calls _pam_free_data() (at line 118):
如果这个函数在第 66 行之后但 74 行之前被 SIGALRM 打断,那么data_entry已经链接到 PAM 的结构体 (pamh),但它的清理字段(函数指针)还没有初始化(因为第 57 行的 malloc() 没有初始化它的内存)。如果我们能够控制清理(通过先前堆分配的剩余部分),那么我们可以在 pam_end()(在 SIGALRM 处理程序内部)调用 _pam_free_data()(第 118 行)时执行任意代码:


------------------------------------------------------------------------ 104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ... 112 data = pamh->data; 113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status); ------------------------------------------------------------------------
------------------------------------------------------------------------ 104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ...112 数据 = pamh->data;113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status);------------------------------------------------------------------------


This would have been an extremely simple exploit; unfortunately, we completely overlooked that pam_set_data() can only be called from PAM modules: if we interrupt it with SIGALRM, then pamh->caller_is is still _PAM_CALLED_FROM_MODULE, in which case pam_end() returns immediately, without ever calling _pam_free_data(). Back to the drawing board.
这将是一个非常简单的漏洞;不幸的是,我们完全忽略了 pam_set_data() 只能从 PAM 模块中调用:如果我们用 SIGALRM 中断它,那么 pamh->caller_is 仍然_PAM_CALLED_FROM_MODULE,在这种情况下 pam_end() 立即返回,而无需调用 _pam_free_data()。从头再来。


------------------------------------------------------------------------ Theory, take two ------------------------------------------------------------------------
------------------------------------------------------------------------理论,采取两种------------------------------------------------------------------------


Not giving up, it's not what we do -- The Interrupters, "Title Holder"
不放弃,这不是我们所做的 -- The Interrupters,“Title Holder”


We noticed that, at line 601 below, sshd passes a pointer to its global sshpam_handle pointer directly to pam_start() (which is called once per connection):
我们注意到,在下面的第 601 行,sshd 将指向其全局 sshpam_handle 指针的指针直接传递给 pam_start()(每个连接调用一次):


------------------------------------------------------------------------ 202 static pam_handle_t *sshpam_handle = NULL; ------------------------------------------------------------------------ 584 sshpam_init(Authctxt *authctxt) 585 { ... 600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle); ------------------------------------------------------------------------
------------------------------------------------------------------------ 202 静态pam_handle_t *sshpam_handle = NULL;------------------------------------------------------------------------ 584 sshpam_init(Authctxt *authctxt) 585 { ...600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, 用户, &store_conv, &sshpam_handle);------------------------------------------------------------------------


We therefore decided to look into pam_start() itself: if interrupted by SIGALRM, it might leave the structure pointed to by sshpam_handle in an inconsistent state, which could then be exploited inside the SIGALRM handler, when "pam_end(sshpam_handle, sshpam_err)" is called.
因此,我们决定研究 pam_start() 本身:如果被 SIGALRM 中断,它可能会使 sshpam_handle 指向的结构处于不一致的状态,然后当调用 “pam_end(sshpam_handle, sshpam_err)” 时,可以在 SIGALRM 处理程序中利用它。


------------------------------------------------------------------------ 18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ... 110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) { ------------------------------------------------------------------------ 319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ... 398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY ------------------------------------------------------------------------ 66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ... 252 res = _pam_add_handler(pamh, must_fail, other ------------------------------------------------------------------------ 581 int _pam_add_handler(pam_handle_t *pamh ... 585 { ... 755 the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf; ... 767 handler_p = &the_handlers->authenticate; ... 874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ... 886 (*handler_p)->next = NULL; ------------------------------------------------------------------------
------------------------------------------------------------------------ 18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ...110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) { ------------------------------------------------------------------------ 319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ...398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY ------------------------------------------------------------------------ 66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ...252 分辨率 = _pam_add_handler(PAMH, must_fail, 其他 ------------------------------------------------------------------------ 581 int _pam_add_handler(pam_handle_t *pamh ...585 { ...755 the_handlers = (其他) ?&pamh->handlers.other : &pamh->handlers.conf;...767 handler_p = &the_handlers->authenticate;...874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ...886 (*handler_p)->next = NULL;------------------------------------------------------------------------


At line 32, pam_start() immediately sets sshd's sshpam_handle to a calloc()ated chunk of memory; this is safe, because calloc() initializes this memory to zero. On the other hand, if _pam_add_handler() (which is called multiple times by pam_start()) is interrupted by SIGALRM *after* line 874 but *before* line 886, then a malloc()ated structure is linked into pamh, but its next field is not yet initialized. If we are able to control next (through leftovers from previous heap allocations), then we can pass an arbitrary pointer to free() during the call to pam_end() (inside the SIGALRM handler), at line 1020 (and line 1017) below:
在第 32 行,pam_start() 立即将 sshd 的 sshpam_handle 设置为 calloc() 化的内存块;这是安全的,因为 calloc() 将此内存初始化为零。另一方面,如果 _pam_add_handler() (被 pam_start() 多次调用)被 SIGALRM 在第 874 行之后但在第 886 行之前中断,则 malloc() 化结构将链接到 pamh,但其下一个字段尚未初始化。如果我们能够控制 next(通过先前堆分配的剩余部分),那么我们可以在调用 pam_end() 期间(在 SIGALRM 处理程序内部),在下面的第 1020 行(和第 1017 行)传递一个指向 free() 的任意指针:


------------------------------------------------------------------------ 11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) { ------------------------------------------------------------------------ 925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ... 954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate)); ------------------------------------------------------------------------ 1009 void _pam_free_handlers_aux(struct handler **hp) 1010 { 1011 struct handler *h = *hp; 1012 struct handler *last; .... 1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); /* This is all alocated in a single chunk */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 } ------------------------------------------------------------------------
------------------------------------------------------------------------ 11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) { ------------------------------------------------------------------------ 925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ...954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate));------------------------------------------------------------------------ 1009 void _pam_free_handlers_aux(结构处理程序 **hp) 1010 { 1011 结构处理程序 *h = *hp; 1012 结构处理程序 *last; ....1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); /* 这些都分布在一个块中 */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 } ------------------------------------------------------------------------


Because the malloc of this Ubuntu's glibc is already hardened against the old unlink() technique, we decided to transform our arbitrary free() into the Malloc Maleficarum's House of Mind (fastbin version): we free() our own NON_MAIN_ARENA chunk, point our fake arena to sshd's .got.plt (this Ubuntu's sshd has ASLR but not PIE), and overwrite _exit()'s entry with the address of our shellcode in the heap (this Ubuntu's heap is still executable by default). For more information on the Malloc Maleficarum:
因为这个 Ubuntu 的 glibc 的 malloc 已经针对旧的 unlink() 技术进行了强化,我们决定将我们的任意 free() 转换为 Malloc Maleficarum 的 House of Mind(fastbin 版本):我们 free() 我们自己的 NON_MAIN_ARENA 块,将我们的假竞技场指向 sshd 的 .got.plt(这个 Ubuntu 的 sshd 有 ASLR 但没有 PIE),并用我们在堆中的 shellcode 的地址覆盖 _exit() 的条目(这个 Ubuntu 的堆仍然可以由default) 的 S S有关 Malloc Maleficarum 的更多信息:


https://seclists.org/bugtraq/2005/Oct/118

------------------------------------------------------------------------ Practice ------------------------------------------------------------------------
------------------------------------------------------------------------实践------------------------------------------------------------------------


I learned everything the hard way -- The Interrupters, "The Hard Way"
我以艰难的方式学到了一切 -- The Interrupters, “The Hard Way”


To mount this attack against sshd, we initially faced three problems:
为了对 sshd 发起攻击,我们最初面临三个问题:


- The House of Mind requires us to store the pointer to our fake arena at address 0x08100000 in the heap; but are we able to store attacker- controlled data at such a high address? Because sshd calls pam_start() at the very beginning of the user authentication, we do not control anything except the user name itself; luckily, a user name of length ~128KB (shorter than DEFAULT_MMAP_THRESHOLD) allows us to store our own data at address 0x08100000.
- House of Mind 要求我们将指向 fake arena 的指针存储在堆中的地址 0x08100000;但是,我们能否将攻击者控制的数据存储在如此高的地址上?因为 sshd 在用户认证的最开始时调用 pam_start(),所以除了用户名本身之外,我们不控制任何东西;幸运的是,长度为 ~128KB(短于 DEFAULT_MMAP_THRESHOLD)的用户名允许我们在地址 0x08100000 存储自己的数据。


- The size field of our fake NON_MAIN_ARENA chunk must not be too large (to pass free()'s security checks); i.e., it must contain null bytes. But our long user name is a null-terminated string that cannot contain null bytes; luckily we remembered that _pam_free_handlers_aux() zeroes the structures that it free()s (line 1019 above): we therefore "patch" the size field of our fake chunk with such a memset(0), and only then free() it.
- 我们的假 NON_MAIN_ARENA 块的 size 字段不能太大(以通过 free() 的安全检查);即,它必须包含 null 字节。但是我们的长用户名是一个以 null 结尾的字符串,不能包含 null 字节;幸运的是,我们记得 _pam_free_handlers_aux() 将它 free() 的结构体归零(上面的第 1019 行):因此,我们用这样的 memset(0) “修补”了假块的 size 字段,然后才 free() 它。


- We must survive several calls to free() (at lines 1017 and 1020 above) before the free() of our fake NON_MAIN_ARENA chunk. We transform these free()s into no-ops by pointing them to fake IS_MMAPPED chunks: free() calls munmap_chunk(), which calls munmap(), which fails because these fake IS_MMAPPED chunks are misaligned; effectively a no-op, because assert()ion failures are not enforced in this Ubuntu's glibc.
- 我们必须在假 NON_MAIN_ARENA 块的 free() 之前对 free() 的多次调用(在上面的第 1017 行和第 1020 行)中幸存下来。我们通过将这些 free() 指向假 IS_MMAPPED 块来将它们转换为无操作:free() 调用 munmap_chunk(),它调用 munmap(),它失败了,因为这些假 IS_MMAPPED 块没有对齐;实际上是无操作,因为 assert() 失败在这个 Ubuntu 的 glibc 中没有强制执行。


Finally, our long user name also allows us to control the potentially uninitialized next field of 20 different structures (through leftovers from temporary copies of our long user name), because pam_start() calls _pam_add_handler() multiple times; i.e., our large race window contains 20 small race windows.
最后,我们的长用户名还允许我们控制 20 个不同结构的潜在未初始化的 next 字段(通过长用户名的临时副本的剩余部分),因为 pam_start() 多次调用 _pam_add_handler();即,我们的大型比赛窗口包含 20 个小型比赛窗口。


------------------------------------------------------------------------ Timing ------------------------------------------------------------------------
------------------------------------------------------------------------定时------------------------------------------------------------------------


Same tricks they used before -- The Interrupters, "Divide Us"
他们以前用过的同样的伎俩 -- The Interrupters, “Divide Us”


For this attack against Ubuntu 6.06.1, we simply re-used the timing strategy that we used against Debian 3.0r6: it takes ~10,000 tries on average to win the race condition, and with 10 connections (MaxStartups) accepted per 120 seconds (LoginGraceTime), it takes ~1-2 days on average to obtain a remote root shell.
对于针对 Ubuntu 6.06.1 的攻击,我们简单地重用了针对 Debian 3.0r6 使用的计时策略:平均需要 ~10,000 次尝试才能赢得争用条件,并且每 120 秒接受 10 个连接 (MaxStartups) (LoginGraceTime),平均需要 ~1-2 天才能获得远程根 shell。


Note: because this Ubuntu's glibc always takes a mandatory lock when entering the functions of the malloc family, an unlucky attacker might deadlock all 10 MaxStartups connections before obtaining a root shell; we have not tried to work around this problem because our ultimate goal was to exploit a modern OpenSSH version anyway.
注意:因为这个 Ubuntu 的 glibc 在进入 malloc 族的函数时总是强制锁,所以不走运的攻击者可能会在获取 root shell 之前死锁所有 10 个 MaxStartups 连接;我们没有尝试解决这个问题,因为我们的最终目标是无论如何都要利用现代的 OpenSSH 版本。


======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, from 2024) ========================================================================
======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2(Debian 12.5.0,2024 年起)========================================================================


------------------------------------------------------------------------ Theory ------------------------------------------------------------------------
------------------------------------------------------------------------理论------------------------------------------------------------------------


Now you're ready, take the demons head on -- The Interrupters, "Be Gone"
现在你准备好了,直面恶魔 -- The Interrupters,“Be Gone”


The SIGALRM handler of this OpenSSH version does not call packet_close() nor pam_end(); in fact it calls only one interesting function, syslog():
此 OpenSSH 版本的 SIGALRM 处理程序不调用 packet_close() 也不调用 pam_end();事实上,它只调用一个有趣的函数 syslog():


------------------------------------------------------------------------ 358 grace_alarm_handler(int sig) 359 { ... 370 sigdie("Timeout before authentication for %s port %d", 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state)); ------------------------------------------------------------------------ 96 #define sigdie(...) sshsigdie(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_ERROR, NULL, __VA_ARGS__) ------------------------------------------------------------------------ 451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ... 457 sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, 458 suffix, fmt, args); ------------------------------------------------------------------------ 464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ... 489 do_log(level, forced, suffix, fmt2, args); ------------------------------------------------------------------------ 337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ... 419 syslog(pri, "%.500s", fmtbuf); ------------------------------------------------------------------------
------------------------------------------------------------------------ 358 grace_alarm_handler(int sig) 359 { ...370 sigdie(“%s 端口 %d 身份验证前超时”, 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state));------------------------------------------------------------------------ 96 #define sigdie(...) sshsigdie(__FILE__, __func__, __LINE__, 0, SYSLOG_LEVEL_ERROR, NULL, __VA_ARGS__) ------------------------------------------------------------------------ 451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ...457 sshlogv(文件、func、行、showfunc、SYSLOG_LEVEL_FATAL、458 后缀、fmt、args);------------------------------------------------------------------------ 464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ...489 do_log(级别、强制、后缀、fmt2、args);------------------------------------------------------------------------ 337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ...419 系统日志(pri, “%.500s”, fmtbuf);------------------------------------------------------------------------


Our two key questions, then, are: Does the syslog() of this Debian's glibc (2.36) call async-signal-unsafe functions such as malloc() and free()? And if yes, does this glibc still take a mandatory lock when entering the functions of the malloc family?
那么,我们的两个关键问题是:这个 Debian 的 glibc (2.36) 的 syslog() 是否调用了异步信号不安全的函数,例如 malloc() 和 free()?如果是,那么在进入 malloc 系列的功能时,这个 glibc 是否仍然需要强制锁定?


- Luckily for us attackers, the answer to our first question is yes; if, and only if, the syslog() inside the SIGALRM handler is the very first call to syslog(), then __localtime64_r() (which is called by syslog()) calls malloc(304) to allocate a FILE structure (at line 166) and calls malloc(4096) to allocate an internal read buffer (at line 186):
- 幸运的是,对于我们攻击者来说,我们第一个问题的答案是肯定的;当且仅当 SIGALRM 处理程序中的 syslog() 是对 syslog() 的第一次调用,则 __localtime64_r()(由 syslog() 调用)调用 malloc(304) 来分配一个 FILE 结构(在第 166 行),并调用 malloc(4096) 来分配一个内部读缓冲区(在第 186 行):


------------------------------------------------------------------------ 28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp); ------------------------------------------------------------------------ 567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ... 577 tzset_internal (tp == &_tmbuf && use_localtime); ------------------------------------------------------------------------ 367 tzset_internal (int always) 368 { ... 405 __tzfile_read (tz, 0, NULL); ------------------------------------------------------------------------ 105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ... 109 FILE *f; ... 166 f = fopen (file, "rce"); ... 186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0) ------------------------------------------------------------------------
------------------------------------------------------------------------ 28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp);------------------------------------------------------------------------ 567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ...577 tzset_internal (tp == &_tmbuf && use_localtime);------------------------------------------------------------------------ 367 tzset_internal (int always) 368 { ...405 __tzfile_read (tz, 0, NULL);------------------------------------------------------------------------ 105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ...109 文件 *f;...166 f = fopen (文件, “RCE”);...186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0) ------------------------------------------------------------------------


Note: because we do not control anything about these malloc()ations (not their order, not their sizes, not their contents), we took the "rce" at line 166 as a much-needed good omen.
注意:因为我们无法控制这些 malloc() 的任何事情(不是它们的顺序,不是它们的大小,不是它们的内容),所以我们将第 166 行的 “rce” 视为急需的好兆头。


- And luckily for us, the answer to our second question is no; since October 2017, the glibc's malloc functions do not take any lock anymore, when single-threaded (like sshd):
- 幸运的是,我们第二个问题的答案是否定的;自 2017 年 10 月以来,glibc 的 malloc 函数在单线程(如 sshd)时不再接受任何锁定:


https://sourceware.org/git?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db https://sourceware.org/git?p=glibc.git;a=commit;h=3f6bb8a32e5f5efd78ac08c41e623651cc242a89 https://sourceware.org/git?p=glibc.git;a=commit;h=905a7725e9157ea522d8ab97b4c8b96aeb23df54

Moreover, this Debian version suffers from the ASLR weakness described in the following great blog posts (by Justin Miller and Mathias Krause, respectively):
此外,这个 Debian 版本还存在以下精彩的博客文章(分别由 Justin Miller 和 Mathias Krause 撰写)中描述的 ASLR 弱点:


https://zolutal.github.io/aslrnt/ https://grsecurity.net/toolchain_necromancy_past_mistakes_haunting_aslr

Concretely, in the case of sshd on i386, every memory mapping is randomized normally (sshd's PIE, the heap, most libraries, the stack), but the glibc itself is always mapped either at address 0xb7200000 or at address 0xb7400000; in other words, we can correctly guess the glibc's address half of the time (a small price to pay for defeating ASLR). In our exploit we assume that the glibc is mapped at address 0xb7400000, because it is slightly more common than 0xb7200000.
具体来说,在 i386 上的 sshd 的情况下,每个内存映射都是随机的(sshd 的 PIE、堆、大多数库、堆栈),但 glibc 本身总是映射在地址 0xb7200000 或地址 0xb7400000;换句话说,我们可以正确猜测 glibc 的地址有一半的时间(击败 ASLR 所付出的很小代价)。在我们的漏洞利用中,我们假设 glibc 映射在地址 0xb7400000,因为它比 0xb7200000 略常见。


Our next question is: which code paths inside the glibc's malloc functions, if interrupted by SIGALRM at the right time, leave the heap in an inconsistent state, exploitable during one of the malloc() calls inside the SIGALRM handler?
我们的下一个问题是:如果 SIGALRM 在正确的时间中断了 glibc 的 malloc 函数中的哪些代码路径,则会使堆处于不一致的状态,在 SIGALRM 处理程序内的一个 malloc() 调用期间可被利用?


We found several interesting (and surprising!) code paths, but the one we chose involves only relative sizes, not absolute addresses (unlike various code paths inside unlink_chunk(), for example); this difference might prove crucial for a future amd64 exploit. This code path, inside malloc(), splits a large free chunk (victim) into two smaller chunks; the first chunk is returned to malloc()'s caller (at line 4345) and the second chunk (remainder) is linked into an unsorted list of free chunks (at lines 4324-4327):
我们发现了几个有趣(且令人惊讶)的代码路径,但我们选择的那个只涉及相对大小,而不涉及绝对地址(例如,与 unlink_chunk() 中的各种代码路径不同);这种差异可能对未来的 AMD64 漏洞至关重要。此代码路径位于 malloc() 内部,将一个大的空闲块(受害者)拆分为两个较小的块;第一个块返回给 malloc() 的调用者(第 4345 行),第二个块(余数)链接到未排序的空闲块列表(第 4324-4327 行):


------------------------------------------------------------------------ 1449 #define set_head(p, s) ((p)->mchunk_size = (s)) ------------------------------------------------------------------------ 3765 _int_malloc (mstate av, size_t bytes) 3766 { .... 3798 nb = checked_request2size (bytes); .... 4295 size = chunksize (victim); .... 4300 remainder_size = size - nb; .... 4316 remainder = chunk_at_offset (victim, nb); .... 4320 bck = unsorted_chunks (av); 4321 fwd = bck->fd; .... 4324 remainder->bk = bck; 4325 remainder->fd = fwd; 4326 bck->fd = remainder; 4327 fwd->bk = remainder; .... 4337 set_head (victim, nb | PREV_INUSE | 4338 (av != &main_arena ? NON_MAIN_ARENA : 0)); 4339 set_head (remainder, remainder_size | PREV_INUSE); .... 4343 void *p = chunk2mem (victim); .... 4345 return p; ------------------------------------------------------------------------
------------------------------------------------------------------------ 1449 #define set_head(p, s) ((p)->mchunk_size = (s)) ------------------------------------------------------------------------ 3765 _int_malloc (mstate av, size_t bytes) 3766 { ....3798 nb = checked_request2size(字节);....4295 大小 = chunksize(受害者);....4300 remainder_size = 大小 - nb;....4316 余数 = chunk_at_offset(受害者,NB);....4320 后退 = unsorted_chunks (av);4321 fwd = 返回->fd;....4324 余数->bk = 返回;4325 余数->fd = fwd;4326 back->fd = 余数;4327 fwd->bk = 余数;....4337 set_head (受害者, NB |PREV_INUSE |4338 (av != &main_arena ?NON_MAIN_ARENA : 0)));4339 set_head (余数, remainder_size |PREV_INUSE);....4343 void *p = chunk2mem (受害者);....4345 返回 p;------------------------------------------------------------------------


- If this code path is interrupted by SIGALRM *after* line 4327 but *before* line 4339, then the remainder chunk of this split is already linked into the unsorted list of free chunks (lines 4324-4327), but its size field (mchunk_size) is not yet initialized (line 4339).
- 如果此代码路径被 SIGALRM 第 4327 行之后但第 4339 行之前中断,则此拆分的剩余块已链接到未排序的空闲块列表中(第 4324-4327 行),但其大小字段 (mchunk_size) 尚未初始化(第 4339 行)。


- If we are able to control its size field (through leftovers from previous heap allocations), then we can make this remainder chunk larger and overlap with other heap chunks, and therefore corrupt heap memory when this enlarged, overlapping remainder chunk is eventually malloc()ated and written to (inside the SIGALRM handler).
- 如果我们能够控制其大小字段(通过先前堆分配的剩余部分),那么我们可以使这个剩余块更大并与其他堆块重叠,因此当这个扩大的、重叠的剩余块最终被 malloc() 化并写入(在 SIGALRM 处理程序内)时,会损坏堆内存。


Our last question, then, is: given that we do not control anything about the malloc() calls inside the SIGALRM handler, what can we overwrite in the heap to achieve arbitrary code execution before sshd calls _exit() (in sshsigdie())?
那么,我们的最后一个问题是:鉴于我们无法控制 SIGALRM 处理程序中 malloc() 调用的任何内容,那么在 sshd 调用 _exit() 之前(在 sshsigdie() 中),我们可以在堆中覆盖什么以实现任意代码执行?


Because __tzfile_read() (inside the SIGALRM handler) malloc()ates a FILE structure in the heap (at line 166 above), and because FILE structures have a long history of abuse for arbitrary code execution, we decided to aim our heap corruption at this FILE structure. This is, however, easier said than done: our heap corruption is very limited, and FILE structures have been significantly hardened over the years (by IO_validate_vtable() and PTR_DEMANGLE(), for example).
因为 __tzfile_read()(在 SIGALRM 处理程序中)malloc() 在堆中存储了一个 FILE 结构(在上面的第 166 行),并且因为 FILE 结构长期以来一直滥用任意代码执行,所以我们决定将堆损坏对准这个 FILE 结构。然而,这说起来容易做起来难:我们的堆损坏非常有限,并且多年来 FILE 结构已经得到了显著的强化(例如,通过 IO_validate_vtable() 和 PTR_DEMANGLE())。


Eventually, we devised the following technique (which seems to be specific to the i386 glibc -- the amd64 glibc does not seem to use _vtable_offset at all):
最终,我们设计了以下技术(似乎是特定于 i386 glibc 的 -- amd64 glibc 似乎根本不使用 _vtable_offset):


- with our limited heap corruption, we overwrite the _vtable_offset field (a single signed char) of __tzfile_read()'s FILE structure;
- 由于我们有限的堆损坏,我们覆盖了 __tzfile_read() 的 FILE 结构的 _vtable_offset 字段(单个签名字符);


- the glibc's libio functions will therefore look for this FILE structure's vtable pointer (a pointer to an array of function pointers) at a non-zero offset (our overwritten _vtable_offset), instead of the default zero offset;
- 因此,glibc 的 libio 函数将在非零偏移量(我们被覆盖的_vtable_offset)处查找此 FILE 结构的 vtable 指针(指向函数指针数组的指针),而不是默认的零偏移量;


- we (attackers) can easily control this fake vtable pointer (through leftovers from previous heap allocations), because the FILE structure around this offset is not explicitly initialized by fopen();
- 我们(攻击者)可以轻松控制这个假的 vtable 指针(通过之前堆分配的剩余部分),因为围绕这个偏移量的 FILE 结构没有被 fopen() 显式初始化;


- to pass the glibc's security checks, our fake vtable pointer must point somewhere into the __libc_IO_vtables section: we decided to point it to the vtable for wide-character streams, _IO_wfile_jumps (i.e., to 0xb761b740, since we assume that the glibc is mapped at address 0xb7400000);
- 为了通过 glibc 的安全检查,我们的假 vtable 指针必须指向 __libc_IO_vtables 部分的某个位置:我们决定将其指向宽字符流的 vtable,_IO_wfile_jumps(即 0xb761b740,因为我们假设 glibc 映射在地址 0xb7400000);


- as a result, __fread_unlocked() (at line 186 above) calls _IO_wfile_underflow() (instead of _IO_file_underflow()), which calls a function pointer (__fct) that basically comes from a structure whose pointer (_codecvt) is yet another field of the FILE structure;
- 因此,__fread_unlocked()(在上面的第 186 行)调用 _IO_wfile_underflow()(而不是 _IO_file_underflow()),它调用一个函数指针 (__fct),该指针基本上来自一个结构,该结构的指针 (_codecvt) 是 FILE 结构的另一个字段;


- we (attackers) can easily control this _codecvt pointer (through leftovers from previous heap allocations, because this field of the FILE structure is not explicitly initialized by fopen()), which also allows us to control the __fct function pointer.
- 我们(攻击者)可以轻松控制这个 _codecvt 指针(通过之前堆分配的剩余部分,因为 FILE 结构的这个字段没有被 fopen() 显式初始化),这也允许我们控制__fct函数指针。


In summary, by overwriting a single byte (_vtable_offset) of the FILE structure malloc()ated by fopen(), we can call our own __fct function pointer and execute arbitrary code during __fread_unlocked().
总之,通过用 fopen() 覆盖 FILE 结构 malloc() 的单个字节 (_vtable_offset),我们可以调用自己的 __fct 函数指针并在 __fread_unlocked() 期间执行任意代码。


------------------------------------------------------------------------ Practice ------------------------------------------------------------------------
------------------------------------------------------------------------实践------------------------------------------------------------------------


I wanted it perfect, no wrinkles in it -- The Interrupters, "In the Mirror"
我希望它完美无瑕 -- The Interrupters, “In the Mirror”


To mount this attack against sshd's privileged child, let us first imagine the following heap layout (the "XXX"s are "barrier" chunks that allow us to make holes in the heap; for example, small memory-leaked chunks):
为了对 sshd 的特权子项发起攻击,让我们首先想象以下堆布局(“XXX”是允许我们在堆上打孔的“屏障”块;例如,内存泄漏的小块):


---|----------------------------------------------|---|------------|--- XXX| large hole |XXX| small hole |XXX ---|----------------------------------------------|---|------------|--- | ~8KB | | 320B |
---|----------------------------------------------|---|------------|--- XXX| 大孔|XXX|小孔|XXX ---|----------------------------------------------|---|------------|--- |~8KB | |型号 320B |


- shortly before sshd receives the SIGALRM, we malloc()ate a ~4KB chunk that splits the large ~8KB hole into two smaller chunks:
- 在 sshd 收到 SIGALRM 之前不久,我们 malloc() 获取了一个 ~4KB 的块,它将 ~8KB 的大洞分成两个较小的块:


---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | free remainder chunk |XXX| small hole |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB | ~4KB | | 320B |
---|-----------------------|----------------------|---|------------|--- XXX|大型已分配块 |free remainder 块 |XXX|小孔 |XXX ---|-----------------------|----------------------|---|------------|--- |~4KB |~4KB | |型号 320B |


- but if this malloc() is interrupted by SIGALRM *after* line 4327 but *before* line 4339, then the remainder chunk of this split is already linked into the unsorted list of free chunks, but its size field is under our control (through leftovers from previous heap allocations), and this artificially enlarged remainder chunk overlaps with the following small hole:
- 但是,如果这个 malloc() 被 SIGALRM *在* 第 4327 行之后但*之前* 第 4339 行中断,那么这个拆分的剩余块已经链接到未排序的空闲块列表中,但它的大小字段在我们的控制之下(通过先前堆分配的剩余部分),并且这个人为放大的剩余块与以下小孔重叠:


---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | real remainder chunk |XXX| small hole |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB |<------------------------------------->| artificially enlarged remainder chunk
---|-----------------------|----------------------|---|------------|--- XXX|大型已分配块 |real remainder chunk (真实余数块) |XXX|小孔 |XXX ---|-----------------------|----------------------|---|------------|--- |~4KB |<------------------------------------->|人为放大的 remainder 块


- when the SIGALRM handler calls syslog() and hence __tzfile_read(), fopen() malloc()ates the small hole for its FILE structure, and __fread_unlocked() malloc()ates a 4KB read buffer, thereby splitting the enlarged remainder chunk in two (the 4KB read buffer and a small remainder chunk):
- 当 SIGALRM 处理程序调用 syslog() 并因此调用 __tzfile_read() 时,fopen() malloc() 为其 FILE 结构分配小孔,__fread_unlocked() malloc() 分配一个 4KB 读取缓冲区,从而将扩大的剩余块一分为二(4KB 读取缓冲区和一个小的剩余块):


---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB read buffer remainder
---|-----------------------|----------------------|---|------------|--- XXX|大分配块 | |XXX|档案 |XXX ---|-----------------------|----------------------|---|--|---------|--- |~4KB |<--------------------------->|<------->|4KB 读取缓冲区剩余部分


- we therefore overwrite parts of the FILE structure with the internal header of this small remainder chunk: more precisely, we overwrite the FILE's _vtable_offset with the third byte of this header's bk field, which is a pointer to the unsorted list of free chunks, 0xb761d7f8 (i.e., we overwrite _vtable_offset with 0x61);
- 因此,我们用这个小的剩余块的内部头覆盖 FILE 结构的一部分:更准确地说,我们用这个头的 bk 字段的第三个字节覆盖 FILE 的 _vtable_offset,该字段是指向未排序的空闲块列表的指针,0xb761d7f8(即,我们用 0x61 覆盖 _vtable_offset);


- then, as explained in the "Theory" subsection, __fread_unlocked() calls _IO_wfile_underflow() (instead of _IO_file_underflow()), which calls our own __fct function pointer (through our own _codecvt pointer) and executes our arbitrary code.
- 然后,如“理论”小节所述,__fread_unlocked() 调用 _IO_wfile_underflow()(而不是 _IO_file_underflow()),它调用我们自己的 __fct 函数指针(通过我们自己的 _codecvt 指针)并执行我们的任意代码。


Note: we have not yet explained how to reliably go from a controlled _codecvt pointer to a controlled __fct function pointer; we will do so, but we must first solve a more pressing problem.
注意:我们尚未解释如何可靠地从受控 _codecvt 指针切换到受控 __fct 函数指针;我们会这样做,但我们必须首先解决一个更紧迫的问题。


Indeed, we learned from our work on older OpenSSH versions that we will never win this signal handler race condition if our large race window contains only one small race window. Consequently, we implemented the following strategy, based on the following heap layout:
事实上,我们从旧 OpenSSH 版本的工作中了解到,如果我们的大型 race window 仅包含一个小的 race window,我们将永远不会赢得这个 signal handler race condition。因此,我们根据以下堆布局实施了以下策略:


---|------------|---|------------|---|------------|---|------------|--- XXX|large hole 1|XXX|small hole 1|XXX|large hole 2|XXX|small hole 2|... ---|------------|---|------------|---|------------|---|------------|--- | ~8KB | | 320B | | ~8KB | | 320B |
---|------------|---|------------|---|------------|---|------------|--- XXX|大孔1|XXX|小孔1|XXX|大孔2|XXX|小孔2|...---|------------|---|------------|---|------------|---|------------|--- |~8KB | |320B 型 | |~8KB | |型号 320B |


The last packet that we send to sshd (shortly before the delivery of SIGALRM) forces sshd to perform the following sequence of malloc() calls: malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), etc.
我们发送到 sshd 的最后一个数据包(在 SIGALRM 交付前不久)强制 sshd 执行以下 malloc() 调用序列:malloc(~4KB)、malloc(304)、malloc(~4KB)、malloc(304) 等。


1/ Our first malloc(~4KB) splits the large hole 1 in two:
1/ 我们的第一个 malloc(~4KB) 将大孔 1 一分为二:


- if this first split is interrupted by SIGALRM at the right time, then the fopen() inside the SIGALRM handler malloc()ates the small hole 1 for its FILE structure, and we achieve arbitrary code execution as explained above;
- 如果第一次拆分在正确的时间被 SIGALRM 中断,那么 SIGALRM 处理程序 malloc() 内的 fopen() 为其 FILE 结构处理小孔 1,我们实现如上所述的任意代码执行;


- if not, then we malloc()ate the small hole 1 ourselves with our first malloc(304), and:
- 如果不是,那么我们用第一个 malloc(304) 自己吃掉小洞 1,然后:


2/ Our second malloc(~4KB) splits the large hole 2 in two:
2/ 我们的第二个 malloc(~4KB) 将大孔 2 一分为二:


- if this second split is interrupted by SIGALRM at the right time, then the fopen() inside the SIGALRM handler malloc()ates the small hole 2 for its FILE structure, and we achieve arbitrary code execution as explained above;
- 如果这第二次拆分在正确的时间被 SIGALRM 打断,那么 SIGALRM 处理程序 malloc() 内的 fopen() 为其 FILE 结构处理小孔 2,我们实现了如上所述的任意代码执行;


- if not, then we malloc()ate the small hole 2 ourselves with our second malloc(304), etc.
- 如果不是,那么我们自己用第二个 malloc(304) 吃掉小洞 2,依此类推。


We were able to make 27 pairs of such large and small holes in sshd's heap (28 would exceed PACKET_MAX_SIZE, 256KB): our large race window now contains 27 small race windows! Achieving this complex heap layout was extremely painful and time-consuming, but the two highlights are:
我们能够在 sshd 的堆中制作 27 对如此大大小小的孔(28 个将超过 PACKET_MAX_SIZE,256KB):我们的大型 race window 现在包含 27 个小的 race window!实现这种复杂的堆布局非常痛苦和耗时,但两个亮点是:


- We abuse sshd's public-key parsing code to perform arbitrary sequences of malloc() and free() calls (at lines 1805 and 573):
- 我们滥用 sshd 的公钥解析代码来执行任意序列的 malloc() 和 free() 调用(第 1805 行和第 573 行):


------------------------------------------------------------------------ 1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { .... 1797 while (sshbuf_len(principals) > 0) { .... 1805 if ((ret = sshbuf_get_cstring(principals, &principal, .... 1820 key->cert->principals[key->cert->nprincipals++] = principal; 1821 } ------------------------------------------------------------------------ 562 cert_free(struct sshkey_cert *cert) 563 { ... 572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]); ------------------------------------------------------------------------
------------------------------------------------------------------------ 1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { ....1797 while (sshbuf_len(principals) > 0) { ....1805 if ((ret = sshbuf_get_cstring(principals, &principal, ....1820 key->cert->principals[key->cert->nprincipals++] = principal;1821 } ------------------------------------------------------------------------ 562 cert_free(struct sshkey_cert *cert) 563 { ...572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]);------------------------------------------------------------------------


- We were unable to find a memory leak for our small "barrier" chunks; instead, we use tcache chunks (which are never really freed, because their inuse bit is never cleared) as makeshift "barrier" chunks.
- 我们无法找到小的 “barrier” 块的内存泄漏;相反,我们使用 tcache 块(从未真正释放,因为它们的 inuse 位从未被清除)作为临时的 “barrier” 块。


To reliably achieve this heap layout, we send five different public-key packets to sshd (packets a/ to d/ can be sent long before SIGALRM; most of packet e/ can also be sent long before SIGALRM, but its very last byte must be sent at the very last moment):
为了可靠地实现这种堆布局,我们向 sshd 发送了五个不同的公钥数据包(数据包 a/ 到 d/ 可以在 SIGALRM 之前很久就发送;大多数数据包 e/ 也可以在 SIGALRM 之前很久发送,但其最后一个字节必须在最后一刻发送):


a/ We malloc()ate and free() a variety of tcache chunks, to ensure that the heap allocations that we do not control end up in these tcache chunks and do not interfere with our careful heap layout.
a/ 我们 malloc()ate 和 free() 各种 tcache 块,以确保我们无法控制的堆分配最终出现在这些 tcache 块中,并且不会干扰我们仔细的堆布局。


b/ We malloc()ate and free() chunks of various sizes, to make our 27 pairs of large and small holes (and the corresponding "barrier" chunks).
b/ 我们 malloc()ate 和 free() 各种大小的块,来制作我们的 27 对大孔和小孔(以及相应的 “barrier” 块)。


c/ We malloc()ate and free() ~4KB chunks and 320B chunks, to:
c/ 我们 malloc()ate 和 free() ~4KB 块和 320B 块,以:


- write the fake header (the large size field) of our potentially enlarged remainder chunk, into the middle of our large holes;
- 将可能扩大的 remainder chunk 的 fake header(大尺寸字段)写入我们的大孔中间;


- write the fake footer of our potentially enlarged remainder chunk, to the end of our small holes (to pass the glibc's security checks);
- 将可能扩大的 remainder chunk 的假页脚写到小孔的末尾(以通过 glibc 的安全检查);


- write our fake vtable and _codecvt pointers, into our small holes (which are potential FILE structures).
- 将我们的假 vtable 和 _codecvt 指针写入我们的小孔(潜在的 FILE 结构)。


d/ We malloc()ate and free() one very large string (nearly 256KB), to ensure that our large and small holes are removed from the unsorted list of free chunks and placed into their respective malloc bins.
d/ 我们 malloc()ate 和 free() 一个非常大的字符串(近 256KB),以确保我们的大孔和小孔从未排序的空闲块列表中删除,并放入它们各自的 malloc bin 中。


e/ We force sshd to perform our final sequence of malloc() calls (malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), etc), to open our 27 small race windows.
e/ 我们强制 sshd 执行我们最后的 malloc() 调用序列(malloc(~4KB)、malloc(304)、malloc(~4KB)、malloc(304) 等),以打开我们的 27 个小的 race 窗口。


Attentive readers may have noticed that we have still not addressed (literally and figuratively) the problem of _codecvt. In fact, _codecvt is a pointer to a structure (_IO_codecvt) that contains a pointer to a structure (__gconv_step) that contains the __fct function pointer that allows us to execute arbitrary code. To reliably control __fct through _codecvt, we simply point _codecvt to one of the glibc's malloc bins, which conveniently contains a pointer to one of our free chunks in the heap, which contains our own __fct function pointer to arbitrary glibc code (all of these glibc addresses are known to us, because we assume that the glibc is mapped at address 0xb7400000).
细心的读者可能已经注意到,我们仍然没有解决(从字面上和比喻上)_codecvt的问题。实际上,_codecvt 是一个指向结构体 (_IO_codecvt) 的指针,该结构体包含一个指向结构体 (__gconv_step) 的指针,该结构体包含允许我们执行任意代码的 __fct 函数指针。为了可靠地控制 __fct 到 _codecvt,我们只需将_codecvt指向 glibc 的 malloc bin 之一,该 bin 方便地包含指向堆中某个空闲块的指针,该块包含我们自己的指向任意 glibc 代码的 __fct 函数指针(所有这些 glibc 地址都是已知的,因为我们假设 glibc 映射到地址 0xb7400000)。


------------------------------------------------------------------------ Timing ------------------------------------------------------------------------
------------------------------------------------------------------------定时------------------------------------------------------------------------


We're running out of time -- The Interrupters, "As We Live"
我们的时间不多了 -- The Interrupters,“As We Live”


As we implemented this third exploit, it became clear that we could not simply re-use the timing strategy that we had used against the two older OpenSSH versions: we were never winning this new race condition. Eventually, we understood why:
当我们实施第三个漏洞时,很明显我们不能简单地重用我们对两个旧 OpenSSH 版本使用的计时策略:我们从未赢得这个新的竞争条件。最终,我们明白了原因:


- It takes a long time (~10ms) for sshd to parse our fifth and last public key (packet e/ above); in other words, our large race window is too large (our 27 small race windows are like needles in a haystack).
- sshd 需要很长时间(~10 毫秒)来解析我们的第五个也是最后一个公钥(数据包 e/ 上面);换句话说,我们的大 Race Window 太大了(我们的 27 个小 Race Window 就像大海捞针一样)。


- The user_specific_delay() that was introduced recently (OpenSSH 7.8p1) delays sshd's response to our last public-key packet by up to ~9ms and therefore destroys our feedback-based timing strategy.
- 最近引入的 user_specific_delay() (OpenSSH 7.8p1) 将 sshd 对我们最后一个公钥数据包的响应延迟了多达 ~9 毫秒,因此破坏了我们基于反馈的计时策略。


As a result, we developed a completely different timing strategy:
因此,我们开发了一种完全不同的计时策略:


- from time to time, we send our last public-key packet with a little mistake that produces an error response (lines 138-142 below), right before the call to sshkey_from_blob() that parses our public key;
- 有时,我们会在调用 sshkey_from_blob() 解析我们的公钥之前发送最后一个带有一个小错误的公钥数据包,该数据包会产生错误响应(下面的第 138-142 行);


- from time to time, we send our last public-key packet with another little mistake that produces an error response (lines 151-155 below), right after the call to sshkey_from_blob() that parses our public key;
- 有时,我们会在调用 sshkey_from_blob() 解析我们的公钥后立即发送带有另一个小错误的小错误,该错误会产生错误响应(下面的第 151-155 行);


- the difference between these two response times is the time that it takes for sshd to parse our last public key, and this allows us to precisely time the transmission of our last packets (to ensure that sshd has the time to parse our public key in the unprivileged child, send it to the privileged child, and start to parse it there, before the delivery of SIGALRM).
- 这两个响应时间之间的差异是 sshd 解析最后一个公钥所花费的时间,这使我们能够精确地计算最后一个数据包的传输时间(以确保 sshd 有时间解析我们在非特权子项中的公钥,将其发送给特权子项, 并在 SIGALRM 交付之前开始解析它)。


------------------------------------------------------------------------ 88 userauth_pubkey(struct ssh *ssh, const char *method) 89 { ... 138 if (pktype == KEY_UNSPEC) { 139 /* this is perfectly legal */ 140 verbose_f("unsupported public key algorithm: %s", pkalg); 141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, "parse key"); 145 goto done; 146 } ... 151 if (key->type != pktype) { 152 error_f("type mismatch for decoded key " 153 "(received %d, expected %d)", key->type, pktype); 154 goto done; 155 } ------------------------------------------------------------------------
------------------------------------------------------------------------ 88 userauth_pubkey(struct ssh *ssh, const char *method) 89 { ...138 if (pktype == KEY_UNSPEC) { 139 /* 这是完全合法的 */ 140 verbose_f(“不支持的公钥算法: %s”, pkalg);141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, “解析密钥”);145 goto done; 146 } ...151 if (key->type != pktype) { 152 error_f(“解码密钥的类型不匹配” 153 “(收到 %d,预期 %d)”, key->type, pktype);154 goto 完成;155 } ------------------------------------------------------------------------


With this change in strategy, it takes ~10,000 tries on average to win the race condition; i.e., with 100 connections (MaxStartups) accepted per 120 seconds (LoginGraceTime), it takes ~3-4 hours on average to win the race condition, and ~6-8 hours to obtain a remote root shell (because of ASLR).
通过这种策略的更改,平均需要 ~10,000 次尝试才能赢得比赛条件;即,每 120 秒接受 100 个连接 (MaxStartups) (LoginGraceTime),平均需要 ~3-4 小时才能赢得争用条件,需要 ~6-8 小时才能获得远程根 shell(由于 ASLR)。


======================================================================== Towards an amd64 exploit ========================================================================
======================================================================== 针对 amd64 漏洞利用 ========================================================================


What's your plan for tomorrow? -- The Interrupters, "Take Back the Power"
你明天的计划是什么?-- The Interrupters,《夺回权力》


We decided to target Rocky Linux 9 (a Red Hat Enterprise Linux 9 derivative), from "Rocky-9.4-x86_64-minimal.iso", for two reasons:
我们决定将 “Rocky-9.4-x86_64-minimal.iso” 的 Rocky Linux 9(Red Hat Enterprise Linux 9 衍生产品)作为目标,原因有二:


- its OpenSSH version (8.7p1) is vulnerable to this signal handler race condition and its glibc is always mapped at a multiple of 2MB (because of the ASLR weakness discussed in the previous "Theory" subsection), which makes partial pointer overwrites much more powerful;
- 它的 OpenSSH 版本 (8.7p1) 容易受到这种信号处理程序竞争条件的影响,并且它的 glibc 总是以 2MB 的倍数映射(因为在前面的“理论”小节中讨论了 ASLR 弱点),这使得部分指针覆盖更加强大;


- the syslog() function (which is async-signal-unsafe but is called by sshd's SIGALRM handler) of this glibc version (2.34) internally calls __open_memstream(), which malloc()ates a FILE structure in the heap, and also calls calloc(), realloc(), and free() (which gives us some much-needed freedom).
- 此 glibc 版本 (2.34) 的 syslog() 函数(异步信号不安全,但由 sshd 的 SIGALRM 处理程序调用)在内部调用 __open_memstream(),该函数 malloc() 在堆中处理 FILE 结构,并且还调用 calloc()、realloc() 和 free()(这为我们提供了一些急需的自由)。


With a heap corruption as a primitive, two FILE structures malloc()ated in the heap, and 21 fixed bits in the glibc's addresses, we believe that this signal handler race condition is exploitable on amd64 (probably not in ~6-8 hours, but hopefully in less than a week). Only time will tell.
以堆损坏为基语,堆中有两个 FILE 结构 malloc(),并且 glibc 的地址中有 21 个固定位,我们相信这种信号处理程序竞争条件可以在 amd64 上被利用(可能不会在 ~6-8 小时内,但希望在不到一周的时间内)。只有时间会证明一切。


Side note: we discovered that Ubuntu 24.04 does not re-randomize the ASLR of its sshd children (it is randomized only once, at boot time); we tracked this down to the patch below, which turns off sshd's rexec_flag. This is generally a bad idea, but in the particular case of this signal handler race condition, it prevents sshd from being exploitable: the syslog() inside the SIGALRM handler does not call any of the malloc functions, because it is never the very first call to syslog().
旁注:我们发现 Ubuntu 24.04 不会重新随机化其 sshd 子项的 ASLR(它只在启动时随机化一次);我们将其跟踪到下面的补丁,该补丁关闭了 sshd 的 rexec_flag。这通常是一个坏主意,但在这种信号处理程序竞争条件的特殊情况下,它可以防止 sshd 被利用:SIGALRM 处理程序中的 syslog() 不调用任何 malloc 函数,因为它从来都不是对 syslog() 的第一次调用。


https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch

======================================================================== Patches and mitigation ========================================================================
======================================================================== 补丁和缓解========================================================================


The storm has come and gone -- The Interrupters, "Good Things"
暴风雨来了又走了 -- The Interrupters, “Good Things”


On June 6, 2024, this signal handler race condition was fixed by commit 81c1099 ("Add a facility to sshd(8) to penalise particular problematic client behaviours"), which moved the async-signal-unsafe code from sshd's SIGALRM handler to sshd's listener process, where it can be handled synchronously:
在 2024 年 6 月 6 日,这个信号处理程序竞争条件被提交 81c1099 修复(“向 sshd(8) 添加一个工具以惩罚特定有问题的客户端行为”),它将 async-signal-unsafe 代码从 sshd 的 SIGALRM 处理程序移动到 sshd 的侦听器进程,在那里它可以被同步处理:


https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29

Because this fix is part of a large commit (81c1099), on top of an even larger defense-in-depth commit (03e3de4, "Start the process of splitting sshd into separate binaries"), it might prove difficult to backport. In that case, the signal handler race condition itself can be fixed by removing or commenting out the async-signal-unsafe code from the sshsigdie() function; for example:
因为这个修复是大型提交 (81c1099) 的一部分,再加上一个更大的深度防御提交 (03e3de4, “开始将 sshd 拆分为单独的二进制文件的过程”),所以可能很难向后移植。在这种情况下,可以通过从 sshsigdie() 函数中删除或注释掉 async-signal-unsafe 代码来修复信号处理程序争用条件本身;例如:


------------------------------------------------------------------------ sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { #if 0 va_list args;
------------------------------------------------------------------------ sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...){ #if 0 va_list args;


va_start(args, fmt); sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, suffix, fmt, args); va_end(args); #endif _exit(1); } ------------------------------------------------------------------------
va_start(args, fmt);sshlogv(文件、func、行、showfunc、SYSLOG_LEVEL_FATAL、后缀、fmt、args);va_end(args);#endif _exit(1);} ------------------------------------------------------------------------


Finally, if sshd cannot be updated or recompiled, this signal handler race condition can be fixed by simply setting LoginGraceTime to 0 in the configuration file. This makes sshd vulnerable to a denial of service (the exhaustion of all MaxStartups connections), but it makes it safe from the remote code execution presented in this advisory.
最后,如果 sshd 无法更新或重新编译,则只需在配置文件中将 LoginGraceTime 设置为 0 即可修复此信号处理程序竞争条件。这使得 sshd 容易受到拒绝服务(所有 MaxStartups 连接耗尽)的攻击,但它使其免受此公告中介绍的远程代码执行的影响。


======================================================================== Acknowledgments ========================================================================
======================================================================== 致谢 ========================================================================


We thank OpenSSH's developers for their outstanding work and close collaboration on this release. We also thank the distros@openwall. Finally, we dedicate this advisory to Sophia d'Antoine.
我们感谢 OpenSSH 的开发人员在此版本中的出色工作和密切合作。我们还要感谢 distros@openwall。最后,我们将这份咨询献给 Sophia d'Antoine。


======================================================================== Timeline ========================================================================
======================================================================== 时间线 ========================================================================


2024-05-19: We contacted OpenSSH's developers. Successive iterations of patches and patch reviews followed.
2024-05-19:我们联系了 OpenSSH 的开发人员。补丁和补丁审查的连续迭代紧随其后。


2024-06-20: We contacted the distros@openwall.
2024-06-20: 我们联系了 distros@openwall。


2024-07-01: Coordinated Release Date.
2024-07-01:协调发布日期。