0x00 List Of Paper

1.Hash Length Extension Attacks
2.Cbc Byte Flipping Attack
3.Cfb reply Attack

0x01 Hash Length extension Attacks

hash长度扩展攻击最初场景为一个下载链接如:

http://example.com/download?file=report.pdf&mac=563162c9c71a17367d44c165b84b85ab59d036f9

其中参数mac是对参数file的一种验证方式,其生成方式为:

def create_mac(key, fileName)
   return Digest::SHA1.hexdigest(key + fileName)
End

之后下载的验证方式为:

def verify_mac(key, fileName, userMac)
    validMac = create_mac(key, filename)
    if (validMac == userMac) do
        initiateDownload()
    else
        displayError()
    end
End

知道大概场景后就应该想如何利用,那么在这里面,我们利用是想控制file这个参数,能够下载任意文件,并且能过verify_mac这个函数,这时候就需要hash长度扩展攻击。

首先我们以md5为例,md5详细流程为:https://www.ietf.org/rfc/rfc1321.txt

接下来我们看hash长度扩展攻击的真正攻击是在哪一步。

如下测试源码:

import hashlib

Secret = "testhash" ##长度为8
User = input('Please Input User:')
AttackHash = input('Please Input AttachHash:')
if hashlib.md5((Secret+User).encode('utf-8')).hexdigest() == AttackHash:
    print('Good Job')
else:
    print('False')
    print('User Md5 Hash is:'+hashlib.md5((Secret+User).encode('utf-8')).hexdigest())

在开始计算时,会初始化四个寄存器A,B,C,D,初始值分别为:

word A: 01 23 45 67
word B: 89 ab cd ef
word C: fe dc ba 98
word D: 76 54 32 10

根据md5的摘要算法可以知道,其计算内容是以512bit一个块进行计算,第一个块计算结束以后四个寄存器的值就会被更新。如果不存在下一个块,那么取出这四个寄存器的值,将其十六进制连接起来就是最终的md5值。如果还存在下一个块,那么就需要使用更新后的A,B,C,D寄存器内的值进行迭代计算,直到最后一个块,得到最终的md5值。

现在我们输入用户名为admin,hash随便输入以后查看:

C:\Users\Cloud\Desktop\test
λ python test.py
Please Input User:admin
Please Input AttachHash:123
False
User Md5 Hash is:9fd138774d33f88f20184e2eb7d14088

根据源码知道,md5加密的数据为:

变量Secret的值 + 我们输入的用户名

即:

testhashadmin

这个长度为:13*8=104 bit 很明显不足512bit,那么在进行运算时,按照rfc的说明:

3.1 Step 1. Append Padding Bits
The message is "padded" (extended) so that its length (in bits) is
congruent to 448, modulo 512. That is, the message is extended so
that it is just 64 bits shy of being a multiple of 512 bits long.
Padding is always performed, even if the length of the message is
already congruent to 448, modulo 512.

Padding is performed as follows: a single "1" bit is appended to the
message, and then "0" bits are appended so that the length in bits of
the padded message becomes congruent to 448, modulo 512. In all, at
least one bit and at most 512 bits are appended.

3.2 Step 2. Append Length

A 64-bit representation of b (the length of the message before the
padding bits were added) is appended to the result of the previous
step. In the unlikely event that b is greater than 2^64, then only
the low-order 64 bits of b are used. (These bits are appended as two
32-bit words and appended low-order word first in accordance with the
previous conventions.)

At this point the resulting message (after padding with bits and with
b) has a length that is an exact multiple of 512 bits. Equivalently,
this message has a length that is an exact multiple of 16 (32-bit)
words. Let M[0 ... N-1] denote the words of the resulting message,
where N is a multiple of 16.

首先 a single "1" bit is appended to the message 即:

Secret + UserInput + \x80

然后 "0" bits are appended so that the length in bits of the padded message becomes congruent to 448, modulo 512 即:

len(Secret + UserInput + \x80 + N*\x00)%512 = 448

最后 A 64-bit representation of b (the length of the message before the padding bits were added) is appended to the result of the previous step. In the unlikely event that b is greater than 2^64, then only the low-order 64 bits of b are used 即:

Secret + UserInput + \x80 + N*\x00 + \x68\x00\x00\x00\x00\x00\x00\x00

\x68等于104bit,Secret+UserInput一共13byte等于104bit,最后八位就是加密数据的长度。

也就是说,刚刚我们输入admin以后,因为"testahshadmin"的长度小于448bit所以在当前块运算结束后,直接可以得到md5值,最终进行运算的是:

testhashadmin + \x80 + 42*\x00 + \x68\x00\x00\x00\x00\x00\x00\x00

到这里以后,这一块刚好是512Bit,在进行md5计算的时候使用的就是如上数据。那么试想,如果我们在刚好的512bit上,在添加一点数据会怎样呢?

如现在使用数据为:

testhashadmin + \x80 + 42*\x00 + \x68\x00\x00\x00\x00\x00\x00\x00 + ‘test'

这个时候是怎样的呢?

按照rfc所说,md5计算分块时,若最后一块不足512bit且大于448bit则讲此块填充满以后,继续填充至448bit。若大于512bit则按512bit分开后,将不足448bit填充至448bit。

故当使用数据为:

testhashadmin + \x80 + 42*\x00 + \x68\x00\x00\x00\x00\x00\x00\x00 + ‘test'

将前面512Bit先计算后,在使用 前512bit 的数据生成的A,B,C,D内的寄存器的值,对'test'填充后的数据进行迭代。

那么问题就出现在这里:

在输入admin的时候,第一轮迭代生成的hash就是

testhashadmin + \x80 + 42*\x00 + \x68\x00\x00\x00\x00\x00\x00\x00 + ‘test'

前512bit生成的hash。

前文中提到了,在分块以后,下一块进行运算是根据上一块生成的hash更新寄存器中的值,以此来迭代下一块的计算。

在上述情况中,前512bit的计算结果我们是知道的,我们只需要前512bit的计算结果更新至寄存器,对512bit以后的值即'test'进行加密,即可完成hash扩展攻击。

现使用c的md5算法代码,在进行一轮运算后,更改寄存器的值为我们已知的hash值即我们输入admin的hash值(注意大小端),然后再使用该值对test进行迭代,得到hash,并用此hash进行验证。

//md5.c


#include <stdio.h>
#include <openssl/md5.h>

int main(int argc, const char *argv[])
{
  int i;
  unsigned char buffer[MD5_DIGEST_LENGTH];
  MD5_CTX c;

  MD5_Init(&c);
  //首先使用任意的值进行一次运算,使得我们可以修改寄存器内的值
  MD5_Update(&c, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 64);
  //已知输入admin得到的hash为:9fd138774d33f88f20184e2eb7d14088,将其更新至寄存器
  c.A = 0x7738d19f;
  c.B = 0x8ff8334d;
  c.C = 0x2e4e1820;
  c.D = 0x8840d1b7;
  //使用更新的寄存器的值对'test'进行迭代
  MD5_Update(&c, "test", 4);

  MD5_Final(buffer, &c);
  for (i = 0; i < 16; i++) {
    printf("%02x", buffer[i]);
  }
  printf("\n");
  return 0;
}

结果:

root@iZj6c0v0csy0zio57uazcoZ:~/test# gcc md5.c -lssl -lcrypto
root@iZj6c0v0csy0zio57uazcoZ:~/test# ./a.out 
9649c6e50ca34fbbb1b458a5855bb549

使用得到的值进行验证看是否成功:

import hashlib

Secret = "testhash"
User='admin' + '\x80' + 42*'\x00' + '\x68\x00\x00\x00\x00\x00\x00\x00' + 'test'
print('Secret+User length is:'+str(len(Secret+User)))
AttackHash = ‘9649c6e50ca34fbbb1b458a5855bb549’
if hashlib.md5((Secret+User)).hexdigest() == AttackHash:
    print('Good Job')
else:
    print('False')
    print('User Md5 Hash is:'+hashlib.md5((Secret+User)).hexdigest())

结果:

(py2) λ python test.py
Secret+User is:testhashadmin€                                          h       test
Secret+User length is:68
AttackHash is:9649c6e50ca34fbbb1b458a5855bb549
Good Job

0x02 Cbc Byte Flipping Attack

AES共有五种加密模式,这次说的是密码分组链接模式(Cipher Block Chaining (CBC))。

加密流程如下:

Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块
Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块

解密流程如下:

Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块
Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

aes是分组密码体制,每一组十六个字节,无论是加密还是解密,都是先将其分组后再代入运算。

由上述两个流程可以知道,在解密时,由上一组密文和下一组密文用key解密后的值异或,得到明文。即

Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1

用图表示为:

图中两红色区域分别对应Ciphertext-N-1和Plaintext-N,并且其值我们是知道的,那么我们可以很轻松的得到

Decrypt(Ciphertext) = Ciphertext-N-1 XOR Plaintext-N

那么如我们由如下字符序列:

a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}

在加密前先对其分组,每一组十六个字节,分组以后为:

Block 1:a:2:{s:4:"name";
Block 2:s:6:"sdsdsd";s:8
Block 3::"greeting";s:20
Block 4::"echo 'Hello sd
Block 5:sdsd!'";}

若我们想更改Block2即 s:6:"sdsdsd";s:8 的6 为 7,那么需要只需要修改第一组秘文中的对应位置的值即可,接下来就说如何实现。

Block2中的6在改组中对应的位置为第二位(下标从0开始)即:

Block2[2]='6'

那么需要修改的也就是第一组密文的第二位,由于第一组密文直接在密文的首部,那么第一组密文的第二位也就是整个密文的第二位,即:

若:

string = 'a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}'
EncryptoData = encrypto(string)

只需要修改 EncryptoData[2]即可达到我们想要的目的:

那么这个值怎么设置才能达到我们想要的目的呢?

假设A = Decrypt(Ciphertext),B = Ciphertext-N-1,C为要修改的明文,那么可以知道的是:

B已知(EncryptoData[2])
C=6 //要修改的明文,已知

因为

A = B XOR C

那么A也知道。

又因为任何数和自己异或为0,也就是:

A xor A = 0

A = B xor C

可以推导出:

A xor B xor C =0

因为任何数和0异或等于他本身。所以假设我们想要得到的值为N,即:

A xor B xor C xor N = N

由于我们可控的只有密文也就是B = Ciphertext-N-1

所以我们只需要把B修改成:

B = B xor C xor N

这样的话,在解密运算时计算的:

A xor B = A xor (B xor C xor N)

也就是

A xor B xor C xor N

从前面推导可以知道这个得到的也就是N了。

那么这样即可让明文修改为我们想要的任意值。

测试代码:

from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES


# Padding for the input string --not
# related to encryption itself.
BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)

    Tested under Python 3 and PyCrypto 2.6.1.

    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')


##
# MAIN
# Just a test.
msg = input('Message...: ')
pwd = 'testtest'

crypto_data = AESCipher(pwd).encrypt(msg).decode('utf8')

print('Ciphertext:', crypto_data)

decrypt_data = AESCipher(pwd).decrypt(crypto_data.encode('utf8'))
print('Decrypto:'+decrypt_data)

输入明文:aaaaaaaaaa0123456789abcdef

λ python aestest.py
Message...: aaaaaaaaaaaaaaaa0123456789abcdef
Ciphertext: vJTgfd2IOzIwN+WOPtCEBFOOhANHmD0+ipkr6SrIaCgrSsHcSKxytACehZGrn1vP
Decrypto:aaaaaaaaaaaaaaaa0123456789abcdef

现更改明文中的2为9,更改代码为:

##
# MAIN
# Just a test.
msg = raw_input('Message...: ')
pwd = 'testtest'

crypto_data = AESCipher(pwd).encrypt(msg)

print(msg)

tmp_data = crypto_data
# print(type(tmp_data))
iv = tmp_data[:16]
tmp_data = tmp_data[16:]
# print(len(tmp_data))
# print(tmp_data[2])
tmp_data_index = chr(ord(tmp_data[2]) ^ ord('2') ^ ord('9'))
tmp_data = tmp_data[:2]+tmp_data_index+tmp_data[3:]
# print(len(tmp_data))
final_crypto_data = b64encode(iv+tmp_data)

decrypt_data = AESCipher(pwd).decrypt(final_crypto_data)
print(decrypt_data)

结果:

