Synthesizing a 64-bit report to Mach-O emits a 32-bit Mach-O whenever the report came from something that is not a Mach-O, so every address above 4 GiB is truncated mod 2^32 and none of the planted code can be read back.
rust_pe_gnu is based at 0x140000000, so all 154207 of its recovered instructions are lost this way. PE and ELF round-trip it intact.
Reproducing
import struct, lief
from pathlib import Path
from smda.Disassembler import Disassembler
from smda.SmdaConfig import SmdaConfig
raw = Path("tests/rust_pe_gnu_xored").read_bytes()
data = bytes(b ^ (i % 256) for i, b in enumerate(raw))
report = Disassembler(SmdaConfig()).disassembleUnmappedBuffer(data)
print("report bitness", report.bitness, "base 0x%x" % report.base_addr)
blob = bytes(report.synthesizeBinary(output_format="macho"))
print("emitted magic 0x%08x" % struct.unpack("<I", blob[:4])[0]) # 0xfeedface, the 32-bit magic
print([hex(s.virtual_address) for s in lief.parse(list(blob)).sections][:3])
report bitness 64 base 0x140000000
emitted magic 0xfeedface
['0x40001000', '0x4009b000', '0x4009c000']
0x140001000 & 0xFFFFFFFF == 0x40001000, so the section addresses are the real ones with the top 32 bits cut off.
Cause
MachoSynthesizer._is64 (src/smda/synthesis/MachoSynthesizer.py:101) reads the stored xheader as a Mach-O header without first checking that it is one:
def _is64(self):
if self._hasHeader(4):
magic = struct.unpack("<I", self.report.xheader[0:4])[0]
return magic in (MH_MAGIC_64, MH_CIGAM_64)
return self.report.bitness != 32
_hasHeader (src/smda/synthesis/BinarySynthesizer.py:109) only tests that an xheader exists and is long enough — it says nothing about the format. For a PE-origin report the first four bytes are 4d 5a 90 00, which is not MH_MAGIC_64, so the method returns False and the correct report.bitness fallback on the last line is never reached.
_getCpuType (src/smda/synthesis/MachoSynthesizer.py:107) has the same shape and reads xheader[4:8] as a Mach-O cputype. For rust_pe_gnu those bytes are 03 00 00 00, so the synthesized Mach-O also claims CPU_TYPE_I386 for a 64-bit x86-64 report.
Both are only reached when an xheader is present but belongs to another format, which is why the Mach-O fixtures are unaffected: komplex supplies a real MH_MAGIC_64 and takes the intended path.
Measured
| fixture |
origin |
base |
PE |
ELF |
Mach-O |
rust_pe_gnu |
PE |
0x140000000 |
154207 / 154207 |
154207 / 154207 |
0 / 154207 |
komplex |
Mach-O |
0x100000000 |
4914 / 4914 |
4914 / 4914 |
4914 / 4914 |
cutwail |
PE |
0x400000 |
1611 / 1611 |
1611 / 1611 |
1611 / 1611 |
cutwail hides it: it is also a PE-origin report that synthesizes to a 32-bit Mach-O, but its base is below 4 GiB so nothing truncates. Only a report based above 4 GiB whose xheader is not a Mach-O exposes the bug, which is why rust_pe_gnu is the only fixture that fails.
Measured with the round-trip loop from #232 against master at 5ad5504.
Suggested direction
Gate both methods on the xheader actually being a Mach-O — the file already has MH_MAGIC/MH_CIGAM/MH_MAGIC_64/MH_CIGAM_64 and a _parseSegments path that validates the magic before trusting the rest — and otherwise fall through to report.bitness and the architecture. Happy to put a PR together if the direction looks right.
Synthesizing a 64-bit report to Mach-O emits a 32-bit Mach-O whenever the report came from something that is not a Mach-O, so every address above 4 GiB is truncated mod 2^32 and none of the planted code can be read back.
rust_pe_gnuis based at0x140000000, so all 154207 of its recovered instructions are lost this way. PE and ELF round-trip it intact.Reproducing
0x140001000 & 0xFFFFFFFF == 0x40001000, so the section addresses are the real ones with the top 32 bits cut off.Cause
MachoSynthesizer._is64(src/smda/synthesis/MachoSynthesizer.py:101) reads the storedxheaderas a Mach-O header without first checking that it is one:_hasHeader(src/smda/synthesis/BinarySynthesizer.py:109) only tests that anxheaderexists and is long enough — it says nothing about the format. For a PE-origin report the first four bytes are4d 5a 90 00, which is notMH_MAGIC_64, so the method returnsFalseand the correctreport.bitnessfallback on the last line is never reached._getCpuType(src/smda/synthesis/MachoSynthesizer.py:107) has the same shape and readsxheader[4:8]as a Mach-Ocputype. Forrust_pe_gnuthose bytes are03 00 00 00, so the synthesized Mach-O also claimsCPU_TYPE_I386for a 64-bit x86-64 report.Both are only reached when an
xheaderis present but belongs to another format, which is why the Mach-O fixtures are unaffected:komplexsupplies a realMH_MAGIC_64and takes the intended path.Measured
rust_pe_gnu0x140000000komplex0x100000000cutwail0x400000cutwailhides it: it is also a PE-origin report that synthesizes to a 32-bit Mach-O, but its base is below 4 GiB so nothing truncates. Only a report based above 4 GiB whosexheaderis not a Mach-O exposes the bug, which is whyrust_pe_gnuis the only fixture that fails.Measured with the round-trip loop from #232 against
masterat5ad5504.Suggested direction
Gate both methods on the
xheaderactually being a Mach-O — the file already hasMH_MAGIC/MH_CIGAM/MH_MAGIC_64/MH_CIGAM_64and a_parseSegmentspath that validates the magic before trusting the rest — and otherwise fall through toreport.bitnessand the architecture. Happy to put a PR together if the direction looks right.