How can I add sections to an existing (OS X) executable?
Asked Answered
M

3

6

Is there any way of adding sections to an already-linked executable?

I'm trying to code-sign an OS X executable, based on the Apple instructions. These include the instruction to create a suitable section in the binary to be signed, by adding arguments to the linker options:

-sectcreate __TEXT __info_plist Info.plist_path

But: The executable I'm trying to sign is produced using Racket (a Scheme implementation), which assembles a standalone executable from Racket/scheme code by cloning the 'real' racket executable and editing the Mach-O file directly.

So the question is: is there a way I can further edit this executable, to add the section which is required for the code-signing?

Using ld doesn't work when used in the obvious way:

% ld -arch i386 -sectcreate __TEXT __info_plist ./hello.txt racket-executable
ld: in racket-executable, can't link with a main executable
%

That seems fair enough, I suppose. Libtool doesn't have any likely-looking options, and neither does the redo_prebinding command (which is at least a command for editing executables).

The two possibilities suggested by the relevant Racket list were (i) to extend the the racket compilation tool to adjust the surgery which is done on the executable (feasible, but scary), or (ii) to create a custom racket executable which has the desired section already in place. Both seem like sledgehammer-and-nut solutions. The macosx-dev list didn't come up with any suggestions.

Monomorphic answered 26/10, 2010 at 9:49 Comment(0)
M
3

I think this is infeasible.

There appear to be no stock commands which edit a Mach-O object file (which includes executables). The otool command allows you to view the structure of such a file (use otool -l), but that's about it.

The structure of a Mach-O object file is documented on the Apple reference site. In summary, a Mach-O object file has the following structure:

  • a header, followed by
  • a sequence of 'load commands', followed by
  • a sequence of 'segments' (some of the load commands are responsible for pointing to the offsets of the segments within the file)

The segments contain zero or more 'sections'. The header and load commands are deemed to be in the first segment of the file, before any of that segment's sections. There are a couple of dozen load commands documented, and other commands defined in the relevant header file, therefore clearly private.

Adding a section would imply changing the length of a segment. Unless the section were very small, this would require pushing the following segment further into the file. That's a no-no, because a lot of the load commands refer to data within the file with absolute offsets from the beginning of the file (as opposed to, say, the beginning of the segment or section which contains them), so that relocating the segments in a putative Mach-O editor would involve patching up a large number of offsets. That doesn't look easy.

There are one or two Mach-O file viewers on the web, but none which do much that's different from otool, as far as I can see (it's not particularly hard: I wrote most of one this afternoon whilst trying to understand the format). There's at least one Mach-O editor, but it didn't seem to do anything that lipo didn't do (called 'moatool', but the source appears to have disappeared from google code).

Oh well. Time to find a Plan B, I suppose.