(py2) λ python aestest.py
Message...: aaaaaaaaaaaaaaaa0123456789abcdef
aaaaaaaaaaaaaaaa0123456789abcdef
(WC0g杸t@tjs??0193456789abcdef

0x03 CFB reply Attack

密文反馈(CFB,Cipher feedback)模式类似于CBC,可以将块密码变为自同步的流密码;工作过程亦非常相似,CFB的解密过程几乎就是颠倒的CBC的加密过程

从解密原理可以看到,IV和key经过encryption算法后和分组之后的第一组密文异或得到明文。其后每前一组密文和key进行计算后和当前组密文进行异或得到明文。可以知道的是,如果我们从中间截断密文则不会对后面的密文转换为明文有影响。

以前比赛遇到过一个场景。

case 'login':
if ($user) {
    header("HTTP/1.1 302 Found");
    header("Location: ?action=home");
}elseif(isset($_POST['user']) && isset($_POST['pwd'])) {
        if ($_POST['user'] == '') echo 'Username Required';
        elseif ($_POST['pwd'] == '') echo 'Password Required';
        elseif (!login((string)$_POST['user'], (string)$_POST['pwd'])) echo 'Incorrect';
        else {
            $user = $_POST['user'];
            // get_indentify() 获取10位的key,做一个身份签名,防止身份伪造

            $md5 = md5(get_indentify().$user);
            $admin = 0;
            // $token = token_encrypt("$user|$admin|$md5");
            $token = token_encrypt("$user|$admin|$md5");
            setcookie('sign',$md5,time()+5*60,"/",'',false,true);
            setcookie('token',$token,time()+5*60,"/",'',false,true);
            header("HTTP/1.1 302 Found");
            header("Location: ?action=home");
        }
    }
    ?>

登陆以后如此设置cookie以及sign。

if (isset($_COOKIE['token'])&&isset($_COOKIE['sign'])) {
    $sign = $_COOKIE['sign'];
    $token = $_COOKIE['token'];
    $arr = explode('|', token_decrypt($token));

    if (count($arr) == 3) {
        if (md5(get_indentify().$arr[0]) === $arr[2] && $sign === $arr[2]) {
            $user = $arr[0];
            $admin = (int)$arr[1];
        }
    }
}

check函数如图,要让$admin=1,但是之前设置时已经将$admin设置为0;

加密函数为:

define('BS', 16);

function getRandChar($length){
    $str = null;
    $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
    for($i=0;$i<$length;$i++){
        $n = rand(0, strlen($strPol) - 1);
        $str.=$strPol[$n];
    }
    return $str;
}

function pad($str) {
    return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS));
}

function unpad($str) {
    return substr($str, 0, -ord(substr($str, -1, 1)));
}

function token_encrypt($str) {
    $key = get_key();
    srand(time() / 300);
    $iv = '1111111111111111';
    return bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, pad($str), MCRYPT_MODE_CFB, $iv));
}

function token_decrypt($str) {
    $key = get_key();
    srand(time() / 300);
    $iv = '1111111111111111';
    return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, hex2bin($str), MCRYPT_MODE_CFB, $iv);
}

$iv为了方便我改成了固定的值。由此来看,如果想绕过那么有一种办法,就是根据重放攻击,自己构造一个能够过check的字符串。

如我现在注册用户名为123

那么组合以后的东西为:

123|0|df96220fa161767c5cbb95567855c86b

其加密以后的密文为:

932985a513da61d2d3ce092031c2c1886ee8d4dd4ce8f7f2bad652954a19b90176edd6615c29c27f5f77db7fb5188c7a

那么我可以将我们的用户名注册为:

123|1|df96220fa161767c5cbb95567855c86b

那么在登陆以后加密之前组合为:

123|1|df96220fa161767c5cbb95567855c86b|0|7645a9e4a54975e09912819e3887978e

由此得到的密文为:

932985a51266feb88e713debab00d401dfa09207b2e2d4713259c13a2d26e30c7ba05fcf90c48716139a2b38039f226d718311194e9a3b0fa25e3064e2a1ceefa606a550e7567fec4ec6942ce0d3173a

因为在加密时采用的时cfb模式加密,那么明文和密文是一一对应的关系,并且我们从后面删除密文不会影响前面的明文破解。所以我们只需要截取我们需要的明文对应的密文长度即可解密得到我们想要构造的东西。

如之前加密以后密文长度为:

>>> len('932985a513da61d2d3ce092031c2c1886ee8d4dd4ce8f7f2bad652954a19b90176edd6615c29c27f5f77db7fb5188c7a')
96

我们截取构造以后的密文的前96位放进去解密:

>>> '932985a51266feb88e713debab00d401dfa09207b2e2d4713259c13a2d26e30c7ba05fcf90c48716139a2b38039f226d718311194e9a3b0fa25e3064e2a1ceefa606a550e7567fec4ec6942ce0d3173a'[:96]
'932985a51266feb88e713debab00d401dfa09207b2e2d4713259c13a2d26e30c7ba05fcf90c48716139a2b38039f226d'

修改php文件代码为:

$test = '932985a51266feb88e713debab00d401dfa09207b2e2d4713259c13a2d26e30c7ba05fcf90c4';
echo 'decrypto_data is : '.$test."\n";
$decrypt_data = token_decrypt($test);
echo "After decrypto data is :".$decrypt_data

执行得:

λ php c:\Users\Cloud\Desktop\test\encrypt.php
decrypto_data is : 932985a51266feb88e713debab00d401dfa09207b2e2d4713259c13a2d26e30c7ba05fcf90c4
After decrypto data is :123|1|df96220fa161767c5cbb95567855c86b