Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

密码学误用分析实例:Cryptcat

0x0 背景

Cryptcat是经典网络工具netcat的加密版,使用twofish算法加密,其密钥协商过程基于发送、接收双发共享的一个password。

Cryptcat密码学模块的源文件主要为farm9crypt.cpptwofish.cppfarm9crypt.cpp作为netcat和twofish之间的接口,twofish.cpp实现了加解密算法。本次分析的源码来自其sourceforge项目主页 ,unix版的cryptcat密码学模块基本相同。

0x1 加解密流程

Cryptcat使用了CBC(密文分组链接)模式,大致流程如下:

  1. 发送端将待发送数据的size与一些“随机数”用password加密后发送,接收端解开后可以得知size
  2. 将第一步的密文与第一步的明文异或后,用password加密发送,密文作为后续CBC模式的IV
  3. 在CBC模式中,采用了ciphertext stealing技术,可以发送不是分组长度倍数的数据

ciphertext stealing

如上图,b为分组长度,b-d是最后一个分组的填充长度,默认用0填充。在传输密文时,倒数第二个分组(C_3)后半部分b-d长度的密文无需传输,因为C_4解密后可以得到这部分的值。所以利用Ciphertext Stealing技术,在无需扩展密文长度的情况下进行分组加密,减少了不必要的网络传输。

0x2 密码学安全问题

2.1 初始化

keystr为用户输入的password,此处使用了固定值的随机数种子。

extern "C" void farm9crypt_init( char* keystr ) {
       printf( "farm9crypt_init: %s\n", keystr );
       encryptor = new TwoFish( generateKey( keystr ), false, NULL, NULL );
       decryptor = new TwoFish( generateKey( keystr ), true, NULL, NULL );
       initialized = true;
       srand( 1000 );
}

2.2 密钥初始化

twofish2.cpp中的generateKey函数,功能是将用户提供的字符串password转换成twofish的128bit密钥。

static char key[32];
char* generateKey( char* s ) {
    int sIdx = 0;
    for ( int i = 0; i < 32; i++ ) {
        char sval = *( s + sIdx );
        if (( sval >= '0' ) && ( sval <= '9' )) {
            key[i] = sval;
        } else if (( sval >= 'a' ) && ( sval <= 'f' )) {
            key[i] = sval;
        } else {
            int q = sval%16;
            if ( q < 10 ) {
                key[i] = ('0' + q);
            } else {
                key[i] = ('a' + q - 10);
            }
        }
        sIdx++;
        if ( *( s + sIdx ) == 0 ) {
            sIdx = 0;
        }
    }
    return( &key[0] );
}

对password的每个字符,函数用取模的方式只取了4个bit,如果password过短的话函数就对passwrod进行循环,超过32个字符的话则会丢弃剩下的部分。造成多个字符串可以对应相同的twofish密钥,并且还存在默认密码metallica的情况。

p.s. 这个bug已经有人报告了,但开发者没有回复 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=715415

2.3 CBC模式

iv的产生与farm9crypt.cpp中的farm9crypt_write函数有关。

char tempbuf[16];
    char outbuf[16];
    sprintf( tempbuf, "%d %d", size, rand() );
    tempbuf[strlen(tempbuf)] = 'x';
    encryptor->setSocket( sockfd );
    encryptor->setOutputBuffer( (unsigned char*)&outBuffer[0] );
    encryptor->resetCBC();
    encryptor->blockCrypt( tempbuf, outbuf, 16 );
    encryptor->blockCrypt( tempbuf, outbuf, 16 );

tempbuf的取值来自传输数据的size和rand(),而rand的种子在farm9crypt_init中被设定为了1000,因此这里rand并没有产生随机意义。随机的来源还是开辟数组空间后栈上原来的值。

encryptor->blockCrypt( tempbuf, outbuf, 16 );

这一语句出现了两次,blockCrypt函数的作用是对输入进行加密或解密。 第一次将tempbuf加密后发送给接受端,接收端解密后可以获得待发送内容的size 第二次将tempbuf与第一次的密文异或后加密后发送,作为CBC模式的IV.

从接受端的函数farm9crypt_read也能看出这一过程,decryptor也调用了blockCrypt函数两次,第一次得到变量limit,就是所要接受数据的大小,atoi的调用对应了发送端的

sprintf( tempbuf, "%d %d", size, rand() );

第二次则是取得了IV的值。

decryptor->resetCBC();
    decryptor->setOutputBuffer( (unsigned char*)&outBuffer[0] );
    decryptor->blockCrypt( buf, outbuf, 16 );
    decryptor->flush();
    decryptor->setOutputBuffer( (unsigned char*)&outBuffer[0] );
    decryptor->blockCrypt( buf + 16, outbuf2, 16 );
    int limit = atoi( outbuf );
    total = 0;
    char* inbuf = &inBuffer[0];
    while ( total < limit ) {
        int result = recv( sockfd, inbuf + total, limit - total, 0 );
        if ( result > 0 ) {
            total += result;
        } else {
            break;
        }
    }

2.4 传输完整性问题

缺少mac验证。尤其是第一个包,包含了待收数据的大小,被攻击者修改后会导致后续的内容都无法解密。

0x3 总结

从cryptcat源码分析中看到了一些密码学的使用问题包括默认密码、固定随机数种子、IV随机性不足、缺少mac验证等。

我们从这个例子总结的一些密码学相关程序源码的审查方法:

  1. 从注释、头文件包含、函数名等确定各源文件的主要功能和关系,确定密码学实现部分和原语调用部分
  2. 特定的敏感函数,如srand
  3. 有老版本和历史bug的话,关注新版本的bug fix
  4. 加密逻辑与解密逻辑是对应的,可以对照着看
  5. 不能快速理解的源码,可以通过输出中间变量、抓包等方式,观察程序行为来理解