有什么方法可以将节添加到已链接的可执行文件中吗?
我正在尝试根据 Apple 说明 对 OS X 可执行文件进行代码签名。其中包括通过向链接器选项添加参数来在要签名的二进制文件中创建合适部分的指令:
-sectcreate __TEXT __info_plist Info.plist_path
但是:我尝试签名的可执行文件是使用Racket(Scheme 实现)生成的,它通过克隆“真正的”racket 可执行文件并编辑 Mach-O 文件,从 Racket/scheme 代码组装独立的可执行文件直接。
所以问题是:有没有办法进一步编辑这个可执行文件,以添加代码签名所需的部分?
以明显的方式使用
ld
不起作用:
% ld -arch i386 -sectcreate __TEXT __info_plist ./hello.txt racket-executable
ld: in racket-executable, can't link with a main executable
%
我想这似乎很公平。 Libtool 没有任何看起来可能的选项,
redo_prebinding
命令也没有(这至少是一个用于编辑可执行文件的命令)。
相关 Racket 列表建议的两种可能性是(i)扩展球拍编译工具来调整在可执行文件上完成的手术(可行,但可怕),或(ii)创建一个自定义球拍可执行文件,其中包含所需的部分已经就位。两者看起来都是大锤加坚果的解决方案。
macosx-dev
列表没有提出任何建议。
我认为这是不可行的。
似乎没有编辑 Mach-O 目标文件(包括可执行文件)的常用命令。
otool
命令允许您查看此类文件的结构(使用 otool -l
),仅此而已。
Mach-O 目标文件的结构记录在 Apple 参考站点。总之,Mach-O 目标文件具有以下结构:
这些段包含零个或多个“部分”。标头和加载命令被视为位于文件的第一个段中,位于该段的任何部分之前。记录了几十个加载命令,以及相关头文件中定义的其他命令,因此显然是私有的。
添加一个部分意味着改变一个部分的长度。除非该部分“非常”小,否则需要将以下段进一步推入文件中。这是一个禁忌,因为大量加载命令引用文件中的数据,这些数据具有距文件开头的绝对偏移量(而不是包含它们的段或节的开头),因此在假定的 Mach-O 编辑器中重新定位段将涉及修补大量偏移。看起来并不容易。
网络上有一两个 Mach-O 文件查看器,但据我所知,没有一个与 otool
有很大不同(这并不特别难:今天下午我在试图理解时写了其中的大部分内容)格式)。至少有一个 Mach-O 编辑器,但它似乎没有做任何
lipo
没有做的事情(称为“moatool”,但源代码似乎已从谷歌代码中消失)。
哦,好吧。我想是时候寻找 B 计划了。
gimmedebugah 工具能够修改现有二进制文件的 __info_plist TEXT 部分。请参阅
https://reverse.put.as/2013/05/28/gimmedebugah-how-to-embedded-a-info-plist-into- Arbitration-binaries/https://alexomara.com/blog/adding-a-segment-to-an-existing-macos-mach-o-binary/
他使用 python 和macholib 来操作二进制文件。
#!/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))