迅雷Hash算法分析
从迅雷离线获得的地址中,存在着大量的Hash值,这些hash看似都是base64,sha1,md5但却有所不同。
比如这是一个典型的离线地址
http://gdl.lixian.vip.xunlei.com/download?
fid=3KUoDlBy9IotAFn5vQAHc5VUIgKiwSgmAAAAAHYA3duVUUeWgaJI4JGu5syK9PLK& // base64: 与cid,size,gcid相关(size为小字节序)
mid=666& // maybe: always 666
threshold=150& // maybe: always 150
tid=A950BAE38A2E7398186D4127315DB76F // unknow: 256bit relate with size
srcid=4 // maybe: always 4
verno=1 // maybe: always 1
g=7600DDDB9551479681A248E091AEE6CC8AF4F2CA& // gcid: for normal download gcid == cid
scn=c7& // section
i=3547930B96AFA7B0A1CFCC80D516ADE97A34DAE0& // cid == infoid == btih == ed2k hash, files share a same cid in a bt task, cid is the btih of the torrent
t=6& // type: 1=normal 4=ed2k 6=bt
ui=18×××9640& //userid
ti=33742×××247& //tid from get free url
s=640205218& //totalByte
m=0& // mayby: always 0
n=01324486025B4775690D459D7F43726F770F6CBF6F345D5B46347DA817445D5B422876D1025B783236556EA51C335D2E6D0A47E45F00000000 // filename
根据已有的数据分析/比对,大致各个字段的含义已经标识出来了。其中除了ui,ti是与用户相关的变元,cid是来源相关的变元,其他的字段对于某一个文件来说一般是相同的。
cid
算法源码:
def cid_hash_file(path):
h = hashlib.sha1()
size = os.path.getsize(path)
with open(path, 'rb') as stream:
if size < 0xF000:
h.update(stream.read())
else:
h.update(stream.read(0x5000))
stream.seek(size/3)
h.update(stream.read(0x5000))
stream.seek(size-0x5000)
h.update(stream.read(0x5000))
return h.hexdigest().upper()
算法来自于https://github.com/iambus/xunlei-lixian。
在api中,cid主要用于文件的索引。观察代码可知,cid并没有hash整个文件,而是根据文件的头/中/尾部的0x5000字节的内容计算Hash。这样就可以在不下载完整个文件,就能够查询到其他服务器上可能的相同文件。于是在下载支持range的文件的时候,即使该地址没有被索引到,但是通过cid,依旧可以被p2sp加速。
当然了,由于没有hash整个文件,文件在事实上有可能是不同的,那么根据下面这个gcid就可以唯一确定一个文件了。
gcid
def gcid_hash_file(path):
h = hashlib.sha1()
size = os.path.getsize(path)
psize = 0x40000
while size / psize > 0x200 and psize < 0x200000:
psize = psize << 1
with open(path, 'rb') as stream:
data = stream.read(psize)
while data:
h.update(hashlib.sha1(data).digest())
data = stream.read(psize)
return h.hexdigest().upper()
这个算法是我完全没有通过逆向黑盒分析而来,虽然没有做完整的测试,但是一般来说是正确的。。分析借助了loli.lu的18万个已有文件的数据,以及迅雷咖啡吧上的一句话:“如果文件很大,则计算gcid非常耗时,因此可以在大文件传输过程中计算gcid,文件传输完毕,则gcid也计算好了“。。
gcid的作用是文件的唯一键,在迅雷服务器上唯一确定一个文件。可以说,只要有了gcid,实际上是可以任意下载到需要的文件的。算法采用了分片hash再二次sha1的算法。。猜测原因是因为分片被限制在512个一下,当hash较大文件的时候,可以边下载边hash,再在最后hash那个不到512*20字节的串即可,当文件下载完成的时候就能立即得出gcid。还有一个原因是bt文件也是用sha1分片Hash的,那么获得种子文件也就同时有可能获得gcid了。同时,如果迅雷服务器保存了每个分片的sha1 hash的话,那么在下载通过cid匹配的文件同时,就能同时比较各个分片是否正确,以此保证最终结果。
fid
算法如下:
def parse_fid(fid):
cid, size, gcid = struct.unpack("<20sq20s", fid.decode("base64"))
return cid.encode("hex").upper(), size, gcid.encode("hex").upper()
def gen_fid(cid, size, gcid):
return struct.pack("<20sq20s", cid.decode("hex"), size, gcid.decode("hex")).encode("base64").strip()
首先这很明显是一个base64,但是一开始我并没有发现她们和cid,size,gcid的关系,直到我膝盖中了一箭。。
fid就是cid,size,gcid的二进制然后再base64而已。但是有了fid,神马cid,size,gcid这三大要素都不是问题了。应该是用于api分析url的便利,所做的一个接口性参数。
tid
未知算法。
根据18万的文件数据,唯一能够知道的是,这个tid和文件大小一一对应。。size相同的文件tid一定相同,但是又不是size的直接hash,目前来说完全不知道这个参数的意义何在。。
如果有兴趣,您可以在https://github.com/binux/lixian.xunlei/blob/master/tid.dict文件里面找到目前已知的映射。。如果分析出算法了请务必告诉我。
n
算法源码:
thunder_filename_mask = "6131E45F00000000".decode("hex")
def thunder_filename_encode(filename, encoding="gbk"):
if isinstance(filename, unicode):
filename = filename.encode(encoding)
result = ["01", ]
for i, word in enumerate(filename):
mask = thunder_filename_mask[i%len(thunder_filename_mask)]
result.append("%02X" % (ord(word)^ord(mask)))
while len(result) % 8 != 1:
mask = thunder_filename_mask[len(result)%len(thunder_filename_mask)-1]
result.append("%02X" % ord(mask))
return "".join(result)
def thunder_filename_decode(code, encoding="gbk"):
assert code.startswith("01")
result = []
for i, word in enumerate(code[2:].decode("hex")):
mask = thunder_filename_mask[i%len(thunder_filename_mask)]
result.append(chr(ord(word)^ord(mask)))
result = "".join(result).rstrip("\0")
return result.decode(encoding)
算法来源于+Zhang Youfu。简单来说这个参数就是将文件名各位用掩码进行了简单转换而已,中文的编码与最终输出的header中的相同,既编码采用utf8,最终输出的也是utf8,值得指出的是默认的编码是gbk的。迅雷在输出文件名时会截断较长的文件名,但是实际上通过传递完整的n参数,可以无视这个限制。在binux/ThunderLixianExporter还有一个js版本的实现。
总结
由上面的分析可见,一个文件的离线地址完全就是根据文件的信息生成的,于是你发现了什么?对了,完全不需要通过迅雷服务器我们就可以生成自己的离线地址!如果这个文件在迅雷服务器上存在,我们可以直接下载回来!(等等,你说n。。那不就是文件名嘛。。我只关心内容。。文件名这种小问题。。)
说到做到,您可以通过https://github.com/binux/lixian.xunlei/blob/master/check_file.py这个文件直接计算出文件的cid,gcid,fid,如果可能的话也能计算出tid。
然后,把fake_url添加到迅雷软件里面。然后。。就可以直接下载了!可以开启高速通道,可以在快盘秒传,运气好可以开启离线秒传。。。
over