#!/usr/bin/env python3

# a script to collide arbitrary Windows Executables with MD5

# Ange Albertini 2018-2021

# takes 2 PE,
# gets both PE headers+DD+sections
# moves them next to each other,
# adjust offsets of section tables
# copy sections next to each other
# update SizeOfHeaders of file 2

import sys, struct, hashlib, os

def getPEhdr(d):
  PEoffset = d.find(b"PE\0\0")
  peHDR = d[PEoffset:]

  Machine = struct.unpack("H", peHDR[4:4+2])[0]

  SecCount = struct.unpack("h", peHDR[0x6:0x6+2])[0]
  bits = None
  if Machine == 0x014C:
    bits = 32
  elif Machine == 0x8664:
    bits = 64
  if bits is None:
    print("ERROR: unknown arch")
    sys.exit()

  NumDiffOff = 0x74 if bits == 32 else 0x84
  NumDD = struct.unpack("i", peHDR[NumDiffOff:NumDiffOff+4])[0]

  SecTblOff = NumDiffOff + 4 + NumDD * 2 * 4

  # get the offset of the first section
  SectsStart = struct.unpack("i", peHDR[SecTblOff+0x14:SecTblOff+0x14+4])[0]

  PElen = SecTblOff + SecCount * 0x28

  return PEoffset, PElen, SecCount, PEoffset + SecTblOff, SectsStart


def relocateSections(d, SecTblOff, SecCount, delta):
  for i in range(SecCount):
    offset = SecTblOff + i*0x28 + 0x14
    PhysOffset = struct.unpack("i", d[offset:offset+4])[0]
    d = b"".join([
      d[:offset],
      struct.pack("i", PhysOffset + delta),
      d[offset+4:]
      ])
  return d


fn1, fn2 = sys.argv[1:3]

# the following 2 depends on computed collision blocks
# to leave room for 9 collision blocks after CPC
HEADER1OFF = 0x2C0
# arbitrary length to support most PE headers
HEADER2OFF = 0x480


# too lazy to get optimal file alignment :p
SECTIONSOFF = 0x1000

with open("pe1.bin", "rb") as f:
  prefix1 = f.read()
with open("pe2.bin", "rb") as f:
  prefix2 = f.read()

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


# best PE validation ever :p
assert d1.startswith(b"MZ")
assert d2.startswith(b"MZ")

PEoff1, HdrLen1, NumSec1, SecTblOff1, SectsStart1 = getPEhdr(d1)
PEoff2, HdrLen2, NumSec2, SecTblOff2, SectsStart2 = getPEhdr(d2)

if HdrLen1 > HEADER2OFF - HEADER1OFF:
  print("ERROR: PE header 1 is too big")
  sys.exit()

if HdrLen2 > SECTIONSOFF - HEADER2OFF:
  print("ERROR: PE header 2 is too big")
  sys.exit()

#SizeOfHeader increased
d2 = b"".join([
  d2[:PEoff2 + 0x54],
  struct.pack("i", SECTIONSOFF),
  d2[PEoff2 + 0x54 + 4:]
  ])

d1 = relocateSections(d1, SecTblOff1, NumSec1, SECTIONSOFF - SectsStart1)
Sections1 = d1[SectsStart1:]
Sections2 = d2[SectsStart2:]

# let's hope the first batch of sections are aligned to file Alignments ;)
OffSec2 = len(Sections1) + SECTIONSOFF

d2 = relocateSections(d2, SecTblOff2, NumSec2, OffSec2 - SectsStart2)

suffix = b"".join([
  d1[PEoff1:PEoff1+HdrLen1],
    (HEADER2OFF - (HEADER1OFF + HdrLen1)) * b"\0",
  d2[PEoff2:PEoff2+HdrLen2],
    (SECTIONSOFF - (HEADER2OFF + HdrLen2)) * b"\0",
  Sections1,
  Sections2
  ])

with open("collision1.exe", "wb") as f:
  f.write(prefix1 + suffix)
with open("collision2.exe", "wb") as f:
  f.write(prefix2 + suffix)

md5 = hashlib.md5(prefix1 + suffix).hexdigest()

assert md5 == hashlib.md5(prefix2 + suffix).hexdigest()

print("Success!")
print("Common MD5: %s" % md5)
