Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions COPYING
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright (c) 2011 Booyah, Inc. <http://www.booyah.com>
Copyright (c) 2013 Skillz, Inc. <http://www.skillz.com>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ send a Pull Request.
Copying
-------

Copyright © 2011, Booyah, Inc. See the `COPYING` file for license
rights and limitations (MIT).
Copyright © 2011, Booyah, Inc. Additional contributions Copyright © 2013, Skillz, Inc.
See the `COPYING` file for license rights and limitations (MIT).
90 changes: 63 additions & 27 deletions macho.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
module MachO

MH_MAGIC = 0xfEEDFACE
FAT_MAGIC = 0xBEBAFECA
MH_MAGIC = 0xFEEDFACE
MH_CIGAM = 0xCEFAEDFE
MH_MAGIC_64 = 0xFEEDFACF
MH_CIGAM_64 = 0xCFFAEDFE
FAT_MAGIC = 0xCAFEBABE
FAT_CIGAM = 0xBEBAFECA
LC_ENCRYPTION_INFO = 0x21
LC_UUID = 0x1b

# Represents a Mach-O load command.
class LoadCmd
Expand All @@ -14,7 +19,12 @@ class LoadCmd
attr_reader :cryptsize
attr_reader :cryptid

def initialize(io)
# LC_UUID
attr_reader :uuid

def initialize(io, little_endian, obj64)
@little_endian = little_endian
@obj64 = obj64
self.parse(io)
end

Expand All @@ -24,7 +34,7 @@ def parse(io)
cmd_start = io.pos

# All commands start with an identifier and size field.
@cmd, @cmdsize = io.read(8).unpack('VV')
@cmd, @cmdsize = io.read(8).unpack(MachO.format(@little_endian) * 2)

# /*
# * The encryption_info_command contains the file offset and size of an
Expand All @@ -38,8 +48,13 @@ def parse(io)
# uint32_t cryptid; /* which enryption system,
# 0 means not-encrypted yet */
# };
if (@cmd == LC_ENCRYPTION_INFO)
@cryptoff, @cryptsize, @cryptid = io.read(12).unpack('VVV')
if @cmd == LC_ENCRYPTION_INFO
@cryptoff, @cryptsize, @cryptid = io.read(12).unpack(
MachO.format(@little_endian) * 3)
end

if @cmd == LC_UUID
@uuid = io.read(16).unpack('H*').join()
end

# Seek to the end of this command (and the start of the next structure).
Expand All @@ -50,7 +65,7 @@ def parse(io)
end

# Represents a Mach-O executable.
class MachO
class MachOExec

attr_reader :offset
attr_reader :magic
Expand All @@ -60,6 +75,7 @@ class MachO
attr_reader :cmds
attr_reader :sizeofcmds
attr_reader :flags
attr_reader :uuid

def initialize(io)
self.parse(io)
Expand All @@ -68,6 +84,11 @@ def initialize(io)
def parse(io)
@offset = io.pos

@magic = io.read(4).unpack('N')[0]

@little_endian = [MH_CIGAM, MH_CIGAM_64].include?(magic)
@obj64 = [MH_MAGIC_64, MH_CIGAM_64].include?(magic)

# /*
# * The 32-bit mach header appears at the very beginning of the object file for
# * 32-bit architectures.
Expand All @@ -81,11 +102,19 @@ def parse(io)
# uint32_t sizeofcmds; /* the size of all the load commands */
# uint32_t flags; /* flags */
# };
@magic, @cputype, @cpusubtype, @filetype, ncmds, @sizeofcmds, @flags =
io.read(28).unpack('VVVVVVV')
@cputype, @cpusubtype, @filetype, ncmds, @sizeofcmds, @flags =
io.read(24).unpack(MachO.format(@little_endian) * 6)
ext64bit = io.read(4).unpack(MachO.format(@little_endian)) if @obj64

# Read all of the individual load commands.
@cmds = Array.new(ncmds) { |i| LoadCmd.new(io) }
@cmds = (1..ncmds).map do |i|
lcd = LoadCmd.new(io, @little_endian, @obj64)

if lcd.cmd == LC_UUID
@uuid = lcd.uuid
end
lcd
end

self
end
Expand All @@ -99,7 +128,8 @@ class FatArch
attr_reader :size
attr_reader :align

def initialize(io)
def initialize(io, little_endian)
@little_endian = little_endian
self.parse(io)
end

Expand All @@ -111,20 +141,20 @@ def parse(io)
# uint32_t size; /* size of this object file */
# uint32_t align; /* alignment as a power of 2 */
# };
@cputype, @cpusubtype, @offset, @size, @align =
io.read(20).unpack('VVVVV')

@cputype, @cpusubtype, @offset, @size, @align =
io.read(20).unpack(MachO.format(@little_endian) * 5)
self
end
end

# Represents the fat binary structure used to indicate that an executable
# supports multiple architectures.
class Fat
attr_reader :magic
attr_reader :archs

def initialize(io)
def initialize(io, little_endian)
@little_endian = little_endian
self.parse(io)
end

Expand All @@ -133,10 +163,11 @@ def parse(io)
# uint32_t magic; /* FAT_MAGIC */
# uint32_t nfat_arch; /* number of structs that follow */
# };
@magic, count = io.read(8).read('NN')
io.seek(4) # skip magic
count = io.read(4).unpack(MachO.format(@little_endian))[0]

# Read the array of architures from the fat binary header.
@archs = Array.new(count) { |i| FatArch.new(io) }
# Read the array of architectures from the fat binary header.
@archs = Array.new(count) { |i| FatArch.new(io, @little_endian) }

self
end
Expand All @@ -152,29 +183,29 @@ class Executable
attr_reader :archs

def initialize(file)
if not file.respond_to?(:seek)
unless file.respond_to?(:seek)
file = File.new(file, 'r')
end
self.parse(file)
end

def parse(io)
# The magic header value indicates the executable type.
magic = io.read(4).unpack('V')[0]

if (magic == FAT_MAGIC)
magic = io.read(4).unpack('N')[0]
if [FAT_MAGIC, FAT_CIGAM].include?(magic)
little_endian = FAT_CIGAM == magic
# This is a fat binary with multiple Mach-O architectures.
@fat = Fat.new(io)
@fat = Fat.new(io, little_endian)
# Parse each Mach-O architecture based on its offet in the binary.
@archs = @fat.archs.collect do |arch|
io.seek(arch.offset)
MachO.new(io)
MachOExec.new(io)
end
elsif (magic == MH_MAGIC)
elsif [MH_MAGIC, MH_CIGAM, MH_MAGIC_64, MH_CIGAM_64].include?(magic)
# This is a non-fat binary with a single Mach-O architecture.
# Reset our IO stream so the start of the Mach-O header.
io.seek(-4, IO::SEEK_CUR)
@archs = [MachO.new(io)]
@archs = [MachOExec.new(io)]
end

self
Expand All @@ -194,7 +225,7 @@ def self.simulate_encrypt(filename)
data_offset = arch_offset + macho.sizeofcmds

for cmd in macho.cmds do
if (cmd.cmd == LC_ENCRYPTION_INFO)
if cmd.cmd == LC_ENCRYPTION_INFO
random_bytes = Array.new(cmd.cryptsize)
random_bytes.fill { rand(256) }

Expand All @@ -204,4 +235,9 @@ def self.simulate_encrypt(filename)
end
end
end

def self.format(little_endian)
little_endian ? 'V' : 'N'
end
end