MySQL Internals ClientServer Protocol

 

mysql 协议地址:http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol

Mysql 认证步骤

一、建立连接
1、使用系统的socket函数建立一个socket
2、使用这个socket初始化一个vio对象
net->vio= vio_new(sock, VIO_TYPE_TCPIP, VIO_BUFFERED_READ);
3、对这个socket执行connect操作,连接到远程主机
4、使用vio初始化net对象
my_net_init(net, net->vio)
并设置为keep alive
vio_keepalive(net->vio,TRUE);

二、第一次交换
客户端执行recv,会收到一个来自server的包,其中第一个字节是协议的版本号。
其它的重要信息还有connection id、scramble

41 00 00 00
0A 35 2E 30 2E 32 30 2D 73 74 61 6E 64 61 72 64 2D 6C 6F 67 00 44 8E 4E 00 5A 66 72 2A 79 43 24 27 00 2C A2 08 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 36 7B 29 58 5E 50 56 41 21 7C 73 4C 00
 
 其格式如下:
 1                            协议的版本号 (0×0A)
 n (Null-Terminated String)   服务器版本
 4                            thread_id
 8                            scramble_buff
 1                            (填充) 永远是 0×00
 2                            server_capabilities
 1                            server_language
 2                            server_status
 13                           (填充) 永远是 0×00 …
 13                           scramble_buff剩余的部分 (4.1)
 
三、然后客户端将密码等发送过去
客户端根据服务器发给的scramble加密,并存放在scramble_buff中发给服务器
发送登录数据:
00000000        3A 00 00 01 85 A6 03 00 00 00 00 01 08 00 00 00
00000010        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000020        00 00 00 00 72 6F 6F 74 00 14 00 00 00 00 00 00
00000030        00 00 00 00 00 00 00 00 00 00 00 00 00 00  

 Bytes                        Name
 —–                        —-
 4                            client_flags
 4                            max_packet_size
 1                            charset_number
 23                           (filler) always 0×00…
 n (Null-Terminated String)   user
 n (Length Coded Binary)      scramble_buff (1 + x bytes)
 1                            (filler) always 0×00
 n (Null-Terminated String)   databasename

databasename字段可有可无。
scramble_buff字段的数据是这样生的:
scramble(end, mysql->scramble, passwd);

四、再度发送scrambled password (可选)
授权信息已经发送过去了,服务器可以会回答说OK(发回一个OK_PACKET),也有可能会要求再度发送scrambled password。
如果要再度发送,服务器会返回一个1字节的包,如果第一个字节是0xFE且mysql.server_capabilities设置了CLIENT_SECURE_CONNECTION,那么
就需要再度发送scrambled password
这个似乎是为了和以前老版本兼容,这次需要使用3.23版的scramble对password进行加密然后发送。
scramble_323(buff, mysql->scramble, passwd);
如:
0×8059000:      0×09    0×00    0×00    0×03    0×4d    0×45    0×46    0×4c
0×8059008:      0×4f    0×44    0×4b    0×4b    0×00
这个包的格式很简单,包头,然后是9个字节的scramble(其中最后一个字节必须是0×00)
不过要注意,此处包头的第4个字节是0×03,因为这是认证过程是双方来回发送的第三个包了。
五、命令
0×20,0×00,0×00,0×00, 包头
0×03 //命令的类型,COM_QUERY
select * from xxx where xxx //arg

========================================================
MYSQL认证漏洞:
1、构造0长度的scramble绕过密码校验
这几乎可以算是mysql目前发现的危害性最严重的安全漏洞了。

出问题的代码:
my_bool
check_scramble_323(const char *scrambled, const char *message,
                   ulong *hash_pass)
{
  struct rand_struct rand_st;
  ulong hash_message[2];
  char buff[16],*to,extra; /* Big enough for check */
  const char *pos;
  hash_password(hash_message, message, SCRAMBLE_LENGTH_323);
  randominit(&rand_st,hash_pass[0] ^ hash_message[0],
             hash_pass[1] ^ hash_message[1]);
  to=buff;
  for (pos=scrambled ; *pos ; pos++)
    *to++=(char) (floor(my_rnd(&rand_st)*31)+64);
  extra=(char) (floor(my_rnd(&rand_st)*31));
  to=buff;
  while (*scrambled)
  {
    if (*scrambled++ != (char) (*to++ ^ extra))
      return 1; /* Wrong password */
  }
  return 0;
}
改正后的 (多加了一句  if (pos-scrambled != SCRAMBLE_LENGTH_323) return 1)