Monomorphic answered 10/12, 2010 at 21:39 Comment(3)
How did you manage to add a new section to an existing executable file? I'm now facing the same problem =[ thanks!Skier
I never solved it, but in the end simply evaded the requirement (it's ugly but in this context bearable to simply press the button at boot time that says 'yes, allow unsigned application X to run').Monomorphic
Addressing the specific point of how to do this in Racket, Seamus Brady and Matthew Flatt worked out a procedure which is described on the racket-users listMonomorphic
G
1

The gimmedebugah tool is able to modify the __info_plist TEXT section of an existing binary. See https://reverse.put.as/2013/05/28/gimmedebugah-how-to-embedded-a-info-plist-into-arbitrary-binaries/

It is available here: https://github.com/gdbinit/gimmedebugah

Grasso answered 28/3, 2018 at 19:51 Comment(0)
R
1

There's a really good post by Alexander O’Mara that explains how to add a section to an existing Mach-O executable.

https://alexomara.com/blog/adding-a-segment-to-an-existing-macos-mach-o-binary/

He uses python and macholib to manipulate the binary.

#!/usr/bin/env python3

import io
import os
import sys
import contextlib
from macholib.ptypes import (
    sizeof
)
from macholib.mach_o import (
    LC_SEGMENT_64,
    load_command,
    segment_command_64,
    section_64,
    dyld_info_command,
    symtab_command,
    dysymtab_command,
    linkedit_data_command
)
from macholib.MachO import (
    MachO
)

VM_PROT_NONE = 0x00
VM_PROT_READ = 0x01
VM_PROT_WRITE = 0x02
VM_PROT_EXECUTE = 0x04

SEG_LINKEDIT = b'__LINKEDIT'

def align(size, base):
    over = size % base
    if over:
        return size + (base - over)
    return size

def copy_io(src, dst, size=None):
    blocksize = 2 ** 23
    if size is None:
        while True:
            d = src.read(blocksize)
            if not d:
                break
            dst.write(d)
    else:
        while size:
            s = min(blocksize, size)
            d = src.read(s)
            if len(d) != s:
                raise Exception('Read error')
            dst.write(d)
            size -= s

def vmsize_align(size):
    return align(max(size, 0x4000), 0x1000)

def cstr_fill(data, size):
    if len(data) > size:
        raise Exception('Pad error')
    return data.ljust(size, b'\x00')

def find_linkedit(commands):
    for i, cmd in enumerate(commands):
        if not isinstance(cmd[1], segment_command_64):
            continue
        if cmd[1].segname.split(b'\x00')[0] == SEG_LINKEDIT:
            return (i, cmd)

def shift_within(value, amount, within):
    if value < within[0] or value > (within[0] + within[1]):
        return value
    return value + amount

def shift_commands(commands, amount, within, shifts):
    for (Command, props) in shifts:
        for (_, cmd, _) in commands:
            if not isinstance(cmd, Command):
                continue
            for p in props:
                v = getattr(cmd, p)
                setattr(cmd, p, shift_within(v, amount, within))

def main(args):
    if len(args) <= 5:
        print('Usage: macho_in macho_out segname sectname sectfile')
        return 1
    (_, macho_in, macho_out, segname, sectname, sectfile) = args

    with contextlib.ExitStack() as stack:
        fi = stack.enter_context(open(macho_in, 'rb'))
        fo = stack.enter_context(open(macho_out, 'wb'))
        fs = stack.enter_context(open(sectfile, 'rb'))

        macho = MachO(macho_in)
        if macho.fat:
            raise Exception('FAT unsupported')
        header = macho.headers[0]

        # Find the closing segment.
        (linkedit_i, linkedit) = find_linkedit(header.commands)
        (_, linkedit_cmd, _) = linkedit

        # Remember where closing segment data is.
        linkedit_fileoff = linkedit_cmd.fileoff

        # Find the size of the new segment content.
        fs.seek(0, io.SEEK_END)
        sect_size = fs.tell()
        fs.seek(0)

        # Create the new segment with section.
        lc = load_command(_endian_=header.endian)
        seg = segment_command_64(_endian_=header.endian)
        sect = section_64(_endian_=header.endian)
        lc.cmd = LC_SEGMENT_64
        lc.cmdsize = sizeof(lc) + sizeof(seg) + sizeof(sect)
        seg.segname = cstr_fill(segname.encode('ascii'), 16)
        seg.vmaddr = linkedit_cmd.vmaddr
        seg.vmsize = vmsize_align(sect_size)
        seg.fileoff = linkedit_cmd.fileoff
        seg.filesize = seg.vmsize
        seg.maxprot = VM_PROT_READ
        seg.initprot = seg.maxprot
        seg.nsects = 1
        sect.sectname = cstr_fill(sectname.encode('ascii'), 16)
        sect.segname = seg.segname
        sect.addr = seg.vmaddr
        sect.size = sect_size
        sect.offset = seg.fileoff
        sect.align = 0 if sect_size < 16 else 4

        # Shift closing segment down.
        linkedit_cmd.vmaddr += seg.vmsize
        linkedit_cmd.fileoff += seg.filesize

        # Shift any offsets that could reference that segment.
        shift_commands(
            header.commands,
            seg.filesize,
            (linkedit_fileoff, linkedit_cmd.filesize),
            [
                (dyld_info_command, [
                    'rebase_off',
                    'bind_off',
                    'weak_bind_off',
                    'lazy_bind_off',
                    'export_off'
                ]),
                (symtab_command, [
                    'symoff',
                    'stroff'
                ]),
                (dysymtab_command, [
                    'tocoff',
                    'modtaboff',
                    'extrefsymoff',
                    'indirectsymoff',
                    'extreloff',
                    'locreloff'
                ]),
                (linkedit_data_command, [
                    'dataoff'
                ])
            ]
        )

        # Update header and insert the segment.
        header.header.ncmds += 1
        header.header.sizeofcmds += lc.cmdsize
        header.commands.insert(linkedit_i, (lc, seg, [sect]))

        # Write the new header.
        header.write(fo)

        # Copy the unchanged data.
        fi.seek(fo.tell())
        copy_io(fi, fo, linkedit_fileoff - fo.tell())

        # Write new section data, padded to segment size.
        copy_io(fs, fo, sect_size)
        fo.write(b'\x00' * (seg.filesize - sect_size))

        # Copy remaining unchanged data.
        copy_io(fi, fo)

    # Copy mode to the new file.
    os.chmod(macho_out, os.stat(macho_in).st_mode)

    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))
Resht answered 7/8, 2023 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.