#!/usr/bin/env python3

# A script to collide PNGs with the same header
# and optionally craft the prefix and launch UniColl

# Ange Albertini 2018-2021

import sys
import struct
import hashlib
import glob
import os
import shutil

def get_data(args):
  fn1, fn2 = args

  with open(fn1, "rb") as f:
    d1 = f.read()
  with open(fn2, "rb") as f:
    d2 = f.read()

  assert d1.startswith(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR")
  assert d2.startswith(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR")

  assert d1[:0x21] == d2[:0x21]

  return d1, d2


d1, d2 = get_data(sys.argv[1:3])

hash = hashlib.sha256(d1[:0x21]).hexdigest()[:8]

print("Header hash: %s" % hash)

if not glob.glob("png1-%s.bin" % hash):
  print("Not found! Launching computation...")

  # make the complete prefix
  with open("prefix", "wb") as f:
    f.write(b"".join([
    # 00-20 - original common header
    d1[:0x21],
    # 21-46 - padding chunk
    b"\0\0\0\x1a", b"aNGE", b":MD5 ISREALLY DEAD NOW!!1!", b"ROFL",
    
    # 47-C7 - collision chunk

    # 47-4F
    # this is the end of the collision prefix,
    # => lengths of 0x75 and 0x175
    b"\0\0\0\x75", b"mARC", b"!",

    # the end of the collision blocks if they're not computed
    # 50-BF
    # " " * 0x70,
  ]))

  # Note: make sure poc_no.sh is unmodified (ie, N=1)
  os.system("../hashclash/scripts/poc_no.sh prefix")

  shutil.copyfile("collision1.bin", "png1-%s.bin" % hash)
  shutil.copyfile("collision2.bin", "png2-%s.bin" % hash)

with open("png1-%s.bin" % hash, "rb") as f:
  block1 = f.read()
with open("png2-%s.bin" % hash, "rb") as f:
  block2 = f.read()

assert len(block1) == 0xC0
assert len(block2) == 0xC0

ascii_art = b"""
/==============\\
|*            *|
|  PNG IMAGE   |
|     with     |
|  identical   |
|   -prefix    |
| MD5 collision|
|              |
|  by          |
| Marc Stevens |
|  and         |
|Ange Albertini|
|              |
|*            *|
\\==============/
BRK!
""".replace(b"\n", b"").replace(b"\r", b"")

assert len(ascii_art) == 0xF4

suffix = b"".join([
    # C0-C7
    b"RealHash", # the remaining of the mARC chunk

    # C8-1C3 the tricky fake chunk
    
    # the length, the type and the data should all take 0x100
      struct.pack(">I", 0x100 - 4*2 + len(d2[0x21:])),
      b"jUMP",
      # it will cover all data chunks of d2,
      # and the 0x100 buffer
      ascii_art,
    b"\xDE\xAD\xBE\xEF", # fake CRC for mARC

    # 1C8 - Img2 + 4
    d2[0x21:],
    b"\x5E\xAF\x00\x0D", # fake CRC for jUMP after d2's IEND
    d1[0x21:],
  ])

with open("%s-1.png" % hash, "wb") as f:
  f.write(b"".join([
    block1,
    suffix
    ]))

with open("%s-2.png" % hash, "wb") as f:
  f.write(b"".join([
    block2,
    suffix
    ]))