my_bool
check_scramble_323(const char *scrambled, const char *message,
                   ulong *hash_pass)
{
  struct rand_struct rand_st;
  ulong hash_message[2];
  char buff[16],*to,extra;                      /* Big enough for check */
  const char *pos;

  hash_password(hash_message, message, SCRAMBLE_LENGTH_323);
  randominit(&rand_st,hash_pass[0] ^ hash_message[0],
             hash_pass[1] ^ hash_message[1]);
  to=buff;
  DBUG_ASSERT(sizeof(buff) > SCRAMBLE_LENGTH_323);
  for (pos=scrambled ; *pos && to < buff+sizeof(buff) ; pos++)
    *to++=(char) (floor(my_rnd(&rand_st)*31)+64);
  if (pos-scrambled != SCRAMBLE_LENGTH_323)
    return 1;
  extra=(char) (floor(my_rnd(&rand_st)*31));
  to=buff;
  while (*scrambled)
  {
    if (*scrambled++ != (char) (*to++ ^ extra))
      return 1;                                 /* Wrong password */
  }
  return 0;
}

2、构造一个足够长的passwd让strlen溢出。
出问题的代码:
  uint passwd_len= thd->client_capabilities & CLIENT_SECURE_CONNECTION ?
    *passwd++ : strlen(passwd);
  db= thd->client_capabilities & CLIENT_CONNECT_WITH_DB ?
    db + passwd_len + 1 : 0;
  uint db_len= db ? strlen(db) : 0;
 
修正后:
  char *passwd= strend(user)+1;
  。。。。
  uint passwd_len= thd->client_capabilities & CLIENT_SECURE_CONNECTION ?
    (uchar)(*passwd++) : strlen(passwd);
  db= thd->client_capabilities & CLIENT_CONNECT_WITH_DB ?
    db + passwd_len + 1 : 0;
  /* strlen() can’t be easily deleted without changing protocol */
  uint db_len= db ? strlen(db) : 0;
设想,客户端发来的包中,如果client_capabilities 指定 CLIENT_SECURE_CONNECTION和CLIENT_SECURE_CONNECTION两个位的值
且passwd的第一个字节大于0×80,那么*passwd作为一个char值将是负的,在其再被转为uint的时候,将会是一个很大的uint。然后db这个指针就会被指向别处,从而对一段意外的内存进行strlen。这时可能会引发内存的读错误。从而造成拒绝服务攻击。但是这个不太容易,因为passwd_len的有效范围在[-128,127]之间,所以所能造成的危害很小。
如果passwd_len不等于-1,而是一个更大的负数,那么strlen函数至多会读到username后的那么’\0′就会终止。这样做的好处是可以借助前面那段填充区来设置db name以供后面用,缺点是此处无法引发内存错误。
如果passwd_len恰好设置为-1,且passwd_len后面的那些字节(SCRAMBLE)全部用非0值填充,那么strlen就会一直朝后面读下去直到找到0。但是由于db指针指向的是堆上,而且在很多操作系统下,如Freebsd,都会把在堆上分配的内存在交给用户使用前都初始化成0,所以……想让strlen读越界也很难。

而接下来的一个关于边界的检查
  if (passwd + passwd_len + db_len > (char *)net->read_pos + pkt_len) …
也因为整数溢出而导致失效。
然后程序会一直向下执行到check_user,如果编译的时候定义了NO_EMBEDDED_ACCESS_CHECKS,那么万事大吉,此时的db指针会被立即传递给mysql_change_db函数,哦……………………否则的话,将会有一处关于password_len的检查
  if (passwd_len != 0 &&
      passwd_len != SCRAMBLE_LENGTH &&
      passwd_len != SCRAMBLE_LENGTH_323)
    DBUG_RETURN(ER_HANDSHAKE_ERROR);
server会立刻给客户端报告一个handshare error而终止连接。
==========================================
附:mysql调试策略
1、mysqld
要在mysqld正在运行的时候挂一个gdb进去是很不容易的,我的方法是在编译的configure的时候加一个选项–with-debug,然后修改我感兴趣的部分的代码,用
DBUG_PRINT(”snnn info”,(”passwd_len:%u,db_len:%u”,passwd_len,db_len));
这样的方式去输出调试语句。
然后用mysqld_safe –defaults-file=xxx –debug &的方式启动mysql,然后去/tmp/mysqld.trace中查找我记录的日志。

2、mysql client
要直接调试mysql client也不好办,通常都是利用mysql client库,自己写一些简单的程序,然后用gdb跟踪到mysql client库中去。其中很重要的两个函数是
my_real_read
my_net_write
通过对这两个函数下断点能够很顺利的跟踪服务器、客户端之间的数据交换流程。

留下评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据