Skip to content

A .NET ReadyToRun image built for a non-Windows target fails to load: its Machine field carries an OS override #751

Description

@zardus

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Description

A .NET ReadyToRun image published for a non-Windows target cannot be loaded at
all: PE.__init__ raises archinfo.arch.ArchNotFound: Can't find architecture info for architecture 0xfd1d with '' bits and unsure endness.

# cle/backends/pe/pe.py
machine_type = self._pe.FILE_HEADER.Machine
self.set_arch(archinfo.arch_from_id(pefile.MACHINE_TYPE.get(machine_type, hex(machine_type))))

0xfd1d is not a machine type. The .NET ReadyToRun compiler alters Machine by
an operating-system-specific constant when the image targets an OS other than
Windows, so that the Windows loader refuses a file that is not for it. Every one
of these images in the corpus is the AMD64 case:

0xfd1d ^ 0x7b79 = 0x8664   (IMAGE_FILE_MACHINE_AMD64)

0x7b79 is what the images observed here use; the .NET sources are the authority
on the constant for each target OS.

The rest of the header is an ordinary PE32+ image with .text, .data and
.reloc, an IMAGE_COR20_HEADER whose ManagedNativeHeader points at an RTR\0
signature, and native AMD64 code in .text — which is the whole point of
ReadyToRun. Nothing else about the image needs special handling.

Reproduction

$ dotnet new console -o r2rdemo && cd r2rdemo
$ dotnet publish -r linux-x64 -p:PublishReadyToRun=true --self-contained

then on the published r2rdemo binary:

import cle
cle.Loader("bin/Release/net*/linux-x64/publish/r2rdemo")   # ArchNotFound: 0xfd1d

Confirming what the file is, without cle:

import struct
data = open("r2rdemo", "rb").read()
pe = struct.unpack_from("<I", data, 0x3c)[0]
machine = struct.unpack_from("<H", data, pe + 4)[0]
print(hex(machine), hex(machine ^ 0x7b79))     # 0xfd1d 0x8664

The IMAGE_COR20_HEADER's ManagedNativeHeader begins with RTR\0 on every one
of these.

Scale

Across a sweep of a large binary corpus, every PE object whose catalogued
architecture is cil and whose Machine is 0xfd1d fails this way — the
architecture is never resolved, so no analysis runs at all. They are the only
ArchNotFound failures in the whole PE population.

Options

This is a judgement call about how much .NET-specific knowledge belongs in the PE
backend, so this is an issue rather than a pull request:

  1. Gate on the image actually being ReadyToRun and undo the override there. The
    IMAGE_COR20_HEADER is already parsed for is_dotnet; its
    ManagedNativeHeader begins with RTR\0 exactly when this applies. With
    that established, Machine ^ c for each OS-override constant the .NET
    toolchain defines resolves to a real IMAGE_FILE_MACHINE_*. This cannot
    accidentally rescue a corrupt Machine field on an unrelated image.
  2. Undo the override on any unrecognised Machine, without the ReadyToRun
    check. Simpler, and looser.
  3. Leave the architecture alone and only improve the error, so a caller can tell
    an unsupported machine from a mangled one.

There is a second question either of the first two raises. PE.__init__ sets
self.os = "windows" unconditionally, and one of these images is not a Windows
binary — the override is there to say so. With the architecture resolved, angr
would build a SimWindows for a Linux program. Whether the backend should read
the override back into self.os is the same maintainer call.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions