chatr

MySQL/MariaDB Memcmp Vulnerability, Are You Vulnerable?

| Comments

A vulnerability was recently announced that allowed an attacker to successfully authenticate against a database without the correct password. The issue was a call to memcpy that resulted in a value outside of the standard -128..127 range. To view the full announcement you can visit http://seclists.org/oss-sec/2012/q2/493.

Testing your install

You can do a quick test to see if you are affected. Find the location of your mysql install and fire up gdb.

$ gdb bin/mysql

Next, load up libmysqlclient so you can get the symbol reference for the check_scramble function. This is the offender in the vulnerability.

(gdb) file lib/mysql/libmysqlclient.so
Load new symbol table from "/packages/encap/mysql-5.1.56_percona_server/lib/mysql/libmysqlclient.so"? (y or n) y
Reading symbols from /packages/encap/mysql-5.1.56_percona_server/lib/mysql/libmysqlclient.so...done.
(gdb) set logging file /home/user/mysql.out
(gdb) set logging on
Copying output to /home/user/mysql.out.
(gdb) disas check_scramble
...

Now check for the presence of memcmp in the dump

$ cat /home/user/mysql.out | grep memcmp

If nothing is returned, you are good to go. If you dig a bit further, you will notice that the call to memcmp has been replaced with

repz cmpsb %es:(%rdi),%ds:(%rsi)

which will prevent for this vulnerability from occurring. If you do see the call to memcmp, then further analysis is required. Your specific build of gcc may or may not expose this issue.

Detailed Analysis

You can do a simple test on your system with a short C program

test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <string.h>
#include <stdio.h>

int main() {
  char *s1 = "b!r";
  char *s2 = "bÿr";
  int result;

  result = memcmp(s1, s2, sizeof(s1));
  if ((result < -128) || (result > 127)) {
    fprintf(stderr, "unexpected memcmp(): %d\n", result);
    return 1;
  }

  return 0;
}

Compile and test:

$ gcc test.c -o test
$ ./test
unexpected memcmp(): -162

Using gdb to dump this, you can see the call to memcmp:

$ gdb test
(gdb) disas main
0x0000000000400534 <+0>:    push   %rbp
0x0000000000400535 <+1>:    mov    %rsp,%rbp
0x0000000000400538 <+4>:    sub    $0x20,%rsp
0x000000000040053c <+8>:    movq   $0x40068c,-0x8(%rbp)
0x0000000000400544 <+16>:   movq   $0x400690,-0x10(%rbp)
0x000000000040054c <+24>:   mov    -0x10(%rbp),%rcx
0x0000000000400550 <+28>:   mov    -0x8(%rbp),%rax
0x0000000000400554 <+32>:   mov    $0x8,%edx
0x0000000000400559 <+37>:   mov    %rcx,%rsi
0x000000000040055c <+40>:   mov    %rax,%rdi
0x000000000040055f <+43>:   callq  0x400430 <memcmp@plt>
0x0000000000400564 <+48>:   mov    %eax,-0x14(%rbp)
0x0000000000400567 <+51>:   cmpl   $0xffffff80,-0x14(%rbp)
0x000000000040056b <+55>:   jl     0x400573 <main+63>
0x000000000040056d <+57>:   cmpl   $0x7f,-0x14(%rbp)
0x0000000000400571 <+61>:   jle    0x400599 <main+101>
0x0000000000400573 <+63>:   mov    $0x400695,%ecx
0x0000000000400578 <+68>:   mov    0x2003d9(%rip),%rax        # 0x600958 <stderr@@GLIBC_2.2.5>
0x000000000040057f <+75>:   mov    -0x14(%rbp),%edx
0x0000000000400582 <+78>:   mov    %rcx,%rsi
0x0000000000400585 <+81>:   mov    %rax,%rdi
0x0000000000400588 <+84>:   mov    $0x0,%eax
0x000000000040058d <+89>:   callq  0x400440 <fprintf@plt>
0x0000000000400592 <+94>:   mov    $0x1,%eax
0x0000000000400597 <+99>:   jmp    0x40059e <main+106>
0x0000000000400599 <+101>:  mov    $0x0,%eax
0x000000000040059e <+106>:  leaveq
0x000000000040059f <+107>:  retq

If you recompile using optimizations, you will notice a slight difference:

$ gcc -O3 test.c -o test
$ ./test
<nothing else happens>
$ gdb test
(gdb) disas main
Dump of assembler code for function main:
0x00000000004004f0 <+0>:    mov    $0x8,%ecx
0x00000000004004f5 <+5>:    sub    $0x8,%rsp
0x00000000004004f9 <+9>:    mov    $0x40062c,%esi
0x00000000004004fe <+14>:   mov    $0x400630,%edi
0x0000000000400503 <+19>:   repz   cmpsb %es:(%rdi),%ds:(%rsi)
0x0000000000400505 <+21>:   seta   %dl
0x0000000000400508 <+24>:   setb   %al
0x000000000040050b <+27>:   sub    %al,%dl
0x000000000040050d <+29>:   xor    %eax,%eax
0x000000000040050f <+31>:   movsbl %dl,%edx
0x0000000000400512 <+34>:   lea    0x80(%rdx),%ecx
x0000000000400518 <+40>:    cmp    $0xff,%ecx
0x000000000040051e <+46>:   jbe    0x400536 <main+70>
0x0000000000400520 <+48>:   mov    0x2003c1(%rip),%rdi        # 0x6008e8 <stderr@@GLIBC_2.2.5>
0x0000000000400527 <+55>:   mov    $0x400635,%esi
0x000000000040052c <+60>:   callq  0x4003f0 <fprintf@plt>
0x0000000000400531 <+65>:   mov    $0x1,%eax
0x0000000000400536 <+70>:   add    $0x8,%rsp
0x000000000040053a <+74>:   retq

Note that the call to memcmp has been replaced by the repeat while zero string operation cmpsb. This will change the result and should actually thwart any bad mojo that was present previously.

If you look at the code in the MySQL project in sql/password.c, the memcmp only happens in one place, the check_scramble function, which has the following code:

password.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
my_bool
check_scramble(const char *scramble_arg,
               const char *message,
               const uint8 *hash_stage2)
{
  SHA1_CONTEXT sha1_context;
  uint8 buf[SHA1_HASH_SIZE];
  uint8 hash_stage2_reassured[SHA1_HASH_SIZE];

  mysql_sha1_reset(&sha1_context);
  /* create key to encrypt scramble */
  mysql_sha1_input(&sha1_context, (const uint8 *) message, SCRAMBLE_LENGTH);
  mysql_sha1_input(&sha1_context, hash_stage2, SHA1_HASH_SIZE);
  mysql_sha1_result(&sha1_context, buf);
  /* encrypt scramble */
    my_crypt((char *) buf, buf, (const uchar *) scramble_arg, SCRAMBLE_LENGTH);
  /* now buf supposedly contains hash_stage1: so we can get hash_stage2 */
  mysql_sha1_reset(&sha1_context);
  mysql_sha1_input(&sha1_context, buf, SHA1_HASH_SIZE);
  mysql_sha1_result(&sha1_context, hash_stage2_reassured);
  return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE);
}

Note the return value is the result of the call to memcmp. The patch just affects that line of code. Here’s what changed:

1
2
-  return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE);
+  return test(memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE));

Parting thoughts

So you can draw the natural conclusion that if you compiled MySQL with optimizations, you are most likely safe against this vulnerability. It’s always a good idea to upgrade when these kinds of patches are provided, but when the upgrade process can be daunting and possibly cause downtime, it’s nice to know that waiting for the right opportunity is a reasonable decision if you aren’t impacted.

Comments