I sent out 2 invitations to all of my friends for my birthday! I'll know if they get stolen because the two invites look similar, and they even have the same md5 hash, but they are slightly different! You wouldn't believe how long it took me to find a collision. Anyway, see if you're invited by submitting 2 PDFs to my website.
Solution
It is now trivial to generate an MD5 collision of two images / PDF files.
Take any two PDFs of different contents, we can make them have the same MD5 hash.
BTW, there is a size limit so choose small PDFs.
import osimport sysimport hashlibdefEnclosedString(d,starts,ends): off = d.find(starts)+len(starts)return d[off:d.find(ends, off)]defgetCount(d): s =EnclosedString(d, "/Count ", "/") count =int(s)return countdefprocreate(l): # :preturn" 0 R ".join(l)+" 0 R"iflen(sys.argv)==1:print("PDF MD5 collider")print("Usage: pdf.py <file1.pdf> <file2.pdf>") sys.exit()os.system('mutool merge -o first.pdf %s'% sys.argv[1])os.system('mutool merge -o second.pdf %s'% sys.argv[2])os.system('mutool merge -o merged.pdf dummy.pdf %s%s'% (sys.argv[1], sys.argv[2]))withopen("first.pdf", "rb")as f: d1 = f.read()withopen("second.pdf", "rb")as f: d2 = f.read()withopen("merged.pdf", "rb")as f: dm = f.read()COUNT1 =getCount(d1)COUNT2 =getCount(d2)kids =EnclosedString(dm, "/Kids[", "]")# we skip the first dummy, and the last " 0 R" stringpages = kids[:-4].split(" 0 R ")[1:]template ="""%%PDF-1.41 0 obj<< /Type /Catalog%% for alignements (comments will be removed by merging or cleaning) /MD5_is__ /REALLY_dead_now__ /Pages 2 0 R%% to make sure we don't get rid of the other pages when garbage collecting /Fakes 3 0 R%% placeholder for UniColl collision blocks /0123456789ABCDEF0123456789ABCDEF012 /0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0>>endobj2 0 obj<</Type/Pages/Count %(COUNT2)i/Kids[%(KIDS2)s]>>endobj 3 0 obj<</Type/Pages/Count %(COUNT1)i/Kids[%(KIDS1)s]>>endobj4 0 obj %% overwritten - was a fake page to fool merging<< >>endobj"""KIDS1 =procreate(pages[:getCount(d1)])KIDS2 =procreate(pages[getCount(d1):])withopen("hacked.pdf", "wb")as f: f.write(template %locals())# adjust parents for the first set of pages f.write(dm[dm.find("5 0 obj"):].replace("/Parent 2 0 R", "/Parent 3 0 R", COUNT1))# let's adjust offsets - -g to get rid of object 4 by garbage collecting# (yes, errors will appear)printprint"KEEP CALM and IGNORE THE NEXT ERRORS"os.system('mutool clean -gggg hacked.pdf cleaned.pdf')withopen("cleaned.pdf", "rb")as f: cleaned = f.read()# some mutool versions do different stuff :(cleaned = cleaned.replace(" 65536 f \n0000000016 00000 n \n"," 65536 f \n0000000018 00000 n \n",1)withopen("pdf1.bin", "rb")as f: prefix1 = f.read()withopen("pdf2.bin", "rb")as f: prefix2 = f.read()file1 = prefix1 +"\n"+ cleaned[192:]file2 = prefix2 +"\n"+ cleaned[192:]withopen("collision1.pdf", "wb")as f: f.write(file1)withopen("collision2.pdf", "wb")as f: f.write(file2)os.remove('first.pdf')os.remove('second.pdf')os.remove('merged.pdf')os.remove('hacked.pdf')os.remove('cleaned.pdf')md5 = hashlib.md5(file1).hexdigest()assert md5 == hashlib.md5(file2).hexdigest()printos.system('mutool info -X collision1.pdf')printprintos.system('mutool info -X collision2.pdf')printprint"MD5: %s"% md5print"Success!"
These are the PDFs I generated. Note that they have different content, yet the same MD5.
After we've generated the two colliding PDFs, uploading them to the challenge page unlocks the flag and source code.