BL-patcher.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import pefile
  2. import copy
  3. from hashlib import sha256
  4. '''
  5. Rip out the DOS stub:
  6. dd if=Blockland.exe bs=1 count=296 skip=64 of=dos_stub.txt
  7. '''
  8. dll_target_name = "RedBlocklandLoader.dll"
  9. print(" **************************************************************************")
  10. print(" * *")
  11. print(" * Blockland-patcher *")
  12. print(" * *")
  13. print(" **************************************************************************")
  14. print()
  15. #one final check: ignore a file that already has newimp
  16. exe_path = "Blockland.r2001.std.exe"
  17. exe_path = "Blockland.r1986.std.exe"
  18. exe_path = "Blockland.r2012.std.exe"
  19. exe_path = "Blockland.exe"
  20. print(' - Input file: '+ exe_path)
  21. print(' - Interpreting EXE file...')
  22. pe = pefile.PE(exe_path)
  23. added_size_counter = 0
  24. print(' - Re-aligning all sections...')
  25. for section in pe.sections:
  26. virt_size = section.Misc_VirtualSize
  27. raw_size = section.SizeOfRawData
  28. phys_addr = section.Misc_PhysicalAddress
  29. if virt_size % 0x200 != 0 and virt_size < raw_size:
  30. old_size = phys_addr
  31. new_size = (int(old_size / 0x200) + 1) * 0x200 #round up to nearest 0x200
  32. section.Misc_PhysicalAddress = new_size
  33. #section.SizeOfRawData = new_size
  34. added_size_counter += new_size-old_size
  35. name_strip = section.Name.decode().strip()
  36. print(" Section boundary does not align to page size. Adjusting section \""+ name_strip +"\":")
  37. print(" From: "+ str(old_size) +" ("+ hex(old_size) +")")
  38. print(" To: "+ str(new_size) +" ("+ hex(new_size) +")")
  39. print()
  40. newSection = copy.copy(pe.sections[0])
  41. newSection.Name=b".newimp"
  42. newSection.Misc = newSection.Misc_PhysicalAddress = newSection.Misc_VirtualSize = 0x1000
  43. newSection.VirtualAddress = pe.sections[-1].SizeOfRawData+pe.sections[-1].VirtualAddress
  44. newSection.SizeOfRawData = 0x1000
  45. newSection.PointerToRawData = pe.sections[-1].SizeOfRawData+pe.sections[-1].PointerToRawData
  46. newSection.Characteristics = 0xC0000040
  47. newSection.set_file_offset(pe.sections[-1].__file_offset__ + newSection.sizeof())
  48. pe.OPTIONAL_HEADER.SizeOfImage += newSection.SizeOfRawData
  49. pe.FILE_HEADER.NumberOfSections += 1
  50. pe.sections.append(newSection)
  51. pe.__structures__.append(newSection)
  52. print(" - Finding the import address table...")
  53. found_import_addr = False
  54. for entry in pe.OPTIONAL_HEADER.DATA_DIRECTORY:
  55. if entry.name == "IMAGE_DIRECTORY_ENTRY_IMPORT":
  56. found_import_addr = True
  57. print(" - Modifying ImportAddressTable and SizeOfImportAddressTable to point to .newimp section:")
  58. print(" Old size: "+ hex(entry.Size))
  59. print(" Old virtual address: "+ hex(entry.VirtualAddress))
  60. entry.Size += 20
  61. entry.VirtualAddress = newSection.VirtualAddress;
  62. print(" New size: "+ hex(entry.Size))
  63. print(" New virtual address: "+ hex(entry.VirtualAddress))
  64. break
  65. if not found_import_addr:
  66. print(" - Unable to find ImportAddressTable.")
  67. quit()
  68. print()
  69. new_import_table=bytearray(0)
  70. print(' - Reading original import table...')
  71. newSection_current_offset = newSection.VirtualAddress
  72. for entry in pe.DIRECTORY_ENTRY_IMPORT:
  73. dll_name=entry.dll.decode('utf-8')
  74. print(' Found '+ dll_name)
  75. struct=entry.struct
  76. data = [
  77. struct.Characteristics, #AKA pointer
  78. struct.TimeDateStamp,
  79. struct.ForwarderChain,
  80. struct.Name,
  81. struct.FirstThunk
  82. ]
  83. for var in data:
  84. new_import_table.extend(var.to_bytes(4, byteorder='little'))
  85. newSection_current_offset += 4
  86. total_imports = len(pe.DIRECTORY_ENTRY_IMPORT)
  87. print(' ('+ str(total_imports) +' imports)')
  88. print()
  89. print(' - Constructing new import table entry...')
  90. dll_target_name_term=dll_target_name +"\0"
  91. #if len(dll_target_name_term) % 2 == 1:
  92. # dll_target_name_term += '\0'
  93. #Each address skips 20 bytes over the current entry and 20 over the terminal empty entry
  94. #import table address
  95. addr=newSection_current_offset+40+len(dll_target_name_term)+8
  96. new_import_table.extend(addr.to_bytes(4, byteorder='little'))
  97. #timestamp
  98. new_import_table.extend(bytearray.fromhex("00 00 00 00"))
  99. #forwarder chain
  100. new_import_table.extend(bytearray.fromhex("00 00 00 00"))
  101. #address of the DLL's name string
  102. addr=newSection_current_offset+40+0
  103. new_import_table.extend(addr.to_bytes(4, byteorder='little'))
  104. #first thunk
  105. addr=newSection_current_offset+40+len(dll_target_name_term)
  106. new_import_table.extend(addr.to_bytes(4, byteorder='little'))
  107. newSection_current_offset += 20
  108. #empty import table entry marks end of import table
  109. new_import_table.extend(bytearray(20))
  110. newSection_current_offset += 20
  111. print(' - Constructing pointers for '+ dll_target_name +'...')
  112. new_loader_lookup_table=bytearray(0)
  113. #byte 0: DLL name
  114. new_loader_lookup_table.extend(str.encode(dll_target_name_term))
  115. #By spec, aligned to even number of bytes, but doesn't seem to matter
  116. newSection_current_offset += len(dll_target_name_term)
  117. #byte 20: first entry of import address table
  118. addr=newSection_current_offset+16
  119. new_loader_lookup_table.extend(addr.to_bytes(4, byteorder='little'))
  120. #byte 24: padding
  121. new_loader_lookup_table.extend(bytearray.fromhex("00 00 00 00"))
  122. #byte 28: first entry of import lookup table
  123. addr=newSection_current_offset+16
  124. new_loader_lookup_table.extend(addr.to_bytes(4, byteorder='little'))
  125. #byte 32: padding
  126. new_loader_lookup_table.extend(bytearray.fromhex("00 00 00 00"))
  127. #byte 36: export name pointer table is size 0
  128. new_loader_lookup_table.extend(bytearray.fromhex("00 00"))
  129. #byte 38: the function referenced in the DLL
  130. new_loader_lookup_table.extend(b"RedBlocklandLoader\0")
  131. section_data = new_import_table + new_loader_lookup_table;
  132. added_size_counter += len(section_data)
  133. if added_size_counter % 4096 == 0:
  134. pe.OPTIONAL_HEADER.SizeOfInitializedData += added_size_counter
  135. else:
  136. pe.OPTIONAL_HEADER.SizeOfInitializedData += (added_size_counter // 4096 + 1) * 4096
  137. print(' - Writing new EXE with modified headers...')
  138. new_exe_path = "Blockland.patched.exe"
  139. pe.write(new_exe_path)
  140. print(' - Re-writing EXE to append content of .newimp section...')
  141. null_size = 4096 - len(section_data)
  142. section_data += bytearray(null_size)
  143. section_data_hash = sha256(section_data).hexdigest()
  144. #DOS stub doesn't actually change, but it is not preserved when patching BL with another program (perhaps StudPE)
  145. #The code to change the stub is kept here in case I want a 100%-matching binary
  146. #with open('dos_stub.txt', 'r') as file:
  147. # dos_stub = file.read()
  148. #with open('Blockland.exe', 'r') as file:
  149. # exe_data = file.read()
  150. #with open("Blockland.exe", "w") as file:
  151. # file.write(exe_data[:64] + dos_stub + exe_data[360:] + section_data)
  152. with open(new_exe_path, 'rb') as file:
  153. exe_data = file.read()
  154. with open(new_exe_path, "wb") as file:
  155. file.write(exe_data + section_data)
  156. pe = pefile.PE(new_exe_path)
  157. print(" - Checking .newimp section checksum")
  158. refound_section = False
  159. for section in pe.sections:
  160. if section.Name.decode() != '.newimp\0':
  161. continue
  162. check_section_data_hash = sha256(section.get_data()).hexdigest()
  163. if check_section_data_hash == section_data_hash:
  164. print(" Hash match.")
  165. else:
  166. print(" Hash mismatch.")
  167. quit()
  168. refound_section = True
  169. break
  170. if not refound_section:
  171. print(" Could not find section.")
  172. quit()
  173. print()
  174. print(" - Checking presence of new "+ dll_target_name +":")
  175. dll_found = False
  176. dll_count = 0
  177. for entry in pe.DIRECTORY_ENTRY_IMPORT:
  178. dll_name=entry.dll.decode('utf-8')
  179. dll_count += 1
  180. if not dll_name == dll_target_name:
  181. continue
  182. print(" Found! "+ dll_name +" functions:")
  183. for func in entry.imports:
  184. print(" %s at 0x%08x" % (func.name.decode('utf-8'), func.address))
  185. dll_found = True
  186. break
  187. if not dll_found:
  188. print(" Could not find reference to "+ dll_target_name +" and its imports.")
  189. quit()
  190. print(" The EXE contains "+ str(dll_count) +" imports.")
  191. print()
  192. print(" **************************************************************************")
  193. print(" * *")
  194. print(" * The patched EXE has passed all checks. *")
  195. print(" * *")
  196. print(" **************************************************************************")
  197. print()